【问题标题】:Does a stack frame really get pushed onto the stack when a function is called?调用函数时,堆栈帧是否真的被压入堆栈?
【发布时间】:2013-10-12 20:01:32
【问题描述】:

很长一段时间以来我一直被教导的方式是,当我运行一个程序时,首先进入堆栈的是 main 方法的堆栈帧。如果我从 main 中调用一个名为 foo() 的函数,那么一个堆栈帧就是局部变量(自动对象)的大小,并且参数也会被推送到堆栈上。

但是,我遇到了一些与此相矛盾的事情。我希望有人能澄清我的困惑或解释为什么真的没有任何矛盾。

第一个矛盾:

在 Bjarne Stroustrup 的《C++ 编程语言》第 3 版一书中,它在第 244 页上说,“每次在程序执行中遇到其声明时,都会创建一个命名的自动对象。”如果这还不够清楚,在下一页它说,“每次控制线程通过局部变量的声明时,都会执行局部变量的构造函数。”

这是否意味着堆栈帧的总内存不是一次全部分配,而是在遇到变量声明时逐块分配? 另外,这是否意味着如果由于 if 语句而没有遇到变量声明,则每次堆栈帧的大小可能都不相同?

第二个矛盾:

我已经在汇编中做了一些编码(具体来说是 ARM),我的课程被教授的方式是,当调用一个函数时,我们立即使用寄存器并且从不压入当前的任何局部变量除非该算法无法使用有限数量的寄存器执行,否则将函数放到堆栈中。即便如此,我们也只推送了剩余的变量。

这是否意味着在调用函数时,可能根本不会创建堆栈帧? 这是否也意味着堆栈帧的大小可能会因使用寄存器而有所不同?

【问题讨论】:

  • 你可能想看看Arm link and frame pointer,主要是我们希望编译器尽可能快,所以没有关于它们必须如何生成汇编代码的一般规则,尤其是优化。如果编译器可以推断事物,例如在叶函数中,它将利用这一事实。如果对象不是 POD 并且它不知道构造函数的实现,则编译器必须为对象创建空间;如果它想减少堆栈使用、选择缓存等,它可以立即分配或不分配。

标签: c++ assembly arm callstack cpu-registers


【解决方案1】:
  1. 构造 C++ 对象与为对象获取内存几乎没有关系。事实上,说“保留内存”会更准确,因为一般来说,计算机没有小型 RAM 构建器团队,每次你请求一个新对象时,它们都会立即行动。内存或多或少是永久的(尽管我们可以对 VM 争论不休)。当然,编译器必须安排其程序一次只为一件事使用特定范围的内存。这可能(并且可能确实)要求它在对象存在之前保留一定范围的内存,并避免将其用于其他对象,直到对象消失后的一段时间。为了提高效率,编译器可以(即使在对象具有动态存储持续时间的情况下)通过一次保留几个内存块来优化保留,如果它知道它会需要它们。无论如何,当 C++ 谈论“构造一个对象”时,它的意思只是:获取一个具有未定义内容的内存范围,并做必要的事情来创建对象的表示(以及世界状态中的任何其他内容)对象的创建暗示了这一点,这可能不限于特定的内存块。)

  2. 不需要存在堆栈帧。不需要存在堆栈。这完全取决于编译器。当然,大多数编译器确实会生成使用堆栈的代码,并且好的编译器会弄清楚何时可以缩写甚至省略堆栈帧。所以,是的,框架的大小可能会有所不同。

【讨论】:

  • 这些是一些非常棒的答案。因此,是否要一次分配所有内存取决于编译器(忽略由于 if else 等条件语句可能不需要某些变量的事实),还是通过逻辑查看绝对必要的内容?
  • @KacyRaye:当然。例如,gcc 将在堆栈帧中为函数中最大程度的自动变量分配足够的内存。例如void f() { {int tiny[3]; g();} { int huge[10000]; g(); } };对g 的两次调用在入口处具有相同的堆栈指针。 (也就是说,在有足够空间容纳 小或大,但不能同时容纳两者之后。)
【解决方案2】:

关于你的第一个问题:

对象的创建与数据本身的分配无关。更具体地说:对象在堆栈上有保留空间这一事实并不意味着它的构造函数何时被调用。

这是否意味着堆栈帧的总内存不是一次全部分配,而是在遇到变量声明时逐块分配?

这个问题确实是特定于编译器的。堆栈指针只是一个指针,二进制文件如何使用它取决于编译器。实际上有的编译器可能会保留整个激活记录,有的可能只是一点一点地保留,有的可能会根据具体的调用动态地保留它等等。这甚至与优化紧密结合,以便编译器能够以它认为更好的方式安排事情。

这是否意味着在调用函数时,可能根本不会创建堆栈帧?这是否也意味着堆栈帧的大小可能会因使用寄存器而有所不同?

同样,这里没有严格的答案。通常编译器依赖register allocation 算法,这些算法能够以最小化“溢出”(堆栈上)变量的方式分配寄存器。当然,如果您是手工编写汇编代码,您可以决定将特定寄存器分配给整个程序中的特定变量,因为您通过它们的内容知道要如何使其工作。

编译器无法猜测这一点,但它可以看到变量何时开始使用或不再需要,并以最小化内存访问(因此堆栈大小)的方式安排事物。例如,它可以实现一个策略,一些寄存器应该由被调用者保存,另一些由被调用者保存并分配或其他。

【讨论】:

    【解决方案3】:

    您说的完全正确,不需要堆栈帧。堆栈帧是管理本地空间问题的快速而肮脏的解决方案,比在函数过程中管理堆栈指针的更改更容易调试。如果函数中需要堆栈,只需在入口处调整堆栈指针并在返回时恢复它会更容易。

    这也不是非黑即白,编译器和其他程序一样是程序,如果您还不知道,那么您会意识到,给定任意数量的程序员,您将获得相同问题的多种解决方案。即使程序员的数量是一个人可能会选择一遍又一遍地解决问题,直到他们满意和/或出于任何原因可能会选择发布各种版本。堆栈的使用对于局部变量来说非常普遍,这实际上是你的做法,但这并不意味着你必须使用在入口处创建并在返回时恢复的堆栈框架。

    正如您在您的课程中所学到的,并且通过实验很容易看出(编译一些简单的函数,从无优化到某些优化的各种级别),例如 gcc 除非必须,否则不会使用堆栈。我们在这里谈论 arm 的地方,正常的调用约定是基于寄存器的(没有任何说明编译器作者必须遵循该约定,如果编译器选择这样做,则可以使用基于 arm 的堆栈)。正常约定是基于堆栈的处理器,因为代码已经在处理堆栈,它可能会选择使用堆栈帧。在这些情况下,可能会使用基于堆栈的约定,因为处理器缺少通用寄存器,并且比其他具有更多寄存器的处理器更依赖堆栈,这意味着处理器可能需要堆栈通常不仅用于调用约定,而且用于大部分本地存储。

    【讨论】:

      猜你喜欢
      • 2012-09-26
      • 1970-01-01
      • 2019-11-22
      • 2013-12-26
      • 2021-08-06
      • 2020-10-21
      • 1970-01-01
      • 2010-11-28
      • 2021-12-06
      相关资源
      最近更新 更多