C 包装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

为了编写统一测试,我需要包装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;
}
运行它会得到以下输出:

$ ./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__特征,知道吗