当谈到C函数的返回值时,返回值存储在EAX
寄存器中。假设我们说的是32位寄存器,整数是受欢迎的,但是当我们返回这些类型时会发生什么呢?long long
, long double
, struct
/union
大于32位。
在常见的x86调用约定中,适合两个寄存器的对象在RDX:RAX
中返回。这是同一个寄存器对,它是div和多指令的隐式输入/输出,以及cdq
/cqo
(符号将e/rax扩展为e/rdx)。
i386 Linux (SysV)调用约定只以这种方式返回64位整数。结构体(即使是由单个int32_t
组成的结构体)使用隐藏参数方法,而不是打包为eax
或edx:eax
。64位Linux和微软当前的标准__vectorcall
都将结构体打包成e/rax
或e/rdx:e/rax
。
许多调用约定通过添加一个隐藏的额外形参来处理较大的对象:一个指向存储返回值的空间的指针。有关您正在使用的特定ABI,请参阅ABI文档。(链接在x86 wiki)。
与注释中讨论的其他调用约定(例如隐式地使用堆栈空间来存储返回的大对象)相比,传递指针可以保存一个副本,因为指针可以指向最终目标,而不是占用堆栈空间。
但通常只有当最终目的地在堆栈上时。被调用者可以假设返回值对象是而不是与/不别名任何全局或任何它可以通过指针到达的内存相同的对象。也就是说,调用者必须进行escape分析。参见如何防止将函数参数用作隐藏指针?-在C抽象机器中,返回值对象直到函数返回的那一刻才被写入,在它完成读取任何内容之后。
但是它可以指向调用者堆栈帧中任何方便的位置,而不是相对于RSP的固定位置,这可以避免复制。
考虑这个程序:
struct object_t
{
int m1;
int m2;
int m3;
};
struct object_t
test1(void)
{
struct object_t o = {1, 2, 3};
return o;
}
long long
test2(void)
{
return 0LL;
}
long double
test3(void)
{
return 0.0L;
}
在Windows上编译(目标文件,最少指令,没有x87指令):
$ gcc -Wall -c -O2 -mno-80387 test.c -o test.o
第一个函数:
00000000 <_test1>:
0: 8b 44 24 04 mov eax,DWORD PTR [esp+0x4]
4: c7 00 01 00 00 00 mov DWORD PTR [eax],0x1
a: c7 40 04 02 00 00 00 mov DWORD PTR [eax+0x4],0x2
11: c7 40 08 03 00 00 00 mov DWORD PTR [eax+0x8],0x3
18: c3 ret
调用者将提供一个指向他的结构体在堆栈上的位置的指针作为第一个参数,test1
将使用该指针填充它。
第二个函数(sizeof(long long) == 8
):
00000020 <_test2>:
20: 31 c0 xor eax,eax
22: 31 d2 xor edx,edx
24: c3 ret
结果将通过两个寄存器eax
和edx
返回,而不仅仅是eax
。
第三函数(sizeof(long double) == 12
):
00000030 <_test3>:
30: 31 c0 xor eax,eax
32: 31 d2 xor edx,edx
34: 31 c9 xor ecx,ecx
36: c3 ret
返回值将通过三个寄存器传递,eax
, edx
, ecx
。