而包装中止()系统调用的奇怪行为(Strange behaviour while wrapping

2019-09-29 03:54发布

我需要,编写统一测试,包装中止()系统调用。

下面是一个代码片段:

#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

所以,一切都很好。 现在,让我们来测试相同的代码,但与中止()调用未注释:

$ ./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。 机器是基于酷睿i7笔记本电脑。

Answer 1:

在glibc的(这是libc的Debian使用)的abort功能(这不是一个系统调用,这是一个正常的功能)可以声明如下:

extern void abort (void) __THROW __attribute__ ((__noreturn__));

该位: __attribute__ ((__noreturn__))是gcc的一个扩展,告诉它的函数不能返回。 你的包装函数不会返回,编译器没有想到的。 正因为如此它会崩溃或者做一些完全出乎意料。

您的代码编译将使用从声明时stdlib.h的呼叫abort ,你给的链接标志不会改变。

不返回的函数被调用不同,编译器不具有保持寄存器,它可以直接跳转到函数,而不是做一个适当的呼叫,它甚至可能只是不是因为代码是定义不可达生成后的任何代码。

这里有一个简单的例子:

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在这个简单的例子,编译器不认为这将是重组的功能是一个好主意。



Answer 2:

这是在讨论的延续艺术的回答 ,而纯粹是指作为一个实验。

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

这个问题可以用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


Answer 3:

尼斯的答案,上面用汇编输出。 我有同样的问题,同样,在创建单元测试和磕碰中止()调用 - 编译器看到__noreturn__characteristic stdlib.h中,知道它可以停止呼叫到__noreturn__功能后生成的代码,但海湾合作委员会和其他编译器一样停止生成代码,即使优化抑制。 在调用存根中止()只是通过对下一功能下降后返回,声明的数据,等等。我试过--wrap办法,同上,但是__wrap_abort()返回后,调用函数就是缺少代码。

我发现覆盖这种行为的一种方法是捕捉在预处理级中止()声明 - 让您的存根中止()在一个单独的源文件,并添加到CFLAGS为的呼唤中止文件()

-D__noreturn __ = “/ * __noreturn__ * /”

这会修改stdlib.h中发现的声明的效力。 检查通过gcc的-E您的预处理器的输出,并验证这个工作。 您也可以通过objdump的的.o文件将检查的编译器的输出。

这整个的做法将具有如下其它中止生成代码源的加入副作用()调用exit()调用,和其他任何出现在与__noreturn__特征stdlib.h中,但我们大多数人没有代码随后退出(),和我们大多数人只是想清理堆栈,并从中止()调用者返回。

你可以保持连接--wrap逻辑,以调用您的__wrap_abort()调用,或者,因为你不会被调用__real_abort(),你可以做一些类似上述的东西)去你的存根中止(:

-Dabort = my_stubbed_abort

希望这可以帮助。



文章来源: Strange behaviour while wrapping abort() system call