Comportement étrange lors du bouclage d’un appel système abort ()

J’ai besoin d’écrire des tests unitaires pour emballer l’appel système abort ().

Voici un extrait de code:

#include  #include  #include  extern void __real_abort(void); extern void * __real_malloc(int c); extern void __real_free(void *); void __wrap_abort(void) { printf("=== Abort called !=== \n"); } void * __wrap_malloc(int s) { void *p = __real_malloc(s); printf("allocated %d bytes @%p\n",s, (void *)p); return p; } void __wrap_free(void *p) { printf("freeing @%p\n",(void *)p); return __real_free((void *)p); } int main(int ac, char **av) { char *p = NULL; printf("pre malloc: p=%p\n",p); p = malloc(40); printf("post malloc p=%p\n",p); printf("pre abort\n"); //abort(); printf("post abort\n"); printf("pre free\n"); free(p); printf("post free\n"); return -1; } 

Ensuite, je comstack ceci en utilisant la ligne de commande suivante:

 gcc -Wl,--wrap=abort,--wrap=free,--wrap=malloc -ggdb -o test test.c 

Son exécution donne la sortie suivante:

 $ ./test pre malloc: p=(nil) allocated 40 bytes @0xd06010 post malloc p=0xd06010 pre abort post abort pre free freeing @0xd06010 post free 

Alors tout va bien. Maintenant testons le même code mais avec un appel abort () non commenté:

 $ ./test pre malloc: p=(nil) allocated 40 bytes @0x1bf2010 post malloc p=0x1bf2010 pre abort === Abort called !=== Segmentation fault (core dumped) 

Je ne comprends pas vraiment pourquoi je reçois une erreur de segmentation en moquant syscall abort () … Chaque conseil est le bienvenu!

J’exécute Debian GNU / Linux 8.5 sur un kernel x86_64. La machine est un ordinateur portable basé sur Core i7.

Dans la glibc (utilisée par la libc utilisée par Debian), la fonction d’ abort (ce n’est pas un appel système, c’est une fonction normale) est déclarée comme ceci:

 extern void abort (void) __THROW __atsortingbute__ ((__noreturn__)); 

Ce bit: __atsortingbute__ ((__noreturn__)) est une extension de gcc qui lui indique que la fonction ne peut pas être retournée. Votre fonction wrapper renvoie des éléments auxquels le compilateur ne s’attendait pas. A cause de cela, il va se bloquer ou faire quelque chose de complètement inattendu.

Votre code, une fois compilé, utilisera les déclarations de stdlib.h pour l’appel à l’ abort . Les drapeaux que vous avez donnés à l’éditeur de liens ne changeront pas cela.

Les fonctions Noreturn sont appelées différemment, le compilateur n’a pas besoin de conserver les registres, il peut simplement sauter à la fonction au lieu de faire un appel approprié, il est même possible qu’il ne génère aucun code après, car ce code est par définition inaccessible.

Voici un exemple simple:

 extern void ret(void); extern void noret(void) __atsortingbute__((__noreturn__)); void foo(void) { ret(); noret(); ret(); ret(); } 

Compilé en assembleur (même sans optimisations):

 $ cc -S foo.c $ cat foo.s [...] foo: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 call ret call noret .cfi_endproc .LFE0: .size foo, .-foo .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)" .section .note.GNU-stack,"",@progbits 

Notez qu’il y a un appel à noret , mais il n’y a pas de code après cela. Les deux appels à ret n’ont pas été générés et il n’existe aucune instruction ret . La fonction se termine juste. Cela signifie que si la fonction noret est noret cause d’un bogue (ce que votre implémentation d’ abort a), tout peut arriver. Dans ce cas, nous allons simplement continuer à exécuter tout ce qui se trouve dans le segment de code après nous. Peut-être une autre fonction, ou des chaînes, ou juste des zéros, ou peut-être avons-nous de la chance et le mappage de la mémoire se termine juste après.

En fait, faisons quelque chose de mal. Ne faites jamais cela dans un code réel. Si vous pensez que c’est une bonne idée, vous devrez remettre les clés à votre ordinateur et vous éloigner lentement du clavier tout en gardant les mains en l’air:

 $ cat foo.c #include  #include  #include  void __wrap_abort(void) { printf("=== Abort called !=== \n"); } int main(int argc, char **argv) { abort(); return 0; } void evil(void) { printf("evil\n"); _exit(17); } $ gcc -Wl,--wrap=abort -o foo foo.c && ./foo === Abort called !=== evil $ echo $? 17 

Comme je le pensais, le code continue juste après tout ce qui est arrivé à être placé après main et dans cet exemple simple, le compilateur ne pensait pas que ce serait une bonne idée de réorganiser les fonctions.

Ceci est une continuation de la discussion sous la réponse de Art , et se veut purement expérimental.

Ne faites pas cela dans du vrai code!

Le problème peut être évité en utilisant longjmp pour restaurer l’environnement, avant d’appeler l’abandon réel.

Le programme suivant n’affiche pas le comportement indéfini:

 #include  #include  #include  _Noreturn void __real_abort( void ) ; jmp_buf env ; _Noreturn void __wrap_abort( void ) { printf( "%s\n" , __func__ ) ; longjmp( env , 1 ) ; __real_abort() ; } int main( void ) { const int abnormal = setjmp( env ) ; if( abnormal ) { printf( "saved!\n" ) ; } else { printf( "pre abort\n" ) ; abort() ; printf( "post abort\n" ) ; } printf( "EXIT_SUCCESS\n" ) ; return EXIT_SUCCESS ; } 

Sortie:

 pre abort __wrap_abort saved! EXIT_SUCCESS 

Bonne réponse, ci-dessus, avec la sortie d’assemblage. J’ai eu le même problème, encore une fois, tout en créant des tests unitaires et en écrasant l’appel abort () – le compilateur voit le caractère __noreturn__ dans stdlib.h, sait qu’il peut arrêter de générer du code après l’appel d’une fonction __noreturn__, mais GCC et d’autres compilateurs le font. arrête de générer du code, même si l’optimisation est supprimée. Les retours après l’appel de la fonction abort () stubbed sont simplement passés à la fonction suivante, aux données déclarées, etc. J’ai essayé l’approche –wrap ci-dessus, mais la fonction appelante manque tout simplement du code après le retour de __wrap_abort ().

L’une des méthodes que j’ai trouvée pour redéfinir ce comportement consiste à intercepter la déclaration abort () au niveau du préprocesseur – conservez votre abort () stubbed dans un fichier source séparé et ajoutez-le à CFLAGS pour le fichier qui appelle abort ()

-D__noreturn __ = “/ * __noreturn__ * /”

Cela modifie l’effet de la déclaration trouvée dans stdlib.h. Vérifiez la sortie de votre préprocesseur via gcc -E et vérifiez que cela a fonctionné. Vous pouvez également vérifier la sortie de votre compilateur via objdump du fichier .o.

Toute cette approche aura pour effet secondaire de générer du code pour la source qui suit les autres appels abort (), exit () et tout ce qui apparaît dans stdlib.h avec la caractéristique __noreturn__, mais la plupart d’entre nous n’ont pas de code. qui suit un exit (), et la plupart d’entre nous veulent simplement nettoyer la stack et revenir de l’appelant abort ().

Vous pouvez conserver la logique –wrap de l’éditeur de liens afin d’appeler votre appel __wrap_abort () ou, puisque vous n’appelez pas __real_abort (), vous pouvez procéder de la même manière que ci-dessus pour obtenir votre abort stubbed ():

-Dabort = mon_stubbed_abort

J’espère que cela t’aides.