以下代码
int main() {
int arr[120];
return arr[0];
}
编译成这样:
sub rsp, 360
mov eax, DWORD PTR [rsp-480]
add rsp, 360
ret
知道整数是 4 个字节,数组大小为 120,数组应该占用 480 个字节,但从 ESP 中只减去 360 个字节......这是为什么呢?
以下代码
int main() {
int arr[120];
return arr[0];
}
编译成这样:
sub rsp, 360
mov eax, DWORD PTR [rsp-480]
add rsp, 360
ret
知道整数是 4 个字节,数组大小为 120,数组应该占用 480 个字节,但从 ESP 中只减去 360 个字节......这是为什么呢?
在函数使用的堆栈区域下方,有一个128 字节的红色区域,保留给程序使用。由于main
不调用其他函数,因此无需将堆栈指针移动超过所需的量,尽管在这种情况下无关紧要。它只减去足够的量rsp
以确保阵列受到红色区域的保护。
您可以通过添加函数调用来查看差异main
int test() {
int arr[120];
return arr[0]+arr[119];
}
int main() {
int arr[120];
test();
return arr[0]+arr[119];
}
这给出了:
test:
push rbp
mov rbp, rsp
sub rsp, 360
mov edx, DWORD PTR [rbp-480]
mov eax, DWORD PTR [rbp-4]
add eax, edx
leave
ret
main:
push rbp
mov rbp, rsp
sub rsp, 480
mov eax, 0
call test
mov edx, DWORD PTR [rbp-480]
mov eax, DWORD PTR [rbp-4]
add eax, edx
leave
ret
您可以看到该main
函数减去了 480,因为它需要数组位于其堆栈空间中,但 test 不需要,因为它不调用任何函数。
数组元素的额外使用不会显着改变输出,但添加它是为了清楚地表明它不是假装这些元素不存在。
您使用的是 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 编译器资源管理器上,我从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 链接。
这通常是一件好事:更多的数组在 a disp8
from RSP 的范围内,因此可以使用arr[0]
up to arr[63
or so的引用[rsp+disp8]
而不是[rsp+disp32]
寻址模式。对于一个大数组来说不是超级有用,但作为一种在堆栈上分配局部变量的通用算法,它完全有意义。(对于 arr,gcc 并没有一直到达红色区域的底部,但是 clang 确实如此,使用sub rsp, 360
而不是 368,因此数组仍然是 16 字节对齐的。(IIRC,至少 x86-64 System V ABI建议对大小 >= 16 字节的自动存储数组使用此方法。)