为什么编译器保留一点堆栈 space 而不是整个数组大小?
Why does the compiler reserve a little stack space but not the whole array size?
下面的代码
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 个字节...这是为什么?
在一个函数使用的栈区下面,有一个是预留给程序使用的。由于 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,因为它需要数组在其堆栈中 space,但测试不需要,因为它不调用任何函数.
数组元素的额外使用不会显着改变输出,但添加它是为了清楚表明它并不是假装这些元素不存在。
您使用的是 x86-64 Linux,其中 ABI 包含一个红色区域(RSP 以下 128 字节)。 https://whosebug.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 只是 returns 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
但是我们可以在不让编译器优化掉 store/reload 的情况下避免叶函数中的 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都把数组底部放在了红区。见神箭 link.
总的来说这是一件好事:数组的更多部分在 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 字节的数组。)
下面的代码
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 个字节...这是为什么?
在一个函数使用的栈区下面,有一个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,因为它需要数组在其堆栈中 space,但测试不需要,因为它不调用任何函数.
数组元素的额外使用不会显着改变输出,但添加它是为了清楚表明它并不是假装这些元素不存在。
您使用的是 x86-64 Linux,其中 ABI 包含一个红色区域(RSP 以下 128 字节)。 https://whosebug.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 只是 returns 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
但是我们可以在不让编译器优化掉 store/reload 的情况下避免叶函数中的 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都把数组底部放在了红区。见神箭 link.
总的来说这是一件好事:数组的更多部分在 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 字节的数组。)