问题 什么在堆栈内?


如果我运行程序,就像

#include <stdio.h>
int main(int argc, char *argv[], char *env[]) {
  printf("My references are at %p, %p, %p\n", &argc, &argv, &env);
}

我们可以看到这些区域实际上在堆栈中。 但还有什么呢?如果我们在Linux 3.5.3中运行所有值的循环(例如,直到segfault),我们可以看到一些奇怪的数字,以及由一堆零分隔的两个区域的种类,可能是为了防止覆盖环境变量偶然。

无论如何,在第一个区域必须有很多数字,例如每个函数调用的所有帧。

我们怎样才能区分每个帧的结尾,参数在哪里,如果编译器添加了一个金丝雀,返回地址,CPU状态等等?


2136
2017-09-15 12:29


起源

我们可以在这里保留对主题的评论。谢谢。 - Kev


答案:


堆栈的内容基本上是:

  • 无论操作系统传递给程序。
  • 调用帧(也称为堆栈帧,激活区域......)

操作系统传递给程序的是什么?典型的* nix将传递环境,程序的参数,可能是一些辅助信息,以及传递给它们的指针 main()

在Linux中,您将看到:

  • 一个NULL
  • 程序的文件名。
  • 环境字符串
  • 参数字符串(包括 argv[0]
  • 填充零填充
  • auxv 数组,用于将信息从内核传递给程序
  • 指向环境字符串的指针,以NULL指针结束
  • 指向参数字符串的指针,以NULL指针结束
  • argc

然后,下面是堆栈帧,其中包含:

  • 参数
  • 退货地址
  • 可能是帧指针的旧值
  • 可能是金丝雀
  • 局部变量
  • 一些填充,用于对齐目的

你怎么知道每个堆栈框架中哪个是哪个?编译器知道,因此它只是适当地处理它在堆栈帧中的位置。如果可用,调试器可以以调试信息的形式为每个函数使用注释。否则,如果存在帧指针,则可以识别与其相关的内容:局部变量位于帧指针下方,参数位于堆栈指针上方。否则,您必须使用启发式方法,看起来像代码地址的东西可能是代码地址,但有时这会导致错误和恼人的堆栈跟踪。


4
2017-09-15 13:45





如果不了解叠加层,您只能看到位或数字。虽然一些地区受机器细节的限制,但大量细节都非常标准。

如果你没有在嵌套例程之外移动太远,你可能正在考虑 调用堆栈 记忆的一部分。对于一些通常被认为是“不安全”的C,您可以编写有趣的函数来访问上面几个“调用”的函数变量,即使这些变量没有“传递”到源代码中编写的函数。

调用堆栈是一个很好的起点,因为第三方库必须可以被尚未编写的程序调用。因此,它是相当标准化的。

步进到进程内存边界之外将为您提供可怕的Segmentation违规,因为内存防护会检测到进程访问未授权内存的尝试。在具有内存分段功能的系统上,Malloc不仅仅“返回”指针,它还“标记”该进程可访问的内存,并检查进程分配未被违反的所有内存访问。

如果你一直遵循这条路径,迟早会对内核或对象格式产生兴趣。在源代码可用的Linux上,研究如何完成工作的方法要容易得多。拥有源代码允许您不通过查看其二进制文件来对数据结构进行反向工程。在开始时,困难的部分将学习如何找到正确的标题。稍后它将学习如何四处寻找并可能改变在非修补条件下你可能不应该改变的东西。

PS。你可能会认为这个内存是“堆栈”,但过了一段时间,你会发现它真的只是一块大容量的可访问内存,其中一部分被认为是堆栈......


5
2017-09-15 12:59



那么,你是说从first_argument向上的所有内容都可以 不只是 堆栈?什么是 后 调用堆栈?或者,我们应该怎么称呼它? - ssice
如果你继续在内存中走动,假设你没有segfault并且OS为程序分配了一个连续的内存范围(比一堆块更容易监视),你可能会遇到堆。另外,一些实现存储甚至更多数据,以帮助调试器或OS和过程之间的通信信道。 - Edwin Buck
注意指针访问内存,一点点知识就可以让子程序做一些语言不能提升的东西。这就是为什么Java抛弃了引用指针,防止访问/修改私有变量等等。 - Edwin Buck
虽然某些操作系统在将程序加载到其中之前可能会将内存归零,但不要忘记C库在调用main()之前可能会调用一大堆初始化子例程。因此,即使你的起始筹码很可能也会混乱。在C ++中变得更糟,其中应用程序初始化可能在调用main()之前发生。 - Gilbert
我不认为它应该是堆,因为我没有比#include <stdio.h>更多的代码并且使用printf(3),所以这仍然是堆栈内存。是* argv []和* env []存储堆栈的地方?那么为什么这么多空间直到调用堆栈,如果堆栈将在相反的方向增长? - ssice


堆栈的内容将根据体系结构而有所不同 ABI,编译器,可能还有各种编译器设置和选项。

一个好的起点是针对目标体系结构发布的ABI,然后检查您的特定编译器是否符合该标准。最终,您可以分析编译器的汇编器输出或观察调试器中的指令级操作。

还要记住,编译器不需要初始化堆栈,并且当它完成它时肯定不会“清除它”,因此当它被分配给进程或线程时,它可能包含任何值 - 即使在加电时也是如此例如,SDRAM将不包含任何特定或可预测的值,如果物理RAM地址先前已由另一个进程使用,因为在同一进程中上电或甚至更早的被调用函数,内容将具有其中剩余的任何进程。所以只看原始堆栈并没有告诉你多少。

通常,通用堆栈帧可能包含控件将在函数返回时跳转到的地址,传递的所有参数的值以及函数中所有自动局部变量的值。但是,ARM ABI例如将前四个参数传递给寄存器R0到R3中的函数,并保存该函数的返回值  LR寄存器中的函数,所以它并不像我建议的“典型”实现那样简单。


3
2017-09-15 13:26



好吧,我们至少可以说堆栈上的所有内容 以上 当前的堆栈框架将被清理,否则它将只是废话。无论如何,上面的任何内容可能会或可能不会被解决到当前进程,因此总是说我们可以查看它是不正确的,因为可能还没有针对特定地址的物理映射。 - ssice
@ssice:是的,当然,堆栈本身之间存在区别,它本身就是有意义的,而且大小可变 堆栈空间  - 堆栈增长和收缩的空间。在我的回答中,这种区别并不明确。 - Clifford


细节非常依赖于您的环境。操作系统通常定义ABI,但实际上只对syscalls强制执行。

事实上,每种语言(以及每个编译器,即使它们编译相同的语言)也可以做一些不同的事情。

但是,存在某种系统范围的约定,至少在与动态加载的库的接口意义上。

然而,细节变化很大。

一个非常简单的“底漆”可能是 http://kernelnewbies.org/ABI

您可以查看非常详细和完整的规范,以了解定义ABI所涉及的复杂程度和详细信息,即“System V应用程序二进制接口AMD64架构处理器补充” http://www.x86-64.org/documentation/abi.pdf


2
2017-09-15 13:34