Ich habe diese Frage zum noreturn
Attribut gelesen , das für Funktionen verwendet wird, die nicht zum Aufrufer zurückkehren.
Dann habe ich ein Programm in C gemacht.
#include <stdio.h>
#include <stdnoreturn.h>
noreturn void func()
{
printf("noreturn func\n");
}
int main()
{
func();
}
Und Montage des Codes erzeugt unter Verwendung dieser :
.LC0:
.string "func"
func:
pushq %rbp
movq %rsp, %rbp
movl $.LC0, %edi
call puts
nop
popq %rbp
ret // ==> Here function return value.
main:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
call func
Warum kehrt die Funktion func()
nach der Angabe des noreturn
Attributs zurück?
noreturn
Attribut in C gelesen " Nein. Die von Ihnen verknüpfte Frage bezieht sich auf "noreturn
Attribut in C ++", eine andere Sprache.exit
oder gleichwertig endet, Blattfunktionen nicht in Nicht-Blattfunktionen verwandelt. Sie können alsoassert
in den tiefsten Tiefen Ihres leistungskritischen Codes oder ähnliches haben und es kostet nicht mehr als eine korrekt vorhergesagte Verzweigung (anstatt dass die Funktion einen Frame einrichten, Register speichern, vom Anrufer gespeicherte Register vermeiden usw.). .ret
Befehl bedeutet lediglich, dass die Ausführung zurück an den Aufrufer übertragen wird. Sie würden dasselbe für eine Funktion mit einem Rückgabewert von sehenvoid
. Mit anderen Worten, es gibt die Kontrolle zurück. kein Wert. Jetzt geben alle x86-Aufrufkonventionen Werte imEAX
Register zurück, aber der Aufrufer und der Angerufene müssen sich darauf einigen. Wenn die Funktion zurückgibtvoid
oder nichts, enthältEAX
sie nur Müll. Wie die Antworten bereits sagten, ist das Verhalten gemäß dem C-Sprachstandard undefiniert, aber genau das bedeutet der asm.Antworten:
Die Funktionsspezifizierer in C sind ein Hinweis auf den Compiler, der Akzeptanzgrad ist implementierungsdefiniert.
Zuallererst ist der
_Noreturn
Funktionsspezifizierer (odernoreturn
verwenden<stdnoreturn.h>
) ein Hinweis für den Compiler auf ein theoretisches Versprechen des Programmierers, dass diese Funktion niemals zurückkehren wird. Basierend auf diesem Versprechen kann der Compiler bestimmte Entscheidungen treffen und einige Optimierungen für die Codegenerierung vornehmen.IIRC, wenn eine mit dem Funktionsbezeichner angegebene
noreturn
Funktion schließlich auch zu ihrem Aufrufer zurückkehrtreturn
AnweisungDas Verhalten ist undefiniert . Sie dürfen NICHT von der Funktion zurückkehren.
Um dies zu verdeutlichen, verhindert die Verwendung des
noreturn
Funktionsbezeichners nicht, dass ein Funktionsformular zu seinem Aufrufer zurückkehrt. Es ist ein Versprechen des Programmierers an den Compiler, ihm mehr Freiheit bei der Generierung von optimiertem Code zu geben.Falls Sie früher und später ein Versprechen abgegeben haben, entscheiden Sie sich dafür, dies zu verletzen. Das Ergebnis ist UB. Compiler werden aufgefordert, aber nicht aufgefordert, Warnungen zu erstellen, wenn eine
_Noreturn
Funktion in der Lage zu sein scheint, zu ihrem Aufrufer zurückzukehren.Gemäß Kapitel §6.7.4
C11
, Absatz 8und der Absatz 12 ( Beachten Sie die Kommentare !! )
Denn
C++
das Verhalten ist ziemlich ähnlich. Zitat aus Kapitel §7.6.4,C++14
Absatz 2 ( Hervorhebung von mir )quelle
noreturn
Attribut einer Funktion, die wirklich nicht zurückgegeben wird, wären die Ergebnisse solcher Tools falsch. Dies kann auch bei Optimierungen hilfreich sein (bei jedem Aufruf einer solchen Funktion werden höchstens einige Bytes eingespart), da der Compiler die zurückgegebene Funktion nicht berücksichtigen muss. Der Kontrollfluss innerhalb einer aufrufenden Funktion hört an diesem Punkt einfach auf.noreturn
bedeutet nicht "diese Funktion wird nicht zurückkehren"; es bedeutet "dem Compiler mitteilen, dass er davon ausgehen kann, dass diese Funktion nicht zurückkehrt". Es ist nicht da, um Ihre Arbeit zu erleichtern, es ist da, um die Arbeit des Compilers zu erleichtern.Unit
für eine Funktion, die nichts zurückgibt, und einen TypNothing
für eine Funktion, die nicht zurückgibt .ud2
, IMO dies (Abbruch des Programms, das gleiche wird in bestimmten anderen Fällen getan, die statisch als undefiniertes Verhalten nachgewiesen werden können) wäre "schöner" als die Rückkehr zu einem Anrufer, der optimiert wurde, um es anzunehmen Gewohnheit.Weil Sie Code geschrieben haben, der es gesagt hat.
Wenn Sie nicht Ihre Funktion zurückzukehren, rufenden
exit()
oderabort()
o.ä. so zurückgibt es nicht.Was würde Ihre Funktion anders tun, als nach dem Aufruf zurückzukehren
printf()
?Der C-Standard in 6.7.4 Funktionsspezifizierer , Absatz 12 enthält speziell ein Beispiel für eine
noreturn
Funktion, die tatsächlich zurückkehren kann - und kennzeichnet das Verhalten als undefiniert :BEISPIEL 2
_Noreturn void f () { abort(); // ok } _Noreturn void g (int i) { // causes undefined behavior if i<=0 if (i > 0) abort(); }
Kurz gesagt,
noreturn
ist eine Einschränkung , dass Sie auf platzieren Ihren Code - es teilt den Compiler „MY - Code wird nicht immer zurückkehren“ . Wenn Sie gegen diese Einschränkung verstoßen, liegt das ganz bei Ihnen.quelle
return
Endlosschleifen enden , anscheinend absichtlich, um Ihnen zu helfen, den Fehler zu erkennen, wenn Sie die Warnung verpasst haben (oder vielleicht Fälle, in denen nicht gewarnt wurde, weil nicht sicher war, ob der Weg jemals eingeschlagen werden würde). Dies ist für C nicht der Fall, da es in C99 / C11 nur UB ist, wenn der Anrufer das Ergebnis verwendet .i = i++;
und es erschien .)noreturn
ist ein Versprechen. Sie sagen dem Compiler: "Es mag offensichtlich sein oder auch nicht, aber ich weiß, basierend auf der Art und Weise, wie ich den Code geschrieben habe, dass diese Funktion niemals zurückkehren wird." Auf diese Weise kann der Compiler vermeiden, die Mechanismen einzurichten, mit denen die Funktion ordnungsgemäß zurückkehren kann. Wenn Sie diese Mechanismen weglassen, kann der Compiler möglicherweise effizienteren Code generieren.Wie kann eine Funktion nicht zurückkehren? Ein Beispiel wäre, wenn es
exit()
stattdessen aufgerufen würde.Wenn Sie dem Compiler jedoch versprechen, dass Ihre Funktion nicht zurückgegeben wird, und der Compiler nicht dafür sorgt, dass die Funktion ordnungsgemäß zurückgegeben wird, schreiben Sie eine Funktion, die zurückgibt, was der Compiler tun soll machen? Grundsätzlich gibt es drei Möglichkeiten:
Der Compiler kann 1, 2, 3 oder eine Kombination ausführen.
Wenn dies nach undefiniertem Verhalten klingt, liegt das daran, dass es so ist.
Das Fazit beim Programmieren wie im wirklichen Leben lautet: Machen Sie keine Versprechen, die Sie nicht halten können. Jemand anderes hat möglicherweise Entscheidungen auf der Grundlage Ihres Versprechens getroffen, und es können schlimme Dinge passieren, wenn Sie dann Ihr Versprechen brechen.
quelle
Das
noreturn
Attribut ist ein Versprechen, das Sie dem Compiler bezüglich Ihrer Funktion geben.Wenn Sie tun Rückkehr von einer solchen Funktion, Verhalten ist nicht definiert, aber das bedeutet nicht , ein vernünftiger Compiler Sie zu verwirren der Anwendung , den Status ermöglicht vollständig durch das Entfernen
ret
Aussage, zumal der Compiler oft sogar in der Lage sein wird, abzuleiten , dass Eine Rückgabe ist in der Tat möglich.Wenn Sie jedoch Folgendes schreiben:
noreturn void func(void) { printf("func\n"); } int main(void) { func(); some_other_func(); }
dann ist es für den Compiler durchaus vernünftig, das
some_other_func
komplett zu entfernen , wenn es sich danach anfühlt.quelle
Wie andere bereits erwähnt haben, ist dies ein klassisches undefiniertes Verhalten. Du hast versprochen
func
, nicht zurückzukehren, aber du hast es trotzdem geschafft. Sie können die Stücke aufheben, wenn das kaputt geht.Obwohl der Compiler
func
(trotz Ihrernoreturn
) auf die übliche Weise kompiliert ,noreturn
wirkt sich dies auf die aufrufenden Funktionen aus.Sie können dies in der Assembly-Liste sehen: Der Compiler hat angenommen
main
, dass diesfunc
nicht zurückkehren wird. Daher wurde buchstäblich der gesamte Code nach dem gelöschtcall func
(überzeugen Sie sich selbst unter https://godbolt.org/g/8hW6ZR ). Die Assembly-Liste wird nicht abgeschnitten, sondern endet buchstäblich erst nach dem,call func
da der Compiler davon ausgeht, dass Code danach nicht mehr erreichbar ist. Wenn alsofunc
tatsächlich zurückkehrt,main
wird mit der Ausführung des Mistes begonnen, der dermain
Funktion folgt - sei es Auffüllen, unmittelbare Konstanten oder ein Meer von00
Bytes. Wieder - sehr undefiniertes Verhalten.Dies ist transitiv - eine Funktion, die eine
noreturn
Funktion in allen möglichen Codepfaden aufruft, kann selbst angenommen werdennoreturn
.quelle
noreturn
scheint die Tailcall-Optimierung zu deaktivieren (und das Inlining zu stoppen, wenn die Noreturn-Funktion printf aufruft). Sie können dies sogar sehen mit-O3
: godbolt.org/g/vFWp8E . Clang 4.0 und gcc 7.2 sind hier gleich.noreturn
Handling von gcc die Tailcall-Optimierung für bessereabort()
Backtraces speziell deaktiviert : gcc.gnu.org/bugzilla/show_bug.cgi?id=55747#c4 .Nach dieser
Es liegt in der Verantwortung des Programmierers, sicherzustellen, dass diese Funktion niemals zurückkehrt, z. B. exit (1) am Ende der Funktion.
quelle
ret
bedeutet einfach, dass die Funktion die Kontrolle an den Anrufer zurückgibt . In diesemmain
Fallcall func
führt die CPU die Funktion aus und setzt dann mitret
die CPU die Ausführung von fortmain
.Bearbeiten
Also, es stellt sich heraus ,
noreturn
nicht machen die Funktion überhaupt nicht zurückkehren, es ist nur ein Spezifizierer, die den Compiler sagt , dass der Code dieser Funktion in einer solchen Art und Weise geschrieben , dass die Funktion wird bestätigt . Sie sollten hier also sicherstellen, dass diese Funktion die Kontrolle nicht an den Angerufenen zurückgibt. Zum Beispiel könnten Sie darin anrufenexit
.Angesichts dessen, was ich über diesen Bezeichner gelesen habe, scheint es, dass man, um sicherzustellen, dass die Funktion nicht zu ihrem Aufrufpunkt zurückkehrt, eine andere
noreturn
Funktion darin aufrufen und sicherstellen sollte, dass letztere immer ausgeführt wird (in der richtigen Reihenfolge) um undefiniertes Verhalten zu vermeiden) und verursacht UB selbst nicht.quelle
noreturn
, etwas zu tun, das die Rückgabe seines Codes verhindert, anstattnoreturn
tatsächlich zu beschränken, was der von ihm geschriebene Code unter dem Standard tun darf .noreturn
muss irgendwo enden, zum Beispiel eine Ausnahme auslösen , aufrufenlongjmp
, einen Systemaufruf wie machen_exit(2)
oder etwas Hackiges mit Inline-Asm machen. Aber ja, es sei denn, Sie tun etwas anderes, das nicht zurückkehrt, sollten Sie eine anderenoreturn
Funktion aufrufen (einschließlichexit(3)
oderabort(3)
.)Keine Rückgabefunktion speichert die Register im Eintrag nicht, da dies nicht erforderlich ist. Dies erleichtert die Optimierung. Ideal zum Beispiel für die Scheduler-Routine.
Sehen Sie sich das Beispiel hier an: https://godbolt.org/g/2N3THC und erkennen Sie den Unterschied
quelle
noreturn
sind ein paar (!) Bytes von der ausführbaren Größe:noreturn
Funktionen können niemals Teil des leistungskritischen Pfads in einem Code sein, der mindestens halbwegs gesund ist. Im gesamten Konstrukt geht es viel mehr darum, unnötige Warnungen zum Schweigen zu bringen als um die Optimierung.noreturn
. Das ist die entgegengesetzte Richtung des Denkens von dem, was Sie anscheinend verstanden haben. Ich würde niemals sagen, dass die Verwendung vonnoreturn
Code mehr oder weniger vernünftig macht.That's not what I said.
Sie haben zu komplizierte Denkweise für mich. Ich verstehe diesen "halbwegs gesunden" Gedanken nichtnoreturn
Funktionen können niemals Teil des leistungskritischen Pfades sein" falsch gewesen wäre: Es ist durchaus möglich, ein Programm zu schreiben, dasnoreturn
Aufrufe im leistungskritischen Pfad enthält (mittelslongjmp()
). Diese Programme werden jedoch unweigerlich geisteskrank sein. Die Bedingung "Code, der mindestens halbwegs gesund ist" schließt diese einfach aus. Hoffe das klärt die Dinge. (Entschuldigung für meine verwirrend präzise Sprache.)TL: DR: Es ist eine Fehloptimierung durch gcc .
noreturn
ist ein Versprechen an den Compiler, dass die Funktion nicht zurückkehren wird. Dies ermöglicht Optimierungen und ist insbesondere in Fällen nützlich, in denen es für den Compiler schwierig ist, zu beweisen, dass eine Schleife niemals beendet wird, oder auf andere Weise zu beweisen, dass es keinen Pfad durch eine zurückkehrende Funktion gibt.GCC optimiert bereits, um
main
das Ende der Funktion zufunc()
verlieren, wenn zurückgegeben wird, selbst mit der Standardeinstellung-O0
(Mindestoptimierungsstufe), die Sie verwendet haben.Die Ausgabe für
func()
sich könnte als verpasste Optimierung angesehen werden. es könnte einfach alles nach dem Funktionsaufruf weglassen (da der Aufruf nicht zurückgegeben werden kann, ist die einzige Möglichkeit, wie die Funktion selbst sein kannnoreturn
). Es ist kein gutes Beispiel, daprintf
eine Standard-C-Funktion bekanntermaßen normal zurückkehrt (es sei denn, Siesetvbuf
gebenstdout
einen Puffer an, der fehlerhaft ist?).Verwenden wir eine andere Funktion, die der Compiler nicht kennt.
void ext(void); //static int foo; _Noreturn void func(int *p, int a) { ext(); *p = a; // using function args after a function call foo = 1; // requires save/restore of registers } void bar() { func(&foo, 3); }
( Code + x86-64 asm im Godbolt-Compiler-Explorer . )
Die Ausgabe von gcc7.2 für
bar()
ist interessant. Es inlinefunc()
und beseitigt denfoo=3
toten Speicher, so dass nur:bar: sub rsp, 8 ## align the stack call ext mov DWORD PTR foo[rip], 1 ## fall off the end
Gcc geht weiterhin davon aus, dass
ext()
nach Rückkehr wird, sonst könnte es nur Schwanz genannt hatext()
mitjmp ext
. Aber gcc ruft keine Tailcall-noreturn
Funktionen auf, da dadurch Backtrace-Informationen für Dinge wie verloren gehenabort()
. Anscheinend ist es in Ordnung, sie zu inlinieren.Gcc hätte optimieren können, indem der
mov
Laden auch nach dem weggelassen wurdecall
. Wennext
zurückgegeben wird, wird das Programm abgespritzt, sodass es keinen Sinn macht, diesen Code zu generieren. Clang macht diese Optimierung inbar()
/main()
.func
selbst ist interessanter und eine größere verpasste Optimierung .gcc und clang strahlen fast dasselbe aus:
func: push rbp # save some call-preserved regs push rbx mov ebp, esi # save function args for after ext() mov rbx, rdi sub rsp, 8 # align the stack before a call call ext mov DWORD PTR [rbx], ebp # *p = a; mov DWORD PTR foo[rip], 1 # foo = 1 add rsp, 8 pop rbx # restore call-preserved regs pop rbp ret
Diese Funktion kann davon ausgehen, dass sie nicht zurückkehrt und verwendet
rbx
undrbp
ohne sie zu speichern / wiederherzustellen.Gcc für ARM32 macht das tatsächlich, gibt aber immer noch Anweisungen aus, um ansonsten sauber zurückzukehren. Eine
noreturn
Funktion, die tatsächlich auf ARM32 zurückkehrt, unterbricht den ABI und verursacht schwer zu debuggende Probleme im Aufrufer oder später. (Undefiniertes Verhalten erlaubt dies, aber es handelt sich zumindest um ein Problem mit der Qualität der Implementierung: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158 .)Dies ist eine nützliche Optimierung in Fällen, in denen gcc nicht beweisen kann, ob eine Funktion zurückkehrt oder nicht. (Es ist jedoch offensichtlich schädlich, wenn die Funktion einfach zurückkehrt. Gcc warnt, wenn sicher ist, dass eine Noreturn-Funktion zurückkehrt.) Andere gcc-Zielarchitekturen tun dies nicht. Das ist auch eine verpasste Optimierung.
Aber gcc geht nicht weit genug: Wenn Sie auch die Rückgabeanweisung wegoptimieren (oder durch eine unzulässige Anweisung ersetzen), sparen Sie Code und garantieren einen lauten Fehler anstelle einer stillen Beschädigung.
Und wenn Sie das
ret
optimieren möchten, ist es sinnvoll, alles zu optimieren , was nur benötigt wird, wenn die Funktion zurückkehrt.So
func()
könnte zusammengestellt werden zu :sub rsp, 8 call ext # *p = a; and so on assumed to never happen ud2 # optional: illegal insn instead of fall-through
Jede andere vorhandene Anweisung ist eine verpasste Optimierung. Wenn
ext
deklariertnoreturn
wird, bekommen wir genau das.Es kann davon ausgegangen werden, dass jeder Basisblock , der mit einer Rückgabe endet, niemals erreicht wird.
quelle