it-swarm.com.de

Warum kehrt die "noreturn" -Funktion zurück?

Ich habe this question über das 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 generierte Assembly des Codes mit this :

.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() zurück, nachdem das noreturn-Attribut bereitgestellt wurde?

66
M.S Chaudhari

Die Funktionsbezeichner in C sind ein Hinweis an den Compiler, der Akzeptanzgrad ist implementierungsdefiniert.

Zuallererst ist _Noreturn Funktionsbezeichner (oder noreturn mit <stdnoreturn.h>) ein Hinweis an den Compiler über ein theoretisches Versprechen, das der Programmierer gemacht hat, dass diese Funktion niemals zurückkehren wird. Basierend auf diesem Versprechen kann der Compiler bestimmte Entscheidungen treffen und einige Optimierungen für die Codegenerierung durchführen. 

IIRC, wenn eine mit noreturn angegebene Funktion spezifizierende Funktion schließlich zu ihrem Aufrufer zurückkehrt

  • mit einer expliziten return-Anweisung
  • durch Erreichen des Funktionskörpers

das Verhalten ist undefiniert . Sie MÜSSEN NICHT von der Funktion zurückkehren. 

Um zu verdeutlichen, dass mit noreturn der Funktionsbezeichner ein Funktionsformular nicht beendet wird, das zu seinem Aufrufer zurückkehrt. Es ist ein Versprechen, das der Programmierer dem Compiler gemacht hat, um ihm mehr Freiheit zu geben, um optimierten Code zu erzeugen.

Falls Sie früher und später ein Versprechen abgegeben haben, entscheiden Sie sich, dies zu verletzen. Das Ergebnis ist UB. Compiler werden aufgefordert, jedoch nicht dazu aufgefordert, Warnungen auszulösen, wenn eine _Noreturn-Funktion in der Lage ist, zu ihrem Aufrufer zurückzukehren.

Gemäß §6.7.4, C11, Absatz 8

Eine mit einem _Noreturn-Funktionsbezeichner deklarierte Funktion darf nicht zu ihrem Aufrufer zurückkehren.

und, der Absatz 12, (Beachten Sie die Kommentare !!)

EXAMPLE 2
_Noreturn void f () {
abort(); // ok
}
_Noreturn void g (int i) { // causes undefined behavior if i <= 0
if (i > 0) abort();
}

Für C++ ist das Verhalten ziemlich ähnlich. Zitat aus Kapitel §7.6.4, C++14, Absatz 2 (Hervorhebung meines)

Wenn eine Funktion f aufgerufen wird, bei der f zuvor mit dem noreturn-Attribut und f schließlich .__ deklariert wurde. kehrt zurück, das Verhalten ist undefiniert.[Hinweis: Die Funktion wird möglicherweise beendet, indem eine Ausnahme ausgelöst wird. -Ende Hinweis ] 

[Hinweis: Implementierungen sollten eine Warnung ausgeben, wenn eine mit [[noreturn]] gekennzeichnete Funktion Rückkehr. —Endnotiz]

3 [Beispiel:

[[ noreturn ]] void f() {
throw "error"; // OK
}
[[ noreturn ]] void q(int i) { // behavior is undefined if called with an argument <= 0
if (i > 0)
throw "positive";
}

- Ende Beispiel]

116
Sourav Ghosh

Warum kehrt die Funktion func () nach Angabe des noreturn-Attributs zurück?

Weil Sie Code geschrieben haben, der es ihm sagte.

Wenn Sie nicht möchten, dass Ihre Funktion zurückkehrt, rufen Sie exit() oder abort() oder Ähnliches auf, damit sie nicht zurückkehrt.

Welche sonst würde Ihre Funktion anders als zurückgeben, nachdem sie printf() aufgerufen hatte?

Der C Standard in 6.7.4 Funktionsspezifizierer, Absatz 12 enthält insbesondere ein Beispiel für eine noreturn-Funktion, die tatsächlich zurückgeben kann - und bezeichnet das Verhalten als undefined:

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 ein Einschränkung, das you auf Ihren -Code platziert - er sagt dem Compiler "Mein Code wird niemals zurückgegeben". Wenn Sie gegen diese Einschränkung verstoßen, ist das alles bei Ihnen.

46
Andrew Henle

noreturn ist ein Versprechen. Sie sagen dem Compiler: "Es mag vielleicht nicht offensichtlich sein, aber I wissen auf Grundlage der Art, wie ich den Code geschrieben habe, dass diese Funktion niemals zurückkehren wird." Auf diese Weise kann der Compiler die Einrichtung der Mechanismen vermeiden, durch die die Funktion ordnungsgemäß zurückkehren kann. Wenn diese Mechanismen ausgelassen werden, kann der Compiler möglicherweise effizienteren Code erzeugen.

