Das Drucken von Nullzeigern mit% p ist ein undefiniertes Verhalten?

93

Ist es undefiniertes Verhalten, Nullzeiger mit dem %pKonvertierungsspezifizierer zu drucken ?

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}

Die Frage bezieht sich auf den C-Standard und nicht auf C-Implementierungen.

Dror K.
quelle
Ich glaube nicht, dass sich irgendjemand (einschließlich des C-Komitees) zu sehr darum kümmert. Es ist ein ziemlich künstliches Problem ohne (oder fast ohne) praktische Bedeutung.
P__J__
es ist so, dass printf nur den Wert anzeigt und nicht berührt (im Sinne des Lesens oder Schreibens des spitzen Objekts) - kann nicht sein UB i Zeiger hat einen gültigen Wert für seinen
Typwert
3
@PeterJ Nehmen wir an, was Sie sagen, ist wahr (obwohl der Standard eindeutig etwas anderes angibt). Allein die Tatsache, dass wir darüber debattieren, macht die Frage zu einer gültigen und korrekten Frage, da sie wie der unten zitierte Teil des Standards aussieht Für einen normalen Entwickler ist es sehr schwer zu verstehen, was zum Teufel los ist. Das heißt: Die Frage verdient nicht die Abwertung, da dieses Problem einer Klärung bedarf!
Peter Varo
1
Siehe auch
alk
2
@ PeterJ das ist eine andere Geschichte als, danke für die Klarstellung :)
Peter Varo

Antworten:

93

Dies ist einer dieser seltsamen Eckfälle, in denen wir den Einschränkungen der englischen Sprache und der inkonsistenten Struktur im Standard unterliegen. Im besten Fall kann ich ein überzeugendes Gegenargument vorbringen, da es unmöglich ist , es zu beweisen :) 1


Der Code in der Frage zeigt ein genau definiertes Verhalten.

Da [7.1.4] die Grundlage der Frage ist, beginnen wir dort:

Jede der folgenden Anweisungen gilt, sofern in den folgenden detaillierten Beschreibungen nicht ausdrücklich anders angegeben: Wenn ein Argument für eine Funktion einen ungültigen Wert hat ( z. B. einen Wert außerhalb des Funktionsbereichs oder einen Zeiger außerhalb des Adressraums des Programms, oder ein Nullzeiger , [... andere Beispiele ...] ) [...] das Verhalten ist undefiniert. [... andere Aussagen ...]

Das ist ungeschickte Sprache. Eine Interpretation ist, dass die Elemente in der Liste für alle Bibliotheksfunktionen UB sind, sofern sie nicht durch die einzelnen Beschreibungen überschrieben werden. Die Liste beginnt jedoch mit "wie", was darauf hinweist, dass sie illustrativ und nicht erschöpfend ist. Beispielsweise wird die korrekte Nullterminierung von Zeichenfolgen nicht erwähnt (kritisch für das Verhalten von z strcpy. B. ).

Somit ist klar, dass die Absicht / der Umfang von 7.1.4 einfach darin besteht, dass ein "ungültiger Wert" zu UB führt ( sofern nicht anders angegeben ). Wir müssen uns die Beschreibung jeder Funktion ansehen, um festzustellen, was als "ungültiger Wert" gilt.

Beispiel 1 - strcpy

[7.21.2.3] sagt nur folgendes:

Die strcpyFunktion kopiert die Zeichenfolge, auf die von zeigt s2(einschließlich des abschließenden Nullzeichens), in das Array, auf das von gezeigt wird s1. Wenn zwischen überlappenden Objekten kopiert wird, ist das Verhalten undefiniert.

Nullzeiger werden nicht explizit erwähnt, aber auch Nullterminatoren werden nicht erwähnt. Stattdessen schließt man aus "Zeichenfolge, auf die gezeigt wird s2", dass die einzigen gültigen Werte Zeichenfolgen sind (dh Zeiger auf nullterminierte Zeichenarrays).

In der Tat ist dieses Muster in den einzelnen Beschreibungen zu sehen. Einige andere Beispiele:

  • [7.6.4.1 (fenv)] speichert die aktuelle Gleitkommaumgebung in dem Objekt, auf das von zeigtenvp

  • [7.12.6.4 (frexp)] speichert die Ganzzahl in dem int- Objekt, auf das von zeigtexp

  • [7.19.5.1 (fclose)] der Stream, auf den von zeigtstream

