第1版:根据一些评论的建议,我打印了变量的地址。
第2版:根据一些评论的建议,我添加了一些操作,这样编译器就不会简单地丢弃我的变量
第3版:根据一个答案的建议,我修改了变量的打印方式。
第4版:添加了一些毫无意义的内容,使gcc 的生活变得困难
Edit5:添加一个片段三来测试@4386427的理论——结果似乎支持了他的想法,即编译器默认情况下可以保留32个字节。因此,我们可能需要定义至少5个变量来查看差异。
我对堆栈内存和堆内存有一些基本的了解。以C为例,如果我在函数中定义一个局部变量,它会占用堆栈内存;如果我定义一个指针并为它分配几个内存块,这些内存块就会占用堆内存。如果一个函数递归地调用自己,那么堆栈将满,并且会发生溢出。所以我做了一个简单的测试,代码段一和代码段二之间唯一的区别是代码段二又定义了一个整数:
片段一:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int function(int depth) {
int tmp = rand() % 65536;
tmp = tmp - 1;
printf("val: %d; addr: %p; depth: %dn", tmp, (void*)&tmp, depth);
tmp = function(++depth) + 1;
return tmp;
}
int main() {
srand(time(NULL));
int res = function(0);
printf("%dn", res);
return 0;
}
输出一:
...
val: 57227; addr: 0x7fff00dff78c; depth: 174626
val: 8288; addr: 0x7fff00dff75c; depth: 174627
val: 24194; addr: 0x7fff00dff72c; depth: 174628
Segmentation fault
第二段:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int function(int depth) {
int tmp0 = rand() % 65536;
int tmp1 = rand() % 65536;
tmp0 = tmp0 - 1;
printf("val: %d, %d; addr: %p, %p; depth: %dn", tmp0, tmp1, (void*)&tmp0, (void*)&tmp1, depth);
tmp1 = function(++depth);
return tmp1 - tmp0;
}
int main() {
srand(time(NULL));
int res = function(0);
printf("%dn", res);
return 0;
}
输出二:
...
val: 40745, 32446; addr: 0x7ffcb80b079c, 0x7ffcb80b0798; depth: 174528
val: 34014, 57470; addr: 0x7ffcb80b076c, 0x7ffcb80b0768; depth: 174529
val: 56801, 34478; addr: 0x7ffcb80b073c, 0x7ffcb80b0738; depth: 174530
Segmentation fault
我使用gcc编译了这两个代码,正如预期的那样,它们都会导致堆栈溢出。然而,我最初预计的是,如果代码段2中的函数使用2倍的内存,那么代码段2的深度会浅得多。然而,虽然第二个代码段稍微早了一点,但两个堆栈的深度实际上非常接近。。。
如果一切都像我天真的理论一样工作,那么片段中的函数调用自己174616次,它需要占用4个字节*174616/1024=682 KB;函数在snippet中两次调用自己174539次,它需要占用(4+4)字节*174539=1363 KB。
为什么会这样?
摘录三
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int function(int depth) {
int tmp0 = rand() % 65536;
int tmp1 = rand() % 65536;
int tmp2 = rand() % 65536;
int tmp3 = rand() % 65536;
int tmp4 = rand() % 65536;
int tmp5 = rand() % 65536;
tmp0 = tmp0 - 1;
tmp1 = tmp1 + 1;
tmp2 = tmp2 - 2;
tmp3 = tmp3 + 2;
tmp4 = tmp4 - 3;
tmp5 = tmp5 + 3;
printf("val: %d, %d, %d; addr: %p, %p, %p; depth: %dn", tmp0, tmp1, tmp2, (void*)&tmp0, (void*)&tmp1, (void*)&tmp2, depth);
tmp1 = function(++depth);
return tmp0 - tmp1 + tmp2 - tmp3 + tmp4;
}
int main() {
srand(time(NULL));
long res = function(0);
printf("%dn", res);
return 0;
}
输出三个
val: 9366, 56113, 48970; addr: 0x7fff063fe830, 0x7fff063fe82c, 0x7fff063fe828; depth: 130920
val: 11924, 11633, 26004; addr: 0x7fff063fe7f0, 0x7fff063fe7ec, 0x7fff063fe7e8; depth: 130921
val: 13316, 42397, 45027; addr: 0x7fff063fe7b0, 0x7fff063fe7ac, 0x7fff063fe7a8; depth: 130922
val: 4285, 58053, 21693; addr: 0x7fff063fe770, 0x7fff063fe76c, 0x7fff063fe768; depth: 130923
Segmentation fault
另一个较小的实验:
#include <stdio.h>
void function(int depth) {
if (depth==0) return;
int tmp = 65536;
printf("%pn",&tmp);
function(--depth);
}
void function2(int depth) {
if (depth==0) return;
int tmp = 65536;
int tmp2 = 65536;
printf("%p %pn",&tmp,&tmp2);
function2(--depth);
}
int main() {
function(2);
function2(2);
return 0;
}
可以产生以下内容:
0x7ffee80f0988
0x7ffee80f0968
0x7ffee80f0988 0x7ffee80f0984
0x7ffee80f0968 0x7ffee80f0964
其中可以观察到两个局部变量之间的距离,每个局部变量在其自己的调用中对于两个函数都是相同的。这可能意味着在这些情况下,堆栈分配的大小是恒定的,并且两者的大小相同。如果在第二种情况(4/5)中添加更多的局部变量,情况就会改变。堆栈帧可能是以四舍五入到某个值的大小分配的。
为什么会这样?
好吧,你所描述的原则上是正确的,但。。。您忘记了编译器优化。编译器可以进行各种优化,只要它不改变程序的可观察行为。
例如,编译器可以决定将所有tmp
变量保留在cpu寄存器中。在这种情况下,将不会为它们分配堆栈内存。
你可以试着通过打印他们的地址来强迫他们记忆,比如:
printf("%pn", (void*)&tmp);
可能发生的另一件事是,编译器为两个函数更改堆栈指针的数量相同。在我的系统上,编译器在这两种情况下都保留32个字节。在编译器将其更改为48之前,我必须使用5个tmp
变量。换句话说,不要指望函数会保留它所需要的东西。允许预订更多。在我的系统中,它似乎总是将堆栈指针更改N x 16。
为了更好地理解,您需要查看生成的机器代码。为此,您可以使用gcc -S
您也可以检查https://godbolt.org/z/x5c3bj7M9
两个功能都以开头
push rbp
mov rbp, rsp
sub rsp, 32 <------ Stack pointer change, i.e. reserving memory
mov DWORD PTR [rbp-20], edi
因此它们对两个调用使用相同数量的堆栈。
用-O2
编译的相同代码给出了完全不同的结果https://godbolt.org/z/f6jMEqaPd
第一个函数给出:
push rbx
mov ebx, edi
sub rsp, 48 <------ Stack pointer change, i.e. reserving memory
但第二个给出:
push rbx
mov ebx, edi
sub rsp, 16 <------ Stack pointer change, i.e. reserving memory
结论是:你不能只看C代码就知道会发生什么。你需要机器代码。
请记住,局部变量并不是存储在堆栈中的唯一内容。还有一些参数和调用函数的返回地址。因此,您更可能看到16字节与20字节之间的最小差异,而不是4字节与8字节之间的差异。
如果你仔细观察每次迭代之间打印的地址,你会发现在这两种情况下,它们都相差48个字节,所以在给定的情况下,堆栈帧大约在同一时间达到顶点。
因此,编译器似乎也在插入一些填充,在后一种情况下,这些填充最终被额外的变量占用。