Nabend,
mir lässt eine Frage keine Ruhe..
Wieso kann
1
ultoa(2323232,NULL,10);
An der Stelle auch ohne "array" arbeiten?
Wenn ich das dann so in eine Ausgaberoutine rein hauen würde, würde er
mir den String raus hauen..
Legt er die Daten auf dem Stack ab bei "NULL" Übergabe?
Wird der Speicher etwa mit der Funktion "malloc()" geschaffen?
Ich sehe keine Sonderbehandlung für NULL in der Dokumentation (IBM, MS,
avrlibc).
Die Funktion wird einfach das Ergebnis an den Adressraumbeginn
schreiben. Wenn da was stand, isses danach weg. Und wenn du eine
ordentliche Hardware hast, kriegst du einen Seg- oder Hardfault für den
Versuch.
Der zweite Parameter ist ein Zeiger, was ein Integer mit der Adresser
der Speicherzelle ist.
NULL ist die Zahl 0. Wird also als Zeiger auf die Speicherzelle mit
Adresse 0 interpretiert.
S. R. schrieb:> Und wenn du eine ordentliche Hardware hast, kriegst du einen Seg- oder> Hardfault für den Versuch.
Das heißt? Was bewirken diese Features? Was lösen die aus?
Also gehe ich davon aus, dass es zufall ist das es Funktioniert.
>> Und wenn du eine ordentliche Hardware hast, kriegst du einen Seg- oder>> Hardfault für den Versuch.>> Das heißt? Was bewirken diese Features? Was lösen die aus?
Sie beenden dein Programm, resetten den Controller, ... kommt auf die
konkrete Hardware an. Gemeinsam ist, dass sie das als fatalen
Programmierfehler ansehen und das Programm nicht mehr weiterlaufen
lassen wollen.
Ist doch egal wo die Interrupts liegen.
Es geht doch darum, dass bei Zugriff auf eine nicht erlaubte Adresse (in
diesem Fall 0) ein Hard Fault ausgelöst wird.
Stefanus F. schrieb:> Der zweite Parameter ist ein Zeiger, was ein Integer mit der Adresser> der Speicherzelle ist.>> NULL ist die Zahl 0. Wird also als Zeiger auf die Speicherzelle mit> Adresse 0 interpretiert.
Nein.
Rufus Τ. F. schrieb:> Stefanus F. schrieb:>> Das sind Interrupts.>> Nicht bei jeder Architektur liegen die Interruptvektoren am unteren Ende> des Adressraums.
Da hast du was missverstanden. Es ging um:
S. R. schrieb:> einen Seg- oder Hardfault
und nicht um die Lage der Interruptvektortabelle.
Ein C99-konformer Compiler wird einen statischen Zugrif auf NULL nicht
ausführen, sondern mit einem abort() quittieren. Schwachsinn, meiner
Meinung nach, aber so sind nun mal die Regeln...
Um tatsächlich auf *0L zuzugreifen, muss man (z.B.) gcc ein
-no-delete-null-pointer-checks mitgeben.
Markus F. schrieb:> Ein C99-konformer Compiler wird einen statischen Zugrif auf NULL nicht> ausführen, sondern mit einem abort() quittieren.
Ernsthaft? Das ist so vorgeschrieben? Was soll das abort() denn tun?
Ich dachte, eine NULL-Dereferenz wäre schlicht implementation-defined.
Dieter B. schrieb:> Also gehe ich davon aus, dass es zufall ist das es Funktioniert.
Ein Programm, was nur zufällig funktioniert, funktioniert nicht.
In C darf NULL "((void*) 0)" oder "0" sein, in C++ "0" oder "nullptr".
Die Zahl "0" in einen Pointer-Typ wie "void*" zu konvertieren resultiert
in einem Null-Pointer, d.h. ein Pointer der ungleich jedem gültigen
Pointer ist. Das heißt aber nicht, dass die tatsächliche Adresse
wirklich 0 sein muss - in "(void*) 0" könnte auch 42 stehen, je nachdem
was der Compiler daraus macht. Somit greift "*((char*)NULL)" nicht
notwendigerweise auf Adresse 0 zu. Tatsächlich machen die meisten
Compiler das aber so; m.W. gibt es z.B. im GCC gar nicht die
Möglichkeit, Null-Pointer auf etwas anderes al 0 zu setzen. Das könnte
aber gerade bei Cortex-M praktisch sein, weil man Null-Pointer dann auf
eine Adresse setzen könnte, die bei Zugriffen garantiert immer zum
Absturz führt, was bei der Adresse "0" nicht immer der Fall ist.
Ich habe in der C99 Spezifikation keinen Hinweis gefunden, das null
pointer irgendwo verboten seien.
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf
Nur das hier:
"If an argument to a function has an invalid value (such as a value
outside the domain of the function, or a pointer outside the address
space of the program, or a null pointer, or a pointer to non-modifiable
storage when the corresponding parameter is not const-qualified) or a
type (after promotion) not expected by a function with variable number
of arguments, the behavior is undefined."
Hier:
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
mal die Option -fdelete-null-pointer-checks (besser mehrfach) studieren
(die Auswirkungen sind - bzw. waren zumindest mir - nicht auf den ersten
Blick offensichtlich).
Das Verhalten ist eine Optimierung (?) und schlägt nur bei optimierten
Builds zu (ist allerdings wohl nicht bei allen Compilern aktiv). Basiert
auf dem Kausalsschluss, dass NULL-Pointer-Dereferenzierungen undefined
behaviour sind und deswegen wegoptimiert werden können. Macht man's
doch, gibt's ein abort().
Jedenfalls war ich erstaunt, als ein altes, neu compiliertes
ColdFire-Programm plötzlich nicht mehr lief, das eigentlich nur seinen
Reset-Vektor (auf Adresse 0) setzen wollte und stattdessen in einen trap
#7 lief (obwohl noch gar keine Vektortabelle da war).
Dr. Sommer schrieb:> Somit greift "*((char*)NULL)" nicht notwendigerweise auf Adresse 0 zu.
Genau. Das meinte ich vorher mit meinem zugegebenermaßen etwas kurz
geratenen "Nein" (ich war etwas in Eile).
Siehe z.B. http://c-faq.com/null/index.html .
Anders wäre es bei
1
inti=0;
2
char*c=(char*)i;
denn die Regel gilt nur für konstante Ausdrücke, und i ist kein
konstanter Ausdruck. Allerdings bleibt die Konvertierung eines int in
einen Zeiger zumindest mal implementationsabhängig.
S. R. schrieb:> Ich dachte, eine NULL-Dereferenz wäre schlicht implementation-defined.
Sie ruft "undefined behavior" hervor.
Stefanus F. schrieb:> Ich habe in der C99 Spezifikation keinen Hinweis gefunden, das null> pointer irgendwo verboten seien.
6.5.3.2 Absatz 4:
If an invalid value has been assigned to the pointer, the behavior of
the unary * operator is undefined. 84)
und in Fußnote 84:
Among the invalid values for dereferencing a pointer by the unary *
operator are a null pointer, an address inappropriately aligned for
the type of object pointed to, and the address of an object after the
end of its lifetime.
> If an argument to a function has an invalid value> (such as ... a null-pointer) ... the behavior is undefined.
Den Satz darf man glaube ich nicht so verstehen, wie ich ihn verstünde,
wenn ich nicht wüsste, dass man ihn so nicht verstehen darf.
> Das meinte ich vorher mit meinem zugegebenermaßen etwas kurz> geratenen "Nein"
Danke, so kann ich auch was damit anfangen. Also NULL ist per
Spezifikation nicht zwingend gleich 0.
Dr. Sommer schrieb:> Das heißt aber nicht, dass die tatsächliche Adresse wirklich 0 sein muss> - in "(void*) 0" könnte auch 42 stehen, je nachdem was der Compiler> daraus macht.
Hast Du schon mal einen real existierenden Compiler gesehen, der das
macht?
Stefanus F. schrieb:>> Das meinte ich vorher mit meinem zugegebenermaßen etwas kurz>> geratenen "Nein">> Danke, so kann ich auch was damit anfangen. Also NULL ist per> Spezifikation nicht zwingend gleich 0.
Auf Quellcode-Ebene schon. Also
1
char*c=0;
und
1
char*c=NULL;
werden beide zu einem Nullzeiger führen. Aber dieser muss nicht zwingend
komplett aus 0-Bits bestehen. Es erfolgt eine Konvertierung von der
Integer-Konstanten 0 in das Bit-Pattern, das einen Nullzeiger darstellt.
Im Prinzip muss es nicht mal sein, dass es nur genau einen Wert für
einen Nullzeiger gibt. Es könnte auch mehrere geben, aber ein Vergleich
zweier Nullzeiger muss immer Gleichheit ergeben.
Rufus Τ. F. schrieb:> Dr. Sommer schrieb:>> Das heißt aber nicht, dass die tatsächliche Adresse wirklich 0 sein muss>> - in "(void*) 0" könnte auch 42 stehen, je nachdem was der Compiler>> daraus macht.>> Hast Du schon mal einen real existierenden Compiler gesehen, der das> macht?
In meinem obigen Link auf die comp.lang.c-FAQ findet man das in Kapitel
5.17 ("Seriously, have any actual machines really used nonzero null
pointers, or different representations for pointers to different
types?")
Jetzt bin ich aber verwirrt.
NULL ist nicht unbedingt gleich 0, könnte auch 42 sein.
Aber wenn ich im Quelltext 0 schreibe, dann ist das immer gleich NULL?
Also wenn ich im Quelltext 0 schreibe, könnte daraus 42 werden.
Hä? Echt jetzt? Was nehmen die Leute ein, die sich so etwas ausdenken?
Rufus Τ. F. schrieb:> Hast Du schon mal einen real existierenden Compiler gesehen, der das> macht?
Leider nein, aber wie gesagt wäre es durchaus hilfreich. An Adresse 0
steht bei ARM gerne mal der Flash, oder ein ROM mit Bootloader oder so,
sodass ein Zugriff wie bereits erwähnt manchmal sogar erwünscht ist.
Stattdessen könnte man sich theoretisch eine andere Adresse suchen an
der kein Speicher oder Peripherie zu finden ist, diese per MMU/MPU
schützen und für Null-Pointer verwenden, sodass jeder Zugriff zu einer
Exception führt. Nur leider können die Compiler das nicht. Dann ginge so
etwas:
1
externcharBootROM[1024];
2
3
intmain(){
4
// Addresse 0xFFFFFFF0 per MMU/MPU schützen ...
5
6
char*pNull=0;// oder NULL oder nullptr - wird vom Compiler auf 0xFFFFFFF0 konvertiert
Rolf M. schrieb:> In meinem obigen Link auf die comp.lang.c-FAQ findet man das in Kapitel> 5.17 ("Seriously, have any actual machines really used nonzero null> pointers, or different representations for pointers to different> types?")
eine dort nicht aufgeführte Plattform, bei der das meiner Erinnerung
nach auch so ist (war), sind die INMOS Transputer.
Die hatten signed pointer. Die Adresse 0 war also eine gültige und lag
mitten im erlaubten Adressraum. Welchen Wert NULL da hatte, weiss ich
allerdings nicht.
Stefanus F. schrieb:> Jetzt bin ich aber verwirrt.>> NULL ist nicht unbedingt gleich 0, könnte auch 42 sein.> Aber wenn ich im Quelltext 0 schreibe, dann ist das immer gleich NULL?> Also wenn ich im Quelltext 0 schreibe, könnte daraus 42 werden.
Auf Quelltext-Ebene bedeutet eine Zuweisung von 0 an einen Zeiger, dass
du einen NULL-Zeiger draus machst. Ein Vergleich mit 0 ergibt auch
wieder Gleichheit. Ein Vergleich mit einem beliebigen gültigen
Objekt-Zeiger gibt keine Gleichheit. Wie das hinter den Kulissen
umgesetzt ist, spielt dabei für dich keine Rolle. Das ist eben
Abstraktion.
> Hä? Echt jetzt? Was nehmen die Leute ein, die sich so etwas ausdenken?
Sie nehmen an, dass der Compiler-Bauer möglichst viele Freiheiten zu
schätzen weiß, um die Sprache möglichst gut auf jeder nur erdenklichen
Plattform umsetzen zu können. Daher wird eben nicht hart vorgegeben,
dass ein NULL-Zeiger auch im Speicher nur aus 0-Bits besteht, denn es
könnte Systeme geben, auf denen das große Nachteile hätte.
Oder sieh es mal so. Wenn du schreibst:
1
inti=5;
2
floatf=i;
würdest du erwarten, dass das Bit-Pattern, das in i steht und das in f
exakt gleich sind? Eher nicht, oder? Ist schließlich ein anderer
Datentyp, bei dem die 5 ganz anders repräsentiert wird. Warum sollte das
bei einem Nullzeiger dann verboten sein?
Rolf M. schrieb:> In meinem obigen Link auf die comp.lang.c-FAQ findet man das in Kapitel> 5.17
Effektiv also uralte obskure und abgesehen von Informatikvorlesungen
völlig irrelevante Systeme.
Rufus Τ. F. schrieb:> Rolf M. schrieb:>> In meinem obigen Link auf die comp.lang.c-FAQ findet man das in Kapitel>> 5.17>> Effektiv also uralte obskure und abgesehen von Informatikvorlesungen> völlig irrelevante Systeme.
Anscheinend gibt's auch einigermassen aktuelle Systeme, die das
betrifft.
Z.B. der STi5517 von ST. Scheinbar ein Transputer-Nachfolger, der auch
die signed-Adressen geerbt hat. Auch da liegt 0 mitten im gültigen
Addressraum.
Dieter B. schrieb:> Also gehe ich davon aus, dass es zufall ist das es Funktioniert.
Funktioniert es denn auf Deinem unbekannten System mit Deinem
unbekannten Compiler?
Markus F. schrieb:> sondern mit einem abort() quittieren
Abgesehen davon, dass dies nicht vorgeschrieben ist, wäre es auch
unschön, jede, aber auch wirklich jede Dereferenzierung erstmal auf
Verschiedenheit von 0 Testen zu müssen. Genau dafür gibt es ja UB & Co,
dass man (hier) den Pointer einfach nutzen kann, ohne Wrapper.
Rolf M. schrieb:> Auf Quelltext-Ebene bedeutet eine Zuweisung von 0 an einen Zeiger, dass> du einen NULL-Zeiger draus machst. Ein Vergleich mit 0 ergibt auch> wieder Gleichheit.
Das wiederum heißt, dass es per Spezifikation keine Möglichkeit gibt,
einen Zeiger auf die Adresse 0 von einem NULL-Zeiger zu unterscheiden.
Daraus folgt ebenfalls, dass die Adresse "0" per Spezifikation nicht mit
C nutzbar ist, unabhängig davon, wie der Compiler seine NULL-Zeiger
definiert, da es grundsätzlich undefined behaviour ist.
Gut, hätten wir das mal geklärt.
Es sei denn, man ruft eine Funktion auf, die der Compiler nicht selbst
übersetzt hat. Ein Pointer, den eine unbekannte Funktion zurückliefert
kann, wie ich das sehe, dem Standard folgend nicht positiv als
Nullpointer bestimmt werden - [i]ist[/i] also niemals ein Nullpointer.
Es kann höchstens sein, dass die binäre Repräsentation seines Wertes mit
der eines Nullpointers [i]identisch[/i] ist.
S. R. schrieb:> Das wiederum heißt, dass es per Spezifikation keine Möglichkeit gibt,> einen Zeiger auf die Adresse 0 von einem NULL-Zeiger zu unterscheiden.> Daraus folgt ebenfalls, dass die Adresse "0" per Spezifikation nicht mit> C nutzbar ist, unabhängig davon, wie der Compiler seine NULL-Zeiger> definiert, da es grundsätzlich undefined behaviour ist.
weder, noch.
Ein konstanter NULL-Zeiger kann nicht dereferenziert werden und zeigt
"irgendwohin" (tatsächlich aber meist doch auf 0L).
Eine auf 0L gesetzte Variable, die ein Pointer ist, zeigt (auf jeder
Plattform, schliesslich könnte das ja eine valide Adresse sein) auf die
Adresse 0.
Wurde aber oben schon mal gesagt.
S. R. schrieb:> Rolf M. schrieb:>> Auf Quelltext-Ebene bedeutet eine Zuweisung von 0 an einen Zeiger, dass>> du einen NULL-Zeiger draus machst. Ein Vergleich mit 0 ergibt auch>> wieder Gleichheit.>> Das wiederum heißt, dass es per Spezifikation keine Möglichkeit gibt,> einen Zeiger auf die Adresse 0 von einem NULL-Zeiger zu unterscheiden.> Daraus folgt ebenfalls, dass die Adresse "0" per Spezifikation nicht mit> C nutzbar ist, unabhängig davon, wie der Compiler seine NULL-Zeiger> definiert, da es grundsätzlich undefined behaviour ist.
Nö, eigentlich heißt es das nicht. Aus Sicht von C gibt es eigentlich
nur Zeiger auf existierende Objekte (die also per Operator & mit der
Adresse einer noch existierenden Variable befüllt worden sind oder von
malloc kommen) und ungültige Zeiger, die man nicht dereferenzieren darf.
Der Nullzeiger ist ein ungültiger Zeiger, der sich von den anderen nur
darin unterscheidet, dass man auf ihn prüfen kann, weil ein Vergleich
zweier Nullzeiger immer Gleichheit ergibt. Ein Vergleich eines
Nullzeigers mit einem Zeiger, in dem nur 0-Bits stehen, muss nicht
zwingend Gleichheit ergeben.
Warum sollte man nun also die Adresse 0 nicht nutzen können? Wenn der
Compiler ein Objekt dort ablegt und mir die Adresse zurückgibt, kann ich
den Zeiger auch dereferenzieren.
Rolf M. schrieb:> Aus Sicht von C gibt es eigentlich> nur Zeiger auf existierende Objekte (die also per Operator & mit der> Adresse einer noch existierenden Variable befüllt worden sind oder von> malloc kommen) und ungültige Zeiger, die man nicht dereferenzieren darf.
Das stimmt für C zumindest nicht ganz:
1
An address constant is a null pointer, a pointer to an lvalue designating an object of static storage duration, or a pointer to a function designator; it shall be created explicitly using the unary & operator or an integer constant cast to pointer type ...
Heiko L. schrieb:> Rolf M. schrieb:>> Aus Sicht von C gibt es eigentlich>> nur Zeiger auf existierende Objekte (die also per Operator & mit der>> Adresse einer noch existierenden Variable befüllt worden sind oder von>> malloc kommen) und ungültige Zeiger, die man nicht dereferenzieren darf.>> Das stimmt für C zumindest nicht ganz:> An address constant
Ich sprach von Zeigern, nicht von Adresskonstanten.
Hmm, ich glaube, ich habe missverstanden, was du mit dem Zitat sagen
willst.
Ja, ich kann natürlich auch einen Integer in einen Zeiger konvertieren,
wobei das Ergebnis der Konvertierung dann auch wieder implementation
defined ist:
"An integer may be converted to any pointer type. Except as previously
specified, the result is implementation-defined, might not be correctly
aligned, might not point to an entity of the referenced type, and might
be a trap representation."
Dein Zitat sagt ja letztendlich nur, dass eine Integerkonstante, die in
einen Zeigertyp konvertiert wird, eine Adreesskonstante ergibt. Ob ein
Zeiger, dem ich diese zuweise, dann gültig ist, ist ja wieder eine
andere Frage.
Rolf M. schrieb:> Ja, ich kann natürlich auch einen Integer in einen Zeiger konvertieren,> wobei das Ergebnis der Konvertierung dann auch wieder implementation> defined ist
Richtig. Der C++-Standard scheint da strikter zu sein: Da ist der Cast
von beliebigen Zahlen auf Pointer nicht positiv erlaubt, also undefined.
Definiert ist da nur &object und ((Type*)0) sowie Arithmetik.
(oder ich habe den Passus nicht gefunden)
Rolf M. schrieb:> Warum sollte man nun also die Adresse 0 nicht nutzen können?
Weil ich keinen Zeiger auf Adresse 0 konstruieren kann, der nicht
gleichzeitig ein NULL-Zeiger ist. Da der Vergleich eines NULL-Zeigers
und der Adresse 0 immer wahr sein muss, gilt das sogar unabhängig vom
Bitmuster des NULL-Zeigers.
Damit ist jeder Versuch, einen beliebigen Zeiger auf Adresse 0 zu
dereferenzieren, per Definition undefined behaviour.
Falls ich unrecht habe, bitte ich um ein Gegenbeispiel. Bevorzugt eines,
welches auf Nicht-Transputer-Systemen funktioniert.
S. R. schrieb:> Da der Vergleich eines NULL-Zeigers> und der Adresse 0 immer wahr sein muss, gilt das sogar unabhängig vom> Bitmuster des NULL-Zeigers.
Er könnte aber auch für Zeiger ein Bit mehr verwenden, als der
Addressraum eigentlich braucht, welches signalisiert, dass ein Zeiger
ein Nullzeiger ist. Zum Beispiel ein Paritätsbit, mit dem man dann auch
prüfen kann, ob irgendwo eine Röhre oder ein Relais durch ist.
Spaß beiseite: Wenn der Compiler nicht zur Kompilierzeit nachweisen
kann, dass etwas ein Nullpointer ist, wird er kaum Runtime-Checks dafür
einbauen, es sei denn, man kompiliert mit einem Sanitizer.
S. R. schrieb:> Rolf M. schrieb:>> Warum sollte man nun also die Adresse 0 nicht nutzen können?>> Weil ich keinen Zeiger auf Adresse 0 konstruieren kann, der nicht> gleichzeitig ein NULL-Zeiger ist. Da der Vergleich eines NULL-Zeigers> und der Adresse 0 immer wahr sein muss, gilt das sogar unabhängig vom> Bitmuster des NULL-Zeigers.
Nein, das hab ich doch oben extra geschrieben: Zwei Nullzeiger müssen
immer Gleichheit ergeben. Ein Nullzeiger und ein Zeiger auf die Adresse
0 nicht unbedingt.
Du musst hier immer unterscheiden zwischen der C-Ebene, also dem, was im
Quellcode steht, und dem, was der Compiler daraus macht, also was
nachher für ein Bitmuster im Speicher steht. Man muss sich von dem
Gedanken lösen, dass die Konstante 0 auf C-Ebene im Zeigerkontext
bedeutet, dass das Adresse 0 wäre. Das sind zwei verschiedene
Abstraktionsebenen.
Mit "Adresse 0" meine ich hier eine Adresse, die ausschließlich aus
0-Bits besteht, nicht die, die man bekommt, wenn man in C die
Integer-Konstante 0 in einen Zeiger konvertiert.
> Damit ist jeder Versuch, einen beliebigen Zeiger auf Adresse 0 zu> dereferenzieren, per Definition undefined behaviour.
Ich sehe da keinen Grund dafür in dem Fall, dass ein Nullzeiger nicht
auf die Adresse 0 zeigt.
> Falls ich unrecht habe, bitte ich um ein Gegenbeispiel. Bevorzugt eines,> welches auf Nicht-Transputer-Systemen funktioniert.
Stellen wir uns mal vor, der Compiler definiert, dass ein NULL-Zeiger im
Speicher mit dem Wert 0x1234 abgelegt wird. Ich schreibe NULL in einen
Zeiger, dann steht also 0x1234 drin. Ich vergleiche ihn mit NULL, dann
wird daraus auf Assembler-Ebene ein Vergleich mit 0x1234 -> gleich. Mit
der Adresse 0 hat das gar nichts zu tun. Wenn wir z.B. nun weiterhin
annehmen, dass der Compiler den Heap direkt am Anfang des Adressraums
beginnen lässt, könnte der erste Aufruf von malloc die Adresse 0
zurückgeben. Die kann ich dann einfach dereferenzieren und benutzen. Ein
Vergleich mit NULL, also dem internen Wert 0x1234 ergibt auch keine
Gleichheit. Wo soll da undefiniertes Verhalten herkommen?
Rolf M. schrieb:> Ich vergleiche ihn mit NULL, dann wird daraus auf Assembler-Ebene ein> Vergleich mit 0x1234 -> gleich.
Das würde bedeuten, daß der Compiler auch bei diesem Code diesen
magischen Vergleich einbauen würde:
1
int*p;
2
3
p=NULL;
4
5
if(p)// <--
6
{
7
machwas();
8
}
9
10
if(!p)// <--
11
{
12
lasswas();
13
}
Hmm.
Stefanus F. schrieb:> Ist das Philosophie?
Mindestens. Mir scheint ein Teil der Diskussion akademisch abgehoben zu
sein.
Rufus Τ. F. schrieb:> Das würde bedeuten, daß der Compiler auch bei diesem Code diesen> magischen Vergleich einbauen würde:
Für das zweite if ganz klar, denn da gilt:
"The expression !E is equivalent to (0==E)."
Für das erste konnte ich eine passende Regel auf die Schnelle allerdings
nicht finden.
Rufus Τ. F. schrieb:> Stefanus F. schrieb:>> Ist das Philosophie?>> Mindestens. Mir scheint ein Teil der Diskussion akademisch abgehoben zu> sein.
Ich mag solche Diskussionen, bin mir aber bewusst, dass nicht jeder
diese Begeisterung teilt. ;-)
Rufus Τ. F. schrieb:> Das würde bedeuten, daß der Compiler auch bei diesem Code diesen> magischen Vergleich einbauen würde:
Definitiv.
Rufus Τ. F. schrieb:> Mindestens. Mir scheint ein Teil der Diskussion akademisch abgehoben zu> sein.
Na und? Programmiersprachen, und insbesondere C und C++, sind abstrakt
definiert um alle möglichen Umgebungen abzudecken. Nur weil jetzt gerade
keine Architektur "in" ist, bei der diese Feinheiten wichtig sind, würde
ich das nicht als abgehoben abtun. C gibt ja z.B. nichtmal das
Zweier-Komplement vor, d.h. es ist nicht garantiert dass "-32768" mit
"int" darstellbar ist. Das sind Dinge die man im Hinterkopf behalten
sollte, und daher auch diskutiert werden können.
Man sollte sich vom Begriff "Null-Pointer" lösen (der ist von C blöd
gewählt), und stattdessen an "Ungültigen Pointer" o.ä. denken. Wenn man
etwas ala "(char*) 0" sieht, sollte man die 0 ignorieren und denken
"Hier wird ein plattformspezifischer Wert für ungültige Pointer
genommen". Wenn du in Java "null" schreibst, weißt du ja auch nicht ob
da wirklich nur 0-Bits in die Referenz geschrieben werden, oder doch
irgendwas anderes.
Da Standard-C sowieso den Zugriff auf beliebige Adressen nicht
unterstützt, ist es auch keine Einschränkung so ggf. nicht direkt auf
die tatsächliche Adresse 0 zugreifen zu können. So Dinge wie "*((char*)
0x1234)=1;" sind eh nicht standardisiert.
S. R. schrieb:> Daraus folgt ebenfalls, dass die Adresse "0" per Spezifikation nicht mit> C nutzbar ist, unabhängig davon, wie der Compiler seine NULL-Zeiger> definiert, da es grundsätzlich undefined behaviour ist.S. R. schrieb:> Damit ist jeder Versuch, einen beliebigen Zeiger auf Adresse 0 zu> dereferenzieren, per Definition undefined behaviour.>> Falls ich unrecht habe, bitte ich um ein Gegenbeispiel. Bevorzugt eines,> welches auf Nicht-Transputer-Systemen funktioniert.
Ich sehe (für ANSI-C) keinen Hinweis, dass dereferenzieren des 0-Zeigers
UB ist. Nur für die Library-Funktionen, was ja verständlich ist.
> 4.6.1 Use of library functions> If an argument to a function has an invalid value (such as a value> outside the domain of the function, or a pointer outside the address> space of the program, or a null pointer), the behavior is undefined.
Und funktionieren tut es auf allen Systemen bisher, wer sollte dafür
extra-Code einbauen? Also egal ob auf 0 was interessantes liegt (was ich
problemlos auslese), oder ob die Maschine dann abstürzt weil es
Spezialregister sind.
Peter D. schrieb:> Man könnte eine Stringfunktion so implementieren, daß bei einem> Nullpointer als Argument die Ausgabe nach stdout oder stderr erfolgt.
Naja, da es UB ist, könnte man (um das gängige Beispiel zu nehmen)
innerhalb der Spezifikation auch WW3 auslösen.
Peter D. schrieb:> Man könnte eine Stringfunktion so implementieren, daß bei einem> Nullpointer als Argument die Ausgabe nach stdout oder stderr erfolgt.
Es gibt ja mittlerweile auch die strxxx_s-Funktionen, die u.a. den
Nullzeiger abfangen,
Nicht jeder Computer muss ein stdout haben. Dafür ist C zu allgemein.
Aber, C ist eigentlich nicht für Anfänger gedacht, sondern für Profis,
die wissen was sie tun.
Achim S. schrieb:> Ich sehe (für ANSI-C) keinen Hinweis, dass dereferenzieren des 0-Zeigers> UB ist. Nur für die Library-Funktionen, was ja verständlich ist.
§6.5.3.2:
If an invalid value has been assigned to the pointer, the
behavior of the unary * operator is undefined. 87)
Fußnote 87 zu §6.5.3.2:
Among the invalid values for dereferencing a pointer by the
unary * operator are a null pointer, an address inappropriately
aligned for the type of object pointed to, and the address of an object
after the end of its lifetime.
Markus F. schrieb:> Achim S. schrieb:>> Ich sehe (für ANSI-C) keinen Hinweis, dass dereferenzieren des 0-Zeigers>> UB ist. Nur für die Library-Funktionen, was ja verständlich ist.>> §6.5.3.2:> If an invalid value has been assigned to the pointer, the> behavior of the unary * operator is undefined. 87)>> Fußnote 87 zu §6.5.3.2:>> Among the invalid values for dereferencing a pointer by the> unary * operator are a null pointer, an address inappropriately> aligned for the type of object pointed to, and the address of an object> after the end of its lifetime.
Und was heißt das?
x = (void*)0 -> is(x, Nullpointer), x = nullptr -> is(x, Nullpointer)
Ich sehe nicht, wie irgendetwas ein Nullpointer sein könnte, wo nicht
"(void*)0" oder "nullptr" steht.
Das gibt -fno-delete-null-pointer-checks eine ganz neue Bedeutung.
Das ist die Antwort auf die Frage, wo definiert sei, dass das
Dereferenzieren eines Nullpointers UB ist.
Ansonsten gilt: eine Rose ist eine Rose ist eine Rose...
Markus F. schrieb:> Ansonsten gilt: eine Rose ist eine Rose ist eine Rose...
Nein, das zeigt die Notwendigkeit zwischen den Ebenen des Quelltextes
und den generierten Anweisungen sauber zu trennen.
Einen Nullpointer zu dereferenzieren ist genau dann UB, wenn der
Quelltext den Schluß zulässt, dass es einer ist.
Ich hatte mal einen 8051 Code, der sich merkwürdig verhielt. Ich hab
dann gesehen, daß der Autor an einigen Stellen Pointerargumente auf 0
testet. Nun fängt aber beim 8051 XDATA bei 0 an, d.h. das ist eine
gültige Datenadresse. Ich hab dann in der Linkerzeile XDATA an 0x0001
anfangen lassen und das Programm lief wieder einwandfrei.
Rolf M. schrieb:>>> Warum sollte man nun also die Adresse 0 nicht nutzen können?>>>> Weil ich keinen Zeiger auf Adresse 0 konstruieren kann, der nicht>> gleichzeitig ein NULL-Zeiger ist. Da der Vergleich eines NULL-Zeigers>> und der Adresse 0 immer wahr sein muss, gilt das sogar unabhängig vom>> Bitmuster des NULL-Zeigers.>> Nein, das hab ich doch oben extra geschrieben: Zwei Nullzeiger müssen> immer Gleichheit ergeben. Ein Nullzeiger und ein Zeiger auf die Adresse> 0 nicht unbedingt.
Das ist korrekt. Andererseits ist ein (char*)0 per Definition ein
Nullzeiger, lässt sich also nicht von einem solchen unterscheiden.
Unabhängig davon, welches Bitmuster ein (char*)0 hat.
Auf einen Nullzeiger kann man mit if(!ptr) testen, äquivalent zu
if(ptr==0), also einem Vergleich mit der Zahl 0. Auch das ist unabhängig
vom Bitmuster des Nullzeigers.
> Du musst hier immer unterscheiden zwischen der C-Ebene, also dem, was im> Quellcode steht, und dem, was der Compiler daraus macht, also was> nachher für ein Bitmuster im Speicher steht.
Nein, ich habe doch mehrfach ausgeführt, dass diese Argumentation vom
Bitmuster des Zeigers unabhängig ist. Ein Vergleich eines Nullzeigers
mit der Zahl 0 ist per Definition wahr.
> Man muss sich von dem Gedanken lösen, dass die Konstante 0> auf C-Ebene im Zeigerkontext bedeutet, dass das Adresse 0 wäre.
Ein Nullzeiger hat das gleiche Bitmuster wie ein Zeiger auf die Adresse
0. Daraus folgt nicht, dass ein Zeiger auf die Adresse 0 nur aus
Nullbits besteht.
> Mit "Adresse 0" meine ich hier eine Adresse, die ausschließlich aus> 0-Bits besteht, nicht die, die man bekommt, wenn man in C die> Integer-Konstante 0 in einen Zeiger konvertiert.
Sicherlich, wenn ich mir "Adresse 0" als etwas bestimmtes definiere,
während ich "das, was ich bekomme, wenn ich einen Zeiger auf Adresse 0"
als etwas unbestimmtes definiere, dann kann ich mir auch immer einen
Fall konstruieren, indem sie unterschiedlich sind.
Der Punkt ist, dass ein Zeiger auf Adresse 0 (egal, wo sie im Speicher
liegt und wie ihr Bitmuster aussieht) immer ein Nullzeiger ist, damit
per Definition ein ungültiger Zeiger, und daraus folgt, dass die Adresse
0 nicht nutzbar ist.
>> Damit ist jeder Versuch, einen beliebigen Zeiger auf Adresse 0 zu>> dereferenzieren, per Definition undefined behaviour.>> Ich sehe da keinen Grund dafür in dem Fall, dass ein Nullzeiger nicht> auf die Adresse 0 zeigt.
Er muss auf die Adresse 0 zeigen, aber kein Bitmuster aus Nullen haben.
>> Falls ich unrecht habe, bitte ich um ein Gegenbeispiel. Bevorzugt eines,>> welches auf Nicht-Transputer-Systemen funktioniert.>> Stellen wir uns mal vor, der Compiler definiert, dass ein NULL-Zeiger im> Speicher mit dem Wert 0x1234 abgelegt wird. Ich schreibe NULL in einen> Zeiger, dann steht also 0x1234 drin.
Ich schreibe (char*)0 in den Zeiger, dann steht da auch 0x1234 drin.
Sonst wäre ja ein Vergleich mit 0 (bzw. auf dessen Ungleichheit) niemals
wahr.
> Ich vergleiche ihn mit NULL, dann> wird daraus auf Assembler-Ebene ein Vergleich mit 0x1234 -> gleich. Mit> der Adresse 0 hat das gar nichts zu tun.
Doch, weil du Adresse 0 als 0x1234 definiert hast.
> Wenn wir z.B. nun weiterhin annehmen, dass der Compiler> den Heap direkt am Anfang des Adressraums beginnen lässt,> könnte der erste Aufruf von malloc die Adresse 0 zurückgeben.
Wenn ich diese in einem intptr_t wandle, steht da also eine 0 drin.
Ein Vergleich dieses intptr_t mit dem Zeiger muss wahr sein, weil if(!x)
äquivalent zu if(x==0) ist.
> Die kann ich dann einfach dereferenzieren und benutzen. Ein> Vergleich mit NULL, also dem internen Wert 0x1234 ergibt auch keine> Gleichheit. Wo soll da undefiniertes Verhalten herkommen?
Die einzige Möglichkeit, dass das funktioniert, wäre, wenn malloc() zwar
einen Zeiger auf die Adresse 0 zurückgibt, dieser aber ungleich 0 ist.
Das ändert übrigens nichts daran, dass ich nach einem Beispiel gefragt
habe, was irgendwo funktioniert (bevorzugt auf Nicht-Transputern). Deins
war nur rein theoretisch. :-)
Heiko L. schrieb:> Einen Nullpointer zu dereferenzieren ist genau dann UB, wenn der> Quelltext den Schluß zulässt, dass es einer ist.
Nein, das ist noch viel schlimmer.
Einen Nullpointer zu dereferenzieren, ist immer UB, und zwar schon
bevor das Programm überhaupt gestartet wurde. Es gibt keine Garantien,
dass vor einem UB auftretende Effekte stattfinden.
Darum ist das Verhalten, dass ein Segfault auch die vor dem Segfault
stattfindenden printf()'s frisst, legal.
S. R. schrieb:> Darum ist das Verhalten, dass ein Segfault auch die vor dem Segfault> stattfindenden printf()'s frisst, legal.
Das hat aber nichts mit der Sprache, sondern mit dem Betriebssystem zu
tun. Und auch eigentlich nichts mit dem Nullpointer. Immer, wenn das
Betriebssystem einen Fault generiert kann das passieren. Auch, wenn die
Sprache da einen garantiert zulässigen Zugriff sähe.
1
static int global;
2
....
3
printf("test");
4
global=1;
kann zu so einer Situation führen, wenn man das Linker-Script ein
bisschen tweaked.
S. R. schrieb:> Einen Nullpointer zu dereferenzieren, ist immer UB, und zwar schon> bevor das Programm überhaupt gestartet wurde.
Und genau das gilt nur, wenn sich das aus dem Quelltext herleiten lässt.
1
((char*)0) = 1;
ist UB.
1
char *x = my_magic_function();
2
*x = 1;
ist NIEMALS ub.
Ganz einfach deshalb, weil x nie ein Nullpointer per Definition sein
kann. Er kann nur wie einer aussehen...
S. R. schrieb:> Er muss auf die Adresse 0 zeigen,
Gibt es dafür einen Beleg? "char *p=0;" ist halt etwas ganz anderes als
"char *p=4;", schalt mal Warnungen ein.
Prinzipiell ist ((int *) 0) etwas anderes als 0, das erste ist ein
int-Pointer auf die Adress 0, das zweite ein Nullpointer. Und die müssen
nichtmal gleich sein! 0 muss nur mit ((void*) 0) gleich sein!
Zwar ist es weit verbreitet, dass der Nullpointer genauso aussieht wie
ein Pointer auf Adress 0, aber nur weil sie gleich aussehen (und deshalb
z.B. Lib-Funktionen die Freiheit UB haben) sind sie nicht das selbe.
S. R. schrieb:>> Nein, das hab ich doch oben extra geschrieben: Zwei Nullzeiger müssen>> immer Gleichheit ergeben. Ein Nullzeiger und ein Zeiger auf die Adresse>> 0 nicht unbedingt.>> Das ist korrekt. Andererseits ist ein (char*)0 per Definition ein> Nullzeiger, lässt sich also nicht von einem solchen unterscheiden.
Dass man einen Nullzeiger nicht von einem Nullzeiger unterscheiden kann,
ist offensichtlich.
> Auf einen Nullzeiger kann man mit if(!ptr) testen, äquivalent zu> if(ptr==0), also einem Vergleich mit der Zahl 0. Auch das ist unabhängig> vom Bitmuster des Nullzeigers.
Richtig.
>> Du musst hier immer unterscheiden zwischen der C-Ebene, also dem, was im>> Quellcode steht, und dem, was der Compiler daraus macht, also was>> nachher für ein Bitmuster im Speicher steht.>> Nein, ich habe doch mehrfach ausgeführt, dass diese Argumentation vom> Bitmuster des Zeigers unabhängig ist. Ein Vergleich eines Nullzeigers> mit der Zahl 0 ist per Definition wahr.
Ja eben. Genau deshalb muss man das ja sauber trennen.
>> Man muss sich von dem Gedanken lösen, dass die Konstante 0>> auf C-Ebene im Zeigerkontext bedeutet, dass das Adresse 0 wäre.>> Ein Nullzeiger hat das gleiche Bitmuster wie ein Zeiger auf die Adresse> 0.
C definiert einen Nullzeiger nicht als einen Zeiger auf Adresse 0. Es
sagt lediglich, dass eine Integerkonstante mit dem Wert 0 im
Zeigerkontext eine Spezialbedeutung hat, nämlich dass sie zu einem
Nullzeiger wird.
Wenn ich (char*)1 - 1 schreibe, muss dabei kein Nullzeiger rauskommen.
Und wie ich oben schon geschrieben habe, darf selbst
1
inti=0;
2
char*c=(char*)i;
einen anderen Zeigerwert ergeben als ein
1
char*c=(char*)0;
denn nur die Konvertierung einer Integer-*Konstanten* mit dem Wert 0 in
einen Zeigertyp muss zu einem Nullzeiger führen. Da i keine Konstante
ist, gilt das dafür nicht.
> Daraus folgt nicht, dass ein Zeiger auf die Adresse 0 nur aus> Nullbits besteht.
Naja, dann hast du eine andere Definition des Begriffs "Adresse 0" als
ich. Deshalb hab ich im letzten Post dazugeschrieben, welche ich meine:
>> Mit "Adresse 0" meine ich hier eine Adresse, die ausschließlich aus>> 0-Bits besteht, nicht die, die man bekommt, wenn man in C die>> Integer-Konstante 0 in einen Zeiger konvertiert.>> Sicherlich, wenn ich mir "Adresse 0" als etwas bestimmtes definiere,> während ich "das, was ich bekomme, wenn ich einen Zeiger auf Adresse 0"> als etwas unbestimmtes definiere, dann kann ich mir auch immer einen> Fall konstruieren, indem sie unterschiedlich sind.
Adresse 0 als die Adresse, die aus dem Nullzeiger kommt, zu definieren,
ist ja auch nicht wirklich sinnvoll. Wenn z.B. der Nullzeiger den Wert
0x1000 hat, dann wäre also Adresse 0 bei 0x1000, Adresse 1 dann
plötzlich bei 1.
Ich (bzw. ISO C) definiere, dass eine Integerkonstante mit dem Wert 0
bei Konvertierung in einen Zeigertyp eine Sonderrolle einnimmt, die aber
nur für genau diesen einen Fall gilt. Statt den Wert 0 hier zu
verwenden, hätte man lieber ein eigenes Schlüsselwort "nullptr"
spendieren sollen, so wie es C++ inzwischen getan hat. Dann wäre klarer,
dass das nichts, aber auch gar nichts mit Adresse 0 zu tun haben muss.
> Der Punkt ist, dass ein Zeiger auf Adresse 0 (egal, wo sie im Speicher> liegt und wie ihr Bitmuster aussieht) immer ein Nullzeiger ist, damit> per Definition ein ungültiger Zeiger, und daraus folgt, dass die Adresse> 0 nicht nutzbar ist.
Ja gut, für deine Definition von "Adresse 0" gilt das.
>>> Damit ist jeder Versuch, einen beliebigen Zeiger auf Adresse 0 zu>>> dereferenzieren, per Definition undefined behaviour.>>>> Ich sehe da keinen Grund dafür in dem Fall, dass ein Nullzeiger nicht>> auf die Adresse 0 zeigt.>> Er muss auf die Adresse 0 zeigen, aber kein Bitmuster aus Nullen haben.
Die Adresse ist doch das Bitmuster.
>>> Falls ich unrecht habe, bitte ich um ein Gegenbeispiel. Bevorzugt eines,>>> welches auf Nicht-Transputer-Systemen funktioniert.>>>> Stellen wir uns mal vor, der Compiler definiert, dass ein NULL-Zeiger im>> Speicher mit dem Wert 0x1234 abgelegt wird. Ich schreibe NULL in einen>> Zeiger, dann steht also 0x1234 drin.>> Ich schreibe (char*)0 in den Zeiger, dann steht da auch 0x1234 drin.
Ja natürlich. NULL ist das selbe wie (char*)0. Es ist nicht weiter als
ein Makro, das vom Präprozessor entweder durch (char*)0 oder durch 0
ersetzt wird.
> Sonst wäre ja ein Vergleich mit 0 (bzw. auf dessen Ungleichheit) niemals> wahr.
Ja.
>> Wenn wir z.B. nun weiterhin annehmen, dass der Compiler>> den Heap direkt am Anfang des Adressraums beginnen lässt,>> könnte der erste Aufruf von malloc die Adresse 0 zurückgeben.>> Wenn ich diese in einem intptr_t wandle, steht da also eine 0 drin.
Ja.
> Ein Vergleich dieses intptr_t mit dem Zeiger muss wahr sein, weil if(!x)> äquivalent zu if(x==0) ist.
Man kann in C einen Zeiger nicht direkt mit einem Integer vergleichen.
Zunächst muss eins davon in den Typ des anderen konvertiert werden.
>> Die kann ich dann einfach dereferenzieren und benutzen. Ein>> Vergleich mit NULL, also dem internen Wert 0x1234 ergibt auch keine>> Gleichheit. Wo soll da undefiniertes Verhalten herkommen?>> Die einzige Möglichkeit, dass das funktioniert, wäre, wenn malloc() zwar> einen Zeiger auf die Adresse 0 zurückgibt, dieser aber ungleich 0 ist.
Ja, ein Vergleich mit (void*)0 ergibt Ungleichheit, da (void*)0 eben
nicht unbedingt auf Adresse 0 zeigen muss, auch wenn hier im Quelltext
eine 0 steht.
Übrigens stehe ich mit der Definition nicht ganz alleine da. Siehe
https://en.wikipedia.org/wiki/Null_pointer , wo folgender Satz steht:
"The C standard does not say that the null pointer is the same as the
pointer to memory address 0, though that may be the case in practice."
> Das ändert übrigens nichts daran, dass ich nach einem Beispiel gefragt> habe, was irgendwo funktioniert (bevorzugt auf Nicht-Transputern). Deins> war nur rein theoretisch. :-)
Dieses Beispiel existiert prinzipbedingt natürlich nur für Compiler, auf
denen ein Nullzeiger nicht nur aus 0-Bits besteht. Also was erwartest
du?
> Einen Nullpointer zu dereferenzieren, ist immer UB, und zwar schon> bevor das Programm überhaupt gestartet wurde.
Sagen wir mal: So gut wie immer. Für den Operator sizeof ist garantiert,
dass er bis auf VLAs den übergebenen Operanden nicht evaluiert und damit
auch im Falle eines Zeiger nicht dereferenziert.
Für
1
char*p=0;
2
sizeof*p;
ist also garantiert, dass es kein UB auslöst.
> Es gibt keine Garantien, dass vor einem UB auftretende Effekte> stattfinden.
Wenn UB im Spiel ist, gibt es überhaupt keine Garantieren mehr. Nicht
eine einzige. Man google mal nach "nasal demons".
Heiko L. schrieb:> ((char*)0) = 1;> ist UB.
Das ist kein UB, sondern löst einen Compiler-Fehler aus.
Du meintest vermutlich
1
*((char*)0)=1;
> char *x = my_magic_function();> *x = 1;> ist NIEMALS ub.> Ganz einfach deshalb, weil x nie ein Nullpointer per Definition sein> kann. Er kann nur wie einer aussehen...
Das kann ich nicht nachvollziehen. Wenn my_magic_function() so aussieht:
1
char*my_magic_function()
2
{
3
returnNULL;
4
}
dann ist x selbstverständlich ein Nullzeiger. In beiden Beispielen hast
du einen Nullzeiger.
Achim S. schrieb:> Prinzipiell ist ((int *) 0) etwas anderes als 0, das erste ist ein> int-Pointer auf die Adress 0, das zweite ein Nullpointer.
Das ist falsch.
> Und die müssen nichtmal gleich sein! 0 muss nur mit ((void*) 0) gleich> sein!
Nein. Der Zieltyp des Zeigers ist egal. Das ergibt sich aus folgender
Passage aus Kapitel 6.3.2.3:
********************************************
An integer constant expression with the value 0, or such an expression
cast to type void *, is called a null pointer constant. If a null
pointer constant is converted to a pointer type, the resulting pointer,
called a null pointer, is guaranteed to compare unequal to a pointer to
any object or function.
Conversion of a null pointer to another pointer type yields a null
pointer of that type. Any two null pointers shall compare equal.
********************************************
(int*)0 ist also keine Nullzeigerkonstante, aber dennoch ein Nullzeiger,
und ein Vergleich mit einem beliebigen anderen Nullzeiger muss
Gleichheit ergeben.
Rolf M. schrieb:> Das kann ich nicht nachvollziehen.
Steht weiter oben, und auch in dem Post, was gemeint ist.
>Wenn my_magic_function() so aussieht:
Weitere Fakten hinzufügen verändert selbstverständlich die Menge an
zulässig erschließlichen Aussagen, meinst du nicht?
Es wird eine Funktion aufgerufen, deren Implementierung nicht bekannt
ist.
Heiko L. schrieb:> Rolf M. schrieb:>> Das kann ich nicht nachvollziehen.> Steht weiter oben, und auch in dem Post, was gemeint ist.>>>Wenn my_magic_function() so aussieht:> Weitere Fakten hinzufügen verändert selbstverständlich die Menge an> zulässig erschließlichen Aussagen, meinst du nicht?
Es kann sie verringern, aber nicht erweitern.
> Es wird eine Funktion aufgerufen, deren Implementierung nicht bekannt> ist.
Wenn die Funktion einen Nullzeiger zurückliefern kann, wenn ihre
Implementation bekannt ist, dann kann sie es auch, wenn die
Implementation unbekannt ist.
Rolf M. schrieb:> Heiko L. schrieb:>> Rolf M. schrieb:>>> Das kann ich nicht nachvollziehen.>> Steht weiter oben, und auch in dem Post, was gemeint ist.>>>>>Wenn my_magic_function() so aussieht:>> Weitere Fakten hinzufügen verändert selbstverständlich die Menge an>> zulässig erschließlichen Aussagen, meinst du nicht?>> Es kann sie verringern, aber nicht erweitern.
Unsinn.
>> Es wird eine Funktion aufgerufen, deren Implementierung nicht bekannt>> ist.>> Wenn die Funktion einen Nullzeiger zurückliefern kann, wenn ihre> Implementation bekannt ist, dann kann sie es auch, wenn die> Implementation unbekannt ist.
Keinen Nullpointer per Definition - das ist der Punkt. Der Pointer
könnte nur später zu einem werden
Heiko L. schrieb:>> Es kann sie verringern, aber nicht erweitern.> Unsinn.
Wenn ich mehr Wissen habe, gibt es weniger Möglichkeiten.
Heiko L. schrieb:> Keinen Nullpointer per Definition - das ist der Punkt.
Ich hab ehrlich gesagt keine Idee, was du damit meinst. Er wird doch
nicht zum Nullpointer dadurch, dass ich prüfe, ob er einer ist.
Die Definition ist, dass ein Nullzeiger durch Konvertierung einer
Nullzeigerkonstante in einen Zeigertyp entsteht. Wenn ich nicht weiß, ob
die Funktion eine solche Konvertierung macht, weiß ich auch nicht, ob
ein Nullzeiger zurückkommt.
> Der Pointer könnte nur später zu einem werden> void* p = my_magic_function();> if(!p) {> // jetzt ist p ein Nullpointer
Wenn du hier reinkommst, war er es aber vorher auch schon.
> } else {> // hier nicht> }> :)
Heiko L. schrieb:>> Darum ist das Verhalten, dass ein Segfault auch die vor dem Segfault>> stattfindenden printf()'s frisst, legal.>> Das hat aber nichts mit der Sprache, sondern mit dem Betriebssystem zu> tun. Und auch eigentlich nichts mit dem Nullpointer. Immer, wenn das> Betriebssystem einen Fault generiert kann das passieren.
Wenn die Programmiersprache garantiert, dass ein Effekt auftreten muss
(printf, fwrite), bevor ein folgender Effekt auftreten darf
(Segfault), dann darfst du dich darauf auch verlassen.
In C ist es legal, dass ein zur Laufzeit auftretendes UB sämtliche
vorherigen Ergebnisse vernichten darf, einschließlich aller offenen
Dateien, dem Leben, dem Universum und dem ganzen Rest. Das hat nichts
mit dem Betriebssystem zu tun, das nutzt diesen Umstand nur aus.
Heiko L. schrieb:>> Einen Nullpointer zu dereferenzieren, ist immer UB, und zwar schon>> bevor das Programm überhaupt gestartet wurde.>> Und genau das gilt nur, wenn sich das aus dem Quelltext herleiten lässt.
Nein, Nullzeiger können auch zur Laufzeit auftreten. Dann ist es
trotzdem UB, sie zu dereferenzieren. Ungültige Zeiger sind immer
ungültig, nicht nur zur Compilezeit.
Rolf M. schrieb:> Dieses Beispiel existiert prinzipbedingt natürlich nur für Compiler, auf> denen ein Nullzeiger nicht nur aus 0-Bits besteht.
Also doch eine rein theoretische Diskussion mit garantiert keiner
praktischen Relevanz. ;-)
Rolf M. schrieb:>> Einen Nullpointer zu dereferenzieren, ist immer UB, und zwar schon>> bevor das Programm überhaupt gestartet wurde.>> Sagen wir mal: So gut wie immer. Für den Operator sizeof ist garantiert,> dass er bis auf VLAs den übergebenen Operanden nicht evaluiert und damit> auch im Falle eines Zeiger nicht dereferenziert.
Womit das Dereferenzieren also doch immer UB ist (und sizeof nicht
dereferenziert, obwohl man's hinschreibt). ;-)
Deinen restlichen Ausführungen kann ich folgen. Danke dafür.
S. R. schrieb:> Rolf M. schrieb:>> Dieses Beispiel existiert prinzipbedingt natürlich nur für Compiler, auf>> denen ein Nullzeiger nicht nur aus 0-Bits besteht.>> Also doch eine rein theoretische Diskussion mit garantiert keiner> praktischen Relevanz. ;-)
Naja, wenn du die real existierenden Systeme, auf denen es das gibt,
ausschließt, bleiben natürlich nur hypothetische übrig.
Aber ja, bei nahezu allen gängigen Compilern wird soweit ich weiß NULL
als Zeiger ausschließlich aus 0-Bits dargestellt, da es viele Dinge
vereinfacht und vermutlich die meisten Programmierer ihre Programme in
der Annahme schreiben, dass es so ist. Es ist also gewissermaßen auch
ein "Teufelskreis": Das Thema ist eher akademischer Natur, da praktisch
keine Plattform es anders macht, was aber mit daran liegt, dass viele
gar nicht wissen, dass es auch anders möglich wäre und die, die es
wissen, es als "akademisches Thema" abtun. ;-)
Rolf M. schrieb:> Naja, wenn du die real existierenden Systeme, auf denen es das gibt,> ausschließt, bleiben natürlich nur hypothetische übrig.
Stimmt. Wobei das mit dem Einerkomplement genauso ist... es ist halt
nachteilig, von der optimalen (für gegebenes Problem) Lösung
abzuweichen, also macht das keiner mehr.
Rolf M. schrieb:> Wenn ich mehr Wissen habe, gibt es weniger Möglichkeiten.
Von nichts kommt nichts.
Rolf M. schrieb:> Wenn du hier reinkommst, war er es aber vorher auch schon.
Das kann man vor dem Vergleich aber nicht wissen. Für mich ist
1
void* p = my_magic_function();
nur irgendein pointer. Um es anders zu formulieren: Nach der Zeile dort
ist die Aussage "p ist ein Nullpointer" nicht positiv aus definierten
Begriffen deduzierbar. Im if-case ist das nicht mehr der Fall.
S. R. schrieb:> Wenn die Programmiersprache garantiert, dass ein Effekt auftreten muss> (printf, fwrite), bevor ein folgender Effekt auftreten darf> (Segfault), dann darfst du dich darauf auch verlassen.
Nur in Sonderfällen. Das Betriebssystem kann den Thread oder Prozess
beenden in dem ein Programm läuft, man kann die Hardware anweisen, einen
Reset durchzuführen, man kann den Instruction-Pointer sonstwohin setzen
lassen.
Der einzige Fall, in dem der C-Standard eine hinreichende Garantie
abgiebt, ist die Theorie in einem Internet-Forum.
S. R. schrieb:> Nein, Nullzeiger können auch zur Laufzeit auftreten. Dann ist es> trotzdem UB, sie zu dereferenzieren.
Nur wenn durch einen Vergleich gezeigt wurde, dass es ein Nullpointer
ist.
Heiko L. schrieb:> Rolf M. schrieb:>> Wenn du hier reinkommst, war er es aber vorher auch schon.> Das kann man vor dem Vergleich aber nicht wissen.
Ja eben, das sage ich ja. Man kann nicht wissen, ob es ein Nullzeiger
ist. Du hattest aber oben im Gegensatz dazu behauptet, dass es keiner
sein kann.
> Um es anders zu formulieren: Nach der Zeile dort ist die Aussage "p ist> ein Nullpointer" nicht positiv aus definierten Begriffen deduzierbar.
Das lässt aber nicht den Schluss zu, dass p definitiv kein Nullzeiger
sein kann.
Heiko L. schrieb:>> Wenn du hier reinkommst, war er es aber vorher auch schon.> Das kann man vor dem Vergleich aber nicht wissen.
Dein Wissen ändert aber nichts daran, was in dem Zeiger steht. Der
Compiler macht natürlich das Verhalten des Programms nicht davon
abhängig, ob du weißt, was in dem Zeiger steht. Das ist völlig
unabhängig davon.
Heiko L. schrieb:> S. R. schrieb:>> Wenn die Programmiersprache garantiert, dass ein Effekt auftreten muss>> (printf, fwrite), bevor ein folgender Effekt auftreten darf>> (Segfault), dann darfst du dich darauf auch verlassen.>> Nur in Sonderfällen. Das Betriebssystem kann den Thread oder Prozess> beenden in dem ein Programm läuft, man kann die Hardware anweisen, einen> Reset durchzuführen, man kann den Instruction-Pointer sonstwohin setzen> lassen.
Ich würde den Normalbetrieb nicht unbedingt als Sonderfall betrachten.
> S. R. schrieb:>> Nein, Nullzeiger können auch zur Laufzeit auftreten. Dann ist es>> trotzdem UB, sie zu dereferenzieren.>> Nur wenn durch einen Vergleich gezeigt wurde, dass es ein Nullpointer> ist.
Nein, eine Dereferenzierung eines Nullzeigers ist immer UB, unabhängig
davon, ob du vorher geprüft hast, ob es einer ist.
Rolf M. schrieb:> Ja eben, das sage ich ja. Man kann nicht wissen, ob es ein Nullzeiger> ist. Du hattest aber oben im Gegensatz dazu behauptet, dass es keiner> sein kann.
Ja, richtig. Die Aussage zu machen "Es ist ein Nullpointer" ist
unzulässig, deshalb darf der Compiler nicht davon ausgehen :)
Rolf M. schrieb:> Das lässt aber nicht den Schluss zu, dass p definitiv kein Nullzeiger> sein kann.
Naja, doch: Der Wert der Expression in einem AST ist nicht NULL zu der
Zeit, also kann der Compiler nicht anfangen, undefinierten Code zu
erzeugen.
1
void *p = my_func();
2
*p = "This assignment will be translated normally";
Wenn man keinen Runtime-Checker mit einkompilieren lässt, ist das oben
ein normal laufendes Programm, auch dann, wenn p == NULL wäre. Das ist
in dem Beispiel, wo 0 (oder was immer) eine gültige Addresse ist,
durchaus drin.
Heiko L. schrieb:> Rolf M. schrieb:>> Ja eben, das sage ich ja. Man kann nicht wissen, ob es ein Nullzeiger>> ist. Du hattest aber oben im Gegensatz dazu behauptet, dass es keiner>> sein kann.>> Ja, richtig. Die Aussage zu machen "Es ist ein Nullpointer" ist> unzulässig, deshalb darf der Compiler nicht davon ausgehen :)
Soweit richtig. Der Compiler darf nicht davon ausgehen, was aber immer
noch nicht bedeutet, dass es keiner ist.
> Rolf M. schrieb:>> Das lässt aber nicht den Schluss zu, dass p definitiv kein Nullzeiger>> sein kann.>> Naja, doch: Der Wert der Expression in einem AST ist nicht NULL zu der> Zeit, also kann der Compiler nicht anfangen, undefinierten Code zu> erzeugen.
Der Compiler erzeugt keinen undefinierten Code, sondern der Code erzeugt
undefiniertes Verhalten, weil die Vorbedingung "Zeiger ist nicht NULL",
die für seine Ausführung erforderlich wäre, nicht gegeben ist. Dazu muss
der Compiler gar nicht wissen, was in dem Zeiger steht, also geht das
auch komplett zur Laufzeit.
Das ist genau wie z.B. bei einer Division durch 0. Der Compiler muss da
auch nicht wissen, dass der Divisor 0 ist damit das in die Hose geht.
Undefiniertes Verhalten ist in der Regel auch nicht etwas, das der
Compiler mit Absicht erzeugt. Also es steht im Compiler selbst nirgends
sowas wie
1
ifzeigerisNULLtheninvoke_undefined_behavior();
Der Compiler darf, wenn ich den Zeiger dereferenziere, davon ausgehen,
dass er nicht null ist. Das zeichnet undefiniertes Verhalten ja aus: Der
Compiler darf bestimmte Annahmen über das Programm treffen, und der
Programmierer ist dafür verantwortlich, dass das auch stimmt. Wenn er
das nicht tut und somit das Programm gegen diese Annahmen verstößt,
gibt's undefiniertes Verhalten, aber nicht, weil der Compiler dies
bewusst herbeiführt, sondern weil er einfach den Fall nicht
berücksichtigt hat. Wobei er natürlich nicht die Pflicht hat, den Fall
nicht zu berücksichtigen, aber er hat die Erlaubnis dazu.
> void *p = my_func();> *p = "This assignment will be translated normally";> Wenn man keinen Runtime-Checker mit einkompilieren lässt, ist das oben> ein normal laufendes Programm, auch dann, wenn p == NULL wäre.
Nein. Der Compiler erzeugt ein Programm, das unter der Annahme
ausgeführt wird, dass p nicht NULL ist. Genau deshalb gibt das ja
undefiniertes Verhalten, wenn es doch NULL ist. Es passiert irgendwas
mehr oder weniger zufälliges, weil die Eingangsdaten des Code nicht der
Vereinbarung entsprechen.
> Das ist in dem Beispiel, wo 0 (oder was immer) eine gültige Addresse ist,> durchaus drin.
Undefiniertes Verhalten heißt nicht, dass das Programm zwingend
abstürzen muss. Es kann auch das gewünschte Verhalten zeigen.
Undefiniert heißt, dass aus Sicht von ISO C alles, und zwar wirklich
alles passieren kann.
Rolf M. schrieb:>> Die Aussage zu machen "Es ist ein Nullpointer" ist unzulässig...> Soweit richtig. Der Compiler darf nicht davon ausgehen, was aber immer> noch nicht bedeutet, dass es keiner ist.
Doch das heißt es. Das folgt aus "tertium non datur".
Rolf M. schrieb:> Der Compiler erzeugt keinen undefinierten Code, sondern der Code erzeugt> undefiniertes Verhalten, weil die Vorbedingung "Zeiger ist nicht NULL",> die für seine Ausführung erforderlich wäre, nicht gegeben ist.
1. Es gibt keine Vorbedingungen für Zuweisungen sondern nur
vorbedingungen für undefiniertes Verhalten.
2. Sind diese Vorbedingungen nicht erfüllt. (s.o.)
Rolf M. schrieb:> Dazu muss der Compiler gar nicht wissen, was in dem Zeiger steht, also> geht das auch komplett zur Laufzeit.
Nope. Nirgendwo im Standard steht, dass eine Zuweisung einen
gespeicherten Wert ändert. Nicht, dass sie einen Vergleich durchführt.
Das könnte höchstens die abstrakte Maschine machen. Deren Eigenschaften
sind aber nicht Gegenstand der Definition.
Rolf M. schrieb:> Das ist genau wie z.B. bei einer Division durch 0. Der Compiler muss da> auch nicht wissen, dass der Divisor 0 ist damit das in die Hose geht.
Das muss es ja auch nicht.
Rolf M. schrieb:> Undefiniertes Verhalten ist in der Regel auch nicht etwas, das der> Compiler mit Absicht erzeugt.
Wenn etwas erwiesen undefiniertes Verhalten ist kann er machen was er
will. Ansonsten ist es idR ziemlich genau durch die konkreten
Eigenschaften der abstrakten Maschine bestimmt.
Rolf M. schrieb:> Also es steht im Compiler selbst nirgends> sowas wie "if zeiger is NULL then invoke_undefined_behavior();"
Hmm, vielleicht nicht buchstabengetreu...
Rolf M. schrieb:> Der Compiler darf, wenn ich den Zeiger dereferenziere, davon ausgehen, dass er
nicht null ist.
Nein, das muss er in diesem Fall - Die Nullpointer-Eigenschaft ist
positiv bestimmt.
Rolf M. schrieb:> Der Compiler darf bestimmte Annahmen über das Programm treffen, und der
Programmierer ist dafür verantwortlich, dass das auch stimmt.
Ja, richtig. Deshalb ist
Heiko L. schrieb:> Rolf M. schrieb:>>> Die Aussage zu machen "Es ist ein Nullpointer" ist unzulässig...>> Soweit richtig. Der Compiler darf nicht davon ausgehen, was aber immer>> noch nicht bedeutet, dass es keiner ist.>> Doch das heißt es. Das folgt aus "tertium non datur".
Der Compiler ist nicht die allwissende Macht. Nur weil der Compiler
etwas annimmt, ist das nicht automatisch eine Tatsache.
> Rolf M. schrieb:>> Der Compiler erzeugt keinen undefinierten Code, sondern der Code erzeugt>> undefiniertes Verhalten, weil die Vorbedingung "Zeiger ist nicht NULL",>> die für seine Ausführung erforderlich wäre, nicht gegeben ist.>> 1. Es gibt keine Vorbedingungen für Zuweisungen
Doch, natürlich gibt es die.
> sondern nur vorbedingungen für undefiniertes Verhalten.> 2. Sind diese Vorbedingungen nicht erfüllt. (s.o.)>> Rolf M. schrieb:>> Dazu muss der Compiler gar nicht wissen, was in dem Zeiger steht, also>> geht das auch komplett zur Laufzeit.> Nope. Nirgendwo im Standard steht, dass eine Zuweisung einen> gespeicherten Wert ändert. Nicht, dass sie einen Vergleich durchführt.
Ein Vergleich hat damit nichts zu tun. Du scheinst zwar zu glauben, dass
es irgendwie notwendig ist, einen Vergleich durchzuführen, damit das
Verhalten undefiniert wird. Das ist aber Unsinn. Nirgendwo in der
C-Definition steht etwas davon, dass das erforderlich wäre.
> Das könnte höchstens die abstrakte Maschine machen. Deren Eigenschaften> sind aber nicht Gegenstand der Definition.
Doch, selbstverständlich sind sie das. Die sind sogar der zentrale
Gegenstand der Definition. Was auch so drin steht (Kapitel 5.1.2.3
"Program Execution"):
"The semantic descriptions in this International Standard describe the
behavior of an abstract machine in which issues of optimization are
irrelevant."
> Rolf M. schrieb:>> Das ist genau wie z.B. bei einer Division durch 0. Der Compiler muss da>> auch nicht wissen, dass der Divisor 0 ist damit das in die Hose geht.> Das muss es ja auch nicht.
Doch, zumindest für Integers. Denn da gibt es keinen korrekten Wert für
das Ergebnis.
> Rolf M. schrieb:>> Undefiniertes Verhalten ist in der Regel auch nicht etwas, das der>> Compiler mit Absicht erzeugt.>> Wenn etwas erwiesen undefiniertes Verhalten ist kann er machen was er> will. Ansonsten ist es idR ziemlich genau durch die konkreten> Eigenschaften der abstrakten Maschine bestimmt.
Ja, und laut ISO C darf dann die machen, was sie will. -> undefiniertes
Verhalten.
> Rolf M. schrieb:>> Also es steht im Compiler selbst nirgends>> sowas wie "if zeiger is NULL then invoke_undefined_behavior();"> Hmm, vielleicht nicht buchstabengetreu...
Wäre ja auch Unsinn. Warum sollte der Compiler, wenn er doch weiß, dass
der Zeiger NULL ist, absichtlich dafür sorgen, dass das Verhalten
undefiniert ist? Die ganze Idee von undefiniertem Verhalten zielt genau
auf den Fall ab, in dem der Compiler sowas nicht weiß. Sie gibt ihm die
Erlaubnis, sich auch nicht darum zu kümmern, was dann möglicherweise
passieren könnte.
Rolf M. schrieb:> Der Compiler ist nicht die allwissende Macht. Nur weil der Compiler> etwas annimmt, ist das nicht automatisch eine Tatsache.
Das hat mit Allwissenheit nichts zu tun, sondern ist nur eine Frage
simpler Logik.
Rolf M. schrieb:> Ein Vergleich hat damit nichts zu tun. Du scheinst zwar zu glauben, dass> es irgendwie notwendig ist, einen Vergleich durchzuführen, damit das> Verhalten undefiniert wird. Das ist aber Unsinn. Nirgendwo in der> C-Definition steht etwas davon, dass das erforderlich wäre.
Das ist genau falsch herum gedacht. Undefiniertes Verhalten hat
Vorbedingungen, die erfüllt sein können oder auch nicht.
Rolf M. schrieb:> Doch, selbstverständlich sind sie das. Die sind sogar der zentrale> Gegenstand der Definition.
Und was steht dort, wie ein "update of the value" stattzufinden habe?
Ich finde da nichts von Belang. Vielleicht heißt das Ding deswegen
abstrakt.
Sonst würde in der Praxis ja nicht viel passieren.
Rolf M. schrieb:> Doch, zumindest für Integers. Denn da gibt es keinen korrekten Wert für> das Ergebnis.
Und was heißt das? UB, also überhaupt nichts.
Rolf M. schrieb:> Wäre ja auch Unsinn. Warum sollte der Compiler, wenn er doch weiß, dass> der Zeiger NULL ist, absichtlich dafür sorgen, dass das Verhalten> undefiniert ist?
Ja, das Frag mal, wenn der Wert von NULL außerhalb des C-Kosmos auf eine
gültige Addresse verwiese.
Rolf M. schrieb:> Die ganze Idee von undefiniertem Verhalten zielt genau> auf den Fall ab, in dem der Compiler sowas nicht weiß. Sie gibt ihm die> Erlaubnis, sich auch nicht darum zu kümmern, was dann möglicherweise> passieren könnte.
Das stimmt so einfach nicht. Positiv definiert undefiniertes Verhalten
hat den Sinn, dem Compiler optimierungen zu ermöglichen. Warum sollte
man Use-Cases durch überflüssige Definitionen ausschließen? Wenn 0 eine
normale Addresse ist, ist sie das halt.
Heiko L. schrieb:>> Wenn die Programmiersprache garantiert, dass ein Effekt auftreten muss>> (printf, fwrite), bevor ein folgender Effekt auftreten darf>> (Segfault), dann darfst du dich darauf auch verlassen.>> Nur in Sonderfällen. Das Betriebssystem kann den Thread oder Prozess> beenden in dem ein Programm läuft, man kann die Hardware anweisen, einen> Reset durchzuführen, man kann den Instruction-Pointer sonstwohin setzen> lassen.
Wenn mir die Programmiersprache für zwei Ereignisse X und Y garantiert,
dass Ereignis X immer vor Ereignis Y auftritt, dann darf ich davon
ausgehen, dass Ereignis X eingetreten ist, wann immer ich Ereignis Y
sehe.
Es gibt Programmiersprachen, die mir garantieren, dass ein Ereignis X
(Daten werden in eine Datei geschrieben) vor Ereignis Y (Programm stürzt
wegen Division durch Null ab) eintritt, und diese Garantien dürfen nicht
"mal eben so" vom Betriebssystem außer kraft gesetzt werden.
Wenn mir ein reales Betriebssystem jederzeit den Prozess aus beliebigen
Gründen wegschießen darf (OOM-Killer, Chaos-Monkey), ist es schlicht
unzuverlässig - und zwar genau so unzuverlässig, wie mir ein kaputtes
Auto keine vom Hersteller garantierte Funktion bereitstellt.
> Der einzige Fall, in dem der C-Standard eine hinreichende Garantie> abgiebt, ist die Theorie in einem Internet-Forum.
Nun, im Gegensatz zu so ziemlich allen anderen Programmiersprachen
garantiert C das oben beschriebene Verhalten nicht. Wenn irgendwann zur
Laufzeit ein UB auftritt, dann war das Programm schon vor dem Start
inkorrekt, also ist jegliches Verhalten des Programms dem Standard
entsprechend.
Heiko L. schrieb:> Das ist genau falsch herum gedacht. Undefiniertes Verhalten hat> Vorbedingungen, die erfüllt sein können oder auch nicht.Definiertes Verhalten hat Vorbedingungen. Undefiniertes Verhalten
tritt genau dann auf, wenn diese nicht erfüllt sind.
Analogie: Eine mathematische Funktion wird durch ihren
Definitionsbereich definiert, nicht durch "alle Bereiche, wo sie nicht
definiert ist".
Heiko L. schrieb:>> Doch, zumindest für Integers. Denn da gibt es keinen>> korrekten Wert für das Ergebnis.>> Und was heißt das? UB, also überhaupt nichts.
Doch: Es heißt, dass du genau keine Annahme über das gesamte
Programm treffen darfst. /Und es auch niemals durftest./ Auch dann, wenn
die Division erst zur Laufzeit eingetreten ist, weil der Benutzerdepp
eine Null ins falsche Formular eingetragen hat.
S. R. schrieb:> Wenn mir die Programmiersprache für zwei Ereignisse X und Y garantiert,> dass Ereignis X immer vor Ereignis Y auftritt, dann darf ich davon> ausgehen, dass Ereignis X eingetreten ist, wann immer ich Ereignis Y> sehe.
Klar - soweit die C-Insel betroffen ist. Wenn da steht
1
__asm_set_instruction_pointer();
2
printf("1");
3
printf("2");
sollte man über eine Erweiterung des Horizonts nachdenken.
S. R. schrieb:> Es gibt Programmiersprachen, die mir garantieren, dass ein Ereignis X> (Daten werden in eine Datei geschrieben) vor Ereignis Y (Programm stürzt> wegen Division durch Null ab) eintritt, und diese Garantien dürfen nicht> "mal eben so" vom Betriebssystem außer kraft gesetzt werden.
Für C trifft das jedenfalls nicht zu. Was soll das überhaupt heißen "in
eine Datei geschrieben"? Und was ist eine Datei? Wenn durch den
Fault-Handler ein Hardware-Reset ausgelöst wird, können die Daten dann
fehlerfrei wieder gelesen werden?
Also ich kenne keine Programmiersprache die so etwas garantieren könnte.
Ist auch nicht Thema der formalen Definition der Sprache.
Für C steht da halt sinngemäß: Wenn man flush aufgerufen hat oder
Buffering abgeschaltet hat sind die Daten an das "external file"
übergeben worden. Zur richtigen Zeit den Stecker ziehen wäre da ein
Härtetest, ob dein Ereignis X da echt so eingetreten ist, wie dein
(schlampiger) Wortlaut es zu implizieren versucht.
S. R. schrieb:> Wenn mir ein reales Betriebssystem jederzeit den Prozess aus beliebigen> Gründen wegschießen darf (OOM-Killer, Chaos-Monkey), ist es schlicht> unzuverlässig - und zwar genau so unzuverlässig, wie mir ein kaputtes> Auto keine vom Hersteller garantierte Funktion bereitstellt.
Für Aufgaben, wo das wichtig wäre, ist es so dann jedenfalls ungeeignet:
Dann gehört der Absturz eben in's Kalkül.
S. R. schrieb:> Definiertes Verhalten hat Vorbedingungen. Undefiniertes Verhalten> tritt genau dann auf, wenn diese nicht erfüllt sind.
Das stimmt so nicht. Er listet positive Bedingungen für UB auf.
Beispiel:
1
If any other characters are encountered in a source file (except in an identifier, a character constant, a string literal, a header name, a comment, or a preprocessing token that is never converted to a token), the behavior is undefined.
Das heißt "Bedingungen => UB", oder auch "UB oder not(Bedingungen)".
Hier müsste man allerdings erst einmal sauber erschließen, was die
Negation einer Bedingung bedeutet. In diesem einfachem Beispiel wäre es
keinesfalls genug zu sagen "no other characters were found" sei ein
Garant für definiertes Verhalten eines Programms. Man müsste alle
Bedingungen auflisten können. Das kann der Standard nicht, da er auf
"implementation defined behaviour" verweist, welches seinerseits einfach
sagen könnte: "undefined".
Zu diesem Problem siehe auch "schwache Negation".
In unserem einfachen Beispiel ist
1
char **p = my_func();
2
*p = "";
ein gültiger Ausschnitt eines C-Programs, soweit es den Compiler
betrifft. Schluß aus.
S. R. schrieb:> Heiko L. schrieb:>>> Doch, zumindest für Integers. Denn da gibt es keinen>>> korrekten Wert für das Ergebnis.>>>> Und was heißt das? UB, also überhaupt nichts.>> Doch: Es heißt, dass du genau keine Annahme über das gesamte> Programm treffen darfst.
Tue ich ja nicht. Meine Aussage in dem Kontext war: Kann laufen, kann
nicht laufen. Rolf meinte, dass es schief gehen müsse.
Heiko L. schrieb:> Rolf M. schrieb:>> Der Compiler ist nicht die allwissende Macht. Nur weil der Compiler>> etwas annimmt, ist das nicht automatisch eine Tatsache.>> Das hat mit Allwissenheit nichts zu tun, sondern ist nur eine Frage> simpler Logik.
Richtig. Und die Logik sagt: Die Wahrheit entsteht nicht durch Annahmen.
vielmehr können Annahmen der Wahrheit entsprechen - oder eben auch
nicht.
> Rolf M. schrieb:>> Doch, selbstverständlich sind sie das. Die sind sogar der zentrale>> Gegenstand der Definition.>> Und was steht dort, wie ein "update of the value" stattzufinden habe?> Ich finde da nichts von Belang.
Ein "update of the value" ist dazu nicht nötig. Das reine
Dereferenzieren reicht. In deinem obigen Beispiel wäre folgendes schon
ausreichend für undefiniertes Verhalten:
1
char*p=my_func();
2
*p;
"If an invalid value has been assigned to the pointer, the behavior of
the unary * operator is undefined."
> Vielleicht heißt das Ding deswegen abstrakt.
Sie heißt abstrakt wegen der so genannten as-if-Regel. Das Verhalten der
abstrakten Maschine ist in C genau definiert (abgesehen von den Stellen,
wo es undefiniert ist ;-) ), aber das physische System, auf dem das dann
läuft, muss nicht exakt das selbe tun, solange bestimmte Teil des
Verhaltens gleich bleiben. Durch Optimierungen können z.B. unnötige
Speicherzugriffe oder Berechnungen von Werten, die nachher nicht benutzt
werden, entfernt werden, die die abstrakte Maschine laut ISO C aber
durchführen müsste. Das ist in ISO C leider nicht so ausdrücklich und
einfach beschrieben wie in ISO C++.
> Rolf M. schrieb:>> Doch, zumindest für Integers. Denn da gibt es keinen korrekten Wert für>> das Ergebnis.>> Und was heißt das? UB, also überhaupt nichts.
Hab ich ja geschrieben: Genau wie bei Zeigern ist das Verhalten
undefiniert. Und zwar immer, egal ob vorher irgendwas geprüft worden ist
oder ob der Compiler den Wert kennt.
> Rolf M. schrieb:>> Wäre ja auch Unsinn. Warum sollte der Compiler, wenn er doch weiß, dass>> der Zeiger NULL ist, absichtlich dafür sorgen, dass das Verhalten>> undefiniert ist?>> Ja, das Frag mal, wenn der Wert von NULL außerhalb des C-Kosmos auf eine> gültige Addresse verwiese.
Ach, das hatten wir doch schon abgehakt.
> Rolf M. schrieb:>> Die ganze Idee von undefiniertem Verhalten zielt genau>> auf den Fall ab, in dem der Compiler sowas nicht weiß. Sie gibt ihm die>> Erlaubnis, sich auch nicht darum zu kümmern, was dann möglicherweise>> passieren könnte.>> Das stimmt so einfach nicht. Positiv definiert undefiniertes Verhalten> hat den Sinn, dem Compiler optimierungen zu ermöglichen.
Optimierungen werden dadurch auch ermöglicht, ja. Aber wenn der Compiler
eh schon weiß, dass der Zeiger NULL ist und damit das Programm nicht wie
vorgesehen funktionieren wird, was soll er dann noch optimieren? Den
Absturz?
Die Optimierung besteht da eher darin, dass er den Zugriff eben einfach
machen kann, ohne vorher prüfen zu müssen, ob der Zeiger auch wirklich
gültig ist. Um die Konsequenzen, wenn das nicht der Fall ist, muss er
sich nicht kümmern. Diese Fall muss nicht abgefangen werden.
Ich würde ich aber nochmal zu einer früheren Aussage fragen:
Heiko L. schrieb:> char *x = my_magic_function();> *x = 1;> ist NIEMALS ub.
Wenn das Verhalten wie du behauptest nicht undefiniert ist, wie sieht
denn dann das nach ISO C genau definierte Verhalten aus, wenn
my_magic_function() da einen Nullzeiger returniert?
Heiko L. schrieb:> Was soll das überhaupt heißen "in eine Datei geschrieben"? Und was ist> eine Datei?
Siehe Kapitel 7.9.13 "Files".
> Wenn durch den Fault-Handler ein Hardware-Reset ausgelöst wird, können> die Daten dann fehlerfrei wieder gelesen werden?
Das garantiert C natürlich nicht. Es gibt da zwei Möglichkeiten:
1 - Der Fehler wurde durch das Hervorrufen von undefiniertem Verhalten
des Programms ausgelöst - dann garantiert C sowieso gar nichts mehr.
2 - Der Fehler kam durch irgendetwas außerhalb des Programms zustande -
dann hat sich die Implementation nicht an die Regeln von ISO C gehalten.
Das sieht nämlich nicht vor, dass das System aufgrund von z.B. einem
Hardware-Fehler was anderes tut als es eigentlich sollte.
> Ist auch nicht Thema der formalen Definition der Sprache.> Für C steht da halt sinngemäß: Wenn man flush aufgerufen hat oder> Buffering abgeschaltet hat sind die Daten an das "external file"> übergeben worden.
Sie sind in einen Datenstrom geschrieben worden. Soweit ich weiß, wird
nirgends garantiert, dass das bedeutet, dass sie auf einem physischen
Gerät gespeichert worden sind.
S. R. schrieb:
> Wenn mir ein reales Betriebssystem jederzeit den Prozess aus beliebigen> Gründen wegschießen darf (OOM-Killer, Chaos-Monkey), ist es schlicht> unzuverlässig - und zwar genau so unzuverlässig, wie mir ein kaputtes> Auto keine vom Hersteller garantierte Funktion bereitstellt.
Das ist ungefähr so unzuverlässig, wie ein Auto, das, nachdem ich es in
den Graben gefahren habe, nicht mehr funktioniert.
> S. R. schrieb:>> Definiertes Verhalten hat Vorbedingungen. Undefiniertes Verhalten>> tritt genau dann auf, wenn diese nicht erfüllt sind.>> Das stimmt so nicht. Er listet positive Bedingungen für UB auf.> Beispiel:If any other characters
"any other" heißt hier soviel wie: Wenn alle vorher genannten
Bedingungen nicht zutreffen. Man hätte den Satz auch weglassen können
(da alles, was nicht explizit definiert wurde, natürlich undefiniert
ist), aber er wurde zur Klarheit noch dazu geschrieben.
> Das kann der Standard nicht, da er auf "implementation defined behaviour"> verweist, welches seinerseits einfach sagen könnte: "undefined".
Nein, kann es nicht. "implementation defined" heißt, dass ISO C der
Implementation mehrere Möglichkeiten gibt und die dann definiert, welche
davon sie nutzt. Das Verhalten muss aber reproduzierbar, wohldefiniert
und auch dokumentiert sein.
> In unserem einfachen Beispiel ist> char **p = my_func();> *p = "";> ein gültiger Ausschnitt eines C-Programs, soweit es den Compiler> betrifft. Schluß aus.
Für den Compiler ja, für die abstrakte Maschine nicht unbedingt.
> S. R. schrieb:>> Heiko L. schrieb:>>>> Doch, zumindest für Integers. Denn da gibt es keinen>>>> korrekten Wert für das Ergebnis.>>>>>> Und was heißt das? UB, also überhaupt nichts.>>>> Doch: Es heißt, dass du genau keine Annahme über das gesamte>> Programm treffen darfst.>> Tue ich ja nicht. Meine Aussage in dem Kontext war: Kann laufen, kann> nicht laufen. Rolf meinte, dass es schief gehen müsse.
Ja, wie oben steht: Ein gültiger Wert für das Ergebnis existiert in
diesem Fall nicht, also kann hier selbstverständlich kein gültiges
Ergebnis rauskommen.
Heiko L. schrieb:>> Wenn mir die Programmiersprache für zwei Ereignisse X und Y garantiert,>> dass Ereignis X immer vor Ereignis Y auftritt, dann darf ich davon>> ausgehen, dass Ereignis X eingetreten ist, wann immer ich Ereignis Y>> sehe.>> Klar - soweit die C-Insel betroffen ist.
Das ist Unfug. Wenn mir eine Maschine garantiert, dass Lichtschranke A
vor Lichtschranke B unterbrochen wurde, dann darf ich die Steuerung
davon ausgehen lassen, dass "Lichtschranke B unterbrochen" bedeutet,
dass Lichtschranke A vorher unterbrochen war.
Und wenn dem nicht so ist, dann ist die Maschine schlicht kaputt (bzw.
nicht mehr standardkonform), was wiederum UB ist und keine Aussage über
die Funktion zulässt.
Das hat mit C nichts zu tun.
Heiko L. schrieb:> Für C trifft das jedenfalls nicht zu. Was soll das überhaupt heißen "in> eine Datei geschrieben"? Und was ist eine Datei? Wenn durch den> Fault-Handler ein Hardware-Reset ausgelöst wird, können die Daten dann> fehlerfrei wieder gelesen werden?
Für C trifft das nicht zu. Das schrieb ich bereits.
Was "in eine Datei geschrieben" bedeutet, ist genau definiert, und zwar
an der gleichen Stelle, wo "was ist eine Datei" definiert wird. Das kann
in der Programmiersprache, im Laufzeitsystem oder in einer
unterstützenden Bibliothek der Fall sein.
Du hackst da auf einer Strohkrähe rum.
Heiko L. schrieb:> Also ich kenne keine Programmiersprache die so etwas garantieren könnte.
Java? C#? Rust? Kotlin? ObjC?
Heiko L. schrieb:> Zur richtigen Zeit den Stecker ziehen wäre da ein Härtetest,> ob dein Ereignis X da echt so eingetreten ist, wie dein> (schlampiger) Wortlaut es zu implizieren versucht.
Eine Grundvoraussetzung für die Funktion eines Computerprogramms ist,
dass der Computer fehlerfrei läuft. Im Übrigen sind gerade Datenbanken
darauf angewiesen, dass Ereignisse zumindest an bestimmten
Sequenzpunkten in der garantierten Reihenfolge aufgetreten sind. Das
Betriebssystem springt durch einige brennende Reifen, um genau dieses
Verhalten ebenfalls zu garantieren. Lies mal die Probleme, die ein neues
Dateisystem lösen muss, um performant und zuverlässig zu sein.
Heiko L. schrieb:> Das kann der Standard nicht, da er auf> "implementation defined behaviour" verweist,> welches seinerseits einfach sagen könnte: "undefined".
Das ist falsch. C definiert den Unterschied zwischen "implementation
defined" und "undefined" sogar sehr genau, im Gegensatz zu so ziemlich
allen anderen Sprachen.
Heiko L. schrieb:>>> Und was heißt das? UB, also überhaupt nichts.>>>> Doch: Es heißt, dass du genau keine Annahme über das gesamte>> Programm treffen darfst.>> Tue ich ja nicht. Meine Aussage in dem Kontext war: Kann laufen, kann> nicht laufen. Rolf meinte, dass es schief gehen müsse.
"UB" heißt, dass jedes mögliche Ergebnis im Sinne des Standards korrekt
ist. Wenig überraschend zählt auch "intended behaviour" dazu.
Ein kaputtes Auto erfüllt die Herstellergarantien nicht. Daraus
abzuleiten, dass und wie es noch fährt, erfordert zusätzliche Annahmen.
Die sind aber weder garantiert (sonst würde der Hersteller darüber eine
Aussage treffen) noch konstant.
Im Allgemeinen läuft es nicht.
Im Speziellen kann es laufen, unter gewissen Bedingungen
(Optimierungen abgeschaltet, bestimmte Compilerversion, etc).
Rolf M. schrieb:> Richtig. Und die Logik sagt: Die Wahrheit entsteht nicht durch Annahmen.> vielmehr können Annahmen der Wahrheit entsprechen - oder eben auch> nicht.
Und wenn für eine Annahme kein Grund besteht, nimmt man sie nicht an.
Rolf M. schrieb:> Siehe Kapitel 7.9.13 "Files".
Ja - und dieser Text heißt so viel wie "Also wir sind dann hier an
dieser Stelle fertig."
Rolf M. schrieb:> Wenn das Verhalten wie du behauptest nicht undefiniert ist, wie sieht> denn dann das nach ISO C genau definierte Verhalten aus, wenn> my_magic_function() da einen Nullzeiger returniert?
Also in genau dem Fall würde evtl. noch ein volatile oder eine
Speicherbarriere fehlen, weil der Code sonst ggf. entfernt werden wird.
Ich würde aber sagen, dass das Kompilat in dem Fall das enthalten muss,
was ein Werteupdate der abstrakten Maschine repräsentiert. Das wäre
nicht unbedingt der Fall, ließe sich a-priori auf undefiniertes
Verhalten schließen.
Rolf M. schrieb:> Aber wenn der Compiler> eh schon weiß, dass der Zeiger NULL ist und damit das Programm nicht wie> vorgesehen funktionieren wird, was soll er dann noch optimieren? Den> Absturz?
Wenn er schließen kann, dass er NULL ist, kann er "if(p)..."-Passagen
entfernen. Und Warnungen ausgeben, falls er auf UB schließen kann.
Rolf M. schrieb:>> Wenn durch den Fault-Handler ein Hardware-Reset ausgelöst wird, können>> die Daten dann fehlerfrei wieder gelesen werden?>> Das garantiert C natürlich nicht. Es gibt da zwei Möglichkeiten:>> 1 - Der Fehler wurde durch das Hervorrufen von undefiniertem Verhalten> des Programms ausgelöst - dann garantiert C sowieso gar nichts mehr.
Das ist a-priori korrekt.
Rolf M. schrieb:> 2 - Der Fehler kam durch irgendetwas außerhalb des Programms zustande -> dann hat sich die Implementation nicht an die Regeln von ISO C gehalten.> Das sieht nämlich nicht vor, dass das System aufgrund von z.B. einem> Hardware-Fehler was anderes tut als es eigentlich sollte.
Und das ist das Argument, warum man ein Programm selbstverständlich
inhaltlich betrachten muss, um einen "korrekten" Ablauf vorherzusagen.
Wenn in einem Windows Programm etwas á la
1
ExitProcess(0);
2
printf("Never get here!");
steht, ist nicht damit zu rechnen, dass das printf erreicht wird.
S. R. schrieb:> Das ist Unfug. Wenn mir eine Maschine garantiert, dass Lichtschranke A> vor Lichtschranke B unterbrochen wurde, dann darf ich die Steuerung> davon ausgehen lassen, dass "Lichtschranke B unterbrochen" bedeutet,> dass Lichtschranke A vorher unterbrochen war.
Nicht, wenn du weißt, dass du die Drähte vertauscht hast. ;)
Die Integration eines Softwaresystems in ein größeres und das
Zusammenwirken der Komponenten ist natürlich nichts, was sich innerhalb
einer Komponente bestimmen ließe. Wenn ich per Debugger mit den Werten
von Variablen herumspiele darf man nicht unbedingt davon ausgehen, das
Ergebnis würde sich nicht ändert. Außer man meint, eine Deduktion
a-priori aus einem unvollständigen wäre mit der Realität deckungsgleich.
S. R. schrieb:> Das hat mit C nichts zu tun.
Nee, richtig. C ist nur die Form eines Inhalts. In dem
ExitProcess-Beispiel oben ist nichts kaputt.
S. R. schrieb:> Eine Grundvoraussetzung für die Funktion eines Computerprogramms ist,> dass der Computer fehlerfrei läuft. Im Übrigen sind gerade Datenbanken> darauf angewiesen, dass Ereignisse zumindest an bestimmten> Sequenzpunkten in der garantierten Reihenfolge aufgetreten sind. Das> Betriebssystem springt durch einige brennende Reifen, um genau dieses> Verhalten ebenfalls zu garantieren. Lies mal die Probleme, die ein neues> Dateisystem lösen muss, um performant und zuverlässig zu sein.
Ja - und das sind Sachen, die der C-Standard nicht löst, lösen kann und
lösen will. Nach einem fflush sind die Daten jedenfalls nicht mehr in
einem Buffer der libc, sondern in einem "external File", was immer das
auch sein mag. A-priori ist auch eine Speicherbarriere kein Garant, dass
ein Event vor einem anderen auftritt, wenn danach noch UB auftritt -
geschenkt. Allderfings muss dieses UB dann seinerseits a-priori
deduzierbar sein. Ansonsten wäre das eine Sache der abstrakten Maschine.
Und da gibt es, ebenso wie bei Dateisystemen, solche und solche. Ganz zu
schweigen von Hardwaretreibern und was da noch alles so auf dem Weg
liegen mag.
Heiko L. schrieb:> Nicht, wenn du weißt, dass du die Drähte vertauscht hast. ;)
Du erwartest also ernsthaft, dass ein fehlerhaftes System korrekt
funktioniert? Mann, musst du ein Gottvertrauen haben...
Heiko L. schrieb:> Nach einem fflush sind die Daten jedenfalls nicht mehr in> einem Buffer der libc, sondern in einem "external File",> was immer das auch sein mag.
Und wenn ich nach dem fflush ein UB hervorrufe, ist das nicht mehr
garantiert, denn dann war es nie garantiert.
Ich weiß ja nicht, warum du ständig auf dem a-priori rumreitest. Ein UB
zur Laufzeit invalidiert rückwirkend sämtliche Ergebnisse, die du
hättest haben können.
Heiko L. schrieb:> Und das ist das Argument, warum man ein Programm selbstverständlich> inhaltlich betrachten muss, um einen "korrekten" Ablauf vorherzusagen.
Das wäre dann äquialent zum Halteproblem.
> Wenn in einem Windows Programm etwas á la> ExitProcess(0);> printf("Never get here!");> steht, ist nicht damit zu rechnen, dass das printf erreicht wird.
Doch, damit ist zu rechnen (z.B. könnte ExitProcess fehlschlagen oder
eine schlecht benamte MsgBox sein). Der Standard kennt ExitProcess
nicht, also ist der Aufruf keine hinreichende Bedingung für "der Code
endet hier".
Heiko L. schrieb:> Rolf M. schrieb:>> Richtig. Und die Logik sagt: Die Wahrheit entsteht nicht durch Annahmen.>> vielmehr können Annahmen der Wahrheit entsprechen - oder eben auch>> nicht.>> Und wenn für eine Annahme kein Grund besteht, nimmt man sie nicht an.
Es gibt ja einen: Der Programmierer hat garantiert, dass der Zeiger
nicht NULL ist. Da so ein Programmierer allerdings ein inhärent
fehlerbehaftetes Element ist, kann die Annahme eben trotzdem falsch
sein.
Wenn man immer nur Sachen annehmen würde, die garantiert wahr sind,
wären es ja auch keine Annahmen mehr.
> Rolf M. schrieb:>> Wenn das Verhalten wie du behauptest nicht undefiniert ist, wie sieht>> denn dann das nach ISO C genau definierte Verhalten aus, wenn>> my_magic_function() da einen Nullzeiger returniert?>> Also in genau dem Fall würde evtl. noch ein volatile oder eine> Speicherbarriere fehlen, weil der Code sonst ggf. entfernt werden wird.> Ich würde aber sagen, dass das Kompilat in dem Fall das enthalten muss,> was ein Werteupdate der abstrakten Maschine repräsentiert. Das wäre> nicht unbedingt der Fall, ließe sich a-priori auf undefiniertes> Verhalten schließen.
Es gibt in ISO C vier Möglichkeiten, was passieren kann:
1. Exakt das, was in ISO C selbst definiert ist.
2. "unspecified". Dann muss der Compiler das Verhalten aus einer von
mehreren Möglichkeiten festlegen.
3. "implementation defined". Wie 2, nur dass es auch dokumentiert sein
muss.
4. "undefined". Irgendwas in ISO C nicht weiter beschriebenes kann
passieren.
Mehr Möglichkeiten lässt die Sprachdefinition nicht offen.
Variante 1 ist natürlich genau in ISO C beschrieben, Variante 2 und 3
werden auch explizit erwähnt. Ansonsten gilt Variante 4 (übrigens auch,
wenn nichts anderes explizit angegeben ist, denn was nicht definiert
ist, ist eben undefiniert).
Wenn das Verhalten nicht undefined ist, dann muss es eine der drei
anderen Varianten sein. Meine Frage wäre also, welche davon es deiner
Meinung nach ist und in welcher Passage der C-Definition das steht.
> S. R. schrieb:>> Das ist Unfug. Wenn mir eine Maschine garantiert, dass Lichtschranke A>> vor Lichtschranke B unterbrochen wurde, dann darf ich die Steuerung>> davon ausgehen lassen, dass "Lichtschranke B unterbrochen" bedeutet,>> dass Lichtschranke A vorher unterbrochen war.>> Nicht, wenn du weißt, dass du die Drähte vertauscht hast. ;)
Auch hier ändert sich das Verhalten der Maschine nicht abhängig davon,
ob du weißt, dass sie falsch verdrahtet ist oder nicht. Sie arbeitet
basierend auf der Annahme, dass alles richtig verdrahtet ist. So wie der
Compiler basierend auf der Annahme arbeitet, dass du ihm keinen
Nullzeiger zum dereferenzieren gibst.
> Die Integration eines Softwaresystems in ein größeres und das> Zusammenwirken der Komponenten ist natürlich nichts, was sich innerhalb> einer Komponente bestimmen ließe. Wenn ich per Debugger mit den Werten> von Variablen herumspiele darf man nicht unbedingt davon ausgehen, das> Ergebnis würde sich nicht ändert.
Wenn die abstrakte Maschine sich nicht so verhält, wie es die
Sprachdefinition verlangt, wird das Programm natürlich auch nicht das
Verhalten zeigen, das von ihr sonst garantiert wird.
Ob jetzt kaputter Computer, OOM-Kill oder das Verändern von Variablen
per Debugger, spielt da keine Rolle.
Rolf M. schrieb:> Wenn man immer nur Sachen annehmen würde, die garantiert wahr sind,> wären es ja auch keine Annahmen mehr.
Deswegen ist die Negation ja auch schwach. Dennoch ist das der Schluß,
den die Sachlage zulässt:
Rolf M. schrieb:> Meine Frage wäre also, welche davon es deiner> Meinung nach ist und in welcher Passage der C-Definition das steht.
Da steht, dass der Zuweisungsoperator ein Update des Wertes der linken
Seite durchführt. Und genau das wird ein Compiler an der Stelle auch als
Maschinencode emitieren.
Rolf M. schrieb:> Auch hier ändert sich das Verhalten der Maschine nicht abhängig davon,> ob du weißt, dass sie falsch verdrahtet ist oder nicht.
Was heißt hier falsch? Wir sind doch gerade dabei, die Steuerlogik zu
schreiben... Da sollte man davon ausgehen, dass das einen Unterschied
macht - oder pflegst Du Anmerkungen von Kollegen a lá "Wir haben da
übrigens die Pinbelegung geändert" zu ignorieren, weil dein Wissen darum
"ja sowieso keinen Unterschied macht"?
Rolf M. schrieb:> Wenn die abstrakte Maschine sich nicht so verhält, wie es die> Sprachdefinition verlangt, wird das Programm natürlich auch nicht das> Verhalten zeigen, das von ihr sonst garantiert wird.
Selbstverständlich nicht. Und wenn man solche Sachen weiß, muss man die
natürlich berücksichtigen. Dass die Zeile nach dem ExitProcess nicht
ausgeführt werden sollte, lässt sich aus dem Inhalt des Programms
schließen. Wenn der OOM-Killer ein Programm beenden soll, läuft alles
nach Plan, wenn das geschieht.
> Rolf M. schrieb:>> Meine Frage wäre also, welche davon es deiner>> Meinung nach ist und in welcher Passage der C-Definition das steht.>> Da steht, dass der Zuweisungsoperator ein Update des Wertes der linken> Seite durchführt. Und genau das wird ein Compiler an der Stelle auch als> Maschinencode emitieren.
Ich hab nicht gefragt, welchen Code der Compiler generiert, sondern was
das System macht (bzw. die abstrakte Maschine). Denn das ist es, was ISO
C definiert (oder in dem Fall eben auch nicht).
> Rolf M. schrieb:>> Auch hier ändert sich das Verhalten der Maschine nicht abhängig davon,>> ob du weißt, dass sie falsch verdrahtet ist oder nicht.>> Was heißt hier falsch? Wir sind doch gerade dabei, die Steuerlogik zu> schreiben...
Naja, irgendwas ist falsch. Ob nun falsch verdrahtet ist oder der Rest
des Systems von einer falschen Verdrahtung ausgeht, ist doch egal. Es
passt jedenfalls nicht zusammen.
> Da sollte man davon ausgehen, dass das einen Unterschied> macht - oder pflegst Du Anmerkungen von Kollegen a lá "Wir haben da> übrigens die Pinbelegung geändert" zu ignorieren, weil dein Wissen darum> "ja sowieso keinen Unterschied macht"?
Ich hab nicht gesagt, dass es globalgalaktisch gesehen keinen
Unterschied macht, sondern dass das Verhalten der Maschine sich nicht
abhängig von deinem Wissenstand verändert. Das tut es nur, wenn du
entweder die Verdrahtung oder die Logik der Maschine an das jeweils
andere anpasst.
Rolf M. schrieb:> Ich hab nicht gefragt, welchen Code der Compiler generiert, sondern was> das System macht (bzw. die abstrakte Maschine). Denn das ist es, was ISO> C definiert (oder in dem Fall eben auch nicht).
In welchem "Fall"? Der C Standard definiert genau 2 Möglichkeiten, wie
einem Zeiger die Nullpointer-Eigenschaft zukommen kann:
1. Er wurde (direkt oder indirekt) auf NULL gesetzt.
2. Ein Vergleich mit NULL zeigt, dass er NULL ist.
Genau genommen könnte der Compiler auch eine Liste von Variablen zur
Laufzeit führen, in der er sich merkt, welche NULL sind, weil er sie
NULL gesetzt hat und der Vergleich mit NULL nur nachsehen, ob der Wert
in der Liste steht.
Auf deinen Beweis, dass ein Zeiger NULL sein könnte, ohne, dass eine der
beiden Bedingungen da oben zutrifft, warte ich...
Aber, bitte! Zur Ablenkung:
1
In the abstract machine, all expressions are evaluated as specified by the semantics. An actual implementation need not evaluate part of an expression if it can deduce that its
2
value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object).
Heiko L. schrieb:> 2. Ein Vergleich mit NULL zeigt, dass er NULL ist.
Ein Nullzeiger wird nicht zu einem Nullzeiger, indem man ihn auf seine
Nullzeigereigenschaft testet.
S. R. schrieb:> Ein Nullzeiger wird nicht zu einem Nullzeiger, indem man ihn auf seine> Nullzeigereigenschaft testet.
Alternativ kann man einen Zeiger auf NULL setzen. Sonst gibt es da keine
Möglichkeit, wie ich das sehe.
Heiko L. schrieb:> Nicht? Transitivität?
Ihm wurde der Wert eines Nullzeigers zugewiesen, nicht NULL.
Statt "char* a = NULL" hätte da auch "char* a = func()" stehen können,
was du weiter oben ja kategorisch als Grund für Nullzeigerei
ausgeschlossen hast, falls der Compiler nicht "func() == NULL" schon zur
Compilezeit diagnostizieren kann.
S. R. schrieb:> Ihm wurde der Wert eines Nullzeigers zugewiesen, nicht NULL.
Wenn der listenführende Compiler weiß, dass a NULL ist, weil er a NULL
gesetzt hat, kann er nach b=a wissen, dass b NULL ist. Der Schluß
scheint mir ziemlich sicher.
Ich will hier noch einmal darauf hinweisen:
1
In the abstract machine, all expressions are evaluated as specified by the semantics.
In meinem Beispiel ist
1
*p = 1;
eine Zuweisung. Das wäre nicht der Fall, wenn da stünde
Heiko L. schrieb:> Rolf M. schrieb:>> Ich hab nicht gefragt, welchen Code der Compiler generiert, sondern was>> das System macht (bzw. die abstrakte Maschine). Denn das ist es, was ISO>> C definiert (oder in dem Fall eben auch nicht).>> In welchem "Fall"?
Na in dem, über den wir schon die ganze Zeit sprechen: Die
Dereferenzierung eines Nullzeigers, unabhängig davon, ob der Compiler an
der Stelle weiß, dass es ein Nullzeiger ist. Nirgends steht nämlich,
dass er das wissen soll, darf oder muss.
> Der C Standard definiert genau 2 Möglichkeiten, wie> einem Zeiger die Nullpointer-Eigenschaft zukommen kann:> 1. Er wurde (direkt oder indirekt) auf NULL gesetzt.> 2. Ein Vergleich mit NULL zeigt, dass er NULL ist.
Das zweite ist die Art, wie man erkennt, dass er NULL ist, nicht die
Art, wie er NULL wird. Wie ich schon die ganze Zeit sage: Er kann auch
NULL sein, ohne dass man das weiß.
> Genau genommen könnte der Compiler auch eine Liste von Variablen zur> Laufzeit führen, in der er sich merkt, welche NULL sind, weil er sie> NULL gesetzt hat und der Vergleich mit NULL nur nachsehen, ob der Wert> in der Liste steht.
Du versteifst dich immer auf den Compiler. Nochmal: ISO C definiert das
Verhalten einer abstrakte Maschine, nicht eines Compilers.
Aber ja, im Prinzip sollte das gehen.
> Auf deinen Beweis, dass ein Zeiger NULL sein könnte, ohne, dass eine der> beiden Bedingungen da oben zutrifft, warte ich...
Warum sollte ich das beweisen müssen? Deine Hypothese war, wenn der
Zeiger von einer Funktion zurückgegeben wird, könne er nicht NULL sein.
Auch dann nicht, wenn da innerhalb der Funktion explizit NULL
reingeschrieben wird. Das hat aber gar nichts damit zu tun, ob das nun
in einer eigenen Funktion passiert oder nicht. Und es gilt auch, wenn du
den Inhalt der Funktion nicht kennst und wenn auch der Compiler den
Inhalt der Funktion nicht kennt. Ein return NULL innerhalb der Funktion
wurde nämlich von der abstrakten Maschine ganz genauso ausgeführt, wie
ein p = NULL außerhalb der Funktion.
Ich warte übrigens noch darauf, wie das exakt von ISO C definierte
Verhalten der abstrakten Maschine deiner Meinung nach aussieht, wenn so
ein Zeiger dereferenziert wird.
> Aber, bitte! Zur Ablenkung:In the abstract machine, all expressions> are evaluated as specified by the semantics. An actual> implementation need not evaluate part of an expression if it> can deduce that its value is not used and that no needed side effects> are produced (including any caused by calling a function or accessing a> volatile object).
Das ist die as-if-Regel, die wichtig für Optimierung ist. Was hat die
jetzt damit zu tun?
S. R. schrieb:> Heiko L. schrieb:>> 2. Ein Vergleich mit NULL zeigt, dass er NULL ist.>> Ein Nullzeiger wird nicht zu einem Nullzeiger, indem man ihn auf seine> Nullzeigereigenschaft testet.
Da waren wir schon mal:
Beitrag "Re: char Array für ultoa()"Heiko L. schrieb:> S. R. schrieb:>> Ihm wurde der Wert eines Nullzeigers zugewiesen, nicht NULL.>> Wenn der listenführende Compiler weiß, dass a NULL ist, weil er a NULL> gesetzt hat, kann er nach b=a wissen, dass b NULL ist. Der Schluß> scheint mir ziemlich sicher.
Ja. Er muss nicht zwingend (C schreibt es nicht vor), aber er kann.
> Ich will hier noch einmal darauf hinweisen:In the abstract machine,> all expressions are evaluated as specified by the semantics.In> meinem Beispiel ist> *p = 1; eine Zuweisung. Das wäre nicht der Fall, wenn da stünde> char *p = NULL;> *p = 1;
Warum nicht? a = b ist eine Zuweisung, egal was a und b sind. Das ist
unabhängig davon, ob dein p direkt davor oder in einer anderen Funktion
auf NULL gesetzt wird. Und in beiden Fällen wird ein Nullzeiger
dereferenziert, was zu undefiniertem Verhalten der abstrakten Maschine
führt.
Rolf M. schrieb:> Die> Dereferenzierung eines Nullzeigers, unabhängig davon, ob der Compiler an> der Stelle weiß, dass es ein Nullzeiger ist. Nirgends steht nämlich,> dass er das wissen soll, darf oder muss.
Tja, wenn es eine "Dereferenzierung" ist und nicht irgendetwas
undefiniertes - du kriegst die Kurve noch...
Rolf M. schrieb:> Warum nicht? a = b ist eine Zuweisung, egal was a und b sind.
Das eine ist eine Zuweisung, das andere irgendetwas Undefiniertes. Das
eine muss kompiliert werden, das andere nicht. Ganz einfach.
Rolf M. schrieb:> Das zweite ist die Art, wie man erkennt, dass er NULL ist, nicht die> Art, wie er NULL wird. Wie ich schon die ganze Zeit sage: Er kann auch> NULL sein, ohne dass man das weiß.
...
> Du versteifst dich immer auf den Compiler.
Nein, du versteifst dich auf Annahmen, die nicht abgedeckt sind. Wo im
Standard steht z.B. bitte, dass etwas ein Nullzeiger sein könnte, ohne
dass eine der beiden Bedingungen de-facto gegeben wäre? Du
argumentierst: "Ja, eigentlich müsste er da und da doch auch NULL sein
können." Das muß aber keineswegs so sein.
Heiko L. schrieb:> Das muß aber keineswegs so sein.
Ein Zeiger ohne weitere Information ist ein Zeiger - er ist kein
Nullzeiger und kein Nicht-Nullzeiger. Dem Compiler ist es freigestellt,
in die eine oder die andere Richtung zu beweisen. Er muss es nicht tun.
Damit ist jeder Zeiger möglicherweise ein Nullzeiger. Sollte er zum
Zeitpunkt der Dereferenzierung zufällig einer sein, gibt's UB. Dafür
muss nicht bewiesen worden sein, dass der Zeiger NULL sein musste - es
reicht völlig aus, dass er es war.
Und eine Zuweisung ist auch dann eine Zuweisung, wenn sie zu UB führt.
UB macht ja die Zuweisungseigenschaft nicht kaputt, sondern nur das
bestimmte Verhalten der abstrakten Maschine.
S. R. schrieb:> Heiko L. schrieb:>> Das muß aber keineswegs so sein.>> Ein Zeiger ohne weitere Information ist ein Zeiger - er ist kein> Nullzeiger und kein Nicht-Nullzeiger. Dem Compiler ist es freigestellt,> in die eine oder die andere Richtung zu beweisen. Er muss es nicht tun.>> Damit ist jeder Zeiger möglicherweise ein Nullzeiger. Sollte er zum> Zeitpunkt der Dereferenzierung zufällig einer sein, gibt's UB. Dafür> muss nicht bewiesen worden sein, dass der Zeiger NULL sein musste - es> reicht völlig aus, dass er es war.>> Und eine Zuweisung ist auch dann eine Zuweisung, wenn sie zu UB führt.> UB macht ja die Zuweisungseigenschaft nicht kaputt, sondern nur das> bestimmte Verhalten der abstrakten Maschine.
In dem einen Fall darf der Compiler die Kompilierung ganz verweigern,
oder auch nur einige Anweisungen ignorieren, oder, oder, oder.
In dem anderen Fall muss ein Standard-konformer Compiler ein Kompilat
erzeugen. Und zwar eines, welches der Semantik des Programms entspricht.
Dort steht dann mindestens eine Anweisung a lá "mov [eax], 1". Ich sehe
da nichts undefiniertes. Ich glaube, was dann passiert, ist im Gegenteil
auch wieder ziemlich genau spezifiziert.
Heiko L. schrieb:> Rolf M. schrieb:>> Die>> Dereferenzierung eines Nullzeigers, unabhängig davon, ob der Compiler an>> der Stelle weiß, dass es ein Nullzeiger ist. Nirgends steht nämlich,>> dass er das wissen soll, darf oder muss.>> Tja, wenn es eine "Dereferenzierung" ist und nicht irgendetwas> undefiniertes - du kriegst die Kurve noch...
Natürlich ist es eine Dereferenzierung. Undefiniert ist das Verhalten,
wenn man das versucht. Das ist eigentlich ein simpler kausaler
Zusammenhang: Wäre es keine Dereferenzierung, dann gäbe es kein
undefiniertes Verhalten, denn das entsteht ja erst durch die
Dereferenzierung.
> Rolf M. schrieb:>> Warum nicht? a = b ist eine Zuweisung, egal was a und b sind.>> Das eine ist eine Zuweisung, das andere irgendetwas Undefiniertes.
Unsinn. ISO C definiert ganz genau, was das ist.
> Das eine muss kompiliert werden, das andere nicht. Ganz einfach.
Wenn irgendwo im Programm undefiniertes Verhalten vorkommt, muss gar
nichts mehr.
> Rolf M. schrieb:>> Das zweite ist die Art, wie man erkennt, dass er NULL ist, nicht die>> Art, wie er NULL wird. Wie ich schon die ganze Zeit sage: Er kann auch>> NULL sein, ohne dass man das weiß.> ...>> Du versteifst dich immer auf den Compiler.>> Nein, du versteifst dich auf Annahmen, die nicht abgedeckt sind. Wo im> Standard steht z.B. bitte, dass etwas ein Nullzeiger sein könnte, ohne> dass eine der beiden Bedingungen de-facto gegeben wäre?
Wenn ich NULL reinschreibe, ist das gegeben. Egal, ob in einer Funktion
oder nicht. Das hab ich doch ziemlich klar geschrieben. Das ist deine
"Bedingung 1". Ich weiß auch nicht, wie du auf die Idee kommst, die
Zusagen und Vorgaben von C würden nicht mehr gelten, sobald man eine
Funktion aufruft.
Heiko L. schrieb:> 1. Er wurde (direkt oder indirekt) auf NULL gesetzt.
Und nein, das muss nicht in der Codezeile davor geschehen, sondern kann
an beliebiger anderer Stelle im Programm passiert sein. Sogar in einer
anderen Funktion.
> Du argumentierst: "Ja, eigentlich müsste er da und da doch auch NULL sein> können." Das muß aber keineswegs so sein.
Nein. Ich argumentiere: "Wenn die Funktion (oder wer auch immer) da NULL
reingeschrieben hat, steht da NULL drin. Das muss man weder wissen, noch
prüfen, damit das so ist. Es steht einfach so drin."
Du argumentierst dagegen: "Wenn es aus einer Funktion zurückgegeben
wird, kann da nicht NULL drin stehen, auch wenn sie es da
reingeschrieben hat. Erst wenn ich es auf NULL prüfe, ändert sich der
Wert auf magische Weise, und danach steht dann NULL drin."
Heiko L. schrieb:> In dem einen Fall darf der Compiler die Kompilierung ganz verweigern,> oder auch nur einige Anweisungen ignorieren, oder, oder, oder.
Nicht nur der Compiler! Auch die Laufzeitumgebung, die Hardware, das
Betriebssystem oder das Kraftwerk, von dem der Strom für deinen Rechner
kommt. Der Monitor darf explodieren, das Programm darf vom Prozessor ab
der Stelle rückwärts ausgeführt werden, oder dir können Dämonen aus der
Nase fliegen ( https://en.wiktionary.org/wiki/nasal_demon ). All das
wäre nach ISO C korrektes Verhalten dieses Konstrukts.
> In dem anderen Fall muss ein Standard-konformer Compiler ein Kompilat> erzeugen.
Nö. Auch hier gelten genau die selben Dinge wie oben. Gar keiner muss
irgendwas.
> Und zwar eines, welches der Semantik des Programms entspricht.> Dort steht dann mindestens eine Anweisung a lá "mov [eax], 1". Ich sehe> da nichts undefiniertes. Ich glaube, was dann passiert, ist im Gegenteil> auch wieder ziemlich genau spezifiziert.
Ich hab ja schon mehrfach gefragt, wo in ISO C das genau spezifiziert
sein soll.
Rolf M. schrieb:> Natürlich ist es eine Dereferenzierung.
Nee, das sieht nur so aus, als ob das eine werden sollte.
Wäre es eine Dereferenzierung, wäre das Verhalten definiert: der Pointer
würde dereferenziert werden. Kein definiertes Verhalten -> keine
festgelegte "Semantik" -> keine Dereferenzierung.
Man kann etwas nicht ein "Sitzen" nennen, wenn es nicht sitzt.
1
Das Sein soll erfaßt und zum Thema gemacht werden. Sein ist jeweils Sein von Seiendem und wird demnach zunächst nur im Ausgang von einem Seienden zugänglich. Dabei muß sich der erfassende phänomenologische Blick zwar auf Seiendes mitrichten, aber so, daß dabei das Sein dieses Seienden zur Abhebung und zur möglichen Thematisierung kommt. Das Erfassen des Seins geht zwar zunächst und notwendig je auf Seiendes zu, wird aber dann von dem Seienden in bestimmter Weise weg- und zurückgeführt auf dessen Sein.
2
(Martin Heidegger, Grundprobleme)
Auf deutsch: Eine Dereferenzierung dereferenziert. Damit ist sie nicht
undefiniert.
2 + 1 = 4 ist keine Formel der Arithmetik - sowas existiert noch nicht
einmal.
Rolf M. schrieb:>> In dem einen Fall darf der Compiler die Kompilierung ganz verweigern,>> oder auch nur einige Anweisungen ignorieren, oder, oder, oder.> Nicht nur der Compiler! Auch die Laufzeitumgebung, die Hardware, das> Betriebssystem oder das Kraftwerk, von dem der Strom für deinen Rechner> kommt.
DAS ist sowieso der Fall - siehe ExitProcess(). Das, was dort nach wie
vor garantiert ist, ist, dass die abstrakte Maschine den Befehl bekommt
"Rufe die Funktion auf." Und die ist z.B. in Assembler geschrieben - was
sagt der C-Standard, was dann passiert? Ist die Maschine kaputt?
Rolf M. schrieb:> Ich hab ja schon mehrfach gefragt, wo in ISO C das genau spezifiziert> sein soll.
Und darauf habe ich mehrfach geantwortet. Lese doch einfach noch einmal
nach!
Heiko L. schrieb:> Kein definiertes Verhalten -> keine> festgelegte "Semantik" -> keine Dereferenzierung.
Du denkst rückwärts. Ich bin dann mal raus, das wird nix.