webentwicklung-frage-antwort-db.com.de

Wie verhält es sich, NULL mit dem% s-Bezeichner von printf zu drucken?

Ist auf eine interessante Interviewfrage gestoßen:

test 1:
printf("test %s\n", NULL);
printf("test %s\n", NULL);

prints:
test (null)
test (null)

test 2:
printf("%s\n", NULL);
printf("%s\n", NULL);
prints
Segmentation fault (core dumped)

Obwohl dies auf einigen Systemen problemlos funktioniert, löst mindestens meine einen Segmentierungsfehler aus. Was wäre die beste Erklärung für dieses Verhalten? Der obige Code ist in C.

Folgendes ist meine gcc info:

[email protected]:~$ gcc --version
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
45

Das Wichtigste zuerst: printf erwartet einen gültigen (d. H. Nicht NULL-) Zeiger für sein% s-Argument, sodass die Übergabe eines NULL-Werts offiziell undefiniert ist. Möglicherweise wird "(null)" gedruckt oder es werden alle Dateien auf Ihrer Festplatte gelöscht. In Bezug auf ANSI ist dies entweder korrekt (zumindest sagen Harbison und Steele dies).

Davon abgesehen, ja, das ist wirklich seltsames Verhalten. Es stellt sich heraus, dass Folgendes passiert, wenn Sie ein einfaches printf ausführen:

printf("%s\n", NULL);

gcc ist (ahem) klug genug, dies in einen Aufruf von puts zu dekonstruieren. Das erste printf, dies:

printf("test %s\n", NULL);

ist kompliziert genug, dass gcc stattdessen einen Aufruf an real printf ausgibt.

(Beachten Sie, dass gcc beim Kompilieren Warnungen über Ihr ungültiges Argument printf ausgibt. Das liegt daran, dass es seit langem möglich ist, Zeichenfolgen im Format *printf Zu analysieren.)

Sie können dies selbst sehen, indem Sie mit der Option -save-temps Kompilieren und dann die resultierende Datei .s Durchsehen.

Als ich das erste Beispiel kompilierte, bekam ich:

movl    $.LC0, %eax
movl    $0, %esi
movq    %rax, %rdi
movl    $0, %eax
call    printf      ; <-- Actually calls printf!

(Kommentare wurden von mir hinzugefügt.)

Aber der zweite erzeugte diesen Code:

movl    $0, %edi    ; Stores NULL in the puts argument list
call    puts        ; Calls puts

Das Seltsame ist, dass die folgende Zeile nicht gedruckt wird. Es ist, als hätte man herausgefunden, dass dies einen Segfehler verursachen wird, so dass es nicht stört. (Was es hat - es hat mich gewarnt, als ich es kompiliert habe.)

55
Chris Reuter

In Bezug auf die C-Sprache ist der Grund, dass Sie undefiniertes Verhalten aufrufen und alles passieren kann.

Was die Mechanik angeht, warum dies geschieht, optimiert der moderne GCC printf("%s\n", x) auf puts(x), und puts hat nicht den albernen Code, um (null) Zu drucken. Wenn ein Nullzeiger angezeigt wird, haben allgemeine Implementierungen von printf diesen Sonderfall. Da gcc (im Allgemeinen) nicht-triviale Formatzeichenfolgen wie diese nicht optimieren kann, wird printf tatsächlich aufgerufen, wenn in der Formatzeichenfolge ein anderer Text vorhanden ist.

28
R..

In Abschnitt 7.1.4 (von C99 oder C11) heißt es:

§7.1.4 Verwendung von Bibliotheksfunktionen

¶1 Jede der folgenden Anweisungen gilt, sofern in den folgenden ausführlichen Beschreibungen nicht ausdrücklich anders angegeben: Wenn ein Argument für eine Funktion einen ungültigen Wert aufweist (z. B. einen Wert außerhalb der Domäne der Funktion oder einen Zeiger außerhalb des Adressraums des Programm oder ein Nullzeiger oder ein Zeiger auf nicht änderbaren Speicher, wenn der entsprechende Parameter nicht const-qualifiziert ist) oder ein Typ (nach der Heraufstufung), der von einer Funktion mit variabler Anzahl von Argumenten nicht erwartet wird, ist das Verhalten undefiniert.

Da die Angabe von printf() nichts darüber aussagt, was passiert, wenn Sie für den Bezeichner %s Einen Nullzeiger darauf übergeben, ist das Verhalten explizit undefiniert. (Beachten Sie, dass die Übergabe eines Nullzeigers, der vom %p - Bezeichner ausgegeben werden soll, kein undefiniertes Verhalten ist.)

Hier ist das 'Kapitel und der Vers' für das Verhalten der fprintf() -Familie (C2011 - es ist eine andere Abschnittsnummer in C1999):

§7.21.6.1 Die Funktion fprintf

s Wenn kein Längenmodifikator l vorhanden ist, soll das Argument ein Zeiger auf das Anfangselement eines Arrays des Zeichentyps sein. [...]

Wenn ein Längenmodifizierer l vorhanden ist, muss das Argument ein Zeiger auf das Anfangselement eines Arrays vom Typ wchar_t sein.

p Das Argument soll ein Zeiger auf ungültig sein. Der Wert des Zeigers wird auf implementierungsdefinierte Weise in eine Folge von Druckzeichen konvertiert.

Die Angaben für den Konvertierungsspezifizierer s schließen die Möglichkeit aus, dass ein Nullzeiger gültig ist, da der Nullzeiger nicht auf das Anfangselement eines Arrays des entsprechenden Typs verweist. Die Spezifikation für den Konvertierungsspezifizierer p erfordert nicht, dass der Leerzeiger auf etwas Bestimmtes zeigt, und NULL ist daher gültig.

Die Tatsache, dass viele Implementierungen eine Zeichenfolge wie (null) Ausgeben, wenn ein Nullzeiger übergeben wird, ist eine Art Güte, auf die man sich gefährlich verlassen kann. Das Schöne an undefiniertem Verhalten ist, dass eine solche Reaktion zulässig ist, aber nicht erforderlich ist. Ebenso ist ein Absturz erlaubt, aber nicht erforderlich (mehr schade - Leute werden gebissen, wenn sie an einem verzeihenden System arbeiten und dann auf andere, weniger verzeihende Systeme portieren).

18

Der Zeiger NULL zeigt nicht auf eine Adresse, und der Versuch, sie zu drucken, führt zu undefiniertem Verhalten. Undefiniert bedeutet, es liegt an Ihrem Compiler oder Ihrer C-Bibliothek, zu entscheiden, was zu tun ist, wenn versucht wird, NULL zu drucken.

6
Yunchi