我有一个简单的 c 函数
start.c
$ cat start.c
int main(int,char**);
void _start(){
char*v[2]={"k",0};
main(1,v);
}
当我编译为汇编程序时,使用
clang -O -march=cannonlake -S start.c
我得到start.s,其中包含以下代码:
_start: # @_start
.cfi_startproc
# %bb.0:
subq $24, %rsp
.cfi_def_cfa_offset 32
vmovaps .L__const._start.v(%rip), %xmm0
vmovaps %xmm0, (%rsp)
当我运行此代码(在具有定制操作系统的 bochs 模拟器中)时,我收到一个异常,因为
vmovaps %xmm0, (%rsp)
发生在未 16 字节对齐的 rsp 上。当调用 _start 时,堆栈的对齐方式是 16 字节,并且 subq $24, %rsp
将更改该对齐方式。
我尝试过 clang 13 和 17,结果非常相似。 clang 是错误的,还是我看错了?
当我在 _start 的开头添加
asm volatile ("subq $8,%%rsp"::);
时,它解决了问题。
这与 主和堆栈对齐
有相似之处您将
_start()
定义为 C 函数,因此 Clang 会编译代码,以便在使用 RSP % 16 == 8
调用时工作,就像 ABI 保证一样。
但是随后您将其链接到可执行文件中,它实际上是进程入口点,而不是函数。 它没有返回地址。 没有
call
推送返回地址;正如你所说,进入RSP % 16 == 0
时_start
。 这本质上是未定义的行为,从某种意义上说,编译器可以假设正常函数不会在堆栈未对齐的情况下被调用(或以其他方式跳转到)。
至少对于 GCC,您可以使用
__attribute__((force_align_arg_pointer))
来告诉它传入的对齐方式低于正常值。
或者使用更改 ABI 的命令行选项,例如
gcc -mpreferred-stack-boundary=3
(1<<3 == 8
而不是 16 字节对齐的默认 4
。)
参见 How to get argument value in _start and call main() using inline assembly in C, without Glibc or CRT start files? 对于用 GNU C 编写的 x86-64 System V ABI,这是一个非常 hacky 但有效的
_start
没有任何内联汇编,但仍然从堆栈中获取 argc
和 argv
(作为返回地址和第一个堆栈参数)。 并让 RSP 为未来的call
正确调整。 我没有用clang测试过,不知道是否支持相同的属性。