it-swarm.com.de

Überprüft C, ob ein Zeiger außerhalb des Bereichs liegt, ohne dass der Zeiger dereferenziert wird?

Ich hatte dieses Argument mit einigen Leuten, die sagten, dass C-Out-of-Bound-Zeiger undefiniertes Verhalten verursachen, auch wenn sie nicht dereferenziert werden. Beispiel:

int a;
int *p = &a;
p = p - 1;

die dritte Zeile hier führt zu undefiniertem Verhalten, auch wenn p niemals dereferenziert wird (*p wird nie verwendet).

Meiner Meinung nach klingt es unlogisch, dass C prüfen würde, ob ein Zeiger außerhalb des Bereichs liegt, ohne dass der Zeiger verwendet wird (es ist so, als würde jemand auf der Straße Leute inspizieren, um zu sehen, ob sie Waffen tragen, falls sie sein Haus betreten. Ideal ist es, Menschen zu inspizieren, wenn sie das Haus betreten. Ich denke, wenn C das prüft, wird viel Laufzeitaufwand anfallen.

Und wenn C wirklich nach OOB-Zeigern sucht, wird dies nicht zu einer UB führen:

int *p; // uninitialized thus pointing to a random adress

warum passiert also nichts, wenn p auf eine OOB-Adresse verweist.

HINZUFÜGEN:

int a;
int *p = &a;
p = p - 1;

sagen, &a ist 1000. Wird der Wert von p nach Auswertung der dritten Zeile sein:

  • 996 aber immer noch undefiniertes Verhalten, da p an anderer Stelle dereferenziert werden könnte und das eigentliche Problem verursachen könnte.
  • undefined value und das ist das undefinierte Verhalten.

ich denke, "die dritte Zeile wurde als unbestimmtes Verhalten bezeichnet", lag in erster Linie an der potenziellen zukünftigen Verwendung dieses OOB-Zeigers (Dereferenzierung), und die Menschen nahmen ihn im Laufe der Zeit als undefiniertes Verhalten an. Ist nun der Wert von p 100% 996 und dass noch undefiniertes Verhalten oder sein Wert undefiniert ist?

40
ibrahim mahrir

C tut nicht Prüfen Sie, ob ein Zeiger außerhalb der Grenzen liegt. Die zugrunde liegende Hardware verhält sich jedoch möglicherweise auf seltsame Weise, wenn eine Adresse berechnet wird, die außerhalb der Objektgrenzen liegt und direkt auf das Ende eines Objekts zeigt, das eine Ausnahme darstellt. Der C-Standard beschreibt dies explizit als unbestimmtes Verhalten.

In den meisten aktuellen Umgebungen stellt der obige Code kein Problem dar, aber ähnliche Situationen können vor etwa 25 Jahren im x86-geschützten 16-Bit-Modus zu Segmentierungsfehlern führen.

In der Sprache des Standards könnte ein solcher Wert ein Trap-Wert sein, der nicht manipuliert werden kann, ohne undefiniertes Verhalten aufzurufen.

Der relevante Abschnitt des C11-Standards ist:

6.5.6 Additive Operatoren

  1. Wenn ein Ausdruck, der den Typ "Integer" hat, zu einem Zeiger hinzugefügt oder von ihm abgezogen wird, hat das Ergebnis den Typ des Zeigeroperanden. Wenn der Zeigeroperand auf ein Element eines Array-Objekts zeigt und das Array groß genug ist, zeigt das Ergebnis auf ein Element, das vom ursprünglichen Element versetzt ist, sodass die Differenz der Indizes der resultierenden und der ursprünglichen Array-Elemente dem Ganzzahlausdruck entspricht. [...] Wenn sowohl der Zeigeroperand als auch das Ergebnis auf Elemente desselben Arrayobjekts oder auf ein Element hinter dem letzten Element des Arrayobjekts zeigen, darf die Auswertung keinen Überlauf erzeugen. Andernfalls ist das Verhalten undefiniert. Wenn das Ergebnis einen Punkt hinter das letzte Element des Array-Objekts zeigt, darf es nicht als Operand eines unären *-Operators verwendet werden, der ausgewertet wird.

Ein ähnliches Beispiel für undefiniertes Verhalten ist folgendes:

char *p;
char *q = p;

Wenn Sie lediglich den Wert des nicht initialisierten Zeigers p laden, wird undefiniertes Verhalten aufgerufen, auch wenn er niemals dereferenziert wird.

EDIT: Es ist ein Streitpunkt, der versucht, darüber zu streiten. Der Standard besagt, dass das Berechnen einer solchen Adresse ein undefiniertes Verhalten hervorruft. Die Tatsache, dass einige Implementierungen nur einen bestimmten Wert berechnen und speichern oder nicht speichern, ist unerheblich. Verlassen Sie sich nicht auf Annahmen in Bezug auf undefiniertes Verhalten: Der Compiler nutzt möglicherweise die von Natur aus unvorhersehbare Natur, um Optimierungen durchzuführen, die Sie sich nicht vorstellen können.

Zum Beispiel diese Schleife:

for (int i = 1; i != 0; i++) {
    ...
}

kann zu einer unendlichen Schleife ohne jeglichen Test kompiliert werden: i++ ruft undefiniertes Verhalten auf, wenn iINT_MAX ist. Die folgende Analyse des Compilers lautet:

  • anfangswert von i ist > 0.
  • für jeden positiven Wert von i < INT_MAX ist i++ noch > 0
  • für i = INT_MAX ruft i++ ein undefiniertes Verhalten auf, sodass wir i > 0 annehmen können, da wir alles annehmen können, was uns gefällt.

Daher ist i immer > 0 und der Testcode kann entfernt werden.

67
chqrlie

In der Tat ist das Verhalten eines C-Programms undefiniert, wenn versucht wird, einen Wert durch Zeigerarithmetik zu berechnen, der nicht zu einem Zeiger auf ein Element oder einen über das Ende desselben Arrayelements hinausgeht. Ab C11 6.5.6/8:

Wenn sowohl der Zeiger Operand und das Ergebnis zeigen auf Elemente desselben Arrayobjekts oder auf eines der letzten Element des Array-Objekts darf die Auswertung keinen Überlauf erzeugen; ansonsten der Verhalten ist undefiniert.

(Für die Zwecke dieser Beschreibung kann die Adresse eines Objekts vom Typ T als Adresse des ersten Elements eines Arrays T[1] behandelt werden.)

21
Kerrek SB

Zur Verdeutlichung bedeutet "undefiniertes Verhalten", dass das Ergebnis des fraglichen Codes nicht in den Standards definiert ist, die die Sprache regeln. Das tatsächliche Ergebnis hängt von der Implementierung des Compilers ab und kann von nichts bis zu einem vollständigen Absturz und allem dazwischen reichen.

In den Standards ist nicht festgelegt, dass eine Bereichsüberprüfung der Zeiger erfolgen soll. Aber in Bezug auf Ihr spezifisches Beispiel sagen sie Folgendes:

Wenn ein Ausdruck mit ganzzahligem Typ hinzugefügt oder subtrahiert wird von einem Zeiger ... Wenn sowohl der Zeigeroperand als auch das Ergebnis auf .__ zeigen. Elemente desselben Arrayobjekts oder eines hinter dem letzten Element von Array-Objekt darf die Auswertung keinen Überlauf erzeugen; Andernfalls, das Verhalten ist undefiniert. Wenn das Ergebnis einen Punkt nach dem letzten Punkt zeigt Element des Array-Objekts darf es nicht als Operand einer .__ verwendet werden. unärer * Operator, der ausgewertet wird.

Das obige Zitat stammt aus C99 §6.5.6 Abs. 8 (der neuesten Version, die ich zur Verfügung habe).

Beachten Sie, dass das Vorstehende auch für Nicht-Array-Zeiger gilt, da es in der vorherigen Klausel heißt:

Für diese Operatoren ein Zeiger auf ein Objekt, das .__ ist. kein Element eines Arrays verhält sich wie ein Zeiger auf die erste Element eines Arrays der Länge Eins mit dem Typ des Objekts als Elementtyp.

Wenn Sie also eine Zeigerarithmetik durchführen und das Ergebnis entweder innerhalb der Grenzen liegt oder auf eine über das Ende des Objekts hinaus zeigt, erhalten Sie ein gültiges Ergebnis. Dieses Verhalten könnte sein, dass Sie mit einem verirrten Zeiger enden, aber es könnte etwas anderes sein.

15
harmic

Ja, es ist undefiniertes Verhalten, auch wenn der Zeiger nicht dereferenziert ist.

C erlaubt nur, dass Zeiger auf nur ein Element hinter den Arraygrenzen zeigen.

7
Kornel

Einige Plattformen behandeln Zeiger als Ganzzahlen und verarbeiten Zeigerarithmetik auf dieselbe Weise wie Ganzzahlarithmetik, wobei jedoch bestimmte Werte entsprechend der Objektgröße nach oben oder unten skaliert werden. Auf solchen Plattformen definiert dies effektiv ein "natürliches" Ergebnis aller Zeigerarithmetikoperationen mit Ausnahme der Subtraktion von Zeigern, deren Differenz kein Vielfaches der Größe des Zieltyps des Zeigers ist.

Andere Plattformen können Zeiger auf andere Weise darstellen, und die Addition oder Subtraktion bestimmter Kombinationen von Zeigern kann zu unvorhersehbaren Ergebnissen führen.

Die Autoren des C-Standards wollten keine Bevorzugung einer der beiden Plattformen zeigen, so dass keine Vorbedingungen dafür bestehen, was passieren kann, wenn Zeiger so manipuliert werden, dass sie auf einigen Plattformen Probleme verursachen. Vor dem C-Standard und noch einige Jahre später konnten Programmierer vernünftigerweise erwarten, dass allgemeine Implementierungen für Plattformen, die Zeigerarithmetik wie skalierte Ganzzahlarithmetik behandeln, selbst Zeigerarithmetik ebenfalls behandeln, aber Implementierungen für Plattformen, die Zeigerarithmetik unterschiedlich behandeln würde es wahrscheinlich selbst anders behandeln.

In den letzten zehn Jahren haben sich Compiler-Autoren jedoch entschlossen, das Prinzip des Least Astonishment aus dem Fenster zu werfen. Selbst wenn ein Programmierer wissen würde, welche Auswirkungen bestimmte Zeigeroperationen auf die natürlichen Zeigerrepräsentationen einer Plattform haben, gibt es keine Garantie, dass Compiler Code generieren, der sich so verhält, wie sich die natürlichen Zeigerrepräsentationen verhalten. Die Tatsache, dass der Standard das Verhalten als undefiniert bezeichnet, wird als eine Aufforderung für Compiler interpretiert, "Optimierungen" zu erzwingen, die Programmierer zwingen, Code zu schreiben, der langsamer und unübersichtlicher ist, als dies bei Implementierungen erforderlich wäre, die sich einfach in einer konsistenten Weise mit dem Dokument verhalten Verhalten der zugrunde liegenden Umgebung (eine der drei Behandlungen, die die Autoren des C89 ausdrücklich als alltäglich bezeichnet haben).

Wenn man also nicht weiß, dass man einen Compiler verwendet, für den keine verrückten "Optimierungen" aktiviert sind, kann die Tatsache, dass ein Zwischenschritt in einer Folge von Zeigerberechnungen Undefined Behavior aufruft, es unmöglich machen, überhaupt darüber zu argumentieren, egal Wie stark würde der gesunde Menschenverstand bedeuten, dass sich Qualitätsimplementierungen für eine bestimmte Plattform auf eine bestimmte Weise verhalten.

4
supercat

Wenn Spezifikationen besagen, dass etwas undefiniert, ist, kann das ziemlich verwirrend sein.

Unter diesen Umständen kann die Implementierung der Spezifikation nach Belieben erfolgen. In einigen Fällen wird etwas getan, das intuitiv korrekt erscheint. In anderen Fällen wird es nicht.

Bei Adressgrenzenspezifikationen weiß ich, dass meine Intuition von meinen Annahmen über ein flaches einheitliches Speichermodell herrührt. Es gibt aber auch andere Speichermodelle.

Das Wort "undefiniert" wird in einer abgeschlossenen Spezifikation niemals ungewollt angezeigt. Normungsgremien entscheiden sich normalerweise für die Verwendung des Wortes, wenn know verschiedene Implementierungen der Norm unterschiedliche Aufgaben erfüllen müssen. In vielen Fällen liegt der Grund für die unterschiedlichen Dinge in der Leistung. Also: Das Erscheinen des Wortes in der Spezifikation ist eine Warnung der roten Fahne darüber, dass für uns bloße Sterbliche, Benutzer der Spezifikation, unsere Intuition sein kann falsch.

Diese Art von "was auch immer es will" -Spezifikation ärgerte sich bekanntermaßen vor einigen Jahren rms . Also ließ er einige Versionen seiner Gnu Compiler Collection (gcc) versuchen, ein Computerspiel zu spielen, wenn es auf etwas Unbestimmtes stieß.

IBM verwendete das Wort unvorhersehbar in ihren Spezifikationen in den 360/370-Tagen. Das ist ein besseres Wort. Das Ergebnis klingt zufälliger und gefährlicher. Im Rahmen von "unvorhersehbarem" Verhalten liegen problematische Ergebnisse wie "Anhalten und Feuer fangen."

Hier ist das Ding. "Zufällig" ist eine schlechte Methode, um diese Art von unvorhersehbarem Verhalten zu beschreiben, da "zufällig" impliziert, dass das System bei jedem Auftreten des Problems etwas anderes tun kann. Wenn es jedes Mal etwas anderes macht, haben Sie die Möglichkeit, das Problem im Test zu erkennen. In der Welt des "undefinierten"/"unvorhersehbaren" Verhaltens macht das System jedes Mal dasselbe , bis es dies nicht tut Mal, wenn es nicht ist , wird es Jahre dauern, bis Sie glauben, dass Sie mit dem Testen fertig sind.

Also, wenn die Spezifikation sagt, dass etwas undefiniert ist, tun Sie das nicht. Es sei denn, Sie sind ein Freund von Murphy . OK?

4
O. Jones

"Undefiniertes Verhalten" bedeutet "alles kann passieren". Übliche Werte für "irgendetwas" sind "nichts passiert überhaupt nicht" und "Ihr Code stürzt ab". Andere gebräuchliche Werte für "irgendetwas" sind "schlechte Dinge passieren, wenn Sie die Optimierung aktivieren", oder "schlechte Dinge passieren, wenn Sie den Code nicht in der Entwicklung ausführen, aber ein Kunde ihn ausführt" und noch andere Werte sind "Ihr Code" tut etwas Unerwartetes "und" Ihr Code tut etwas, was er nicht tun sollte ". 

Wenn Sie also sagen "es klingt unlogisch, dass C prüfen würde, ob ein Zeiger außerhalb des Bereichs liegt, ohne dass der Zeiger verwendet wird", befinden Sie sich in einem sehr, sehr gefährlichen Bereich. Nimm diesen Code:

int a = 0;
int b [2] = { 1, 2 };
int* p = &a; p - 1;
printf ("%d\n", *p);

Der Compiler kann davon ausgehen, dass kein undefiniertes Verhalten vorliegt. p - 1 wurde bewertet. Der Compiler schließt (legal), dass entweder p = & a [1], p = & b [1] oder p = & b [2] ist, da in allen anderen Fällen undefiniertes Verhalten entweder bei der Auswertung von p oder bei der Auswertung von p-1 auftritt. Der Compiler geht dann davon aus, dass * p kein undefiniertes Verhalten ist, und schließt (legal), dass p = & b [1] ist, und gibt den Wert 2 aus. 

Das ist legal, und es passiert . Die Lektion lautet also: Rufen Sie kein undefiniertes Verhalten auf. 

4
gnasher729

Der Teil der Frage, der sich auf undefiniertes Verhalten bezieht, ist sehr klar, die Antwort lautet: "Ja, sicherlich ist es undefiniertes Verhalten".

Ich werde die Formulierung "Does C check ..." wie folgt interpretieren:

  1. Überprüft der C-Compiler ...?
  2. Überprüft mein kompiliertes Programm ...?

(C selbst ist eine Sprachspezifikation, prüft oder erledigt nichts)

Die Antwort auf die erste Frage lautet: Ja, aber nicht zuverlässig und nicht so, wie Sie es wünschen. Moderne Compiler sind ziemlich intelligent, manchmal intelligenter als Sie möchten. In einigen Fällen kann der Compiler die unzulässige Verwendung von Zeigern diagnostizieren. Da diese per-Definition ein undefiniertes Verhalten auslöst und der Compiler daher keine besonderen Aktionen mehr erfordert, wird der Compiler häufig auf unvorhersehbare Weise optimieren. Dies kann zu Code führen, der sich stark von dem von Ihnen beabsichtigten unterscheidet. Seien Sie nicht überrascht, wenn ein ganzer Umfang oder sogar die gesamte Funktion völlig ausgefallen ist. Dies gilt für viele unerwünschte "Überraschungsoptimierungen" in Bezug auf undefiniertes Verhalten.
Obligatorisch zu lesen: Was jeder C-Programmierer über undefiniertes Verhalten wissen sollte .

Die Antwort auf die zweite Frage lautet: Nein, es sei denn, Sie verwenden einen Compiler, der Begrenzungsprüfungen unterstützt, und wenn Sie mit aktivierten Laufzeitprüfungen kompilieren, was einen nicht unerheblichen Laufzeit-Overhead impliziert.
In der Praxis bedeutet dies, dass, wenn Ihr Programm den Compiler "überlebt" hat, das undefinierte Verhalten optimiert, das Programm nur hartnäckig das tut, was Sie ihm gesagt haben, mit unvorhersehbaren Ergebnissen - normalerweise entweder gelesene Speicherwerte oder Ihr Programm einen Segmentierungsfehler verursachen.

3
Damon

Aber was ist undefiniertes Verhalten? Das bedeutet einfach, dass niemand bereit ist zu sagen, was passieren wird.

Ich bin ein alter Mainframe-Hund von vor Jahren, und ich mag IBMs Satz für dasselbe: Ergebnisse sind unvorhersehbar.

BTW: Ich mag die Idee, dass NICHT Array-Grenzen überprüft. Wenn ich beispielsweise einen Zeiger in eine Zeichenfolge habe und sehen möchte, was kurz vor dem Byte ist, auf das gezeigt wird, kann ich Folgendes verwenden:

pointer[-1]

um es anzusehen.

1
Jennifer