Beispiel 2 - printf

[7.19.6.1] sagt dies über %p:

p- Das Argument soll ein Hinweis auf sein void. Der Wert des Zeigers wird implementierungsdefiniert in eine Folge von Druckzeichen konvertiert.

Null ist ein gültiger Zeigerwert, und in diesem Abschnitt wird weder ausdrücklich erwähnt, dass Null ein Sonderfall ist, noch dass der Zeiger auf ein Objekt zeigen muss. Somit ist es definiertes Verhalten.


1. Es sei denn, ein Standardautor meldet sich oder wir finden etwas Ähnliches wie ein Begründungsdokument , das die Dinge klarstellt.

Oliver Charlesworth
quelle
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Bhargav Rao
1
"dennoch werden Nullterminatoren nicht erwähnt" ist in Beispiel 1 schwach - strcpy, da in der Spezifikation "kopiert den String " steht. Zeichenfolge wird explizit als Nullzeichen definiert .
chux
1
@chux - Das ist etwas mein Punkt - man muss aus dem Kontext ableiten, was gültig / ungültig ist, anstatt anzunehmen, dass die Liste in 7.1.4 vollständig ist. (Die Existenz dieses Teils meiner Antwort war jedoch im Zusammenhang mit Kommentaren, die inzwischen gelöscht wurden, etwas sinnvoller und argumentierte, dass strcpy ein Gegenbeispiel sei.)
Oliver Charlesworth
1
Der Kern des Problems ist , wie der Leser interpretiert wie . Bedeutet dies, dass einige Beispiele für mögliche ungültige Werte vorliegen ? Bedeutet das, dass einige Beispiele, bei denen es sich immer um ungültige Werte handelt, es sind ? Für die Aufzeichnung gehe ich mit der ersten Interpretation.
Ninjalj
1
@ninjalj - Ja, stimmte zu. Das ist im Wesentlichen das, was ich in meiner Antwort hier zu vermitteln versuche, dh "dies sind Beispiele für die Arten von Dingen, die ungültige Werte sein könnten". :)
Oliver Charlesworth
20

Die kurze Antwort

Ja . Das Drucken von Nullzeigern mit dem %pKonvertierungsspezifizierer hat ein undefiniertes Verhalten. Trotzdem ist mir keine vorhandene konforme Implementierung bekannt, die sich schlecht verhalten würde.

Die Antwort gilt für alle C-Standards (C89 / C99 / C11).


Die lange Antwort

Der %pKonvertierungsspezifizierer erwartet, dass ein Argument vom Typ Zeiger ungültig wird. Die Konvertierung des Zeigers in druckbare Zeichen ist implementierungsdefiniert. Es wird nicht angegeben, dass ein Nullzeiger erwartet wird.

Die Einführung in die Standardbibliotheksfunktionen besagt, dass Nullzeiger als Argumente für (Standardbibliotheks-) Funktionen als ungültige Werte betrachtet werden, sofern nicht ausdrücklich anders angegeben.

C99 /. C11 §7.1.4 p1

