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
?
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,
return
explicitele 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 quef
a déjà été déclaré avec l'attributnoreturn
etf
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]
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.
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:
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.
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.
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
.
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.
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.
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
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.