Wie kann eine Funktion nicht zurückkehren? Ein Beispiel wäre, wenn stattdessen exit() 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 werden kann, und Sie dann eine Funktion schreiben, die zurückgibt, was ist? der Compiler soll das tun? Grundsätzlich gibt es drei Möglichkeiten:

  1. Seien Sie "nett" zu Ihnen und finden Sie heraus, wie die Funktion trotzdem ordnungsgemäß wiederhergestellt werden kann.
  2. Geben Sie Code aus, der bei unkorrekter Rückkehr der Funktion abstürzt oder sich auf unvorhersehbare Weise verhält.
  3. Geben Sie eine Warnung oder Fehlermeldung, die Sie darauf hinweist, dass Sie Ihr Versprechen gebrochen haben.

Der Compiler kann 1, 2, 3 oder eine beliebige Kombination ausführen.

Wenn dies wie undefiniertes Verhalten klingt, liegt das daran, dass es so ist.

Im Endeffekt lautet das Programmieren wie im wirklichen Leben: Versprechen Sie nicht, was Sie nicht halten können. Möglicherweise hat jemand anderes aufgrund Ihres Versprechens Entscheidungen getroffen, und es können schlechte Dinge passieren, wenn Sie dann Ihr Versprechen brechen.

25
Steve Summit

Das noreturn-Attribut ist ein Versprechen, das Sie dem Compiler über Ihre Funktion machen.

Wenn Sie do return von einer solchen Funktion ausführen, ist das Verhalten undefiniert. Dies bedeutet jedoch nicht, dass ein normaler Compiler es Ihnen ermöglicht, den Status der Anwendung vollständig zu ändern, indem Sie die ret -Anweisung entfernen daraus schließen können, dass eine Rückgabe tatsächlich möglich ist. 

Wenn Sie jedoch Folgendes schreiben:

noreturn void func(void)
{
    printf("func\n");
}

int main(void)
{
    func();
    some_other_func();
}

dann ist es durchaus sinnvoll für den Compiler, den some_other_func vollständig zu entfernen, wenn es sich anfühlt.

15
Groo

Wie andere bereits erwähnt haben, ist dies ein klassisches undefiniertes Verhalten. Sie haben versprochen, func würde nicht wiederkommen, aber Sie haben es trotzdem zurückgegeben. Sie können die Stücke aufheben, wenn das bricht.

Obwohl der Compiler func auf übliche Weise kompiliert (trotz Ihrer noreturn), wirkt sich noreturn auf aufrufende Funktionen aus.

