考虑程序:
主.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
之前对齐堆栈,如前所述,使用调用abort
的my_asm_func
。确保符合 ABI 是确保代码按预期工作的关键。
关于重定位错误,这可能是因为正在使用的 Ubuntu 版本默认使用位置独立代码 (PIC) 生成 GCC 代码。您可以通过过程链接表进行C库调用来解决此问题,方法是将@plt
附加到您CALL
的函数名称。Peter Cordes写了一个关于这个主题的相关Stackoverflow答案。