Comportamiento extraño al envolver la llamada al sistema abort()
linux debugging (3)
En glibc (que es la libc que usa Debian), la función abort
(no es una llamada al sistema, es una función normal) se declara así:
extern void abort (void) __THROW __attribute__ ((__noreturn__));
Este bit: __attribute__ ((__noreturn__))
es una extensión de gcc que le dice que la función no puede regresar. Su función de envoltura regresa, lo que el compilador no esperaba. Por eso se bloqueará o hará algo completamente inesperado.
Su código cuando se compilará usará las declaraciones de stdlib.h
para que la llamada se abort
, las banderas que le dio al enlazador no cambiarán eso.
Las funciones de Noreturn se llaman de forma diferente, el compilador no tiene que conservar los registros, puede saltar a la función en lugar de hacer una llamada adecuada, incluso puede que simplemente no genere ningún código después porque el código por definición no es alcanzable.
Aquí hay un ejemplo simple:
extern void ret(void);
extern void noret(void) __attribute__((__noreturn__));
void
foo(void)
{
ret();
noret();
ret();
ret();
}
Compilado en ensamblador (incluso sin optimizaciones):
$ 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
Observe que hay una llamada a noret
, pero no hay ningún código después de esto. Las dos llamadas a ret
no se generaron y no hay instrucción ret
. La función simplemente termina. Esto significa que si la función noret
realmente regresa debido a un error (que tiene su implementación de abort
), cualquier cosa puede suceder. En este caso, seguiremos ejecutando lo que sea que esté en el segmento de código después de nosotros. Tal vez otra función, o algunas cuerdas, o simplemente ceros, o tal vez tenemos suerte y el mapeo de memoria termina justo después de esto.
De hecho, hagamos algo malvado. Nunca hagas esto en código real. Si alguna vez piensa que es una buena idea, deberá entregar las llaves de su computadora y alejarse lentamente del teclado mientras mantiene las manos en alto:
$ cat foo.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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
Como pensé, el código simplemente continúa después de que lo que pasó se coloque después de main
y en este ejemplo simple el compilador no pensó que sería una buena idea reorganizar las funciones.
Necesito, para escribir pruebas unitarias, para envolver la llamada al sistema abort ().
Aquí hay un fragmento de código:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
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;
}
Luego compilo esto usando la siguiente línea de comando:
gcc -Wl,--wrap=abort,--wrap=free,--wrap=malloc -ggdb -o test test.c
Al ejecutarlo se obtiene el siguiente resultado:
$ ./test
pre malloc: p=(nil)
allocated 40 bytes @0xd06010
post malloc p=0xd06010
pre abort
post abort
pre free
freeing @0xd06010
post free
Entonces todo está bien. Ahora probemos el mismo código pero con abort () call uncommented:
$ ./test
pre malloc: p=(nil)
allocated 40 bytes @0x1bf2010
post malloc p=0x1bf2010
pre abort
=== Abort called !===
Segmentation fault (core dumped)
Realmente no entiendo por qué obtengo una falla de segmentación mientras me burlo de la llamada al sistema abort () ... ¡Todos los consejos son bienvenidos!
Ejecuto Debian GNU / Linux 8.5 en un kernel x86_64. Machine es una computadora portátil basada en Core i7.
Buena respuesta, arriba, con la salida de ensamblaje. Tuve el mismo problema, nuevamente, mientras creaba pruebas unitarias y anulaba la llamada abort () - el compilador ve __noreturn__characteristic en stdlib.h, sabe que PUEDE detener la generación de código después de la llamada a una función __noreturn__, pero GCC y otros compiladores deje de generar código, incluso con la optimización suprimida. Retorna después de que la llamada al aborto aplazado () acaba de pasar a la siguiente función, datos declarados, etc. Probé el enfoque --wrap, arriba, pero la función de llamada es el código que falta después de que devuelve __wrap_abort ().
Una forma que encontré para anular este comportamiento es capturar la declaración de abort () en el nivel del preprocesador: mantenga su stubbed abort () en un archivo fuente separado, y agregue a las CFLAGS para el archivo que está llamando abort ()
-D__noreturn __ = "/ * __noreturn__ * /"
Esto modifica el efecto de la declaración encontrada en stdlib.h. Verifique la salida de su preprocesador a través de gcc -E y verifique que funcionó. También puede verificar la salida de su compilador a través de objdump del archivo .o.
Todo este enfoque tendrá el efecto secundario adicional de generar código para la fuente que sigue a otras llamadas abort (), llamadas a exit () y cualquier otra cosa que aparezca en stdlib.h con la característica __noreturn__, pero la mayoría de nosotros no tenemos código que sigue a una salida (), y la mayoría de nosotros solo queremos limpiar la pila y regresar del llamador abort ().
Puede mantener el enlazador --wrap logic para invocar su llamada __wrap_abort (), o, como no va a llamar a __real_abort (), puede hacer algo similar a lo anterior para acceder a su aborto aplazado ():
-Dabort = my_stubbed_abort
Espero que esto ayude.
Esta es una continuación de la discusión bajo la respuesta de Art , y se entiende puramente como un experimento.
¡No hagas esto en código real!
El problema se puede evitar utilizando longjmp para restaurar el entorno, antes de llamar al aborto real.
El siguiente programa no muestra un comportamiento indefinido:
#include <stdlib.h>
#include <stdio.h>
#include <setjmp.h>
_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 ;
}
Salida:
pre abort
__wrap_abort
saved!
EXIT_SUCCESS