4

作为扩展我的课程的一部分,我正在慢慢地从编程抽象的阶梯上下来。现在我已经很好地掌握了 C,并且正在为编写一些程序集(特别是 ARM 程序集)做准备。

我遇到了调用约定的话题,虽然我大体上理解它们的含义,但似乎从未被问或回答的问题是:

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

到处都说被调用函数不知道传递了多少参数,但是在那种情况下,为什么不能简单地将数据放入寄存器或将其压入堆栈顶部以供被调用函数利用?

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

4

5 回答 5

5

被调用者可以从堆栈中清除变量参数。实际上,我成功过一次。但是代码很大。

无论如何,在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 条指令和 1 条寄存器的使用被保留:

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

proc:
    ret

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

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

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

于 2013-11-06T21:05:12.323 回答
4

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

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

希望这可以帮助!

于 2013-11-06T20:31:01.813 回答
2

被调用者当然可以清理堆栈。绝对没有根本原因为什么不能这样,事实上许多编译器支持显式声明调用约定的代码。

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

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

有关许多编译器的通用调用约定的详细信息(概念对于任何基于堆栈的功能架构都是相同的,无论是 x86、powerpc、arm、avr 等),请参阅http://www.agner.org/optimize/ call_conventions.pdf

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

许多编译器通常支持一些广泛使用的调用约定(例如“cdecl”、“stdcall”、“fastcall”),但如果您使用汇编程序进行编码或者您想编写编译器补丁,您可以自由选择与您可以想象的任何奇怪和古怪的约定(好吧,在合理范围内)。

我不确定你的陈述“到处都说被调用的函数......”中的“无处不在”在哪里,但你要么误解了,要么你对“无处不在”的选择非常糟糕/不幸。

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

于 2013-11-06T22:40:13.057 回答
0

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

这只是一个约定。这是一个很好的约定,因为事物的创建者会清理它。它是自给自足的。事物的用户(堆栈框架/参数)只是使用它们。如果它成为调用者,那么它管理堆栈的那个方面......

归根结底,这只是一个约定。欢迎您创建或修改使用不同标准的编译器。

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

于 2013-11-06T22:43:56.230 回答
0

我能想到几个原因:

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

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

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

于 2013-11-06T23:22:26.973 回答