我想知道如何将参数传递给C中的函数。存储的值在哪里以及如何检索它们?可变参数传递如何工作?此外,因为它是相关的:返回值怎么样?
我对CPU寄存器和汇编器有基本的了解,但还不足以让我彻底了解GCC向我吐出的ASM。一些简单的带注释的例子将非常受欢迎。
答案 0 :(得分:18)
考虑此代码:
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
和{ {1}}),然后发出0
指令,将控制转移到call
例程。 foo
例程从堆栈中的相应偏移量中读取其参数,恢复它,并将其返回值放在foo
寄存器中,以便调用者可以使用它。
答案 1 :(得分:5)
这是特定于平台的,也是“ABI”的一部分。事实上,一些编译器甚至允许您在不同的约定之间进行选择。
例如,Microsoft的Visual Studio提供了__fastcall调用约定,该约定使用寄存器。其他平台或调用约定仅使用堆栈。 Variadic参数的工作方式非常相似 - 它们通过寄存器或堆栈传递。在寄存器的情况下,它们通常按照类型的升序排列。如果你有(int a,int b,float c,int d),PowerPC ABI可能会将a
放在r3中,b
放在r4中,d
放在r5中,{ {1}}在fp1中(我忘记了浮动寄存器的起始位置,但你明白了。)
返回值再次以相同的方式工作。
不幸的是,我没有很多例子,我的大部分程序集都在PowerPC中,你在程序集中看到的只是代码直接用于r3,r4,r5,并将返回值放在r3中。
答案 2 :(得分:3)
你的问题比任何人都可以合理地在SO帖子中回答的问题更多,更不用说它的定义也是如此。
但是,如果您对x86答案感兴趣,我建议您观看这个标题为Programming Paradigms的斯坦福CS107讲座,其中您提出的问题的所有答案都将得到非常详细的解释(并且非常有说服力)在前6-8个讲座中。
答案 3 :(得分:2)
这取决于您的编译器,要编译的目标体系结构和操作系统,以及您的编译器是否支持更改调用约定的非标准扩展。但是有一些共性。
C调用约定通常由操作系统的供应商建立,因为他们需要确定系统库使用哪种约定。
最近的CPU(例如ARM或PowerPC)往往具有其调用约定,这些约定由CPU供应商定义并且在不同的操作系统之间兼容。 x86是一个例外:不同的系统使用不同的调用约定。 16位8086和32位80386的调用约定比x86_64的调用约定要多得多(尽管即使不是这样)。 32位x86 Windows程序有时会在同一程序中使用多个调用约定。
一些观察:
STDCALL
,最初为FAR PASCAL
),并且还支持FORTRAN
和FASTCALL
约定。所有这四个版本在16位操作系统上都具有NEAR
和FAR
变体。因此,几乎所有Windows程序在同一程序中至少使用两种不同的约定。FASTCALL
。printf("%d\n", x);
,编译器将把x
,然后是格式字符串,然后是返回地址,压入堆栈。这样可以保证第一个参数与堆栈指针的偏移量是已知的,并且<varargs.h>
具有它需要工作的信息。PASCAL
约定,而在Windows上则作为STDCALL
约定保留下来。它不支持可变参数功能。 (https://en.wikibooks.org/wiki/X86_Disassembly/Calling_Conventions)-fomit-frame-pointer
)。 您可以让交叉编译器使用不同的调用约定来发出代码,并与-S -target
(在clang
上)进行比较。
答案 4 :(得分:0)
基本上,C通过在堆栈上推送它们来传递参数。对于指针类型,指针被推入堆栈。
关于C的一件事是调用者恢复堆栈而不是被调用的函数。这样,参数的数量可以变化,被调用的函数不需要提前知道将传递多少个参数。
返回值在AX寄存器中返回,或其变体。