web-dev-qa-db-fra.com

Pourquoi la fonction "noreturn" revient-elle?

J'ai lu this question sur l'attribut noreturn, qui est utilisé pour les fonctions qui ne retournent pas à l'appelant.

Ensuite, j'ai fait un programme en C.

#include <stdio.h>
#include <stdnoreturn.h>

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

int main()
{
        func();
}

Et généré Assembly du code en utilisant 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

Pourquoi la fonction func() est-elle renvoyée après avoir fourni l'attribut noreturn?

66
M.S Chaudhari

Les spécificateurs de fonction en C sont un {indice au compilateur, le degré d'acceptation est défini par l'implémentation.

Tout d'abord, le spécificateur de fonction _Noreturn (ou, noreturn, utilisant <stdnoreturn.h>) est un indice pour le compilateur concernant une promesse théorique faite par le programmeur que cette fonction ne reviendra jamais. Sur la base de cette promesse, le compilateur peut prendre certaines décisions, effectuer certaines optimisations pour la génération de code. 

IIRC, si une fonction spécifiée avec le spécificateur de fonction noreturn retourne à son appelant,

  • en utilisant et une instruction return explicite
  • en atteignant la fin du corps de la fonction

le comportement est indéfini . Vous NE DOIT PAS revenir de la fonction. 

Pour clarifier, utiliser noreturn spécificateur de fonction n'arrête pas un formulaire de fonction retournant à son appelant. C'est une promesse faite par le programmeur au compilateur de lui laisser un degré de liberté supplémentaire pour générer du code optimisé.

Maintenant, au cas où vous auriez fait une promesse plus tôt et plus tard, choisissez de violer cela, le résultat est UB. Les compilateurs sont encouragés, mais pas obligés, à produire des avertissements lorsqu'une fonction _Noreturn semble être capable de retourner à son appelant.

Selon le chapitre §6.7.4, C11, paragraphe 8

Une fonction déclarée avec un spécificateur de fonction _Noreturn ne doit pas retourner à son appelant.

et le paragraphe 12, (Notez les commentaires !!)

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

Pour C++, le comportement est assez similaire. Citant le chapitre §7.6.4, C++14, paragraphe 2 (italiques)

Si une fonction f est appelée alors que f a déjà été déclaré avec l'attribut noreturn et f finalement retourne, le comportement est indéfini.[Remarque: la fonction peut être fermée en générant une exception. —End Note]

[Note: les implémentations sont invitées à émettre un avertissement si une fonction marquée [[noreturn]] peut Renvoyer. —End note]}

