2013-03-25 13 views
15

Es sieht für mich wie das folgende Programm einen ungültigen Zeiger berechnet, da NULL für alles andere als Zuordnung und Vergleich für Gleichheit ist nicht gut:Führt die Arithmetik einen Nullzeiger undefiniertes Verhalten aus?

#include <stdlib.h> 
#include <stdio.h> 

int main() { 

    char *c = NULL; 
    c--; 

    printf("c: %p\n", c); 

    return 0; 
} 

Jedoch scheint es, wie keine der Warnungen oder Instrumentierungen in GCC oder Clang, das auf undefiniertes Verhalten abzielt, sagt, dass dies tatsächlich UB ist. Ist diese Arithmetik tatsächlich gültig und ich bin zu pedantisch, oder ist das ein Mangel an Kontrollmechanismen, die ich melden sollte?

Geprüft:

$ clang-3.3 -Weverything -g -O0 -fsanitize=undefined -fsanitize=null -fsanitize=address offsetnull.c -o offsetnull 
$ ./offsetnull 
c: 0xffffffffffffffff 

$ gcc-4.8 -g -O0 -fsanitize=address offsetnull.c -o offsetnull 
$ ./offsetnull 
c: 0xffffffffffffffff 

Es scheint ziemlich gut, dass AddressSanitizer dokumentiert zu werden, wie durch Clang verwendet und GCC konzentriert sich mehr auf dereferenzieren schlechter Zeiger, so dass fair genug ist. Aber auch die anderen Kontrollen nicht fangen es entweder: -/

bearbeiten: Ein Teil des Grundes, dass ich diese Frage gestellt ist, dass die -fsanitize Fahnen dynamische Kontrollen von gut Definiert in dem generierten Code ermöglichen. Ist das etwas, was sie hätten fangen sollen?

+5

Die Ausführung von Arithmetik für jeden Zeiger, der nicht Teil eines Arrays ist, ist UB, mit Ausnahme von +1 für Zeiger, die nicht über den Array hinausgehen. – chris

+0

Der Compiler betrachtet nur eine Zeile nach der anderen, also hat er keine Ahnung, dass c NULL ist. Etwas wie LINT würde dies jedoch erfassen. Bei diesem Programm wird die c-Variable niemals dereferenziert, so dass nichts Ungültiges passiert. Es ist völlig in Ordnung, dies zu tun, und der Vorteil ist, dass Sie jetzt sehen können, dass Sie auf einem 64-Bit-System aufgrund aller f laufen! (vielleicht der Punkt des Programms?) –

+4

@ c.fogelklou: Sie haben den Punkt völlig verpasst, und sollten lesen, was von anderen ziemlich sorgfältig gepostet wird - sie bestätigen, dass das Bilden dieses Zeigers ein undefiniertes Verhalten ist, egal was irgendjemandes Compiler tut es tatsächlich. – Novelocrat

Antwort

19

Zeigerarithmetik auf einen Zeiger, der nicht auf ein Array zeigt, ist nicht definiert.
Auch Dereferenzierung eines Null-Zeigers ist undefiniertes Verhalten.

char *c = NULL; 
c--; 

definiert Verhalten nicht definiert, weil c nicht auf ein Array nicht zeigen.

C++ 11 Standard 5.7.5:

Wenn ein Ausdruck, der Integraltyp hat hinzugefügt oder von einem Zeiger subtrahiert wird, muss das Ergebnis die Art des Zeigers Operanden. 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, so dass die Differenz der Indizes der resultierenden und ursprünglichen Arrayelemente dem Integralausdruck entspricht. Mit anderen Worten, wenn der Ausdruck P auf das i-te Element eines Array-Objekts zeigt, zeigen die Ausdrücke (P) + N (äquivalent N + (P)) und (P) -N (wobei N den Wert n hat) jeweils zu den i + n-ten und i-n-ten Elementen des Array-Objekts, sofern sie existieren. Wenn der Ausdruck P auf das letzte Element eines Array-Objekts zeigt, verweist der Ausdruck (P) +1 außerdem auf das letzte Element des Array-Objekts, und wenn der Ausdruck Q auf das letzte Element eines Array-Objekts zeigt, Der Ausdruck (Q) -1 zeigt auf das letzte Element des Array-Objekts. Wenn sowohl der Zeigeroperand als auch das Ergebnis auf Elemente desselben Arrayobjekts oder eines der letzten Elemente des Arrayobjekts zeigen, soll die Auswertung keinen Überlauf erzeugen. Andernfalls ist das Verhalten nicht definiert.

+2

Offensichtlich ist die Dereferenzierung eines 'NULL'-Zeigers UB, ebenso wie die Indirektion durch einen 'NULL'-Zeiger, wie in C++ 11 beschrieben. – Novelocrat

+1

Die "Arithmetik eines Zeigers zu keinem Teil eines Arrays" ist möglicherweise der Schlüssel hier. – Novelocrat

+0

Ist ein Memory-Block auch das, was Sie ein Array nennen? oder ist Zeiger-Arithmetik im zugewiesenen Speicher auch UB? – dhein

15

