【问题标题】:What's inside the stack?栈里面有什么?
【发布时间】:2012-09-15 12:29:41
【问题描述】:

如果我运行一个程序,就像

#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状态等等?

【问题讨论】:

  • 我们可以在这里保留 cmets 的主题吗?谢谢。

标签: c x86 stack low-level


【解决方案1】:

如果不了解叠加层,您只能看到位或数字。虽然某些区域受机器细节的限制,但很多细节都是相当标准的。

如果您没有在嵌套例程之外移动太远,您可能正在查看内存的the call stack 部分。对于一些通常被认为是“不安全”的 C,您可以编写访问函数变量的有趣函数,即使这些变量没有像源代码中所写的那样“传递”给函数。

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

超出进程内存边界会给您带来可怕的分段违规,因为内存防护会检测到进程访问未授权内存的尝试。 Malloc 所做的不仅仅是“仅仅”返回一个指针,在具有内存分段功能的系统上,它还“标记”该进程可访问的内存,并检查所有未违反进程分配的内存访问。

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

PS。您可能会将此内存视为“堆栈”,但一段时间后,您会发现它实际上只是一大块可访问内存,其中一部分被视为堆栈...

【讨论】:

  • 那么,您是说从 &first_argument 向上的所有内容都可能不仅仅是堆栈吗?调用堆栈之后是什么?或者,我们应该怎么称呼它?
  • 如果你一直在内存中走动,假设你没有段错误并且操作系统为程序分配了一个连续的内存范围(这比一堆块更容易监控),你可能会运行入堆。此外,一些实现存储更多数据,以帮助调试器或作为操作系统和进程之间的通信通道。
  • 请注意,指针访问内存,一点点知识可以让子例程做一些语言不支持的事情。这就是为什么 Java 放弃了引用指针,以防止访问/修改私有变量等。
  • 虽然某些操作系统在将程序加载到其中之前可能会将内存归零,但不要忘记 C 库可能会在调用 main() 之前调用大量初始化子例程。所以即使你的起始筹码也很可能已经混乱了。在 C++ 中变得更糟,应用程序初始化可以在 main() 被调用之前发生。
  • 我认为它不应该是堆,因为我没有比 #include 更多的代码并使用 printf(3),所以它仍然是堆栈内存。是 *argv[] 和 *env[] 存储堆栈的地方吗?那么,如果堆栈会以相反的方向增长,那么为什么在调用堆栈之前还有这么多空间呢?
【解决方案2】:

栈的内容基本上是:

  • 操作系统传递给程序的任何内容。
  • 调用帧(也称为堆栈帧、激活区域...)

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

在 Linux 中,您会看到:

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

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

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

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

【讨论】:

    【解决方案3】:

    堆栈的内容会因架构 ABI、编译器以及可能的各种编译器设置和选项而异。

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

    还请记住,编译器不需要初始化堆栈,当然也不会“清除它”,当它完成时,所以当它被分配给一个进程或线程时,它可能包含任何值 - 即使在上电时,例如 SDRAM 将不包含任何特定或可预测的值,如果物理 RAM 地址自上电以来已被另一个进程使用,或者甚至在同一进程中更早调用的函数,则内容将包含该进程剩余的任何内容在里面。因此,仅查看原始堆栈并不能告诉您太多信息。

    一般的堆栈帧通常可能包含函数返回时控件将跳转到的地址、传递的所有参数的值以及函数中所有自动局部变量的值。但是,例如 ARM ABI 将前四个参数传递给寄存器 R0 到 R3 中的函数,并将 leaf 函数的返回值保存在 LR 寄存器中,因此并非在所有情况下都那么简单作为我建议的“典型”实现。

    【讨论】:

    • 好吧,我们至少可以说,当前栈帧之上的栈上的所有东西都会被清理,否则就只是一派胡言。无论如何,以上任何内容可能会或什至可能不会针对当前进程,因此总是说我们可以查看它是不正确的,因为可能还没有特定地址的物理映射。
    • @ssice:是的,当然,堆栈本身是有区别的,堆栈本身是完全有意义且大小可变的,而 堆栈空间 - 堆栈所在的空间增长和缩小。我的回答中并不清楚这种区别。
    【解决方案4】:

    细节很大程度上取决于您的环境。操作系统通常定义一个 ABI,但实际上这仅对系统调用强制执行。

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

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

    然而,细节差异很大。

    一个非常简单的“入门”可以是http://kernelnewbies.org/ABI

    您可以查看一个非常详细和完整的规范,以了解定义 ABI 所涉及的复杂程度和细节是“System V Application Binary Interface AMD64 Architecture Processor Supplement”http://www.x86-64.org/documentation/abi.pdf

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-14
      • 1970-01-01
      • 2019-10-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多