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

Posted

技术标签:

【中文标题】包装 abort() 系统调用时的奇怪行为【英文标题】:Strange behaviour while wrapping abort() system call 【发布时间】:2016-09-27 12:48:24 【问题描述】:

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

这是一段sn-p代码:

#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() 系统调用时会出现分段错误... 欢迎任何建议!

我在 x86_64 内核上运行 Debian GNU/Linux 8.5。机器是基于 Core i7 的笔记本电脑。

【问题讨论】:

以双下划线开头的名称保留给标准库和任何使用的实现。不应在用户代码中使用它们。 malloc 采用size_t,而不是int @Olaf 是使用 --wrap 强制使用双下划线 @olaf: 1/ 看看 --wrap 用法 2/ 在我的问题中毫无意义。提醒一下,这是一个测试代码......不是生产代码...... @LPs:好的,在链接器中找到了(忽略了文本中的命令行-应该正确格式化) 【参考方案1】:

在 glibc(这是 Debian 使用的 libc)中,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 指令。功能刚刚结束。这意味着,如果函数 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 之后发生的任何事情之后继续运行,在这个简单的示例中,编译器认为重新组织函数不是一个好主意。

【讨论】:

很好的答案,我想知道在中止中调用 longjmp 到中止之前调用的 setjmp 是否会有所帮助。 @2501 我也在想同样的事情,但是添加setjmp 来解决一个自己造成的问题(--wrap 的东西很少是一个好主意)就像用石棉浸泡氰化物泄漏。 是的,但这很有趣。 @Art 非常感谢这个全面的答案。我必须找到另一种编码或运行单元测试的方法;-) @binarym 包装的东西只是一个快速而肮脏的快捷方式。当您没有其他选择时,您会使用它。如果单元测试在您的控制之下,请为它们构建一个适当的框架来调用包装的 malloc/free 而不是中止,请使用您自己的函数使用适当的错误报告。即使这意味着更改程序的其余部分以使用一些包装宏。【参考方案2】:

这是Art's answer 下讨论的继续,纯粹是一个实验。

不要在实际代码中这样做!

在调用真正的中止之前,可以使用 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

【讨论】:

【参考方案3】:

上面的答案很好,带有汇编输出。在创建单元测试和存根 abort() 调用时,我再次遇到了同样的问题 - 编译器在 stdlib.h 中看到 __noreturn__ 特性,知道它可以在调用 __noreturn__ 函数后停止生成代码,但是 GCC 和其他编译器确实如此停止生成代码,即使优化被抑制。在对存根 abort() 的调用刚刚落入下一个函数、声明数据等之后返回。我尝试了上面的 --wrap 方法,但调用函数只是在 __wrap_abort() 返回后缺少代码。

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

-D__noreturn__="/* __noreturn__ */"

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

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

您可以保留链接器 --wrap 逻辑以调用您的 __wrap_abort() 调用,或者,由于您不会调用 __real_abort(),您可以执行与上述类似的操作来获取您的存根 abort( ):

-Dabort=my_stubbed_abort

希望这会有所帮助。

【讨论】:

以上是关于包装 abort() 系统调用时的奇怪行为的主要内容,如果未能解决你的问题,请参考以下文章

从 cython c 调用 python 函数时的奇怪行为

通过与另一个成员的类成员函数调用线程时的奇怪行为。 CPP

按值传递参数时的奇怪行为

不区分大小写的OSX文件系统 - 奇怪的更改目录(cd)bash行为

Thread.Abort 方法

DCMTK 的奇怪库行为