Warum ist Out-of-Bounds-Zeiger arithmetisch undefiniertes Verhalten?

Lesezeit: 4 Minuten

Warum ist Out of Bounds Zeiger arithmetisch undefiniertes Verhalten
NFRCR

Das folgende Beispiel ist aus Wikipedia.

int arr[4] = {0, 1, 2, 3};
int* p = arr + 5;  // undefined behavior

Wenn ich p nie dereferenziere, warum ist dann arr + 5 allein undefiniertes Verhalten? Ich erwarte, dass sich Zeiger als Ganzzahlen verhalten – mit der Ausnahme, dass der Wert eines Zeigers beim Dereferenzieren als Speicheradresse betrachtet wird.

  • Ich bin mir ziemlich sicher, dass der “undefinierte” Teil nur der Standard ist, der besagt, dass er Ihnen nicht sagen kann, wohin dieser Zeiger jetzt zeigt. Wie bei den meisten “undefinierten” Zeigerdingen bin ich sicher, dass es in Ordnung ist, es zu machen, aber illegal, es zu respektieren.

    Benutzer406009

    6. Mai 2012 um 19:35 Uhr

  • @EthanSteinberg: Das wäre nur wahr, wenn sie das Ergebnis sagen würden Wert war undefiniert. Wenn die Verhalten undefiniert ist, ist es nicht sicher, es auszuführen, selbst wenn Sie es nie dereferenzieren.

    – Benutzer541686

    6. Mai 2012 um 19:36 Uhr


  • Zeiger sind nicht intergers. Unter der Haube mag die Darstellung zufällig sein, aber soweit es die “abstrakte Maschine von C++” betrifft, sind das völlig unterschiedliche Dinge, die zufällig eine Syntax gemeinsam haben, wie z struct { int a; int x; } und struct { char x; }.

    Benutzer395760

    6. Mai 2012 um 19:37 Uhr


  • Denn nicht alle Maschinen verhalten sich so wie Ihr PC. Sie erwarten ein bestimmtes Verhalten, je nachdem, wie es auf Ihrem Computer funktioniert. Das Standardkomitee hat mehr Erfahrung und versteht, dass andere Architekturen Zeiger anders implementieren und daher das obige Verhalten nicht für alle Plattformen garantieren können (daher ist es undefiniert).

    – Martin York

    6. Mai 2012 um 19:48 Uhr

  • Ich habe eine Situation gefunden, in der dieses undefinierte Verhalten die Berechnung tatsächlich falsch macht (auf einem normalen x86): stackoverflow.com/questions/23683029/…

    – Bernd Elkemann

    28. Januar 2015 um 20:14 Uhr

Warum ist Out of Bounds Zeiger arithmetisch undefiniertes Verhalten
Luchian Grigore

Das liegt daran, dass sich Zeiger nicht wie ganze Zahlen verhalten. Es ist ein undefiniertes Verhalten, weil der Standard es so vorschreibt.

Auf den meisten Plattformen (wenn nicht allen) kommt es jedoch nicht zu einem Absturz oder zweifelhaftem Verhalten, wenn Sie das Array nicht dereferenzieren. Aber wenn Sie es nicht dereferenzieren, wozu dann die Addition?

Beachten Sie jedoch, dass ein Ausdruck, der eins über das Ende eines Arrays geht, technisch zu 100 % „korrekt“ ist und gemäß §5.7 Abs. 5 der C++11-Spezifikation garantiert nicht abstürzt. Das Ergebnis dieses Ausdrucks ist jedoch nicht spezifiziert (nur garantiert kein Überlauf); während jeder andere Ausdruck, der mehr als eins über die Array-Grenzen hinausgeht, explizit ist nicht definiert Verhalten.

Hinweis: Das bedeutet nicht, dass es sicher ist, von einem Over-by-One-Offset zu lesen und zu schreiben. Sie wahrscheinlich Wille Daten bearbeiten, die nicht zu diesem Array gehören, und Wille Zustands-/Speicherbeschädigung verursachen. Sie werden einfach keine Überlaufausnahme verursachen.

