2016-09-27 53 views
1

我需要编写单一测试来包装abort()系统调用。包装中止()系统调用时的奇怪行为

这里是一个代码段:

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 

所以

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

然后我使用下面的命令行编译此一切安好。 现在让我们来测试相同的代码,但与中止()调用未注释:

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

我真的不明白,为什么我得到一个分段错误而中止嘲讽()系统调用... 每一个建议是欢迎!

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

+0

打印错误后你不需要真的中止吗?因为错误是不可恢复的。 –

+1

以双下划线开头的名称保留给标准库和用于任何用途的实现。它们不应该用于用户代码中。 – Olaf

+0

@ Jean-FrançoisFabre事实上,我们的目标是避免在单一的测试环境中调用真正的abort()。 (以便我的测试框架可以检索测试状态并打印出报告)。 – binarym

回答

6

在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后发生的任何事情,在这个简单的例子中,编译器不认为重新组织函数是一个好主意。

+0

很好的答案,我想知道如果在中止之前调用longjmp到一个在中止之前调用的setjmp会有所帮助。 – 2501

+0

@ 2501我在想同样的事情,但是增加'setjmp'来解决一个自己造成的问题(''wrap'东西很少是一个好主意)就像使用石棉吸收氰化物溢出一样。 – Art

+0

是的,但很有趣。 – 2501

0

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

不要这样做在真正的代码!

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

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

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

输出:

pre abort 
__wrap_abort 
saved! 
EXIT_SUCCESS 
0
尼斯

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

我发现重写此行为的一种方法是在预处理器级别捕获abort()声明 - 将您的残段abort()保存在单独的源文件中,并添加到调用abort()的文件的CFLAGS中

-D__noreturn __ = “/ * __noreturn__ * /”

此修改中stdlib.h中找到的声明的效果。通过gcc -E检查你的预处理器输出,并验证它是否正常工作。你也可以通过.o文件的objdump来检查你的编译器的输出。

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

您可以保留链接器 - 打包逻辑以调用__wrap_abort()调用,或者,由于您不会调用__real_abort(),因此您可以执行类似于上述操作的操作, ():

-Dabort = my_stubbed_abort

希望这有助于。