Kann ein C++-Array an der Speichergrenze enden?

Lesezeit: 10 Minuten

Benutzer-Avatar
Abi

Der C++-Standard (und C für diese Angelegenheit) erlaubt es, einen Zeiger auf ein Element hinter dem Ende des Arrays zu erstellen (allerdings nicht zu dereferenzieren). Bedeutet dies, dass ein Array niemals an einer solchen Stelle zugewiesen wird, dass sein letztes Element an der Speichergrenze endet? Ich verstehe, dass in der Praxis einige / alle Implementierungen dieser Konvention folgen könnten, aber welche der folgenden gilt:

  1. Es ist tatsächlich falsch, und ein Array kann an der Speichergrenze ODER enden
  2. Der C++-Standard schreibt vor, den Speicher mindestens eines Elements vor der Grenze OR zu beenden
  3. Weder 1 noch 2, aber in aktuellen Compilern ist es immer noch so, weil es die Implementierung erleichtert.

Ist bei C etwas anders?

Aktualisieren:
Wie es scheint 1 ist die richtige Antwort. Siehe Antwort von James Kanze unten und siehe auch efence (http://linux.die.net/man/3/efence – danke an Michael Chastain für den Hinweis darauf)

  • Ich glaube nicht, dass der Standard, sicherlich für C++, irgendetwas über die Speicherzuweisung dazu vorschreibt, also wären 1 und 2 nicht wahr und wahrscheinlich ist 3 eher ein Implementierungsdetail

    – EdChum

    17. Oktober 2014 um 7:53 Uhr


  • Ich sollte meinen ersten Kommentar klarstellen, für 1 könnte er an einer Speichergrenze enden, aber dies ist nicht im Standard angegeben und würde dem Implementierer überlassen

    – EdChum

    17. Oktober 2014 um 8:03 Uhr

  • @JamesKanze: Mein Punkt war, dass 0x40000000 in einer 32-Bit-Zeigervariablen gespeichert werden kann, während 0x100000000 dies nicht konnte. Einen Zeiger eins nach dem Ende des Arrays zu haben, kann also trivial zum Laufen gebracht werden, es sei denn, dieser Zeiger würde den Zeigertyp überlaufen lassen. Und das könnte nur passieren, wenn der adressierbare Bereich den Bereich des Zeigertyps erreicht oder überschreitet. Ich kann mir keine Plattform vorstellen, auf der das zutrifft. Offensichtlich können Fallenbedingungen bestehen (wie Sie in Ihrer Antwort darauf hinweisen), aber diese können umgangen werden.

    – Sander DeDycker

    17. Oktober 2014 um 8:43 Uhr

  • @SanderDeDycker Es kann immer zum Laufen gebracht werden; Verwenden Sie einfach nicht das letzte Byte / Wort des zugewiesenen Blocks. Was Maschinen betrifft, bei denen der adressierbare Bereich den Bereich des Zeigertyps überschreitet: Es gibt Intel, wenn Ihre Zeiger das Segmentregister nicht enthalten.

    – James Kanze

    17. Oktober 2014 um 8:52 Uhr


  • @abi: Ich stelle mir Zeiger lieber so vor, dass sie auf Leerzeichen “zwischen” Speicherorten zeigen und sagen, dass “*foo„bedeutet „das Ding unmittelbar nach dem Ort foo Punkte”. Ein 12-Zoll-Lineal hat dreizehn Markierungen bei 0″, 1″, 2″, bis zu 12″ und hat 12 Zwischenräume zwischen den Markierungen; Jeder der zwölf Zoll hat eine Markierung am Anfang und eine Markierung am Ende. Unter diesem Gesichtspunkt ist die 12″-Marke einfach eine Marke wie jede andere, auch wenn ihr nichts folgt.

    – Superkatze

    17. Oktober 2014 um 22:13 Uhr

Benutzer-Avatar
James Kanze

Eine Implementierung muss zulassen, dass ein Zeiger auf einen nach dem Ende existiert. Wie es das macht, ist seine Sache. Auf vielen Maschinen können Sie ohne Risiko jeden Wert in einen Zeiger setzen (es sei denn, Sie dereferenzieren ihn); Auf solchen Systemen kann dasjenige hinter dem Endzeiger auf nicht zugeordneten Speicher zeigen – ich bin tatsächlich auf einen Fall unter Windows gestoßen, in dem dies der Fall war.

Auf anderen Maschinen führt das bloße Laden eines Zeigers auf einen nicht zugeordneten Speicher in ein Register zu einem Trap, was zum Absturz des Programms führt. Auf solchen Maschinen muss die Implementierung sicherstellen, dass dies nicht passiert, indem entweder die Verwendung des letzten Bytes oder Wortes des zugewiesenen Speichers abgelehnt wird oder indem sichergestellt wird, dass jede Verwendung des Zeigers außer der Dereferenzierung alle Anweisungen vermeidet, die die Hardware verursachen könnten als ungültigen Zeiger zu behandeln. (Die meisten dieser Systeme haben separate Adress- und Datenregister und fangen nur ab, wenn der Zeiger in ein Adressregister geladen wird. Wenn die Datenregister groß genug sind, kann der Compiler den Zeiger sicher in ein Datenregister laden, z. B. zum Vergleich. Dies ist oft ohnehin notwendig, da die Adressregister nicht immer den Vergleich unterstützen.)

Zu Ihrer letzten Frage: C und C++ sind in dieser Hinsicht genau identisch; C++ hat einfach die Regeln von C übernommen.

  • Das ist sehr informativ. Und ziemlich verrückt/beängstigend. Ich würde niemals vernünftigerweise erwarten, dass es mir nicht erlaubt ist, Zeiger zu manipulieren, wo immer es mir gefällt. Dies macht tatsächlich Dinge ungültig, die ich in der Vergangenheit implementiert habe (End-Iterator zeigt auf nicht zugeordneten Speicher). Werden solche Systeme normalerweise ausgelagert (dadurch wird die Wahrscheinlichkeit des Einfangens verringert, da große Speicherblöcke zugeordnet werden)?

    – v.oddou

    17. Oktober 2014 um 8:50 Uhr

  • @v.oddou Es gibt viele Maschinen, bei denen dies der Fall ist. Beginnend mit der Intel-Architektur, wenn Sie die Segmentregister verwenden. (Das Laden eines ungültigen Segments in ein Segmentregister führt zu einem Trap. Die meisten heutigen Compiler unterstützen dies nicht, aber ich habe an Intel mit 48-Bit-Zeigern gearbeitet, und das Lesen eines zufälligen Zeigerwerts könnte einen Trap verursachen.)

    – James Kanze

    17. Oktober 2014 um 8:55 Uhr

  • @v.oddou Oder wie ich es einmal in den frühen Tagen von C geschrieben sah: “Ah, für die gute alte Zeit, als Männer Männer waren, Frauen Frauen waren und Zeiger Ints waren.” Es scheint, als hätte sich der Kreis geschlossen, und wieder einmal denken die meisten Leute heute, dass Zeiger auf einen ganzzahligen Typ abgebildet werden müssen. (Damals, als ich C lernte, waren die Maschinen, an denen ich gearbeitet habe, hauptsächlich 16-Bit-Maschinen, aber wir hatten weit mehr als 64-KB-Speicher. Und alle möglichen “strukturierten” Zeiger, um sie anzusprechen. Und dann waren da noch die 36-Bit-Maschinen wortadressierte Maschine, in der die Byte-Adresse und Größe von a char* waren in den höherwertigen Bits.)

    – James Kanze

    17. Oktober 2014 um 9:00 Uhr

  • @psyill Dann deckt dieses Zitat es ab. Es ist eine ziemlich standardmäßige Redewendung: in modernem C++ for ( p = std::begin(a); p != std::end(a); ++ p ) wird verlassen p zeigt auf eins nach dem Ende.

    – James Kanze

    17. Oktober 2014 um 16:48 Uhr

  • @James Kanze Stimme nicht zu “Heute, wenn 32-Bit-Systeme mehr oder weniger ein Minimum sind”. Bei eingebetteten Prozessoren verwenden im Jahr 2014 mindestens 100 Millionen pro Jahr 16-Bit int und C ist dort sehr beliebt. Ich habe keine Zahlen zu allen Prozessoren, die 2013 hergestellt wurden, aber ich wäre nicht überrascht, wenn die meisten Prozessoren heute Sub-32-Bit sind.

    – chux – Wiedereinsetzung von Monica

    17. Oktober 2014 um 18:52 Uhr

Benutzer-Avatar
psychisch

Es gibt eine interessante Passage bei §3.9.2/3 [Compound types]:

Der Typ eines Zeigers auf void oder eines Zeigers auf einen Objekttyp wird als Objektzeigertyp bezeichnet. […] Ein gültiger Wert eines Objektzeigertyps repräsentiert entweder die Adresse eines Bytes im Speicher (1.7) oder einen Nullzeiger (4.10).

Zusammen mit dem Text bei §5.7/5 [Additive operators]:

[…] Wenn außerdem der Ausdruck P auf das letzte Element eines Array-Objekts zeigt, zeigt der Ausdruck (P)+1 um eins nach dem letzten Element des Array-Objekts, und wenn der Ausdruck Q um eins nach dem letzten Element eines Array-Objekts zeigt, der Ausdruck (Q)-1 zeigt auf das letzte Element des Array-Objekts.

Es scheint, dass ein Array, das mit dem letzten Byte im Speicher endet, nicht zugewiesen werden kann, wenn die Anforderung besteht, dass der One-Past-the-End-Zeiger gültig sein muss. Wenn der One-Past-the-End-Zeiger ungültig sein darf, kenne ich die Antwort nicht.

Der Abschnitt §3.7.4.2/4 [Deallocation functions] besagt, dass:

Die Auswirkung der Verwendung eines ungültigen Zeigerwerts (einschließlich der Übergabe an eine Freigabefunktion) ist nicht definiert.

Wenn also der Vergleich eines Eins-nach-dem-Ende-Zeigers für ein zugewiesenes Array unterstützt werden muss, muss der Eins-nach-dem-Ende-Zeiger gültig sein.

Basierend auf den Kommentaren, die ich erhalten habe, gehe ich davon aus, dass eine Implementierung ein Array zuweisen kann, ohne sich darum kümmern zu müssen, ob der One-Past-the-End-Zeiger des Arrays verwendbar ist oder nicht. Allerdings würde ich gerne die entsprechenden Passagen in der Norm dazu herausfinden.

  • Ihr Zitat von 5.7/5 sagt nicht, dass der One-past-the-end-Zeiger gültig sein muss.

    – rubenvb

    17. Oktober 2014 um 8:50 Uhr

  • Nein, das tut es ausdrücklich nicht. Ich versuche herauszufinden, ob es einen anderen Teil des Standards gibt, der die Fähigkeit zur Verwendung eines One-Past-the-End-Zeigers vorschreibt.

    – psychisch

    17. Oktober 2014 um 8:55 Uhr

  • Ihre Schlussfolgerung ist falsch (und ich habe tatsächlich einmal einen Fall gesehen, in dem das Ende des Arrays am absoluten Ende des zugewiesenen Blocks war). Alles, was erforderlich ist, ist, dass die Operationen (außer der Dereferenzierung) auf einem solchen Zeiger funktionieren. Was sie in den meisten aktuellen Umgebungen tun werden.

    – James Kanze

    17. Oktober 2014 um 9:02 Uhr

  • Das bedeutet, dass es im Standard nichts gibt, was die Fähigkeit erfordert, jeden möglichen One-Past-the-End-Zeiger zu verwenden?

    – psychisch

    17. Oktober 2014 um 9:09 Uhr

  • Der Ausschnitt foo* end = myarray+ arraylen; for (foo* p = myarray; p != end; p++) funktioniert auch wenn myarray+arraylen wertet zu NULL (oder (foo*)0). Sie sollten nur nicht verwenden p<end stattdessen.

    – Hagen von Eitzen

    17. Oktober 2014 um 15:00 Uhr


Du hast halb recht. Angenommen, eine hypothetische Implementierung verwendet einen linear adressierten Speicher und Zeiger, die als vorzeichenlose 16-Bit-Ganzzahlen dargestellt werden. Nehmen Sie außerdem an, dass der Nullzeiger als Null dargestellt wird. Und schließlich, nehmen Sie an, Sie fragen nach 16 Byte Speicher, mit char *p = malloc(16);. Dann erhalten Sie garantiert einen Zeiger, dessen numerischer Wert kleiner als 65520 ist. Der Wert 65520 selbst wäre nicht gültig, denn wie Sie richtig anmerken, wenn die Zuweisung erfolgreich war, p + 16 ist ein gültiger Zeiger, der kein Nullzeiger sein darf.

Nehmen wir nun jedoch an, dass eine hypothetische Implementierung einen linear adressierten Speicher und Zeiger verwendet, die als vorzeichenlose 32-Bit-Ganzzahlen dargestellt werden, aber nur einen Adressraum von 16 Bit hat. Nehmen Sie auch wieder an, dass der Nullzeiger als Null dargestellt wird. Und schließlich nehmen Sie noch einmal an, Sie fragen nach 16 Byte Speicher, mit char *p = malloc(16);. Dann ist nur garantiert, dass Sie einen Zeiger erhalten, dessen numerischer Wert kleiner oder gleich 65520 ist. Der Wert 65520 selbst wäre gültig, solange die Implementierung dafür sorgt, dass die Addition von 16 dazu den Wert 65536 ergibt, und das Subtrahieren von 16 bringt Sie zurück zu 65520. Dies gilt auch dann, wenn an Adresse 65536 überhaupt kein Speicher (physisch oder virtuell) vorhanden ist.

  • Ich mag die hypothetische Überlegung, dass ein Speicherblock so hoch zurückgegeben werden könnte, dass die end Zeiger würde überlaufen und umbrechen.

    – v.oddou

    17. Oktober 2014 um 8:58 Uhr

Benutzer-Avatar
rubenvb

Der Standard gibt explizit an, was passiert, wenn Sie den Zeiger auf das letzte Element inkrementieren. Es gibt Ihnen einen Wert, der nur als Vergleich verwendet werden kann, um zu überprüfen, ob Sie am oder vor dem Ende des Arrays sind oder nicht. Der Zeiger kann durchaus auf gültig zugewiesenen Speicher für ein anderes Objekt zeigen, aber das ist ein vollständig undefiniertes (implementierungsdefiniertes?) Verhalten, und die Verwendung dieses Zeigers als solches ist definitiv ein undefiniertes Verhalten.

Worauf ich hinaus will, ist, dass der One-past-the-end-Zeiger genau das ist: Es ist der Zeiger, den Sie erhalten, wenn Sie den Zeiger auf das letzte Element erhöhen, um das Ende des Arrays auf sehr billige Weise zu markieren. Beachten Sie jedoch, dass das Vergleichen von Zeigern von nicht verwandten Objekten völlig unsinnig ist (und sogar undefiniertes Verhalten, wenn ich mich nicht irre). Die Tatsache, dass sich Zeiger-“Werte” über verschiedene Objekte hinweg überschneiden könnten, ist also kein Problem, denn wenn Sie dies ausnutzen, betreten Sie das Land des undefinierten Verhaltens.

Dies hängt von der Implementierung ab. Zumindest in Visual C++ könnten Sie ohne Verwendung einer Form- oder Array-Bound-Prüfung einen Zeiger auf eine beliebige Anzahl von Elementen nach dem Ende des Arrays erstellen. Wenn Sie es dereferenzieren, funktioniert es weiterhin, solange sich die Speicheradresse, auf die Sie zugreifen, innerhalb des zugewiesenen Heap / Stack Ihres Programms befindet. Sie werden jeden Wert in diesem Speicherplatz lesen/ändern. Wenn die Adresse außerhalb des zugewiesenen Speicherplatzes liegt, wird ein Fehler ausgegeben.

Debugger haben Überprüfungen, um diese zu erkennen, da diese Art der Codierung Fehler verursacht, die sehr schwer zu verfolgen sind.

  • Die Frage bezieht sich nicht auf VC++, sondern darauf, was der Standard vorschreibt.

    – Matteo Italien

    17. Oktober 2014 um 8:10 Uhr

  • you could create a pointer any number of elements past the end of the array VC++ kann UBs zulassen (und “arbeiten”), aber das beantwortet die Frage nicht.

    – PP

    17. Oktober 2014 um 8:10 Uhr

  • Die Frage bezieht sich nicht auf VC++, sondern darauf, was der Standard vorschreibt.

    – Matteo Italien

    17. Oktober 2014 um 8:10 Uhr

  • you could create a pointer any number of elements past the end of the array VC++ kann UBs zulassen (und “arbeiten”), aber das beantwortet die Frage nicht.

    – PP

    17. Oktober 2014 um 8:10 Uhr

1384790cookie-checkKann ein C++-Array an der Speichergrenze enden?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy