您使用的是 x86-64 Linux,其中 ABI 包含一个红色区域(低于 RSP 128 字节)。 https://stackoverflow.com/tags/red-zone/info.
所以数组从红色区域的底部到 gcc 保留的顶部附近。使用-mno-red-zone 编译以查看不同的代码生成。
另外,您的编译器使用的是 RSP,而不是 ESP。 ESP 是 RSP 的低 32 位,x86-64 通常在低 32 位之外有 RSP,所以如果你将 RSP 截断为 32 位,它会崩溃。
在Godbolt compiler explorer 上,我从gcc -O3 得到这个(使用 gcc 6.3、7.3 和 8.1):
main:
sub rsp, 368
mov eax, DWORD PTR [rsp-120] # -128, not -480 which would be outside the red-zone
add rsp, 368
ret
您是否伪造了您的 asm 输出,或者其他版本的 gcc 或其他编译器是否真的从红色区域之外加载了这种未定义的行为(读取未初始化的数组元素)? clang 只是将其编译为ret,而 ICC 只是返回 0 而不加载任何内容。 (未定义的行为是不是很有趣?)
int ext(int*);
int foo() {
int arr[120]; // can't use the red-zone because of later non-inline function call
ext(arr);
return arr[0];
}
# gcc. clang and ICC are similar.
sub rsp, 488
mov rdi, rsp
call ext
mov eax, DWORD PTR [rsp]
add rsp, 488
ret
但我们可以在叶函数中避免 UB,而无需让编译器优化存储/重新加载。 (我们可以只使用volatile 而不是内联asm)。
int bar() {
int arr[120];
asm("nop # operand was %0" :"=m" (arr[0]) ); // tell the compiler we write arr[0]
return arr[0];
}
# gcc output
bar:
sub rsp, 368
nop # operand was DWORD PTR [rsp-120]
mov eax, DWORD PTR [rsp-120]
add rsp, 368
ret
请注意,编译器仅假定我们编写的是 arr[0],而不是 arr[1..119] 中的任何一个。
但无论如何,gcc/clang/ICC 都将数组的底部放在红色区域中。请参阅 Godbolt 链接。
这通常是一件好事:更多的数组位于 RSP 的 disp8 范围内,因此对 arr[0] 到 arr[63 的引用可以使用 [rsp+disp8] 而不是 [rsp+disp32] 寻址模式。对于一个大数组来说不是超级有用,但作为一种在堆栈上分配局部变量的通用算法,它完全有意义。 (gcc 不会一直到 arr 的红色区域的底部,但 clang 会,使用 sub rsp, 360 而不是 368,因此数组仍然是 16 字节对齐的。(IIRC,x86-64 System V ABI 至少建议对大小 >= 16 字节的自动存储数组使用此方法。)