Ich studiere über undefiniertes Verhalten in C und kam zu einer Aussage, die besagt, dass
Es gibt keine bestimmte Reihenfolge der Auswertung von Funktionsargumenten
Aber was ist dann mit den Standard-Aufrufkonventionen wie _cdecl
und _stdcall
, deren Definition besagt (in einem Buch), dass Argumente von rechts nach links ausgewertet werden.
Nun bin ich verwirrt mit diesen beiden Definitionen, die gemäß UB anders sind als die andere, die der Definition der Aufrufkonvention entspricht. Bitte begründe die beiden.
Wie die Antwort von Graznarak richtig zeigt, unterscheidet sich die Reihenfolge, in der die Argumente evaluiert werden aus der Reihenfolge, in der die Argumente übergeben werden .
Ein ABI gilt normalerweise nur für die Reihenfolge, in der die Argumente übergeben werden , z. B. welche Register verwendet werden und / oder in welcher Reihenfolge die Argumentwerte auf den Stapel geschoben werden.
Was der C-Standard sagt, ist, dass die Reihenfolge der Auswertung nicht spezifiziert ist. Zum Beispiel (daran erinnern, dass printf
ein int
Ergebnis zurückgibt):
Der C-Standard sagt, dass die zwei Nachrichten in einige Reihenfolge gedruckt werden (Auswertung ist nicht verschachtelt), aber sagt nicht explizit, welche Reihenfolge gewählt wird. Es kann sogar von einem Anruf zum nächsten variieren, ohne den C-Standard zu verletzen. Es könnte sogar das erste Argument auswerten, dann das zweite Argument auswerten, dann das Ergebnis des zweiten Arguments auf den Stapel schieben und dann das Ergebnis des ersten Arguments auf den Stapel schieben.
Ein ABI könnte angeben, welche Register verwendet werden, um die zwei Argumente zu übergeben, oder genau, wo auf dem Stapel die Werte übertragen werden, was vollständig mit den Anforderungen des C-Standards übereinstimmt.
Aber selbst wenn ein ABI tatsächlich verlangt, dass die Auswertung in einer bestimmten Reihenfolge auftritt (so dass zum Beispiel das Drucken von "second\n"
gefolgt von "first\n"
die ABI verletzt), wäre dies immer noch der Fall im Einklang mit dem C-Standard.
Was der C-Standard sagt, ist, dass der C-Standard selbst nicht die Reihenfolge der Auswertung definiert. Irgendein Sekundärstandard ist noch dazu frei.
Übrigens, dies beinhaltet nicht selbst undefiniertes Verhalten. Es gibt Fälle, in denen die nicht spezifizierte Reihenfolge der Auswertung zu undefiniertem Verhalten führen kann, zum Beispiel:
%Vor% _cdecl
und _stdcall
geben lediglich an, dass die Argumente in der Reihenfolge von rechts nach links auf den Stapel geschoben werden >, nicht dass sie in dieser Reihenfolge ausgewertet werden . Denken Sie darüber nach, was passieren würde, wenn Aufrufkonventionen wie _cdecl
, _stdcall
und pascal
die Reihenfolge ändern, in der die Argumente ausgewertet wurden.
Wenn die Evaluierungsreihenfolge durch Aufruf der Konvention geändert wurde, müssten Sie die Aufrufkonvention der Funktion kennen, die Sie aufrufen, um zu verstehen, wie sich Ihr eigener Code verhält. Das ist eine undichte Abstraktion, wenn ich jemals eine gesehen habe. Irgendwo in einer Header-Datei vergraben, die jemand anderes geschrieben hatte, wäre ein kryptischer Schlüssel, um nur diese eine Codezeile zu verstehen; aber du hast ein paar hunderttausend Zeilen, und das Verhalten ändert sich für jeden? Das wäre Wahnsinn.
Ich habe das Gefühl, dass ein Großteil des undefinierten Verhaltens in C89 auf die Tatsache zurückzuführen ist, dass der Standard nach mehreren konfligierenden Implementierungen geschrieben wurde. Sie waren vielleicht mehr damit beschäftigt, sich auf eine vernünftige Basislinie zu einigen, die die meisten Implementierer akzeptieren könnten, als sie es bei der Definition des gesamten Verhaltens waren. Ich denke gerne, dass alles undefinierte Verhalten in C nur ein Ort ist, an dem eine Gruppe von intelligenten und leidenschaftlichen Leuten zustimmte, anderer Meinung zu sein, aber ich war nicht dort.
Ich bin jetzt versucht, einen C-Compiler zu forchen und Funktionsargumente auszuwerten, als ob es sich um einen binären Baum handelt, auf dem ich eine Breite-zuerst-Traversierung durchführe. Sie können nie zu viel Spaß mit undefiniertem Verhalten haben!
Argumentauswertung und Argumentübergabe sind verwandte, aber unterschiedliche Probleme.
Argumente werden oft von links nach rechts übergeben, oft mit einigen Argumenten, die in Registern und nicht auf dem Stack übergeben werden. Dies wird durch die ABI und _cdecl
und _stdcall
angegeben.
Die Reihenfolge der Auswertung von Argumenten vor der Platzierung an den Stellen, die der Funktionsaufruf erfordert, ist nicht spezifiziert. Es kann sie von links nach rechts, von rechts nach links oder einer anderen Reihenfolge auswerten. Dies ist compilerabhängig und kann je nach Optimierungsebene variieren.
Überprüfen Sie das Buch, das Sie erwähnt haben, auf "Sequenzpunkte" , weil ich denke, dass Sie das versuchen.
Im Grunde genommen ist ein Sequenzpunkt ein Punkt, an dem Sie, nachdem Sie dort angekommen sind, sicher sind, dass alle vorhergehenden Ausdrücke vollständig ausgewertet wurden und deren Nebenwirkungen sicher nicht mehr vorhanden sind.
Zum Beispiel ist das Ende eines Initialisierers ein Sequenzpunkt. Dies bedeutet, dass nach:
%Vor% Sie sind sicher, dass i
dem Anfangswert von i
+1 entspricht und dass foo
true
oder false
zugewiesen wurde. Ein anderes Beispiel:
ist perfekt vorhersehbar. Er lautet wie folgt: Wenn der aktuelle Wert von i
größer als j
ist, und nach diesem Vergleich eins zu i
hinzufügt (das Fragezeichen ist ein Sequenzpunkt, also nach dem Vergleich , i
wird erhöht), weisen Sie i
(NEW VALUE) bar
zu, andernfalls weisen Sie j
zu. Dies ist darauf zurückzuführen, dass das Fragezeichen im ternären Operator auch ein gültiger Sequenzpunkt ist.
Alle in der C99-Norm (Anhang C) aufgelisteten Sequenzpunkte sind:
Das Folgende sind die in 5.1.2.3 beschriebenen Sequenzpunkte:
- Der Aufruf einer Funktion, nachdem die Argumente ausgewertet wurden (6.5.2.2).
- Das Ende des ersten Operanden der folgenden Operatoren: logisches AND & amp; & amp; (6.5.13); logisches ODER || (6.5.14); bedingt ? (6.5.15); Komma, (6.5.17).
- Das Ende eines vollständigen Deklarators: Deklaratoren (6.7.5); - Das Ende eines vollständigen Ausdrucks: ein Initialisierer (6.7.8); der Ausdruck in einem Ausdruck Aussage (6.8.3); der steuernde Ausdruck einer Auswahlanweisung (if oder switch) (6.8.4); der steuernde Ausdruck einer while- oder do-Anweisung (6.8.5); jedes von den Ausdrücke einer for-Aussage (6.8.5.3); der Ausdruck in einer return-Anweisung (6.8.6.4).
- Unmittelbar bevor eine Bibliotheksfunktion (7.1.4) zurückkehrt.
- Nach den Aktionen, die mit jeder formatierten Eingabe- / Ausgabefunktionskonvertierung verknüpft sind Spezifizierer (7.19.6, 7.24.2).
- unmittelbar vor und unmittelbar nach jedem Aufruf einer Vergleichsfunktion, und auch zwischen jedem Aufruf einer Vergleichsfunktion und jeder Bewegung der Objekte als Argumente an diesen Aufruf übergeben (7.20.5).
Dies bedeutet im Wesentlichen, dass jeder Ausdruck, dem kein Sequenzpunkt folgt, ein undefiniertes Verhalten auslösen kann, wie zum Beispiel:
%Vor%In dieser Anweisung ist der folgende Sequenzpunkt "Der Aufruf einer Funktion, nachdem die Argumente ausgewertet wurden" . Nachdem die Argumente ausgewertet wurden. Wenn wir uns dann die Semantik im selben Standard unter 6.5.2.2 Punkt zehn ansehen, sehen wir:
10 Die Reihenfolge der Auswertung des Funktionsbezeichners, der tatsächlichen Argumente und Teilausdrücke innerhalb der eigentlichen Argumente sind nicht spezifiziert, aber es gibt einen Sequenzpunkt vor dem eigentlichen Anruf.
Das bedeutet für i = 1
, dass die Werte, die an printf übergeben werden, sind:
Aber genauso gültig wäre:
%Vor% Was Sie kann sicher sein, ist, dass der neue Wert von i
nach diesem Aufruf wird 2.
Aber alle oben aufgeführten Werte sind theoretisch gleichwertig und zu 100% normkonform.
Aber der Anhang zu undefiniertem Verhalten listet dies explizit als Code auf, der auch undefiniertes Verhalten aufruft:
Zwischen zwei Sequenzpunkten wird ein Objekt mehr als einmal modifiziert oder modifiziert und der vorherige Wert wird gelesen, um den zu speichernden Wert zu bestimmen (6.5).
Theoretisch könnte Ihr Programm abstürzen, statt printinf 1, 2, and 3
, wäre auch die Ausgabe "666, 666 and 666"
möglich
Also endlich habe ich es gefunden ... ja. Das liegt daran, dass die Argumente übergeben werden, nachdem sie ausgewertet wurden. Das Übergeben von Argumenten ist eine völlig andere Geschichte als die Auswertung. Der Compiler von c, wie er traditionell erstellt wird, um die Geschwindigkeit und Optimierung zu maximieren, kann den Ausdruck in irgendeiner Weise auswerten also sind sowohl die Argumentation als auch die Auswertung verschiedene Geschichten.
Da der C-Standard keine Reihenfolge für die Auswertung von Parametern angibt, ist es für jede Compiler-Implementierung frei, eine zu übernehmen. Das ist ein Grund, warum das Kodieren von etwas wie foo(i++)
völliger Wahnsinn ist - Sie können beim Umschalten von Compilern unterschiedliche Ergebnisse erhalten.
Eine weitere wichtige Sache, die hier nicht hervorgehoben wurde - wenn Ihr bevorzugter ARM-Compiler Parameter von links nach rechts auswertet, wird dies für alle Fälle und für alle nachfolgenden Versionen gemacht. Die Lesereihenfolge der Parameter für einen Compiler ist lediglich eine Konvention ...
Tags und Links c undefined-behavior calling-convention