包装abort()系统调用

时间:2016-09-27 12:48:24

标签: c linux debugging mocking abort

我需要编写单一测试来包装abort()系统调用。

以下是一段代码:

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

然后我使用以下命令行编译它:

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

运行它会提供以下输出:

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

所以一切都很好。 现在让我们测试相同的代码,但使用abort()调用取消注释:

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

我真的不明白为什么在模拟abort()系统调用时出现分段错误... 欢迎提出建议!

我在x86_64内核上运行Debian GNU / Linux 8.5。 Machine是基于Core i7的笔记本电脑。

3 个答案:

答案 0 :(得分:7)

在glibc中(这是libc Debian使用的)abort函数(它不是系统调用,它是一个正常的函数)声明如下:

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

这一位:__attribute__ ((__noreturn__))是一个gcc扩展名,告诉它该函数无法返回。您的包装器函数确实返回编译器没有预料到的。因为它会崩溃或做一些完全出乎意料的事情。

编译后的代码将使用stdlib.h的声明来调用abort,你给链接器的标志不会改变它。

Noreturn函数的调用方式不同,编译器不必保留寄存器,它只能跳转到函数而不是正确的调用,它甚至可能不会生成任何代码,因为代码是通过定义无法到达。

这是一个简单的例子:

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

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

编译成汇编程序(即使没有优化):

$ 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

请注意,有noret的来电,但此后没有任何代码。未生成对ret的两次调用,并且没有ret指令。功能刚刚结束。这意味着如果函数noret由于某个错误(您的abort的实现具有)而实际返回,则可能发生任何事情。在这种情况下,我们将继续执行在我们之后的代码段中发生的任何事情。也许是另一个函数,或者一些字符串,或者只是零,或者我们很幸运,内存映射就在此之后结束。

事实上,让我们做一些邪恶的事情。永远不要在实际代码中这样做。如果您认为这是一个好主意,您需要将钥匙交给您的计算机,并在保持双手的同时慢慢离开键盘:

$ 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

正如我所想的那样,代码只是继main之后发生的事情继续进行,在这个简单的例子中,编译器并不认为重新组织函数是个好主意。

答案 1 :(得分:0)

这是Art's answer下讨论的延续,纯粹是一个实验。

请勿在实际代码中执行此操作!

在调用真正的中止之前,可以使用longjmp恢复环境来避免此问题。

以下程序不显示未定义的行为:

f

输出:

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

答案 2 :(得分:0)

上面的答案很好,装配输出。我有同样的问题,同样,在创建单元测试和存根abort()调用时 - 编译器在stdlib.h中看到__noreturn__特性,知道它在调用__noreturn__函数后停止生成代码,但是GCC和其他编译器DO停止生成代码,即使优化被抑制。在调用stubbed abort()之后返回到下一个函数,声明数据等等。我尝试了上面的--wrap方法,但调用函数只是在__wrap_abort()返回后丢失代码。

我发现覆盖此行为的一种方法是在预处理器级别捕获abort()声明 - 将stubbed abort()保存在单独的源文件中,并将CFLAGS添加到调用abort()<的文件中/ p>

-D__noreturn __ =“/ * __noreturn__ * /”

这会修改stdlib.h中声明的效果。通过gcc -E检查预处理器输出并验证这是否有效。您还可以通过.o文件的objdump检查编译器的输出。

这整个方法会产生额外的副作用,即在其他abort()调用,exit()调用以及stdlib.h中出现的具有__noreturn__特性的任何其他内容的源代码生成代码,但我们大多数人都不会我的代码跟在exit()之后,我们大多数人只想清理堆栈并从abort()调用者返回。

你可以保留链接器--wrap逻辑以调用你的__wrap_abort()调用,或者,因为你不会调用__real_abort(),你可以做类似于上面的操作来获取你的存根中止( ):

-Dabort = my_stubbed_abort

希望这有帮助。