Ich vermute, dass es so ist, weil nicht nur die Dereferenzierung falsch ist. Auch Zeigerarithmetik, Vergleichen von Zeigern usw. So ist es einfach einfacher zu sagen mach das nicht anstatt die Situationen aufzuzählen, in denen es kann sei gefährlich.

  • Wie geht man über die Grenzen?

    – Mahmoud Al-Qudsi

    6. Mai 2012 um 19:39 Uhr

  • @MahmoudAl-Qudsi Der Standard sagt, es ist in Ordnung, so ist es.

    Benutzer395760

    6. Mai 2012 um 19:40 Uhr

  • @MahmoudAl-Qudsi es ist einfach 🙂 Es gibt eine Menge Dinge, die davon abhängen. Wie Standard-Iteratoren.

    – Luchian Grigore

    6. Mai 2012 um 19:40 Uhr

  • Kannst du deinen Beitrag präzisieren? Gemäß §5.7 Abs. 5 der C++11-Spezifikation ist es nur die Ausdruck das ist gültig (dh garantiert kein Überlauf/keine Ausnahmen), aber nicht das Ergebnis. Ich weiß, dass du das nicht gesagt hast Ergebnis definiert, könnte aber leicht als solche missverstanden werden. “Wenn sowohl der Zeigeroperand als auch das Ergebnis auf Elemente desselben Array-Objekts zeigen oder eins nach dem letzten Element des Array-Objekts, darf die Auswertung keinen Überlauf erzeugen; andernfalls ist das Verhalten undefiniert.”

    – Mahmoud Al-Qudsi

    6. Mai 2012 um 19:49 Uhr


  • Das Ergebnis der Erhöhung eines Zeigers auf das letzte Element eines Arrays ist nicht unspezifiziert; es wird auf einen Zeiger direkt hinter dem letzten Element des Arrays angegeben; Subtrahieren von einem solchen Zeiger einen Wert von 1 bis zur Größe des Arrays ergibt einen gültigen Zeiger auf ein Array-Element.

    – Superkatze

    9. Juli 2014 um 17:44 Uhr

Das ursprüngliche x86 kann Probleme mit solchen Aussagen haben. Bei 16-Bit-Code sind Zeiger 16 + 16 Bit. Wenn Sie den unteren 16 Bits einen Offset hinzufügen, müssen Sie möglicherweise mit dem Überlauf umgehen und die oberen 16 Bits ändern. Das war eine langsame Operation und sollte am besten vermieden werden.

Auf diesen Systemen array_base+offset wurde garantiert nicht überlaufen, wenn Offset im Bereich war (<=Array-Größe). Aber array+5 würde überlaufen, wenn das Array nur 3 Elemente enthält.

Die Folge dieses Überlaufs ist, dass Sie einen Zeiger erhalten, der nicht zeigt hinter das Array, aber vor. Und das ist möglicherweise nicht einmal RAM, sondern speicherabgebildete Hardware. Der C++-Standard versucht nicht einzuschränken, was passiert, wenn Sie Zeiger auf zufällige Hardwarekomponenten konstruieren, dh es ist undefiniertes Verhalten auf realen Systemen.

Wenn arr befindet sich dann zufällig ganz am Ende des Speicherplatzes der Maschine arr+5 möglicherweise außerhalb dieses Speicherplatzes, sodass der Zeigertyp möglicherweise nicht in der Lage ist, den Wert darzustellen, dh er könnte überlaufen, und der Überlauf ist undefiniert.

“Undefiniertes Verhalten” bedeutet nicht, dass es in dieser Codezeile abstürzen muss, aber es bedeutet, dass Sie das Ergebnis nicht garantieren können. Zum Beispiel:

int arr[4] = {0, 1, 2, 3};
int* p = arr + 5; // I guess this is allowed to crash, but that would be a rather 
                  // unusual implementation choice on most machines.

