以下代码段:
void (*foo)();
char X[1];
char Y[10];
可能会给我一个可能的堆栈布局:
| Y[10] |
|---------|
| X[1] |
|---------|
| foo |
|---------|
我通过使用:
生成ASM文件来检查这一点gcc -S -o stack stack.c
然后我意识到推动这些变量的顺序是不同的。所以,如果我不小心做了X[1]
我希望解决Y[0]
但是在实际布局中,写入X[1]
的内容会覆盖分配给{{1}的内存位置的第一个字节}。重组是编译器优化步骤还是有人能告诉我为什么会发生这种情况?
答案 0 :(得分:4)
你为什么说“应该”?
当然,您建议的堆栈布局将是实现自动变量的一种特定 - 非常明显 - 的方式的结果,但没有任何需要它。
因此,没有“应该”。
要强制某些项目在内存中的顺序,以便您可以播放(行为未指定,完全不安全且不可移植!)具有覆盖的游戏,请使用struct
和编译器的填充#pragma
。< / p>
答案 1 :(得分:2)
即使没有优化,内存中变量的排序通常也不是你可以依赖的东西。无论如何,他们最终的排序取决于你如何看待它们。如果你看到一群人从最短到最高排在一排,另一个人可能会说这些人实际上是从最高到最短的订购。
影响这些变量在内存中的顺序的第一件事就是编译器的实现方式。它有一个列表,列表可以从头到尾或从头到尾处理。所以编译器读取你的代码,生成中间代码,这个中间代码有一个需要放在堆栈上的局部变量列表。编译器并不关心它们在代码中的顺序,所以它只是以最方便的顺序查看它们。
第二件事是许多处理器使用倒置堆栈。如果你:
push A
push B
然后A的地址大于B,即使B位于堆栈顶部(并且位于A之上)。设想这个的好方法是使用C数组:
int stk[BIG];
int stk_top = BIG;
然后
void stk_push(int x) {
stk_top--;
stk[stk_top] = x;
}
正如您所看到的,stk_top索引实际上会随着堆栈中的更多项目而缩小。
现在,回到优化 - 当重新排序不在结构中的东西时,编译器是相当自由的。这意味着您的编译器可以很好地重新排序堆栈上的局部变量,并在其中添加额外的填充字节以保持对齐。此外,编译器也可以自由地甚至不在堆栈上放置一些局部变量。仅仅因为你命名一个局部变量并不意味着编译器必须在程序中真正生成它。如果实际上没有使用变量,它可能会被排除在程序之外。如果使用了很多变量,它可以保存在寄存器中。如果变量仅用于程序的一部分,那么它实际上只能暂时存在,并且它所使用的内存可以在函数期间在其他几个临时变量之间共享。
答案 2 :(得分:1)
可能的推测:编译器会尝试将char
数组放在一起,以最大限度地减少插入的填充总量。
通常,CPU最乐于在某些“整体”位对齐上检索多字节数据,这几乎总是对应于机器的位宽。因此,32字节int
将与32位边界对齐。为了实现这一点,编译器将使用从未访问过的字节“填充”堆栈。
但是,当您一次检索一个字节时,这种对齐没有任何好处。
答案 3 :(得分:1)
| var | address |
|---------|---------|
| Y[10] | x |
|---------|---------|
| X[1] | x + 10 |
|---------|---------|
| foo | x + 11 |
|---------|---------|
堆栈增长到较低的地址,因此如果您访问下一个地址(更高的地址),就像数组的下一个元素一样,您可以访问更大地址的内存。所以X[1] = *(x + 10 + 1) = foo
答案 4 :(得分:1)
这是因为大多数架构都是stack grows down。
答案 5 :(得分:1)
堆栈在大多数平台上都会增长,但为什么要依赖它呢?编译器优化也可能将变量与4字节边界对齐。为什么不这样做?
char x[11];
char *y = &x[1];