Ja, das ist undefiniertes Verhalten, und ist etwas, das -fsanitize=undefined hätte gefangen werden sollen; es ist bereits in meiner TODO-Liste, um einen Haken dafür zu setzen.

FWIW, die C- und C++ - Regeln unterscheiden sich geringfügig: das Hinzufügen von 0 zu einem Nullzeiger und das Subtrahieren eines Nullzeigers von einem anderen haben nicht definiertes Verhalten in C, aber nicht in C++. Alle anderen Arithmetikfunktionen für Nullzeiger haben in beiden Sprachen ein nicht definiertes Verhalten.

+3

Die Absicht von [expr.add] p7 scheint sicher, dass das Hinzufügen von 0 zu einem Nullzeiger, oder Subtrahieren von zwei Nullzeiger, ist in C++ wohldefiniert, aber p5 und p6 sagen ausdrücklich, dass das Verhalten nicht definiert ist und normalerweise, wenn ein Teil des Standards das Verhalten eines Programms zu definieren scheint, während ein anderer Teil sagt, dass das Verhalten undefiniert ist, der Teil, der sagt Das Verhalten ist nicht definiert und gewinnt. – hvd

+3

Ich habe versucht, den Wortlaut von P5 und P6 zu verbessern (es gibt ein paar andere Dinge mit ihnen falsch), aber bisher keinen Erfolg gefunden. Beachten Sie auch, dass die Fußnote von p6 ein anderes, anderes und subtil inkompatibles Modell der Zeigerarithmetik beschreibt. –

+0

@RichardSmith Wird es einfach sein, Code auf einer Plattform zu handhaben, auf der die Schreib-/Leseadresse 0 gültig ist, oder müssen wir nur eine Desinfektions-Blacklist dafür verwenden? –

6

Nicht nur ist Arithmetik auf einen Nullzeiger verboten, sondern der Ausfall von Implementierungen, die versuchte Dereferenzierungen auffangen, um auch Arithmetik auf Nullzeiger abzufangen, verschlechtert den Nutzen von Nullzeigerfallen erheblich.

Es gibt keine durch den Standard definierte Situation, in der das Hinzufügen von Objekten zu einem Nullzeiger einen gültigen Zeigerwert ergeben kann. Des Weiteren sind Situationen, in denen Implementierungen ein nützliches Verhalten für solche Aktionen definieren könnten, selten und könnten im Allgemeinen besser über Compiler-Intrinsics (*) gehandhabt werden. Bei vielen Implementierungen kann jedoch, wenn die Nullzeigerarithmetik nicht abgefangen wird, das Hinzufügen eines Offsets zu einem Nullzeiger einen Zeiger ergeben, der, obwohl er nicht gültig ist, nicht mehr als Nullzeiger ist. Ein Versuch, einen solchen Zeiger zu dereferenzieren, würde nicht abgefangen, sondern könnte willkürliche Effekte auslösen.

Trapping-Zeiger Berechnungen der Form (Null + Offset) und (Null-Offset) würde diese Gefahr beseitigen. Beachten Sie, dass der Schutz nicht unbedingt Trapping (Zeiger-Null), (Null-Zeiger) oder (Null-Null) erfordert, während die von den ersten beiden Ausdrücken zurückgegebenen Werte wahrscheinlich keinen Nutzen haben [wenn eine Implementierung spezifiziert werden sollte dass Null-Null Null ergeben würde, wäre Code, der auf diese bestimmte Implementierung abzielte, manchmal effizienter als Code, der im Spezialfall null war, sie würden keine ungültigen Zeiger erzeugen. Ferner würde das Vorhandensein von (Null + 0) und (Null-0) Null-Zeigern anstelle von Überfüllung die Sicherheit nicht gefährden und könnte die Notwendigkeit vermeiden, Benutzercode-Spezial-Nullzeiger zu haben, aber die Vorteile wären weniger zwingend seit dem Compiler müsste zusätzlichen Code hinzufügen, um das zu ermöglichen.

(*) solche intrinsische auf einem 8086-Compilern, zum Beispiel, könnte eine vorzeichenlose 16-Bit-Integer „SEG“ und „OFS“, und lesen, das Wort bei der Adresse seg akzeptieren: ofs ohne Nullfalle selbst wenn Adress war zufällig null. Die Adresse (0x0000: 0x0000) auf dem 8086 ist ein Interrupt-Vektor, auf den einige Programme zugreifen müssen, und während die Adresse (0xFFFF: 0x0010) auf älteren Prozessoren mit nur 20 Adreßzeilen auf dieselbe physikalische Stelle wie (0x0000: 0x0000) zugreift Zugriff auf den physischen Standort 0x100000 auf Prozessoren mit 24 oder mehr Adresszeilen). In einigen Fällen wäre eine Alternative eine spezielle Bezeichnung für Zeiger zu haben, die erwarten, um auf Dinge zu zeigen, die nicht vom C-Standard erkannt werden (Dinge wie die Interrupt-Vektoren würden sich qualifizieren) und davon absehen, sie null zu machen oder anders zu spezifizieren dass volatile Zeiger auf solche Weise behandelt werden. Ich habe das erste Verhalten in mindestens einem Compiler gesehen, aber glaube nicht, dass ich das zweite gesehen habe.

Verwandte Themen