为什么在x86_64汇编代码中调用C语言abort()函数会导致分段错误(SIGSEGV),而不是一个abort信号?

4
考虑下面的程序:
main.c
#include <stdlib.h>

void my_asm_func(void);
__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "call abort;"
    "ret;"
);

int main(int argc, char **argv) {
    if (argv[1][0] == '0') {
        abort();
    } else if (argv[1][0] == '1') {
        __asm__("call abort");
    } else {
        my_asm_func();
    }
}

我编译为:

gcc -ggdb3 -O0 -o main.out main.c

然后我有:

$ ./main.out 0; echo $?
Aborted (core dumped)
134
$ ./main.out 1; echo $?
Aborted (core dumped)
134
$ ./main.out 2; echo $?
Segmentation fault (core dumped)
139

为什么我只在最后一次运行中得到分段错误而不是预期的中止信号?
man 7 signal:
   SIGABRT       6       Core    Abort signal from abort(3)
   SIGSEGV      11       Core    Invalid memory reference

确认由128 + SIGNUM规则产生的信号。

为了进行健全性检查,我还尝试从汇编中调用其他函数,如下:

#include <stdlib.h>

void my_asm_func(void);
__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "lea puts_message(%rip), %rdi;"
    "call puts;"
    "ret;"
    "puts_message: .asciz \"hello puts\""
);

int main(void) {
    my_asm_func();
}

这样做可以正常运行并打印:

hello puts

在Ubuntu 19.04 amd64、GCC 8.3.0和glibc 2.29中进行了测试。

我还在一个Ubuntu 18.04的Docker容器中尝试了它,结果相同,只是程序运行时会输出:

./main.out: Symbol `abort' causes overflow in R_X86_64_PC32 relocation          
./main.out: Symbol `abort' causes overflow in R_X86_64_PC32 relocation

这感觉像是一个很好的线索。


重定位溢出错误是一个单独的问题:您需要使用call abort@pltcall *abort@GOTPCREL(%rip)。我不知道为什么在Ubuntu 19.04上您没有遇到这个问题。 - Peter Cordes
1个回答

5
在全局作用域定义了一个基本汇编语言函数的代码如下:
void my_asm_func(void);

__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "call abort;"
    "ret;"
);

在进行CALL之前,根据x86-64(AMD64) System V ABI规则,必须在某个点上保证16字节的堆栈对齐(根据参数可能更高)。

3.2.2 堆栈框架

除了寄存器外,每个函数在运行时栈上都有一个框架。该堆栈从高地址向下增长。图3.3显示了堆栈的组织。

输入参数区域的末尾必须对齐到16(如果在堆栈上传递__m256,则为32)字节边界。换句话说,当控制转移到函数入口点时, 值(%rsp + 8)始终是16(32)的倍数。堆栈指针%rsp始终指向最新分配的堆栈帧的结尾。

进入函数时,由于8字节的返回地址已经在堆栈上,因此堆栈会错位8个字节。为了将堆栈重新对齐到16字节边界,需要在函数开头从RSP中减去8,并在结束时将8加回RSP。也可以在开头推送任何寄存器如RBP,并在结束后弹出来达到相同的效果。

以下代码版本应该可以正常工作:

void my_asm_func(void);

__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "push %rbp;"
    "call abort;"
    "pop %rbp;"
    "ret;"
);

关于这段偶然可行的代码:

__asm__("call abort");

编译器很可能是以某种方式在调用之前将堆栈对齐到16字节边界上生成了main函数,因此它恰好起作用。 你不应该依赖这种行为。 这段代码还存在其他潜在问题,但在这种情况下并未以失败的形式呈现出来。 调用前应正确地对齐堆栈; 通常应关注红色区域; 并且应指定调用约定中的所有易失寄存器作为破坏者,包括RAX / RCX / RDX / R8 / R9 / R10 / R11 ,FPU寄存器和SIMD寄存器。 在本例中,abort永远不会返回,因此与你的代码无关。
ABI定义了红色区域如下:
“%rsp指向的位置之后的128字节区域被认为是保留的,并且不得由信号或中断处理程序修改。” 因此,函数可以使用此区域进行临时数据,这些数据在函数调用之间不需要。 特别是,叶子函数可以将此区域用作其整个堆栈帧,而不是在序言和结语中调整堆栈指针。 “此区域称为红色区域”。
通常不建议在内联汇编中调用函数。 调用printf的示例可以在此其他Stackoverflow答案中找到,该答案展示了在64位代码中使用CALL的复杂性,特别是红区。 David Wohlferd的Dont Use Inline Asm始终是一个好文章。
这段代码恰好起作用。
void my_asm_func(void);
__asm__(
    ".global my_asm_func;"
    "my_asm_func:;"
    "lea puts_message(%rip), %rdi;"
    "call puts;"
    "ret;"
    "puts_message: .asciz \"hello puts\""
);

你可能有点幸运,因为puts并不需要正确的对齐方式,而且你恰好没有出现失败情况。在调用puts之前应该像之前描述的my_asm_func那样对齐栈。确保遵守ABI是确保代码按预期工作的关键。
关于重定位错误,这可能是因为Ubuntu版本默认使用位置无关代码(PIC)生成GCC代码。您可以通过将@plt附加到CALL的函数名以通过过程链接表进行C库调用来解决此问题。Peter Cordes在该主题上撰写了相关的Stackoverflow答案

2
谢谢Michael!我知道那些堆栈要求,但是忘记考虑它们了! - Ciro Santilli OurBigBook.com
1
@Ciro:对于一个独立的函数,一个简单的解决方案是使用jmp而不是call进行尾调用,如果你不需要它在abort()打印的回溯中可见。说到这一点;如果没有CFI指令来创建.eh_frame元数据,你可能会破坏abort()回溯和转储调用堆栈的能力。 - Peter Cordes
@Michael:abort()是一个noreturn函数;你不必担心破坏调用者的寄存器。总的来说,这一部分关于从inline-asm调用函数是一个很好的观点,但是asm("call abort")可能不是正确的标题。 - Peter Cordes
1
@CiroSantilli新疆改造中心996ICU六四事件:更新:abort()不能单独执行堆栈回溯。并且通过Michael的push %rbp,即使我使用了-O3编译(意味着-fomit-frame-pointer),GDB仍然能够通过my_asm_func回溯到main中的调用点。所以我错误地认为不是尾调用会破坏回溯。我想GDB在这里很聪明。但是如果在调用abort之前加上sub $128, %rsp,那么GDB就会迷失方向,找不到调用者。或者如果你像push %rcx一样推一个虚拟寄存器而不是RBP,GDB会认为在main之前有一个_nl_current_default_domain - Peter Cordes
1
@PeterCordes 谢谢您提供这些信息。我曾经为了理解 GDB 的回溯算法而疯狂地尝试过。但后来我变得懒惰并停止了。谁能理解它,就应该在 https://dev59.com/klLTa4cB1Zd3GeqPZ2L1 上写点东西 :-) - Ciro Santilli OurBigBook.com
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接