本文记录了函数参数以及返回值是结构体时,汇编代码是什么样的。本文使用的编译器为Visual Studio中的cl.exe,版本为用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.28.29910 版
。
定义结构体
定义这样一个结构体:
1 2 3 4 5
| struct tagTest { int a; long long b; char c[4]; };
|
编译后,各成员的偏移为:
1 2 3 4 5 6 7
| tagTest struc a dd ? field_4 dd ? b dq ? c db 4 dup(?) field_14 dd ? tagTest ends
|
其中field_4和field_14是填充,结构体大小为24字节。
函数参数为结构体
编写函数并调用:
1 2 3 4 5 6 7 8 9 10
| long long paramTest(int p1, struct tagTest p2, int p3) { return p1 + p2.a + p2.b + p3; }
int main() { struct tagTest param = {0xAAAAAAAA, 0xBBBBBBBBCCCCCCCC, "aaaa"}; paramTest(1, param, 3);
return 0; }
|
调用paramTest前将参数压入堆栈的汇编代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| push 3 sub esp, 24 mov edx, esp mov eax, [ebp+param.a] mov [edx], eax mov ecx, [ebp+param.field_4] mov [edx+4], ecx mov eax, dword ptr [ebp+param.b] mov [edx+8], eax mov ecx, dword ptr [ebp+param.b+4] mov [edx+12], ecx mov eax, dword ptr [ebp+param.c] mov [edx+16], eax mov ecx, [ebp+param.field_14] mov [edx+20], ecx push 1 call paramTest
|
首先压入第三个参数3,然后将24字节的结构体全部复制到栈中,最后压入第一个参数1,所以在调用paramTest前栈的结构为:
总结:当函数参数为结构体时,在将参数压栈的过程中,结构体中的所有内容都会复制到栈中。
函数返回值为结构体
编写函数并调用:
1 2 3 4 5 6 7 8 9 10
| struct tagTest retTest(int a, long long b) { struct tagTest retVal = {a, b, "cccc"}; return retVal; }
int main() { struct tagTest result = retTest(0x11111111, 0x2222222233333333);
return 0; }
|
调用retTest函数前将参数压栈的代码:
1 2 3 4 5 6
| push 22222222h push 33333333h push 11111111h lea edx, [ebp+retVal] push edx call retTest
|
除了函数原有的两个参数外,还压了位于栈中的结构体变量retVal的地址,命名为pRetVal。
进入retTest函数,这个函数有一个struct tagTest
类型的局部变量,命名为tmp。函数开头的这段汇编代码将tmp赋值为{a, b, "cccc"}
:
1 2 3 4 5 6 7 8
| mov eax, [ebp+a] mov [ebp+tmp.a], eax mov ecx, [ebp+b_low] mov edx, [ebp+b_high] mov dword ptr [ebp+tmp.b], ecx mov dword ptr [ebp+tmp.b+4], edx mov eax, dword_419000 mov dword ptr [ebp+tmp.c], eax
|
接下来这段汇编代码则将tmp的内容复制到pRetVal指向的结构体retVal:
1 2 3 4 5 6 7 8 9 10 11 12 13
| mov ecx, [ebp+pRetVal] mov edx, [ebp+tmp.a] mov [ecx], edx mov eax, [ebp+tmp.field_4] mov [ecx+4], eax mov edx, dword ptr [ebp+tmp.b] mov [ecx+8], edx mov eax, dword ptr [ebp+tmp.b+4] mov [ecx+0Ch], eax mov edx, dword ptr [ebp+tmp.c] mov [ecx+10h], edx mov eax, [ebp+tmp.field_14] mov [ecx+14h], eax
|
函数的返回值为pRetVal:
总结:当函数返回值为结构体,函数的调用方main函数会在栈中预留一段空间retVal用于存放返回值。当main函数调用retTest函数时,除了本身的两个参数外,还会压入retVal的地址&retVal。retTest函数会根据传入的retVal的地址,将返回值复制到retVal当中,并返回&retVal。
使用IDA的Set type...功能也能看出这一点,如果在Set type...窗口输入:
1
| tagTest retTest(int a, long long b)
|
IDA会将其自动转换为:
1
| tagTest *retTest(tagTest *__return_ptr __struct_ptr retstr, int a, __int64 b);
|
其中retstr就是指向retVal的指针。
下面这两个C语言函数编译后的汇编代码应该是一样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| struct tagTest retTest(int a, long long b) { struct tagTest retVal = {a, b, "cccc"}; return retVal; }
struct tagTest *retTest2(struct tagTest *pRetVal, int a, long long b) { struct tagTest tmp = {a, b, "cccc"}; *pRetVal = tmp; return pRetVal; }
int main() { struct tagTest result1 = retTest(0x11111111, 0x2222222233333333); struct tagTest retVal; struct tagTest *pRetVal = retTest2(&retVal, 0x11111111, 0x2222222233333333); struct tagTest result2 = *pRetVal; return 0; }
|