包装abort()系统调用时出现奇怪的行为

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

这是一段代码:

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

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

 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的笔记本电脑。

在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指令。 function刚刚结束。 这意味着如果函数noret实际上因为一个bug(你的abort实现)而返回,那么任何事情都可能发生。 在这种情况下,我们将继续执行在我们之后的代码段中发生的任何事情。 也许是另一个函数,或者一些字符串,或者只是零,或者我们很幸运,内存映射在此之后结束。

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

 $ 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 

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

这是艺术答案下的讨论的延续,纯粹是一个实验。

不要在实际代码中执行此操作!

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

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

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

输出:

 pre abort __wrap_abort saved! EXIT_SUCCESS 

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

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

-D__noreturn __ =“/ * __noreturn__ * /”

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

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

您可以保留链接器–wrap逻辑以调用__wrap_abort()调用,或者,因为您不会调用__real_abort(),所以您可以执行与上面类似的操作来获取存根中止():

-Dabort = my_stubbed_abort

希望这可以帮助。