我想知道如何将参数传递给 C 中的函数。值存储在哪里以及如何检索它们?可变参数传递如何工作?也因为它是相关的:返回值呢?
我对 CPU 寄存器和汇编器有基本的了解,但还不足以让我彻底了解 GCC 向我吐口水的 ASM。一些简单的带注释的例子将不胜感激。
我想知道如何将参数传递给 C 中的函数。值存储在哪里以及如何检索它们?可变参数传递如何工作?也因为它是相关的:返回值呢?
我对 CPU 寄存器和汇编器有基本的了解,但还不足以让我彻底了解 GCC 向我吐口水的 ASM。一些简单的带注释的例子将不胜感激。
考虑到这段代码:
int foo (int a, int b) {
return a + b;
}
int main (void) {
foo(3, 5);
return 0;
}
编译它gcc foo.c -S
会给出汇编输出:
foo:
pushl %ebp
movl %esp, %ebp
movl 12(%ebp), %eax
movl 8(%ebp), %edx
leal (%edx,%eax), %eax
popl %ebp
ret
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
movl $5, 4(%esp)
movl $3, (%esp)
call foo
movl $0, %eax
leave
ret
所以基本上调用者(在这种情况下main
)首先在堆栈上分配 8 个字节来容纳两个参数,然后将两个参数放在堆栈上相应的偏移量(4
和0
)处,然后call
发出指令,将控制权转移到foo
常规。该foo
例程从堆栈中的相应偏移量读取其参数,将其恢复,并将其返回值放入eax
寄存器中,以便调用者可以使用它。
那是特定于平台的,也是“ABI”的一部分。事实上,一些编译器甚至允许您在不同的约定之间进行选择。
例如,Microsoft 的 Visual Studio 提供了使用寄存器的 __fastcall 调用约定。其他平台或调用约定专门使用堆栈。
可变参数以非常相似的方式工作 - 它们通过寄存器或堆栈传递。对于寄存器,它们通常根据类型按升序排列。如果你有类似 (int a, int b, float c, int d) 的东西,PowerPC ABI 可能会放在a
r3、b
r4、d
r5 和c
fp1 中(我忘记了浮点寄存器从哪里开始,但你明白了) .
返回值再次以相同的方式工作。
不幸的是,我没有太多示例,我的大部分程序集都在 PowerPC 中,您在程序集中看到的只是代码直接用于 r3、r4、r5,并将返回值也放入 r3。
您的问题比任何人都可以在 SO 帖子中合理地尝试回答更多,更不用说它的实现也定义了。
但是,如果您对 x86 的答案感兴趣,我建议您观看这个名为Programming Paradigms的斯坦福 CS107 讲座,其中您提出的问题的所有答案都将在前 6-8 堂课中得到非常详细(并且非常雄辩)的解释.
这取决于您的编译器、您正在为其编译的目标体系结构和操作系统,以及您的编译器是否支持更改调用约定的非标准扩展。但也有一些共同点。
C 调用约定通常由操作系统的供应商建立,因为他们需要决定系统库使用什么约定。
较新的 CPU(例如 ARM 或 PowerPC)往往具有由 CPU 供应商定义的调用约定,并在不同的操作系统之间兼容。x86 是一个例外:不同的系统使用不同的调用约定。过去,16 位 8086 和 32 位 80386 的调用约定比 x86_64 的调用约定要多得多(尽管这还不是一个)。32 位 x86 Windows 程序有时在同一个程序中使用多个调用约定。
一些观察:
STDCALL
最初是),并且还支持和约定。所有这四个都在16 位操作系统上出现并变体。因此,几乎所有 Windows 程序在同一个程序中至少使用两种不同的约定。FAR PASCAL
FORTRAN
FASTCALL
NEAR
FAR
FASTCALL
在 MS-DOS 和 Windows 上。printf("%d\n", x);
编译器会将x
、然后是格式字符串、然后是返回地址,压入堆栈。这保证了第一个参数与堆栈指针的偏移量是已知的,并且<varargs.h>
具有它工作所需的信息。PASCAL
MS-DOS 上的约定,并且作为STDCALL
Windows 上的约定而存在。它不支持可变参数函数。(https://en.wikibooks.org/wiki/X86_Disassembly/Calling_Conventions)-fomit-frame-pointer
)。 您可以让交叉编译器使用不同的调用约定发出代码,并将它们与诸如-S -target
(on clang
) 之类的开关进行比较。
基本上,C 通过将参数压入堆栈来传递参数。对于指针类型,指针被压入堆栈。
关于 C 的一件事是调用者恢复堆栈而不是被调用的函数。这样,参数的数量可以变化,并且被调用的函数不需要提前知道将传递多少参数。
返回值在 AX 寄存器或其变体中返回。