本文基于win32下汇编。
函数栈增长方向与地址方向相反,从高地址向低地址增长。esp指向函数栈顶,ebp指向函数栈底。
sub esp XXX 将XXX长度的内存块压入栈
add esp XXX将XXX长度的内存块弹出栈
参数压栈顺序示例
int param1 = 1;
00C939DF mov dword ptr [param1],1
int param2 = 3;
00C939E6 mov dword ptr [param2],3
int sum = pushParametersOrderApp(param1, param2); # 压栈顺序跟参数申明顺序相反!
00C939F0 mov eax,dword ptr [param2] # 将param2值放入eax
00C939F6 push eax # 压栈eax
00C939F7 mov ecx,dword ptr [param1] # 将param1值放入ecx
00C939FA push ecx # 压栈ecx
00C939FB call pushParametersOrderApp (0C911F9h) # 函数调用
00C93A00 add esp,8 # 将之前压栈的eax和ecx两个值移除。pop操作是将esp加8,说明栈增长方向是从高地址到低地址。
00C93A03 mov dword ptr [sum],eax # 函数返回值在eax中,存入sum变量中
# 函数调用现场
int pushParametersOrderApp(int param1, int param2)
{
00E41F80 push ebp # 保存之前ebp指针,esp += 4
00E41F81 mov ebp,esp # 将新的esp赋给ebp指向当前函数栈底
00E41F83 sub esp,0D8h # 当前函数栈帧 预留216(0D8)的字节,存放自动变量
00E41F89 push ebx
00E41F8A push esi
00E41F8B push edi
00E41F8C lea edi,[ebp-0D8h] # 栈顶指针地址存入edi
00E41F92 mov ecx,36h # 栈上54(36h)个整数,即216(0D8)个字节
00E41F97 mov eax,0CCCCCCCCh # 初始化的数值
00E41F9C rep stos dword ptr es:[edi] # 初始化栈空间
int temp1 = param1;
00E41F9E mov eax,dword ptr [param1] # param1的值存入eax
00E41FA1 mov dword ptr [temp1],eax # eax值存入temp1中
int temp2 = param2;
00E41FA4 mov eax,dword ptr [param2] # 同理
00E41FA7 mov dword ptr [temp2],eax
return temp1 + temp2;
00E41FAA mov eax,dword ptr [temp1] # temp1值存入eax
00E41FAD add eax,dword ptr [temp2] # 相加结果存入eax,与函数返回后00C93A03行操作对应,函数结果在eax中
}
00FE1FB0 pop edi
00FE1FB1 pop esi
00FE1FB2 pop ebx
00FE1FB3 mov esp,ebp # 还原esp
00FE1FB5 pop ebp # 还原ebp,同时esp-=4
00FE1FB6 ret
pushParametersOrderApp函数调用前后堆栈:
上图栈从下往上增长。代码比较简单,对两个变量进行求和,结果返回。通过汇编源码说明几点:
1)C/C++ 中函数参数压栈顺序:与申明顺序相反
2)函数栈帧的定义:esp和ebp之间的内存块
3)栈上自动变量的初始化,使用0CCCCCCCC
4)函数参数在函数栈帧的外面,见00C939F6、00C939FA行;函数局部变量参数定义在栈上。
传值拷贝
struct TestItem
{
int a;
int b;
int arr[10];
};
int passValueApp(TestItem item)
{
int value = item.a;
int value1 = item.b;
return value;
}
int passReferenceApp(TestItem& item)
{
int value = item.a;
int valu1 = item.b;
return value;
}
int passPointerApp(TestItem* item)
{
int value = item->a;
int value1 = item->b;
return value;
}
TestItem item = {1, 4, {1,2,3}};
00FE3948 mov dword ptr [item],1 # &item.a==item,a置为1
00FE394F mov dword ptr [ebp-30h],4 # ebp-48 通过ebp指针访问b
00FE3956 mov dword ptr [ebp-2Ch],1 # ebp-44
00FE395D mov dword ptr [ebp-28h],2 # ebp-40
00FE3964 mov dword ptr [ebp-24h],3 # ebp-36
00FE396B xor eax,eax # 异或操作将eax置零
00FE396D mov dword ptr [ebp-20h],eax # ebp-32
00FE3970 mov dword ptr [ebp-1Ch],eax # ebp-28
00FE3973 mov dword ptr [ebp-18h],eax # ebp-24
00FE3976 mov dword ptr [ebp-14h],eax # ebp-20
00FE3979 mov dword ptr [ebp-10h],eax # ebp-16
00FE397C mov dword ptr [ebp-0Ch],eax # ebp-12
00FE397F mov dword ptr [ebp-8],eax # ebp-8
passValueApp(item); ###### 传结构体函数 ######
00FE3982 sub esp,30h # 参数值拷贝,esp减去48(30h)正好sizeof TestItem大小
00FE3985 mov ecx,0Ch # 指定要拷贝次数,0Ch即12个整数,对用48byte
00FE398A lea esi,[item] # 获取item地址
00FE398D mov edi,esp # 拷贝基地址指定
00FE398F rep movs dword ptr es:[edi],dword ptr [esi] # rep循环12次拷贝
00FE3991 call passValue (0FE11E5h) #
00FE3996 add esp,30h # 将参数拷贝弹出栈空间,esp+=48
passPointerApp(&item); ###### 传指针函数 ######
00FE3999 lea eax,[item] # 获取item地址存入eax
00FE399C push eax # 参数值拷贝,压栈eax esp-=4
00FE399D call passPointerApp (0FE11EFh) #
00FE39A2 add esp,4 # 将参数拷贝弹出栈空间,esp+=4
passReferenceApp(item);
00FE39A5 lea eax,[item] # 汇编层面引用版本和指针一样
00FE39A8 push eax
00FE39A9 call passReferenceApp (0FE11EAh)
00FE39AE add esp,4
该示例说明了函数传值拷贝问题:
1)passValueApp函数调用需要在栈帧上预留48字节的空间,00FE398D、00FE398F拷贝item结构体,函数执行完成以后将esp加上48字节将参数拷贝出栈。
2)passPointerApp函数传指针,00FE399C行将指针变量拷贝入栈,函数调用完成将esp+4完成参数拷贝出栈操作。
3)passReferenceApp 传引用于传指针原理一样。
由此可见,大数据结构一定要传指针或者引用。
# 函数内部堆栈现场
TestItem returnStructApp()
{
00EC3850 push ebp
00EC3851 mov ebp,esp
00EC3853 sub esp,0F8h
00EC3859 push ebx
00EC385A push esi
00EC385B push edi
00EC385C lea edi,[ebp-0F8h]
00EC3862 mov ecx,3Eh
00EC3867 mov eax,0CCCCCCCCh
00EC386C rep stos dword ptr es:[edi]
TestItem item = { 1,2, {3,4,5} };
00EC386E mov dword ptr [item],1
00EC3875 mov dword ptr [ebp-30h],2
00EC387C mov dword ptr [ebp-2Ch],3
00EC3883 mov dword ptr [ebp-28h],4
00EC388A mov dword ptr [ebp-24h],5
00EC3891 xor eax,eax
00EC3893 mov dword ptr [ebp-20h],eax
00EC3896 mov dword ptr [ebp-1Ch],eax
00EC3899 mov dword ptr [ebp-18h],eax
00EC389C mov dword ptr [ebp-14h],eax
00EC389F mov dword ptr [ebp-10h],eax
00EC38A2 mov dword ptr [ebp-0Ch],eax
00EC38A5 mov dword ptr [ebp-8],eax
return item;
00EC38A8 mov ecx,0Ch # 拷贝长度12个整数
00EC38AD lea esi,[item] # 获取item地址
00EC38B0 mov edi,dword ptr [ebp+8] # 临时对象1地址
00EC38B3 rep movs dword ptr es:[edi],dword ptr [esi] # 将item内容拷贝到edi指向内存区,edi被改变
00EC38B5 mov eax,dword ptr [ebp+8] # 将临时对象1地址存入eax寄存器,传出函数!
}
00EC38B8 push edx
00EC38B9 mov ecx,ebp
00EC38BB push eax
00EC38BC lea edx,[ (0EC38D0h)]
00EC38C2 call @ILT+135(@_RTC_CheckStackVars@8) (0EC108Ch)
00EC38C7 pop eax
00EC38C8 pop edx
00EC38C9 pop edi
00EC38CA pop esi
00EC38CB pop ebx
00EC38CC mov esp,ebp
00EC38CE pop ebp
00EC38CF ret
# 函数调用堆栈现场
TestItem item1 = returnStructApp();
00EC39B1 lea eax,[ebp-188h]
00EC39B7 push eax # returnStructApp是个无参函数,但是也会压eax入栈
00EC39B8 call returnStructApp (0EC11F4h)
00EC39BD add esp,4 # 弹出eax
00EC39C0 mov ecx,0Ch # 拷贝长度12整数
00EC39C5 mov esi,eax # eax指向函数返回的临时对象数据区,在returnStructApp函数内部被修改
00EC39C7 lea edi,[ebp-1C0h] # 临时对象2内存块地址
00EC39CD rep movs dword ptr es:[edi],dword ptr [esi] # 循环执行数据拷贝,从函数内部临时对象1到临时对象2
00EC39CF mov ecx,0Ch # 拷贝长度12整数
00EC39D4 lea esi,[ebp-1C0h] # 临时对象2内存块地址
00EC39DA lea edi,[item1] # 目标item1数据区
00EC39DD rep movs dword ptr es:[edi],dword ptr [esi] # 循环执行数据拷贝,从临时对象2到item1
上面代码中,函数中返回一个结构体对象,然后函数返回值赋给item1,函数返回过程中构造了两个临时对象,一共执行了3次数据拷贝。所以效率比较低。尽量避免直接返回结构对象。
1. http://blog.csdn.net/ENO_REZ/article/details/2158682
2. http://blog.csdn.net/vagrxie/article/details/2501238