[...] Wenn ein Argument für eine Funktion einen ungültigen Wert hat (z. B. [...] einen Nullzeiger, ist das Verhalten [...] undefiniert.

Beispiele für (Standardbibliotheks-) Funktionen, die Nullzeiger als gültige Argumente erwarten:

  • fflush() verwendet einen Nullzeiger zum Löschen "aller zutreffenden Streams".
  • freopen() verwendet einen Nullzeiger, um die Datei anzuzeigen, die dem Stream "aktuell zugeordnet" ist.
  • snprintf() Ermöglicht die Übergabe eines Nullzeigers, wenn 'n' Null ist.
  • realloc() verwendet einen Nullzeiger zum Zuweisen eines neuen Objekts.
  • free() ermöglicht die Übergabe eines Nullzeigers.
  • strtok() verwendet einen Nullzeiger für nachfolgende Aufrufe.

Wenn wir den Fall für annehmen snprintf(), ist es sinnvoll, die Übergabe eines Nullzeigers zuzulassen, wenn 'n' Null ist, aber dies ist nicht der Fall für andere (Standardbibliotheks-) Funktionen, die eine ähnliche Null 'n' zulassen. Zum Beispiel: memcpy(), memmove(), strncpy(), memset(), memcmp().

Es wird nicht nur in der Einführung in die Standardbibliothek angegeben, sondern auch noch einmal in der Einführung in diese Funktionen:

C99 §7.21.1 p2 /. C11 §7.24.1 p2

Wenn ein als size_tn deklariertes Argument die Länge des Arrays für eine Funktion angibt, kann n bei einem Aufruf dieser Funktion den Wert Null haben. Sofern in der Beschreibung einer bestimmten Funktion in diesem Unterabschnitt nicht ausdrücklich anders angegeben, müssen Zeigerargumente für einen solchen Aufruf weiterhin gültige Werte haben, wie in 7.1.4 beschrieben.


Ist es beabsichtigt?

Ich weiß nicht, ob die UB von %pmit einem Nullzeiger tatsächlich beabsichtigt ist, aber da der Standard explizit angibt, dass Nullzeiger als ungültige Werte als Argumente für Standardbibliotheksfunktionen betrachtet werden, werden die Fälle explizit angegeben, in denen eine Null vorliegt Zeiger ist ein gültiges Argument (snprintf, frei, etc.), und dann geht es und einmal wiederholt erneut die Forderung nach den Argumenten gelten sogar in Null ‚n‘ Fälle ( memcpy, memmove, memset), dann denke ich , es vernünftig anzunehmen , dass die Das C-Normungskomitee ist nicht allzu besorgt darüber, dass solche Dinge nicht definiert sind.

Dror K.
quelle
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Bhargav Rao
1
@JeroenMostert: Was ist die Absicht dieses Arguments? Das gegebene Zitat von 7.1.4 ist ziemlich klar, nicht wahr? Worüber kann man streiten, "sofern nicht ausdrücklich anders angegeben", wenn nichts anderes angegeben ist? Was gibt es zu argumentieren, dass die (nicht verwandte) String-Funktionsbibliothek einen ähnlichen Wortlaut hat, sodass der Wortlaut nicht zufällig zu sein scheint? Ich denke, diese Antwort (obwohl sie in der Praxis nicht wirklich nützlich ist ) ist so richtig wie möglich.
Damon
3
@Damon: Ihre mythische Hardware ist nicht mythisch. Es gibt viele Architekturen, in denen Werte, die keine gültigen Adressen darstellen, möglicherweise nicht in Adressregister geladen werden. Das Übergeben von Nullzeigern als Funktionsargumente ist jedoch weiterhin erforderlich, um auf diesen Plattformen als allgemeiner Mechanismus zu funktionieren. Nur einen auf den Stapel zu legen, wird die Dinge nicht in die Luft jagen.
Jeroen Mostert
1
@anatolyg: Auf x86-Prozessoren bestehen Adressen aus zwei Teilen - einem Segment und einem Offset. Auf dem 8086 ist das Laden eines Segmentregisters wie das Laden eines anderen, aber auf allen späteren Computern wird ein Segmentdeskriptor abgerufen. Das Laden eines ungültigen Deskriptors verursacht eine Falle. Viele Code für 80386 und spätere Prozessoren verwenden jedoch nur ein Segment, und somit nie Lasten Segmentregister überhaupt .
Supercat
1
Ich denke, jeder würde zustimmen, dass das Drucken eines Nullzeigers mit %pnicht undefiniertes Verhalten sein soll
MM
-1

Die Autoren des C-Standards haben keine Anstrengungen unternommen, um alle Verhaltensanforderungen, die eine Implementierung erfüllen muss, um für einen bestimmten Zweck geeignet zu sein, vollständig aufzulisten. Stattdessen erwarteten sie, dass Leute, die Compiler schreiben, einen gewissen gesunden Menschenverstand üben würden, unabhängig davon, ob der Standard dies erfordert oder nicht.

Die Frage, ob etwas UB aufruft, ist an und für sich selten nützlich. Die wirklichen Fragen von Bedeutung sind:

  1. Sollte jemand, der versucht, einen Qualitätscompiler zu schreiben, dafür sorgen, dass er sich vorhersehbar verhält? Für das beschriebene Szenario lautet die Antwort eindeutig Ja.

  2. Sollten Programmierer das Recht haben zu erwarten, dass sich Qualitätscompiler für alles, was normalen Plattformen ähnelt, vorhersehbar verhalten? In dem beschriebenen Szenario würde ich sagen, dass die Antwort ja ist.

  3. Könnten einige stumpfe Compiler-Autoren die Interpretation des Standards erweitern, um zu rechtfertigen, etwas Seltsames zu tun? Ich würde nicht hoffen, aber es nicht ausschließen.

  4. Sollten Desinfektions-Compiler über das Verhalten kreischen? Das würde vom Paranoia-Level ihrer Benutzer abhängen; Ein Desinfektions-Compiler sollte wahrscheinlich nicht standardmäßig über ein solches Verhalten kreischen, sondern möglicherweise eine Konfigurationsoption für den Fall bereitstellen, dass Programme auf "clevere" / dumme Compiler portiert werden, die sich seltsam verhalten.

Wenn eine vernünftige Interpretation des Standards implizieren würde, dass ein Verhalten definiert ist, aber einige Compiler-Autoren die Interpretation erweitern, um etwas anderes zu rechtfertigen, spielt es dann wirklich eine Rolle, was der Standard sagt?

Superkatze
quelle
1. Es ist nicht ungewöhnlich, dass Programmierer feststellen, dass die Annahmen moderner / aggressiver Optimierer im Widerspruch zu dem stehen, was sie als "vernünftig" oder "Qualität" betrachten. 2. Wenn es um Unklarheiten in der Spezifikation geht, ist es nicht ungewöhnlich, dass Implementierer sich nicht einig sind, welche Freiheiten sie annehmen können. 3. Wenn es um Mitglieder des C-Normungsausschusses geht, sind sich selbst sie nicht immer einig, was die „richtige“ Interpretation ist, geschweige denn, was sie sein sollte. Wessen vernünftiger Interpretation sollten wir angesichts der oben genannten Punkte folgen?
Dror K.
6
Die Beantwortung der Frage "Ruft dieser bestimmte Code UB auf oder nicht" mit einer Dissertation darüber, was Sie über die Nützlichkeit von UB denken oder wie sich Compiler verhalten sollten, ist ein schlechter Versuch, eine Antwort zu finden, zumal Sie dies als kopieren und einfügen können eine Antwort auf fast jede Frage zu bestimmten UB. Als Gegenerwiderung zu Ihrem rhetorischen Erfolg: Ja, es ist wirklich wichtig, was der Standard sagt, egal was einige Compiler-Autoren tun oder was Sie davon halten, denn der Standard ist das, wovon sowohl Programmierer als auch Compiler-Autoren ausgehen.
Jeroen Mostert
1
@JeroenMostert: Die Antwort auf "Ruft X undefiniertes Verhalten auf" hängt oft davon ab, was man mit der Frage meint. Wenn ein Programm als undefiniertes Verhalten angesehen wird, wenn der Standard keine Anforderungen an das Verhalten einer konformen Implementierung stellen würde, rufen fast alle Programme UB auf. Die Autoren des Standards erlauben es Implementierungen eindeutig, sich willkürlich zu verhalten, wenn ein Programm Nester-Funktionsaufrufe zu tief aufruft, solange eine Implementierung mindestens einen (möglicherweise erfundenen) Quelltext korrekt verarbeiten kann, der die Übersetzungsgrenzen im Stadard ausübt.
Supercat
@supercat: sehr interessant, aber ist printf("%p", (void*) 0)undefiniertes Verhalten nach dem Standard oder nicht? Tief verschachtelte Funktionsaufrufe sind dafür ebenso relevant wie der Preis für Tee in China. Und ja, UB ist in realen Programmen sehr verbreitet - was ist damit?
Jeroen Mostert
1
@JeroenMostert: Da der Standard es einer stumpfen Implementierung ermöglichen würde, fast jedes Programm als UB zu betrachten, sollte das Verhalten nicht stumpfer Implementierungen von Bedeutung sein. Falls Sie es nicht bemerkt haben, habe ich nicht nur ein Copy / Paste über UB geschrieben, sondern die Frage %pfür jede mögliche Bedeutung der Frage beantwortet.
Supercat