对于以下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被调用方会弹出其参数,而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页的“函数返回结构或联合”部分中,它表示:
被调用的函数必须在返回之前从堆栈中删除该地址。
这就是为什么您ret $0x4在末尾获得a的原因test_callee5(),这是为了遵守ABI。
ret $0x4
test_callee5()
现在大约sub $0x4, %esp在每个test_callee5()调用站点之后,它是上述规则的副作用,与C编译器生成的优化代码结合在一起。由于本地存储堆栈空间是完全通过以下方式预先保留的:
sub $0x4, %esp
3: sub $0x38,%esp
无需压入/弹出隐藏的指针,只需将其写在预先保留的空间的底部(由指向esp),使用mov %eax,(%esp)第9行和第17行即可。由于堆栈指针没有减少,因此sub $0x4,%esp可以求反的效果ret $0x4,并保持堆栈指针不变。
esp
mov %eax,(%esp)
sub $0x4,%esp
在Win32(我猜是使用MSVC编译器)上,没有这样的ABI规则,使用了一个简单的规则ret(如cdecl中所预期的那样),隐藏的指针在第7行和第11行被压入堆栈。作为一种优化,但仅在被调用者退出之前,使用调用,以add esp,1Ch释放隐藏的指针堆栈插槽(2 * 0x4字节)和本地AStruct结构(0x14字节)。
ret
add esp,1Ch
AStruct
cdecl不会为返回结构的函数定义调用约定吗?
不幸的是,事实并非如此,它随C编译器和操作系统的不同而不同。