【发布时间】:2015-12-18 23:30:45
【问题描述】:
两个小时前,我以为我已经完全理解了堆栈的工作原理(至少在 C 中是如何处理的)。但我开始注意到我的程序中有一些(对我而言)意想不到的行为。
我们知道堆栈向着较低的内存地址增长(我说的是 PC,在我的例子中:Intel 64 位,Ubuntu)。因此,当创建一个新的堆栈帧时,属于该帧的对象的内存地址低于之前的所有对象。令我惊讶的是:一个框架内的对象在声明的时间越晚,内存地址越高。这让我很震惊,因为我认为之前声明的变量获得了更高的内存地址。
让我用一个 C 语言的例子来说明我的意思。
#include <stdio.h>
void foo()
{
int firstVar = 1;
int secondVar = 2;
printf("firstVar is at: %p\n", &firstVar);
printf("secondVar is at: %p\n", &secondVar);
}
int main(void)
{
int mainVar = 0;
printf("mainVar is at: %p\n", &mainVar);
foo();
return 0;
}
使用 gcc(-g、-ansi 和 -pedantic 标志)编译后,输出为:
mainVar is at: 0x7ffd1ec0fadc
firstVar is at: 0x7ffd1ec0fab8
secondVar is at: 0x7ffd1ec0fabc
正如预期的那样,mainVar 的内存地址比 foo() 堆栈帧中的内存地址高。然而,firstVar 的内存地址比 secondVar 低,即使它之前已声明过。查看 foo() 的反汇编显示了这种行为:
0x000000000040052d <+0>: push %rbp
0x000000000040052e <+1>: mov %rsp,%rbp
0x0000000000400531 <+4>: sub $0x10,%rsp
0x0000000000400535 <+8>: movl $0x1,-0x8(%rbp)
0x000000000040053c <+15>: movl $0x2,-0x4(%rbp)
...
1 放在 2 之前的四个字节,再次表明 firstVar 的内存地址低于 secondVar。
我的问题是:为什么会这样?根据我读过的所有参考书目,同一个堆栈帧中的对象应该越早声明它们的内存地址越高。参考书目是指互联网(例如这个网站)和著名的书籍。我使用的是一个非常标准的系统,所以我怀疑任何 ELF 或 ABI 奇怪的事情正在发生......
有什么想法吗?感谢您的阅读。
【问题讨论】:
-
您无法控制数据的布局。编译器可以以它选择的任何方式对堆栈上的变量进行排序。有时,他们会在简单变量之后移动数组,这样数组溢出就不会影响到简单变量。有时它不会为变量分配任何堆栈存储空间;他们将只生活在寄存器中。但是你无法预测它——除了对汇编程序进行逆向工程(并且在下次编译代码时该分析可能无效)。
-
注意 sub $0x10,%rsp,它们在堆栈上分配空间。这是他们将放置整数的本地分配的空间。此时,布局取决于编译器,并且在大多数编译器(和汇编器)中,这会解析为“自上而下的图表”,即按照文本的写入顺序。但是,对齐问题和其他考虑因素会导致优化器根据复杂的规则对该区域进行排序,因此并非总是如此。它是 rsp 上的 sub 以您期望的顺序和方向工作..但是打开的区域更加“手动”组织
-
这些 cmets 很短……还有一点。如果它们是 PUSHES,那么您是正确的……它们会按相反的顺序出现,但事实并非如此。该区域由 rsp 上的 sub 打开,因此此时它只是一块 RAM,并且可以使用编译器喜欢的任何方式。
-
cdecl 定义了 C 函数调用协议的布局。对于 cdecl,参数按在函数中声明的相反顺序推送。此链接:tenouk.com/Bufferoverflowc/Bufferoverflow2a.html> 详细介绍。注意:编译器不会重新安排参数的顺序超出“反向排序”谷歌
cdecl调用约定以获取详细信息/示例。请记住,pascal 和其他语言有不同的约定 -
这里是
cdecl约定(C 调用约定)的链接,任何兼容的 C/C++ 编译器都必须实现该链接:msdn.microsoft.com/en-us/library/zkwh89ks.aspx> 您可能还想查看stdcallWINAPI 使用的约定 msdn.microsoft.com/en-us/library/zxk0tw93.aspx>