为什么从x86_64汇编函数调用 C abort() 函数会导致分段错误 (SIGSEGV) 而不是中止信号?



考虑程序:

主.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信号:

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 中测试。

我也在Ubunt 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

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

在此代码中,在全局范围内定义函数(具有基本程序集):

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

您违反了 x86-64 (AMD64) System V ABI 规则之一,该规则要求在进行CALL之前的某个点进行 16 字节堆栈对齐(根据参数可能更高)。

3.2.2 堆栈帧

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

输入参数区域的末端应对齐 16(32,如果传递__m256 在堆栈上)字节边界。换句话说,值 (%rsp + 8) 为 当控制权转移到 函数入口点。堆栈指针 %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");

编译器可能生成了main函数,以至于在此调用之前堆栈在 16 字节边界上对齐,因此它恰好有效。不应依赖此行为。此代码还有其他潜在问题,但在这种情况下不会显示为失败。堆栈应在调用前正确对齐;您一般应该关注红色区域;并且应将调用约定中的所有易失寄存器指定为干扰器,包括RAX/RCX/RDX/R8/R9/R10/R11、FPU 寄存器和 SIMD 寄存器。在这种情况下,abort永远不会返回,因此这不是与您的代码相关的问题。

红色区域在 ABI 中定义的方式如下:

超出 %rsp 所指向的位置的 128 字节区域被视为 保留,不得被信号或中断处理程序修改.8 因此, 函数可以将此区域用于跨函数不需要的临时数据 调用。特别是,叶函数可以将此区域用于其整个堆栈帧, 而不是在序幕和尾声中调整堆栈指针。这个区域是 称为红色区域

在内联程序集中调用函数通常是一个坏主意。调用printf的示例可以在另一个 Stackoverflow 答案中找到,它显示了执行CALL的复杂性,尤其是在带有红色区域的 64 位代码中。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之前对齐堆栈,如前所述,使用调用abortmy_asm_func。确保符合 ABI 是确保代码按预期工作的关键。


关于重定位错误,这可能是因为正在使用的 Ubuntu 版本默认使用位置独立代码 (PIC) 生成 GCC 代码。您可以通过过程链接表进行C库调用来解决此问题,方法是将@plt附加到您CALL的函数名称。Peter Cordes写了一个关于这个主题的相关Stackoverflow答案。

最新更新