包装 abort() 系统调用时的奇怪行为
Strange behaviour while wrapping abort() system call
为了编写单一测试,我需要包装 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() 系统调用时会出现分段错误...
欢迎大家提出建议!
我 运行 Debian GNU/Linux 8.5 x86_64 内核。机器是基于 Core i7 的笔记本电脑。
在 glibc(Debian 使用的 libc)中,abort
函数(它不是系统调用,它是一个普通函数)声明如下:
extern void abort (void) __THROW __attribute__ ((__noreturn__));
这个位:__attribute__ ((__noreturn__))
是一个 gcc 扩展,告诉它函数不能 return。您的包装函数执行编译器未预期的 return 。因此它会崩溃或做一些完全意想不到的事情。
您的代码在编译时将使用 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
实际上是 return 因为一个错误(你的 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
之后发生的任何事情之后继续执行,在这个简单的示例中,编译器认为重新组织函数并不是一个好主意。
这是 下讨论的延续,纯粹是一个实验。
不要在实际代码中这样做!
在调用真正的中止之前,使用 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
上面的答案很好,带有汇编输出。我再次遇到同样的问题,同时创建单元测试和存根 abort() 调用 - 编译器在 stdlib.h 中看到 __noreturn__characteristic,知道它可以在调用 [=30 后停止生成代码=] 函数,但 GCC 和其他编译器确实停止生成代码,即使优化被抑制。 Returns 在调用 stubbed abort() 之后直接进入下一个函数、声明的数据等。我尝试了上面的 --wrap 方法,但是调用函数只是缺少 [=31 之后的代码=]() returns.
我发现覆盖此行为的一种方法是在预处理器级别捕获 abort() 声明 - 将存根的 abort() 保存在单独的源文件中,并添加到调用 abort( )
-D__noreturn__="/* __noreturn__ */"
这修改了 stdlib.h 中声明的效果。通过 gcc -E 检查您的预处理器输出并验证它是否有效。您还可以通过 .o 文件的 objdump 检查编译器的输出。
整个方法会产生额外的副作用,即为源代码生成其他 abort() 调用、exit() 调用以及 stdlib.h 中出现的任何其他内容以及 __noreturn__特性,但我们大多数人都没有 exit() 后面的代码,我们大多数人只想清理堆栈和来自 abort() 调用者的 return。
您可以保留链接器 --wrap 逻辑以调用您的 __wrap_abort() 调用,或者,由于您不会调用 __real_abort(),您可以执行类似的操作到上面去你的 stubbed abort():
-Dabort=my_stubbed_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() 系统调用时会出现分段错误... 欢迎大家提出建议!
我 运行 Debian GNU/Linux 8.5 x86_64 内核。机器是基于 Core i7 的笔记本电脑。
在 glibc(Debian 使用的 libc)中,abort
函数(它不是系统调用,它是一个普通函数)声明如下:
extern void abort (void) __THROW __attribute__ ((__noreturn__));
这个位:__attribute__ ((__noreturn__))
是一个 gcc 扩展,告诉它函数不能 return。您的包装函数执行编译器未预期的 return 。因此它会崩溃或做一些完全意想不到的事情。
您的代码在编译时将使用 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
实际上是 return 因为一个错误(你的 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
之后发生的任何事情之后继续执行,在这个简单的示例中,编译器认为重新组织函数并不是一个好主意。
这是
不要在实际代码中这样做!
在调用真正的中止之前,使用 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
上面的答案很好,带有汇编输出。我再次遇到同样的问题,同时创建单元测试和存根 abort() 调用 - 编译器在 stdlib.h 中看到 __noreturn__characteristic,知道它可以在调用 [=30 后停止生成代码=] 函数,但 GCC 和其他编译器确实停止生成代码,即使优化被抑制。 Returns 在调用 stubbed abort() 之后直接进入下一个函数、声明的数据等。我尝试了上面的 --wrap 方法,但是调用函数只是缺少 [=31 之后的代码=]() returns.
我发现覆盖此行为的一种方法是在预处理器级别捕获 abort() 声明 - 将存根的 abort() 保存在单独的源文件中,并添加到调用 abort( )
-D__noreturn__="/* __noreturn__ */"
这修改了 stdlib.h 中声明的效果。通过 gcc -E 检查您的预处理器输出并验证它是否有效。您还可以通过 .o 文件的 objdump 检查编译器的输出。
整个方法会产生额外的副作用,即为源代码生成其他 abort() 调用、exit() 调用以及 stdlib.h 中出现的任何其他内容以及 __noreturn__特性,但我们大多数人都没有 exit() 后面的代码,我们大多数人只想清理堆栈和来自 abort() 调用者的 return。
您可以保留链接器 --wrap 逻辑以调用您的 __wrap_abort() 调用,或者,由于您不会调用 __real_abort(),您可以执行类似的操作到上面去你的 stubbed abort():
-Dabort=my_stubbed_abort
希望对您有所帮助。