*p; //may cause a crash, or it may read data out of some other data structure
assert(arr < p); // this statement may not be true
                 // (arr may be so close to the end of the address space that 
                 //  adding 5 overflowed the address space and wrapped around)
assert(p - arr == 5); //this statement may not be true
                      //the compiler may have assigned p some other value

Ich bin sicher, es gibt viele andere Beispiele, die Sie hier einwerfen können.

Einige Systeme, sehr seltene Systeme und ich kann keins nennen, werden Fallen verursachen, wenn Sie solche Grenzen überschreiten. Darüber hinaus ermöglicht es eine Implementierung, die den Grenzschutz bietet … wieder, obwohl mir keine einfällt.

Im Wesentlichen sollten Sie es nicht tun, und daher gibt es keinen Grund, anzugeben, was passiert, wenn Sie es tun. Zu spezifizieren, was passiert, bedeutet eine ungerechtfertigte Belastung für den Implementierungsanbieter.

  • Systeme, die kann Das ist eigentlich ziemlich üblich, mit Intel x86 (und kompatiblen) als Paradebeispiel. Es ist normalerweise nicht benutzt Auf diese Weise funktioniert der segmentbasierte Speicherschutz von x86 jedoch wie beschrieben – er kann eine Ausnahme auslösen, selbst wenn versucht wird, eine ungültige Adresse zu bilden. Die meisten typischen Betriebssysteme richten jedoch alle Segmente mit einer Basis von 0 und einem Limit von 4Gig ein, wodurch alle möglichen Offsets gültig werden. Für das, was es wert ist, wurde diese Fähigkeit tatsächlich in OS/2 1.x verwendet.

    – Jerry Sarg

    6. Mai 2012 um 23:07 Uhr

  • @JerryCoffin: Ich wünschte, Intel hätte 32-Bit-Segmentregister auf dem 80386 verwendet, wobei der obere Teil einen Segmentdeskriptor auswählt und der untere Teil als skalierter Multiplikator fungiert, dessen Verhalten von diesem Segmentdeskriptor gesteuert wird. Eine solche Architektur hätte es ermöglicht, 32-Bit-Objektreferenzen ohne eine Adressierungsbeschränkung von vier Gigabyte zu verwenden (die Anzahl unterschiedlicher Objekte wäre auf weit unter vier Milliarden begrenzt, aber ihre Gesamtgröße könnte viel größer sein).

    – Superkatze

    9. Juli 2014 um 17:47 Uhr

Wie aktualisiere ich WordPress wp usermeta
Bohne helfen

Dieses Ergebnis, das Sie sehen, ist auf den segmentbasierten Speicherschutz von x86 zurückzuführen. Ich finde diesen Schutz gerechtfertigt, da Sie beim Erhöhen der Zeigeradresse und Speichern zu einem späteren Zeitpunkt in Ihrem Code den Zeiger dereferenzieren und den Wert verwenden. Der Compiler möchte also solche Situationen vermeiden, in denen Sie am Ende den Speicherort eines anderen ändern oder den Speicher löschen, der einem anderen in Ihrem Code gehört. Um ein solches Szenario zu vermeiden, hat der Compiler die Einschränkung vorgenommen.

  • Systeme, die kann Das ist eigentlich ziemlich üblich, mit Intel x86 (und kompatiblen) als Paradebeispiel. Es ist normalerweise nicht benutzt Auf diese Weise funktioniert der segmentbasierte Speicherschutz von x86 jedoch wie beschrieben – er kann eine Ausnahme auslösen, selbst wenn versucht wird, eine ungültige Adresse zu bilden. Die meisten typischen Betriebssysteme richten jedoch alle Segmente mit einer Basis von 0 und einem Limit von 4Gig ein, wodurch alle möglichen Offsets gültig werden. Für das, was es wert ist, wurde diese Fähigkeit tatsächlich in OS/2 1.x verwendet.

    – Jerry Sarg

    6. Mai 2012 um 23:07 Uhr

  • @JerryCoffin: Ich wünschte, Intel hätte 32-Bit-Segmentregister auf dem 80386 verwendet, wobei der obere Teil einen Segmentdeskriptor auswählt und der untere Teil als skalierter Multiplikator fungiert, dessen Verhalten von diesem Segmentdeskriptor gesteuert wird. Eine solche Architektur hätte es ermöglicht, 32-Bit-Objektreferenzen ohne eine Adressierungsbeschränkung von vier Gigabyte zu verwenden (die Anzahl unterschiedlicher Objekte wäre auf weit unter vier Milliarden begrenzt, aber ihre Gesamtgröße könnte viel größer sein).

    – Superkatze

    9. Juli 2014 um 17:47 Uhr

