除了您的代码会因为您返回一个指向堆栈变量的指针而引发未定义行为这一事实之外,您还在询问为什么行为会随着更改 f() 的签名而改变。
之所以
原因在于编译器为函数构建堆栈帧的方式。假设编译器正在为 foo() 构建堆栈帧,如下所示:
Address Contents
0x199 local variable p
0x200 Saved register A that gets overwritten in this function
0x201 parameter n
0x202 return value
0x203 return address
对于 f(int m),堆栈看起来很相似:
Address Contents
0x199 local variable n
0x200 Saved register A that gets overwritten in this function
0x201 parameter m
0x202 return value
0x203 return address
现在,如果你在 foo 中返回一个指向 'n' 的指针会发生什么?结果指针将为 0x201。返回 foo 后,堆栈的顶部位于 0x204。内存保持不变,您仍然可以读取值“1”。这一直有效,直到调用另一个函数(在您的情况下为“f”)。调用 f 后,位置 0x201 被参数 m 的值覆盖。
如果您访问此位置(并且您使用 printf 语句),它会显示“999”。如果您在调用 f() 之前复制了此位置的值,您将找到值“1”。
继续我们的示例,f() 的堆栈帧看起来像这样,因为没有指定参数:
Address Contents
0x200 local variable n
0x201 Saved register A that gets overwritten in this function
0x202 return value
0x203 return address
当您使用“1”初始化局部变量时,您可以在调用 f() 后在位置 0x200 处读取“1”。如果您现在从位置 0x201 读取值,您将获得已保存寄存器的内容。
一些进一步的陈述
- 重要的是要理解上述解释是向您展示观察所见所闻的方法论。
- 真正的行为取决于您使用的工具链和所谓的调用 convections。
- 人们很容易想象,有时很难预测会发生什么。这是一个与释放内存后访问内存类似的安静情况。这就是为什么通常无法预测会发生什么的原因。
- 这种行为甚至可以随着优化级别的改变而改变。例如,我可以想象,如果您打开 -O3 例如,观察结果会有所不同,因为未使用的变量 n 将不再出现在二进制文件中。
- 了解了背后的机制之后,应该可以理解为什么对从 foo 检索到的地址进行写访问会导致严重问题。
对于试图通过实验证明这一解释的勇敢者
首先,重要的是要看到上面的解释不依赖于真正的堆栈框架布局。我只是介绍了布局,以便有一个易于理解的插图。
如果您想在自己的机器上测试行为,我建议您使用您最喜欢的调试器并查看放置局部变量和参数的地址,以了解实际情况。请记住:更改 f 的签名会更改放置在堆栈上的信息。因此,唯一真正的“便携式”测试是更改 f() 的参数并观察 p 指向的值的输出。
在调用 f(void) 的情况下,放入堆栈的信息有很大不同,写入 p 指向的位置的值不再一定取决于参数或局部变量。它还可以依赖于主函数中的堆栈变量。
例如,在我的机器上,复制显示您在第二个变体中读取的“1”来自将用于存储“1”的寄存器保存到“num”,因为它似乎用于加载 n。
我希望这能给你一些见解。如果您还有其他问题,请发表评论。(我知道这理解起来有点奇怪)