调用函数返回结构的约定

对于下面的C代码:

struct _AStruct { int a; int b; float c; float d; int e; }; typedef struct _AStruct AStruct; AStruct test_callee5(); void test_caller5(); void test_caller5() { AStruct g = test_callee5(); AStruct h = test_callee5(); } 

我得到以下反汇编为Win32:

 _test_caller5: 00000000: lea eax,[esp-14h] 00000004: sub esp,14h 00000007: push eax 00000008: call _test_callee5 0000000D: lea ecx,[esp+4] 00000011: push ecx 00000012: call _test_callee5 00000017: add esp,1Ch 0000001A: ret 

而对于Linux32:

 00000000 <test_caller5>: 0: push %ebp 1: mov %esp,%ebp 3: sub $0x38,%esp 6: lea 0xffffffec(%ebp),%eax 9: mov %eax,(%esp) c: call d <test_caller5+0xd> 11: sub $0x4,%esp ;;;;;;;;;; Note this extra sub ;;;;;;;;;;;; 14: lea 0xffffffd8(%ebp),%eax 17: mov %eax,(%esp) 1a: call 1b <test_caller5+0x1b> 1f: sub $0x4,%esp ;;;;;;;;;; Note this extra sub ;;;;;;;;;;;; 22: leave 23: ret 

我试图了解调用之后调用者的行为差异。 为什么Linux32的调用者做这些额外的潜艇?

我会假设两个目标都将遵循cdecl调用约定。 cdecl是否定义了返回结构的函数的调用约定?

编辑:

我添加了一个被调用者的实现。 果然,你可以看到Linux32的被调用者popup了它的参数,而Win32调用者没有:

 AStruct test_callee5() { AStruct S={0}; return S; } 

Win32反汇编:

 test_callee5: 00000000: mov eax,dword ptr [esp+4] 00000004: xor ecx,ecx 00000006: mov dword ptr [eax],0 0000000C: mov dword ptr [eax+4],ecx 0000000F: mov dword ptr [eax+8],ecx 00000012: mov dword ptr [eax+0Ch],ecx 00000015: mov dword ptr [eax+10h],ecx 00000018: ret 

Linux32反汇编:

 00000000 <test_callee5>: 0: push %ebp 1: mov %esp,%ebp 3: sub $0x20,%esp 6: mov 0x8(%ebp),%edx 9: movl $0x0,0xffffffec(%ebp) 10: movl $0x0,0xfffffff0(%ebp) 17: movl $0x0,0xfffffff4(%ebp) 1e: movl $0x0,0xfffffff8(%ebp) 25: movl $0x0,0xfffffffc(%ebp) 2c: mov 0xffffffec(%ebp),%eax 2f: mov %eax,(%edx) 31: mov 0xfffffff0(%ebp),%eax 34: mov %eax,0x4(%edx) 37: mov 0xfffffff4(%ebp),%eax 3a: mov %eax,0x8(%edx) 3d: mov 0xfffffff8(%ebp),%eax 40: mov %eax,0xc(%edx) 43: mov 0xfffffffc(%ebp),%eax 46: mov %eax,0x10(%edx) 49: mov %edx,%eax 4b: leave 4c: ret $0x4 ;;;;;;;;;;;;;; Note this ;;;;;;;;;;;;;; 

为什么Linux32的调用者做这些额外的潜艇?

原因是使用由编译器注入的隐藏指针( 命名为返回值优化 ),用于通过值返回结构。 在SystemV的ABI中 ,在第41页的“函数返回结构或联合”一节中,它说:

被调用的函数在返回之前必须从堆栈中删除这个地址。

这就是为什么你在test_callee5()的末尾得到一个ret $0x4 ,这是为了符合ABI。

现在sub $0x4, %esp在每个test_callee5()调用站点后面都存在sub $0x4, %esp ,这是上述规则的一个副作用,与C编译器生成的优化代码结合在一起。 由于本地存储堆栈空间完全由以下部分预留:

 3: sub $0x38,%esp 

没有必要使用mov %eax,(%esp)在第9行和第17行写入预留空间的底部(由esp指向)的底部。作为堆栈指针不会递减, sub $0x4,%esp在那里可以抵消ret $0x4 ,并保持堆栈指针不变。

在Win32上(我猜测是使用MSVC编译器),没有这样的ABI规则,使用了一个简单的ret (正如cdecl所期望的那样),隐藏的指针在第7行和第11行被压入堆栈。虽然这些槽没有被释放在调用之后,作为优化,但是只有在被调用者退出之前,使用add esp,1Ch ,释放隐藏的指针栈槽(2 * 0x4字节)和本地AStruct结构(0x14字节)。

cdecl是否定义了返回结构的函数的调用约定?

不幸的是,它不是,它随着C编译器和操作系统而变化

没有单一的“cdecl”调用约定。 它由编译器和操作系统定义。

另外阅读程序集我不太确定约定实际上是不同的 – 在这两种情况下调用者提供作为额外的参数输出缓冲区。 这只是海湾合作委员会选择不同的指令(第二个额外的子是奇怪的,是代码优化?)。