Strange behaviour while wrapping abort() system ca

2019-07-28 03:12发布

问题:

I need, to write unitary tests, to wrap the abort() system call.

Here is a snippet of code:

#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;
}

Then i compile this using the following command line :

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

Running it give the following output:

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

So everything is fine. Now let's test the same code but with abort() call uncommented:

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

I don't really understand why i get a segmentation fault while mocking abort() syscall... Every advice is welcome !

I run Debian GNU/Linux 8.5 on an x86_64 kernel. Machine is a Core i7 based laptop.

回答1:

In glibc (which is the libc Debian uses) the abort function (it's not a system call, it's a normal function) is declared like this:

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

This bit: __attribute__ ((__noreturn__)) is a gcc extension that tells it that the function can't return. Your wrapper function does return which the compiler didn't expect. Because of that it will crash or do something completely unexpected.

Your code when compiled will be using the declarations from stdlib.h for the call to abort, the flags you gave to the linker won't change that.

Noreturn functions are called differently, the compiler doesn't have to preserve registers, it can just jump to the function instead of doing a proper call, it might even just not generate any code after it because that code is by definition not reachable.

Here's a simple example:

extern void ret(void);
extern void noret(void) __attribute__((__noreturn__));

void
foo(void)
{
    ret();
    noret();
    ret();
    ret();
}

Compiled into assembler (even without optimizations):

$ 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

Notice that there is a call to noret, but there isn't any code after this. The two calls to ret were not generated and there is no ret instruction. The function just ends. This means that if the function noret actually returns because of a bug (which your implementation of abort has), anything can happen. In this case we'll just continue executing whatever happens to be in the code segment after us. Maybe another function, or some strings, or just zeroes, or maybe we're lucky and the memory mapping ends just after this.

In fact, let's do something evil. Never do this in real code. If you ever think that this is a good idea you'll need to hand over the keys to your computer and slowly step away from the keyboard while keeping your hands up:

$ 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

As I thought, the code just keeps going after whatever happened to be placed after main and in this simple example the compiler didn't think it would be a good idea to reorganize the functions.



回答2:

This is a continuation of the discussion under Art's answer, and is meant purely as an experiment.

Do not do this in real code!

The problem can be averted using longjmp to restore the environment, before calling the real abort.

The following program does not display undefined behavior:

#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 ;
}

Output:

pre abort
__wrap_abort
saved!
EXIT_SUCCESS


回答3:

Nice answer, above, with the assembly output. I had the same problem, again, while creating unit tests and stubbing the abort() call - the compiler sees the __noreturn__characteristic in stdlib.h, knows it CAN stop generating code after the call to a __noreturn__ function, but GCC and other compilers DO stop generating code, even with optimization suppressed. Returns after the call to the stubbed abort() just fell through to the next function, declared data, etc. I tried the --wrap approach, above, but the calling function is just missing code after the __wrap_abort() returns.

One way I found to override this behavior is to catch the abort() declaration at the preprocessor level - keep your stubbed abort() in a separate source file, and add to the CFLAGS for the file that's calling abort()

-D__noreturn__="/* __noreturn__ */"

This modifies the effect of the declaration found in stdlib.h. Check your preprocessor output via gcc -E and verify this worked. You can also check your compiler's output via objdump of the .o file.

This whole approach will have the added side effect of generating code for source that follows other abort() calls, exit() calls, and anything else that appears in stdlib.h with the __noreturn__ characteristic, but most of us don't have code that follows an exit(), and most of us just want to clean the stack and return from the abort() caller.

You can keep the linker --wrap logic in order to invoke your __wrap_abort() call, or, since you won't be calling __real_abort(), you can do something similar to the above to get to your stubbed abort():

-Dabort=my_stubbed_abort

Hope this helps.