在调用函数时,堆栈帧是否真的被压入堆栈?

时间:2013-10-06 01:59:41

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

我已经教了很长一段时间的方式是,当我运行一个程序时,立即进入堆栈的第一件事就是主方法的堆栈框架。如果我从main中调用一个名为foo()的函数,那么堆栈框架就是局部变量(自动对象)的大小,并且参数也会被推送到堆栈中。

但是,我遇到了一些与此相反的事情。而且我希望有人可以解决我的困惑或解释为什么没有任何矛盾。

第一个矛盾:

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

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

第二个矛盾:

我在汇编中做了一些编码(ARM是特定的),我的课程的方式是,当一个函数被调用时,我们立即使用寄存器并且从不推送当前的任何局部变量除非算法不能用有限数量的寄存器执行,否则函数进入堆栈。即便如此,我们只推动了剩余的变量。

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

3 个答案:

答案 0 :(得分:3)

关于你的第一个问题:

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

  

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

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

  

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

同样,这里没有严格的答案。通常编译器依赖register allocation算法,这些算法能够以最小化“溢出”(堆栈)变量的方式分配寄存器。当然,如果您是手工编写程序集,那么您可以决定在整个程序中为特定变量分配特定的寄存器,因为您可以根据其内容了解如何使其工作。

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

答案 1 :(得分:1)

  1. 构造C ++对象与获取对象的内存几乎没有关系。实际上,说“保留内存”会更准确,因为一般来说,计算机上没有很少的RAM构建器团队,每当你要求一个新对象时,它们就会开始行动起来。内存或多或少是永久性的(虽然我们可以对VM进行狡辩)。当然,编译器必须安排其程序一次只使用一个特定范围的内存。这可能(并且可能确实)要求它在对象存在之前保留一系列内存,并且避免在对象消失之后的某个时间将其用于其他对象。为了提高效率,编译器可以(即使在具有动态存储持续时间的对象的情况下)通过一次保留几个内存块来优化预留,如果它知道它将需要它们。无论如何,当C ++谈论“构造一个对象”时,它意味着:获取一系列具有未定义内容的内存,并做必要的事情来创建对象的表示(以及世界其他任何状态)是对象的创建所暗示的,可能不仅限于特定的大块内存。)

  2. 不存在堆栈帧的存在。不需要堆栈存在。这完全取决于编译器。当然,大多数编译器都会生成使用堆栈的代码,好的编译器会在何时可以缩写甚至省略堆栈帧时找出它们。所以,是的,帧的大小可能会有所不同。

答案 2 :(得分:1)

你是绝对正确的,不需要堆叠框架。堆栈帧是管理本地空间问题的快速而肮脏的解决方案,比在功能过程中管理堆栈指针中的更改更容易调试。如果在函数中需要堆栈,则更容易在条目上调整堆栈指针并在返回时恢复它。

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

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