3 [Exemple:

[[ 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";
}

—Fin exemple]

116
Sourav Ghosh

Pourquoi la fonction func () retourne-t-elle après avoir fourni l'attribut noreturn?

Parce que vous avez écrit un code qui le dit.

Si vous ne voulez pas que votre fonction soit renvoyée, appelez exit() ou abort() ou similaire, afin qu'elle ne revienne pas.

Que else votre fonction ferait-elle si elle ne retournait pas après avoir appelé printf()?

Le paramètre C Standard in 6.7.4 Spécificateurs de fonction, paragraphe 12, inclut spécifiquement un exemple de fonction noreturn qui peut réellement renvoyer - et attribue le comportement non défini:

Exemple 2

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

En bref, noreturn est une restriction que vous placez sur votre code - il indique au compilateur "Mon code ne reviendra jamais". Si vous violez cette restriction, c'est tout pour vous.

46
Andrew Henle

noreturn est une promesse. Vous dites au compilateur: "Cela peut être évident ou non, maisIsais, en fonction de la manière dont j'ai écrit le code, que cette fonction ne reviendra jamais." De cette façon, le compilateur peut éviter de configurer les mécanismes qui permettraient à la fonction de revenir correctement. L'abandon de ces mécanismes pourrait permettre au compilateur de générer un code plus efficace.

Comment une fonction ne peut-elle pas revenir? Un exemple serait si elle appelait plutôt exit().

Mais si vous promettez au compilateur que votre fonction ne reviendra pas et si le compilateur ne lui permet pas de retourner correctement, vous écrivez alors une fonction qui fait retourne, le compilateur est censé faire? Il a fondamentalement trois possibilités:

  1. Soyez "gentil" avec vous et trouvez un moyen de récupérer correctement la fonction.
  2. Émettez un code qui, lorsque la fonction est renvoyée de manière incorrecte, se bloque ou se comporte de manière imprévisible de façon arbitraire.
  3. Vous donner un avertissement ou un message d'erreur indiquant que vous avez rompu votre promesse.

Le compilateur peut faire 1, 2, 3 ou une combinaison des deux.

Si cela ressemble à un comportement indéfini, c'est parce qu'il l'est.

La programmation, comme dans la vraie vie, est la suivante: ne faites pas de promesses que vous ne pouvez pas tenir. Quelqu'un d'autre aurait pu prendre des décisions en fonction de votre promesse, et de mauvaises choses peuvent arriver si vous ne respectez pas cette promesse.

25
Steve Summit

L'attribut noreturn est une promesse que vous faites au compilateur à propos de votre fonction.

Si vous do revenez d'une telle fonction, le comportement n'est pas défini, mais cela ne signifie pas qu'un compilateur sensé vous permettra de modifier complètement l'état de l'application en supprimant l'instruction ret, d'autant pouvoir en déduire qu'un retour est effectivement possible. 

Cependant, si vous écrivez ceci:

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

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

dans ce cas, il est parfaitement raisonnable que le compilateur supprime le some_other_func complètement.

15
Groo

Comme d’autres l’ont mentionné, c’est un comportement classique non défini. Vous avez promis que func ne reviendrait pas, mais vous l'avez quand même fait revenir. Vous pouvez ramasser les morceaux quand ça casse.

Bien que le compilateur compile func de la manière habituelle (malgré votre noreturn), la noreturn affecte les fonctions d'appel.

Vous pouvez le voir dans la liste Assembly: le compilateur a supposé, dans main, que func ne reviendrait pas. Par conséquent, il a littéralement supprimé tout le code après le call func (voyez par vous-même à https://godbolt.org/g/8hW6ZR ). La liste d'assembly n'est pas tronquée, elle se termine littéralement après le call func car le compilateur suppose que tout code ultérieur serait inaccessible. Ainsi, lorsque func retournera effectivement, main commencera à exécuter la séquence qui suit la fonction main - que ce soit le remplissage, les constantes immédiates ou une mer de 00 octets. Encore une fois - comportement très indéfini.

Ceci est transitif - une fonction qui appelle une fonction noreturn dans tous les chemins de code possibles peut elle-même être supposée être noreturn.

11
nneonneo

Selon this

Si la fonction déclarée _Noreturn retourne, le comportement n'est pas défini. Un diagnostic du compilateur est recommandé si cela peut être détecté.

Il incombe au programmeur de s’assurer que cette fonction ne revient jamais, par exemple. exit (1) à la fin de la fonction.

7
ChrisB

ret signifie simplement que la fonction renvoie control à l'appelant. Donc, main fait call func, le CPU exécute la fonction, puis avec ret, le CPU continue l'exécution de main.

Modifier

Donc, il s’avère que , noreturn ne rend pas la fonction retournée du tout, c’est juste un spécificateur qui indique au compilateur que le code de cette fonction est écrit de telle manière que la fonction ne reviendra pas. Vous devez donc vous assurer que cette fonction ne rend pas le contrôle à l'appelé. Par exemple, vous pouvez appeler exit à l'intérieur.

De plus, étant donné ce que j'ai lu sur ce spécificateur, il semble que pour s'assurer que la fonction ne revienne pas à son point d'invocation, vous devez appeler la fonction une autre fonctionnoreturn à l'intérieur et vous assurer que ce dernier est toujours exécuter (afin d'éviter un comportement indéfini) et ne cause pas UB lui-même.

6
ForceBru

aucune fonction de retour ne sauvegarde les registres de l'entrée car ce n'est pas nécessaire. Cela facilite les optimisations. Idéal pour la routine du planificateur par exemple. 

Voir l'exemple ici: https://godbolt.org/g/2N3THC et repérez la différence

6
P__J__

TL: DR: C'est une optimisation manquée par gcc.


noreturn est une promesse faite au compilateur que la fonction ne reviendra pas. Cela permet des optimisations et est particulièrement utile dans les cas où il est difficile pour le compilateur de prouver qu’une boucle ne se fermera jamais, ou de prouver qu’il n’ya pas de chemin à travers une fonction qui retourne.

GCC optimise déjà main pour qu’il tombe de la fin de la fonction si func() est renvoyé, même avec le -O0 (niveau d’optimisation minimal) par défaut que vous avez utilisé.

La sortie de func() elle-même pourrait être considérée comme une optimisation manquée. il peut simplement tout omettre après l'appel de la fonction (puisqu'il est impossible de renvoyer l'appel, c'est-à-dire que la fonction elle-même peut être noreturn). Ce n'est pas un bon exemple car printf est une fonction standard C qui est connue pour retourner normalement (sauf si vous setvbuf donne à stdout un tampon qui segfault?)

Permet d’utiliser une fonction différente que le compilateur ne connaît pas.

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 sur le Explorateur du compilateur Godbolt .)

la sortie de gcc7.2 pour bar() est intéressante. Il insère func() et élimine le magasin mort foo=3 en laissant simplement:

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

Gcc suppose toujours que ext() va revenir, sinon il aurait pu simplement s'appeler ext() avec jmp ext. Mais gcc n’appelle pas les fonctions noreturn, parce que perd les informations de trace pour des choses comme abort(). Apparemment, les aligner, ça va.

Gcc aurait pu optimiser en omettant le magasin mov après le call. Si ext est renvoyé, le programme est installé, il est donc inutile de générer ce code. Clang effectue cette optimisation dans bar()main().


_/func est lui-même plus intéressant, et une optimisation manquée plus importante.

gcc et clang émettent presque la même chose:

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

Cette fonction peut supposer qu'elle ne retourne pas et utiliser rbx et rbp sans les sauvegarder/restaurer.

Gcc pour ARM32 fait effectivement cela, mais émet toujours des instructions pour retourner autrement proprement. Ainsi, une fonction noreturn qui revient effectivement sur ARM32 cassera l'ABI et causera des problèmes difficiles à déboguer chez l'appelant ou plus tard. (Un comportement non défini permet cela, mais c'est au moins un problème de qualité d'implémentation: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158 .)

C'est une optimisation utile dans les cas où gcc ne peut pas prouver si une fonction renvoie ou non. (Cela est évidemment préjudiciable lorsque la fonction revient simplement, cependant. Gcc avertit quand il est certain qu'une fonction noreturn reviendra.) Les autres architectures cibles de gcc ne le font pas; c'est aussi une optimisation manquée.

Mais gcc ne va pas assez loin: optimiser également l'instruction de retour (ou le remplacer par une instruction illégale) permettrait de réduire la taille du code et de garantir les échecs bruyants au lieu de la corruption silencieuse.

Et si vous voulez optimiser la ret, optimiser tout ce qui n’est nécessaire que si la fonction revient est logique.

Ainsi, func() pourrait être compilé en:

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

Toute autre instruction présente est une optimisation manquée. Si ext est déclaré noreturn, c'est exactement ce que nous obtenons.

Tout bloc de base } qui se termine par un retour peut être supposé ne jamais être atteint.

0
Peter Cordes