1646314216 158 Warum ist Out of Bounds Zeiger arithmetisch undefiniertes Verhalten
Superkatze

Neben Hardwareproblemen war ein weiterer Faktor das Aufkommen von Implementierungen, die versuchten, verschiedene Arten von Programmierfehlern abzufangen. Obwohl viele solcher Implementierungen am nützlichsten sein könnten, wenn sie so konfiguriert würden, dass sie Konstrukte abfangen, die ein Programm bekanntermaßen nicht verwendet, obwohl sie durch den C-Standard definiert sind, wollten die Autoren des Standards das Verhalten von Konstrukten nicht definieren, die –in vielen Programmierbereichen– symptomatisch für Fehler sein.

In vielen Fällen ist es viel einfacher, Aktionen abzufangen, die Zeigerarithmetik verwenden, um die Adresse unbeabsichtigter Objekte zu berechnen, als irgendwie die Tatsache aufzuzeichnen, dass die Zeiger nicht für den Zugriff auf den von ihnen identifizierten Speicher verwendet werden können, aber so modifiziert werden könnten, dass sie dies könnten Zugriff auf andere Speicher. Außer im Fall von Arrays innerhalb größerer (zweidimensionaler) Arrays wäre es einer Implementierung erlaubt, Platz zu reservieren, der “kurz hinter” dem Ende jedes Objekts liegt. Angesichts so etwas wie doSomethingWithItem(someArray+i);, könnte eine Implementierung jeden Versuch abfangen, eine Adresse zu übergeben, die weder auf ein Element des Arrays noch auf das Leerzeichen direkt hinter dem letzten Element zeigt. Wenn die Zuteilung von someArray reservierter Platz für ein zusätzliches ungenutztes Element, und doSomethingWithItem() nur auf das Element zugreift, auf das es einen Zeiger erhält, könnte die Implementierung relativ kostengünstig sicherstellen, dass jede nicht abgefangene Ausführung des obigen Codes – im schlimmsten Fall – auf ansonsten ungenutzten Speicher zugreifen könnte.

Die Fähigkeit, “gerade vorbei”-Adressen zu berechnen, macht die Überprüfung der Grenzen schwieriger, als es sonst der Fall wäre (die häufigste fehlerhafte Situation wäre Passing doSomethingWithItem() ein Zeiger direkt hinter dem Ende des Arrays, aber das Verhalten wäre definiert, es sei denn doSomethingWithItem würde versuchen, diesen Zeiger zu dereferenzieren – etwas, das der Aufrufer möglicherweise nicht beweisen kann). Da der Standard es Compilern jedoch in den meisten Fällen erlauben würde, Platz direkt hinter dem Array zu reservieren, würde eine solche Erlaubnis es Implementierungen ermöglichen, den durch nicht abgefangene Fehler verursachten Schaden zu begrenzen – etwas, das wahrscheinlich nicht praktikabel wäre, wenn eine allgemeinere Zeigerarithmetik erlaubt wäre.

923870cookie-checkWarum ist Out-of-Bounds-Zeiger arithmetisch undefiniertes Verhalten?

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

Privacy policy