Sie können dies in der Assembly-Auflistung sehen: Der Compiler hat in main angenommen, dass func nicht zurückgegeben wird. Daher wurde der gesamte Code nach dem call func buchstäblich gelöscht (siehe unter https://godbolt.org/g/8hW6ZR ). Die Assembly-Auflistung wird nicht abgeschnitten. Sie endet buchstäblich nur hinter call func, da der Compiler voraussetzt, dass der Code danach nicht erreichbar ist. Wenn also func tatsächlich zurückkehrt, wird main damit beginnen, den beliebigen Mist auszuführen, der der main-Funktion folgt - sei es das Auffüllen, unmittelbare Konstanten oder ein Byte aus 00-Bytes. Wieder - sehr undefiniertes Verhalten.

Dies ist transitiv - eine Funktion, die eine noreturn-Funktion in allen möglichen Codepfaden aufruft, kann selbst als noreturn angenommen werden.

11
nneonneo

Nach this

Wenn die mit _Noreturn deklarierte Funktion zurückgegeben wird, ist das Verhalten undefiniert. Eine Compiler-Diagnose wird empfohlen, wenn dies erkannt werden kann.

Es liegt in der Verantwortung des Programmierers, sicherzustellen, dass diese Funktion niemals zurückkehrt, z. exit (1) am Ende der Funktion.

7
ChrisB

ret bedeutet einfach, dass die Funktion control an den Aufrufer zurückgibt. Also, main tut call func, führt die CPU die Funktion aus, und dann führt die CPU mit ret die Ausführung von main fort.

Bearbeiten

Also, stellt sich heraus , noreturn gibt make die Funktion nicht zurück, es ist nur ein Bezeichner, der dem Compiler sagt, dass der Code dieser Funktion so geschrieben ist Die Funktion kehrt nicht zurück. Sie sollten sich also vergewissern, dass diese Funktion die Kontrolle nicht an den Betrüger zurückgibt. Sie können beispielsweise exit darin aufrufen.

Angesichts dessen, was ich über diesen Bezeichner gelesen habe, scheint es so zu sein, dass man, um sicherzustellen, dass die Funktion nicht zu ihrem Aufrufpunkt zurückkehrt, die Funktion anothernoreturn in ihr aufruft und sich vergewissert, dass die letztere Funktion vorhanden ist Immer laufen (um undefiniertes Verhalten zu vermeiden) und verursacht keine UB selbst.

6
ForceBru

keine Return-Funktion speichert die Register des Eintrags nicht, da dies nicht erforderlich ist. Es macht die Optimierungen einfacher. Ideal für die Scheduler-Routine. 

Sehen Sie sich das Beispiel hier an: https://godbolt.org/g/2N3THC und erkennen Sie den Unterschied

6
P__J__

TL: DR: Es ist eine verpasste Optimierung von gcc.


noreturn ist ein Versprechen an den Compiler, dass die Funktion nicht zurückgegeben wird. Dies ermöglicht Optimierungen und ist insbesondere in Fällen nützlich, in denen der Compiler nur schwer nachweisen kann, dass eine Schleife niemals beendet wird, oder dass sonst kein Pfad durch eine zurückkehrende Funktion belegt wird.

GCC optimiert main bereits so, dass es am Ende der Funktion abfällt, wenn func() zurückgegeben wird, selbst wenn der Standardwert -O0 (minimale Optimierungsstufe) so ist, wie er verwendet wurde.

Die Ausgabe für func() selbst kann als verpasste Optimierung betrachtet werden. Es könnte einfach alles nach dem Funktionsaufruf weggelassen werden (da der Aufruf nicht zurückgegeben wird, kann die Funktion selbst noreturn sein). Dies ist kein hervorragendes Beispiel, da printf eine Standard-C-Funktion ist, von der bekannt ist, dass sie normal zurückkehrt (es sei denn, Sie setvbuf geben, um stdout einen Puffer zu geben, der segfault wird?)

Verwenden Sie 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 fügt func() ein und beseitigt den foo=3-Dead Store, so dass nur noch:

bar:
    sub     rsp, 8    ## align the stack
    call    ext
    mov     DWORD PTR foo[rip], 1
   ## fall off the end

Gcc geht immer noch davon aus, dass ext() zurückkehren wird, andernfalls könnte es nur ext() mit jmp ext am Ende heißen. Aber gcc macht keine noreturn-Funktionen, weil das Backtrace-Info verliert für Dinge wie abort(). Anscheinend ist das Inlining aber ok.

Gcc hätte auch optimieren können, indem der mov-Speicher nach der call weggelassen wurde. Wenn ext zurückgegeben wird, wird das Programm abgespritzt, sodass es keinen Sinn macht, Code zu generieren. Clang nimmt diese Optimierung in bar()main() vor.


_/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 könnte davon ausgehen, dass sie nicht zurückkehrt, und rbx und rbp verwendet, ohne sie zu speichern/wiederherzustellen.

Gcc for ARM32 macht das zwar, gibt aber Anweisungen aus, um ansonsten sauber zurückzukehren. Eine noreturn-Funktion, die tatsächlich auf ARM32 zurückkehrt, zerstört also die ABI und führt zu schwer zu debuggenden Problemen im Aufrufer oder zu einem späteren Zeitpunkt. (Undefiniertes Verhalten lässt dies zu, es ist jedoch mindestens ein Problem mit der Implementierungsqualität: 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 offensichtlich schädlich, wenn die Funktion einfach zurückkehrt. Gcc warnt, wenn es 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 den Rückgabebefehl optimieren (oder ihn durch einen unzulässigen Befehl ersetzen), würde dies die Codegröße reduzieren und einen lauten Fehler anstelle von stiller Korruption garantieren.

Wenn Sie die ret wegoptimieren möchten, ist es sinnvoll, alles zu entfernen, was nur erforderlich ist, wenn die Funktion zurückkehrt.

So könnte func() zu kompiliert werden:

    sub     rsp, 8
    call    ext
    # *p = a;  and so on assumed to never happen
    ud2                 # optional: illegal insn instead of fall-through

Jeder andere Befehl ist eine verpasste Optimierung. Wenn ext als noreturn deklariert ist, bekommen wir genau das.

Es kann davon ausgegangen werden, dass ein Basisblock , der mit einer Rückkehr endet, niemals erreicht wird.

0
Peter Cordes