C 包装abort()系统调用时的奇怪行为
为了编写统一测试,我需要包装abort()系统调用 下面是一段代码:C 包装abort()系统调用时的奇怪行为,c,linux,debugging,mocking,abort,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) { prin
#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 @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。机器是基于核心i7的笔记本电脑。在glibc(libc Debian使用的)中,abort
函数(不是系统调用,是正常函数)声明如下:
extern void abort (void) __THROW __attribute__ ((__noreturn__));
此位:\uuuuuuu属性((\uuuuu noreturn\uuuuu))
是一个gcc扩展,它告诉函数不能返回。您的包装函数确实返回了编译器没有预料到的结果。因此,它将崩溃或做一些完全出乎意料的事情
编译时,您的代码将使用stdlib.h
中的声明调用abort
,您给链接器的标志不会改变这一点
Noreturn函数的调用是不同的,编译器不必保留寄存器,它可以直接跳转到函数而不是进行适当的调用,它甚至可能不会在调用之后生成任何代码,因为根据定义,该代码是不可访问的
下面是一个简单的例子:
extern void ret(void);
extern void noret(void) __attribute__((__noreturn__));
void
foo(void)
{
ret();
noret();
ret();
ret();
}
编译成汇编程序(即使没有优化):
请注意,有一个调用noret
,但在此之后没有任何代码。未生成对ret
的两个调用,并且没有ret
指令。函数刚刚结束。这意味着,如果函数noret
由于一个bug(您的abort
实现存在该bug)而实际返回,那么任何事情都可能发生。在本例中,我们将继续执行后面代码段中发生的任何事情。也许是另一个函数,或者是一些字符串,或者仅仅是零,或者也许我们很幸运,内存映射在这之后就结束了
事实上,让我们做点坏事吧。永远不要在真正的代码中这样做。如果您认为这是一个好主意,您需要将键盘交给计算机,然后慢慢地离开键盘,同时保持双手向上:
$ 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
$cat foo.c
#包括
#包括
#包括
作废(作废)(作废)
{
printf(“==调用中止!==\n”);
}
int
主(内部argc,字符**argv)
{
中止();
返回0;
}
无效的
邪恶(空虚)
{
printf(“邪恶”);
_出口(17);
}
$gcc-Wl,--wrap=abort-o foo foo.c&&./foo
==呼叫中止===
恶毒的
$echo$?
17
正如我所想,代码只是一直在跟踪发生在main
之后的内容,在这个简单的示例中,编译器认为重新组织函数不是一个好主意。在glibc中(这是libc Debian使用的)abort
函数(它不是系统调用,它是一个普通函数)声明如下:
extern void abort (void) __THROW __attribute__ ((__noreturn__));
此位:\uuuuuuu属性((\uuuuu noreturn\uuuuu))
是一个gcc扩展,它告诉函数不能返回。您的包装函数确实返回了编译器没有预料到的结果。因此,它将崩溃或做一些完全出乎意料的事情
编译时,您的代码将使用stdlib.h
中的声明调用abort
,您给链接器的标志不会改变这一点
Noreturn函数的调用是不同的,编译器不必保留寄存器,它可以直接跳转到函数而不是进行适当的调用,它甚至可能不会在调用之后生成任何代码,因为根据定义,该代码是不可访问的
下面是一个简单的例子:
extern void ret(void);
extern void noret(void) __attribute__((__noreturn__));
void
foo(void)
{
ret();
noret();
ret();
ret();
}
编译成汇编程序(即使没有优化):
请注意,有一个调用noret
,但在此之后没有任何代码。未生成对ret
的两个调用,并且没有ret
指令。函数刚刚结束。这意味着,如果函数noret
由于一个bug(您的abort
实现存在该bug)而实际返回,那么任何事情都可能发生。在本例中,我们将继续执行后面代码段中发生的任何事情。也许是另一个函数,或者是一些字符串,或者仅仅是零,或者也许我们很幸运,内存映射在这之后就结束了
事实上,让我们做点坏事吧。永远不要在真正的代码中这样做。如果您认为这是一个好主意,您需要将键盘交给计算机,然后慢慢地离开键盘,同时保持双手向上:
$ 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
$cat foo.c
#包括
#包括
#包括
作废(作废)(作废)
{
printf(“==调用中止!==\n”);
}
int
主(内部argc,字符**argv)
{
中止();
返回0;
}
无效的
邪恶(空虚)
{
printf(“邪恶”);
_出口(17);
}
$gcc-Wl,--wrap=abort-o foo foo.c&&./foo
==呼叫中止===
恶毒的
$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 ;
}
这是下面讨论的继续,纯粹是一个实验 不要在真实代码中执行此操作强> 在调用真正的中止之前,使用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 ;
}
上面是一个很好的答案,带有程序集输出。我在创建单元测试和中止abort()调用时也遇到了同样的问题-编译器在stdlib.h中看到了_noreturn__特征,知道吗