int main(void) {
problem2();
}
void doit2(void) {
int overflowme[16];
//overflowme[37] =0;
}
void problem2(void) {
int x = 42;
doit2();
printf("x is %d\n", x);
printf("the address of x is 0x%x\n", &x);
}
有人会帮我理解为什么溢出[37] = 0;从doit2函数将覆盖x的值? (请在说明中包含函数doit2的程序计数器和帧指针)谢谢!
每次都在x86 windows机器上工作(好吧!),项目属性 - >配置属性 - > C / C ++ - >代码生成 - >基本运行时检查设置为“默认”。所以这不是一个未定义的行为。
答案 0 :(得分:5)
就像其他人所说的那样,这取决于目标和编译器,但对于你来说,这些是保持不变的,并且代码中没有任何其他东西看起来像是在向堆栈引入随机性(相对来说) ),所以每次都会做同样的事情。
系统堆栈通常从高地址扩展到低地址。如果堆栈指针是0x1234并且您按下一个值(在32位{4字节}系统上),则堆栈指针将变为0x1230。
从最低地址到最高地址寻址数组。如果你有
char a[2];
且a [0]为0x0122,则[1]为0x0123。
doit2
中的数组是一个自动变量,这意味着它是在输入函数时创建的,并在退出函数时被删除。自动变量必须存在于堆栈或寄存器中。由于它是一个数组,因此编译器将它放在RAM而不是寄存器中要简单得多(这使索引更容易,因为它只是将索引*大小添加到数组的第一个成员的地址)。由于堆栈在RAM中,因此编译器将数组放在堆栈中。
在此堆栈上为此数组分配空间意味着堆栈指针比不存在此数组时的sizeof(int)*16
小。在overflowme[0]
中,堆栈指针很可能指向doit2
。
还有其他东西可能在堆栈上,还有一些东西必须在堆栈上。必须在堆栈上的东西是返回指针,它们在调用函数时被推到那里。在32位系统上,每个应占用4个字节。可能在堆栈上的东西(如果编译器想要使用它)是前一帧指针。 (显式*)x86-32上的堆栈帧只是ESP和EBP之间的空间,但它们不是必需的,因此它们不被使用,而EBP仅用作通用寄存器(更多通用寄存器可用一般都很好)。
但是,使用堆栈帧非常有用,因为它们使调试变得更加容易,因为ESP和EBP充当局部变量边缘的标记。有时需要堆栈帧,例如当您使用alloca
或C99的可变大小的自动数组时,因为它们允许函数的局部变量空间被mov EPB, ESP
或等效的指令丢弃,而不是{{1}所以编译器不必知道帧的大小。它们还允许相对于EBP而不是ESP来解决局部变量,在sub size_of_local_variable, ESP
的情况下,它会发生变化。在这种情况下,EBP在当前函数结束之前不会改变,除非通过调用函数进行更改和恢复。
在没有优化的情况下进行编译时,编译器通常总是使用堆栈帧,因为它们使调试变得更容易。使用堆栈帧对代码进行建模,然后在证明不需要它们之后将代码转换为不使用它们也更容易。
因此,EBP的先前值可能会或可能不会驻留在返回地址(alloca
中的某个地方)与problem2
的{{1}}中的最后一个元素之间的堆栈上。编译器也可以自由地在堆栈上放置任何其他东西,所以谁知道那里还有什么。
doit2
的局部变量overflowme
可以放入寄存器或堆栈中。在没有优化的情况下进行编译时,即使本地变量可以进入寄存器,它们也经常会进入堆栈。
因此,我们假设堆栈上有problem2
的{{1}}数组,旧的帧指针,返回地址和int x
的{{1}}(在{其真正处于更高地址}下的更多东西。)
由于doit2
与将overflowme
的第一个元素的地址添加到(i * {problem2
}的大小)并且旧的EBP位于最后一个之后相同x
的元素和返回地址位于旧EBP之后,&(overflowme[i])
位于返回地址之后,overflowme
肯定正好位于缓冲区溢出运行的路上。< / p>
为什么37的索引发生这种情况尚不清楚。指针数学(假设我上面列出的项目在数组和int
之间的堆栈上)并不表示它应该基于4字节指针(32位机器),但如果这是8字节指针系统(64位机器)然后数学更接近我期望overflowme
处于int x
的地址。编译器也可以继续为调用x
(格式字符串必须在堆栈之后的变量参数)分配堆栈空间,这将影响数学运算(并且还鼓励编译器放置{{1在堆栈上,因为无论如何它都必须将它推到那里。)
如果您想要更详细地回答您的问题,请查看此代码的程序集并确定准确的地址。
答案 1 :(得分:3)
可能没有。堆栈中变量的位置取决于编译器和平台。
答案 2 :(得分:2)
你的堆栈看起来像这样:
char overflowme[16]
return address to problem2() from calling doit2()
parameters for doit2(), if it had any
int x = 42
return address to main() from calling problem2()
parameters for problem2(), if it had any
local variables for main(), if it had any
当您写入overflowme[37]
时,您将超过overflowme
的结尾(因为它只有16个字节)并超过doit2()
调用的返回地址并覆盖x
。
正如其他人所提到的,这在很大程度上取决于平台和编译器,但应该让您对问题有一个很好的可视化。尝试打开调试窗口并显示堆栈,逐步执行代码。
答案 3 :(得分:2)
你很幸运,它只是破坏了x
。通常这种代码可以demons fly out of your nose!
答案 4 :(得分:0)
它没有:
x is 42
the address of x is 0xbff9ea1c
每次在One True Compiler and Platform(地址变更)上都会发生以上情况,所以很明显你没有确定它是不确定的行为。