什么阻止被叫方清理堆栈?

时间:2013-11-06 20:22:44

标签: assembly theory calling-convention

作为扩展课程的一部分,我正慢慢地沿着编程抽象的阶梯走下去。现在我对C有很好的掌握,我正在准备编写一些程序集(特别是ARM程序集)。

我遇到了调用约定的主题,虽然我一般都理解他们的意思,但似乎永远不会被问到或回答的问题是:

为什么被调用者不能处理堆栈上的变量参数?

它到处都说被调用的函数不知道已传递了多少参数,但在这种情况下,为什么不能简单地将这些数据放入寄存器或将其推送到堆栈的顶部叫功能使用?

对于任何利用堆栈进行子程序通信的架构,我都会问这个问题,而不仅仅是ARM或x86。

5 个答案:

答案 0 :(得分:3)

没有根本原因,被调用者无法为变量清理空间。在大多数体系结构中,标准调用约定不会以这种方式处理变量,但这并不意味着它不可能这样做。对于可变长度参数列表,您可以将有关参数数量的数据作为隐藏参数传递(如许多面向对象语言中处理this的方式),或者将指针放在堆栈上以显示争论结束等等。

目前不是以这种方式完成这一事实并不意味着必须以这种方式完成。很好地质疑为什么它们是这样的,在这种情况下我认为原因是“以这种方式实现varargs稍微容易一些,而且因为所有其他很酷的孩子都在这样做,我们也应该这样做。”毕竟,如果所有已编译的C二进制文件都以这种方式处理参数,那么如果您有不同的调用约定,那么尝试与这些二进制文件进行互操作将非常粗糙。 (作为一个例子,请查看Windows API,其中某些函数必须注释为使用非标准调用约定才能与操作系统一起使用。)

希望这有帮助!

答案 1 :(得分:2)

被调用者可以从堆栈中清除变量参数。实际上我做了一次。但代码非常大。

无论如何,在 cdecl 约定中,调用者清理堆栈的主要原因是其他。 (毕竟,变量参数程序很少)

在某些体系结构上(通常非常小,如旧的8080或6800),没有ret n指令可以自动执行堆栈清理,并且通常它们也不能使用堆栈指针进行算术运算。 。

因此,被调用者必须先从堆栈中弹出返回地址才能到达参数,然后弹出所有参数然后再推回返回地址。对于3个参数,它将使用 stdcall 约定:

    push arg1
    push arg2
    push arg3
    call proc

proc:
    pop  r1   ; the return address
    pop  r2
    pop  r2
    pop  r2
    push r1
    ret

当使用 cdecl 约定时,可以省去2条指令和一个寄存器:

    push arg1
    push arg2
    push arg3
    call proc
    pop  r2
    pop  r2
    pop  r2

proc:
    ret

因为对于单一语言,最好在所有平台上使用单一调用约定,CCALL更简单和通用看起来更好。 (C语言是在6800是高科技时创建的。)

但请注意,在这些平台上,汇编程序和本机语言(例如不同类型的BASIC)通常使用寄存器参数传递,这在这些小型系统上当然要快得多。

无论如何,这只是一种传统。您可以将编译器设置为使用您想要的任何约定。例如,WinAPI是用C ++编写的,但仍然使用 stdcall 约定,因为它在x86平台上更好。

答案 2 :(得分:1)

被调用者当然可以清理堆栈。绝对没有根本原因导致事实并非如此,实际上许多编译器都支持显式声明调用约定的代码。

值得注意的是,整个Windows API中几乎每个函数都使用一个调用约定,其中被调用者清理堆栈。

有关x86上常见调用约定的概述,请参阅http://en.wikipedia.org/wiki/X86_calling_conventions

详细了解许多编译器的通用调用约定(概念对于任何基于函数堆栈的架构都是相同的,无论是x86,powerpc,arm,avr等),请参阅http://www.agner.org/optimize/calling_conventions.pdf

对于常见的“stdcall”调用约定,被调用者清理堆栈,这里是一个特定于Microsoft的文档:http://msdn.microsoft.com/en-us/library/zxk0tw93.aspx但是许多编译器都支持该调用约定。请注意,MS编译器使用可变参数cdecl来生成函数。

有一些广泛使用的调用约定(例如“cdecl”,“stdcall”,“fastcall”)通常由许多编译器支持,但如果您使用汇编语言编写或者您想编写编译器补丁,那么可以自由地想出任何你能想象到的奇怪和古怪的惯例(好吧,在合理的范围内)。

我不确定你的声明中的“无处不在”在哪里它说“所谓的被叫函数......”,但你要么是误解,要么是你在“无处不在”的选择。

顺便问一下:你提出这个问题是件好事;如果你正在编写汇编程序,特别是如果你将它与从另一种语言生成的代码集成在一起,重要的是要了解并遵守其他代码所使用的调用约定,无论它是什么。

答案 3 :(得分:0)

被调用函数绝对知道至少定义了多少参数,如果调用者和被调用者不同意你最终会遇到麻烦的参数数量。但是如果调用者和被调用者同意,则被调用者肯定知道已经发送的参数的数量。

这只是一个惯例。这是一个很好的约定,因为事物的创造者清理它。它是独立的。物品的用户(堆栈帧/参数)只使用它们。如果它成为调用者,那么它管理堆栈的那个方面......

在一天结束时,虽然它只是一个惯例。欢迎您创建或修改使用不同标准的编译器。

如果语言和调用约定允许,那么没有理由说被调用者无法处理任意数量的参数。实际上很容易实现这一点。它不是一个常见的用例,因此通常不是一个有趣的话题。它也不是那么有效,如果你有很多想要发送的东西,不是通过参考而是通过参考。因此,当已经支持整体功能时,这也不是一个有趣的话题。

答案 4 :(得分:0)

我可以想到几个原因:

  1. 清理参数涉及更改堆栈地址指针(esp)。当调用者对此负责时,所有被调用者都与esp有关,将其返回到它找到它的方式,并且当调用者重新获得控制时esp与它离开时相同。如果被调用者是清理参数的人,则必须计算esp的新值(而不是仅仅从堆栈中弹出旧值),并且调用者必须考虑被调用者的方式改变了这会使调用者和被调用者的事情变得更复杂。

  2. 在C ++中,清理参数还意味着调用临时对象的析构函数。被调用者不能为作为参数传递的对象调用析构函数 - 因为如果它们是临时对象则不会。只有调用者知道哪些参数是临时对象,哪些参数不是。

  3. 被叫方不知道在执行完毕后发生了什么,并且控件返回给调用者。因此,清除调用者中的参数会留下更多优化选项。例如,使用相同的参数(可能有一些更改)来再次调用另一个函数,或者立即从调用者返回而根本不清除参数(它们将被简单地与堆栈帧的其余部分一起抛出)