函数的工作借助于栈。
栈在内存中是一块特殊的存储空间,它的存储原则是“先进后出”,最先被存储的数据最后被释放。
esp被称为栈顶指针,ebp称为栈底指针,通过这两个指针寄存器保存当前栈的起始地址与结束地址。
esp与ebp之间所构成的空间便成为栈帧。通常,在VC++中,栈帧中可以寻址的数据有局部变量、函数返回地址、函数参数等。不同的两次函数调用,所形成的栈帧也不同。当由一个函数进入到另一个函数中时,就会针对所调用的函数形成所需的栈空间,形成此函数的栈帧。当这个函数结束调用时,需要清除掉它所使用的栈空间,关闭栈帧,这一过程称为栈平衡。
int main() { return 0; }
汇编代码讲解 int main() { 00C81380 push ebp ;进入函数后的第一件事,保存栈底指针ebp 00C81381 mov ebp,esp ;调整栈底指针到栈顶位置 00C81383 sub esp,0C0h ;抬高新的栈顶,开辟栈空间C0h,作为局部变量的存储空间,形成此main函数的栈帧 00C81389 push ebx 00C8138A push esi 00C8138B push edi 00C8138C lea edi,[ebp-0C0h] 00C81392 mov ecx,30h 00C81397 mov eax,0CCCCCCCCh 00C8139C rep stos dword ptr es:[edi] return 0; 00C8139E xor eax,eax } ;在退出时,恢复原来的栈帧,ebp的值是原来的esp,然后再pop出ebp 00C813A0 pop edi 00C813A1 pop esi 00C813A2 pop ebx 00C813A3 mov esp,ebp ;还原esp 00C813A5 pop ebp ;还原ebp 00C813A6 ret
上面代码在退出时并没有检测栈平衡,如下
00C813A0 pop edi 00C813A1 pop esi 00C813A2 pop ebx 多了这一段检测 add esp,40h ;降低栈顶esp,此时局部变量空间被释放 cmp ebp,esp ;检测栈平衡,如ebp与esp不等,则不平衡 call _chkesp ;进入栈平衡错误检查函数 00C813A3 mov esp,ebp ;还原esp 00C813A5 pop ebp ;还原ebp 00C813A6 ret
在VC++环境下有三种函数调用约定:_cdecl、_stdcall、fastcall。
_cdecl:C/C++默认的调用方式,调用方平衡栈,不定参数的函数可以使用。
_stdcall:被调用方平衡栈,不定参数的函数无法使用。
_fastcall:寄存器方式传参,被调用方平衡栈,不定参数的函数无法使用。
#include<stdio.h> void _stdcall showstd(int number) { printf("%d\r\n", number); } void _cdecl showcde(int number) { printf("%d\r\n", number); } void main() { showstd(5); showcde(6); } void _stdcall showstd(int number) { 00391C60 push ebp 00391C61 mov ebp,esp 00391C63 sub esp,0C0h 00391C69 push ebx 00391C6A push esi 00391C6B push edi 00391C6C lea edi,[ebp-0C0h] 00391C72 mov ecx,30h 00391C77 mov eax,0CCCCCCCCh 00391C7C rep stos dword ptr es:[edi] printf("%d\r\n", number); 00391C7E mov esi,esp 00391C80 mov eax,dword ptr [number] 00391C83 push eax 00391C84 push 3958A8h 00391C89 call dword ptr ds:[399114h] 00391C8F add esp,8 00391C92 cmp esi,esp 00391C94 call __RTC_CheckEsp (03911DBh) } 00391C99 pop edi 00391C9A pop esi 00391C9B pop ebx 00391C9C add esp,0C0h 00391CA2 cmp ebp,esp 00391CA4 call __RTC_CheckEsp (03911DBh) } 00391CA9 mov esp,ebp 00391CAB pop ebp 00391CAC ret 4 ;这里结束后平衡栈顶4,相当于esp+4 void _cdecl showcde(int number) { 00391780 push ebp 00391781 mov ebp,esp 00391783 sub esp,0C0h 00391789 push ebx 0039178A push esi 0039178B push edi 0039178C lea edi,[ebp-0C0h] 00391792 mov ecx,30h 00391797 mov eax,0CCCCCCCCh 0039179C rep stos dword ptr es:[edi] printf("%d\r\n", number); 0039179E mov esi,esp 003917A0 mov eax,dword ptr [number] 003917A3 push eax 003917A4 push 3958A8h 003917A9 call dword ptr ds:[399114h] 003917AF add esp,8 003917B2 cmp esi,esp 003917B4 call __RTC_CheckEsp (03911DBh) } 003917B9 pop edi 003917BA pop esi 003917BB pop ebx 003917BC add esp,0C0h 003917C2 cmp ebp,esp 003917C4 call __RTC_CheckEsp (03911DBh) } 003917C9 mov esp,ebp 003917CB pop ebp 003917CC ret ;这里直接返回并没有自己平衡,当执行权到了调用方时平衡 A void main() { 00392520 push ebp 00392521 mov ebp,esp 00392523 sub esp,0C0h 00392529 push ebx 0039252A push esi 0039252B push edi 0039252C lea edi,[ebp-0C0h] 00392532 mov ecx,30h 00392537 mov eax,0CCCCCCCCh 0039253C rep stos dword ptr es:[edi] showstd(5); 0039253E push 5 00392540 call showstd (03911D1h) showcde(6); 00392545 push 6 00392547 call showcde (03911CCh) 0039254C add esp,4 ;A处并没有平衡栈顶,现在平衡 } 0039254F xor eax,eax ;下面的栈帧关闭是main函数的 00392551 pop edi 00392552 pop esi 00392553 pop ebx 00392554 add esp,0C0h 0039255A cmp ebp,esp 0039255C call __RTC_CheckEsp (03911DBh) 00392561 mov esp,ebp 00392563 pop ebp } 00392564 ret
为什么要平衡栈顶呢?以前我一直弄不明白,我一直认为当我调用函数执行时,该函数形成了自己的栈帧,保存了它可以用到的局部变量等等,当它结束时,直接恢复到原来的栈顶就可以了,也就是mov esp,ebp这句就可以了,但为什么会在返回时还要对esp进行更改。现在弄明白了,原因是该函数有参数,在调用函数前,参数会先被压入栈中,所以在函数结束后,该函数的栈帧也关闭了,但是调用方的栈帧中还保存着刚才函数所需要的参数,现在它成了没有用的数据,当然要把它踢出去。
当showcde函数调用结束后,黄色区域栈的数据也就没用了,所以降低栈顶。
下面看一下用寄存器方式传参方式,fastcall
#include<stdio.h> void _fastcall showfast(int one, int two, int three, int four) { printf("%d %d %d %d\r\n",one,two,three,four); } void main() { showfast(1, 2, 3, 4); } void _fastcall showfast(int one, int two, int three, int four) { 00132F90 push ebp 00132F91 mov ebp,esp 00132F93 sub esp,0D8h 00132F99 push ebx 00132F9A push esi 00132F9B push edi 00132F9C push ecx 00132F9D lea edi,[ebp-0D8h] 00132FA3 mov ecx,36h 00132FA8 mov eax,0CCCCCCCCh 00132FAD rep stos dword ptr es:[edi] 00132FAF pop ecx 00132FB0 mov dword ptr [two],edx ;用临时变量存储参数二的值2,在ida中显示mov dword ptr [ebp-8],edx 00132FB3 mov dword ptr [one],ecx ;用临时变量存储参数一的值1,mov dword ptr [ebp-4],ecx printf("%d %d %d %d\r\n",one,two,three,four); 00132FB6 mov esi,esp 00132FB8 mov eax,dword ptr [four] ;mov eax,dword ptr [ebp+0ch],取得参数4 00132FBB push eax 00132FBC mov ecx,dword ptr [three] ;mov ecx,dword ptr [ebp+8],取得参数3 00132FBF push ecx 00132FC0 mov edx,dword ptr [two] 00132FC3 push edx 00132FC4 mov eax,dword ptr [one] 00132FC7 push eax 00132FC8 push 1358A8h 00132FCD call dword ptr ds:[139114h] 00132FD3 add esp,14h 00132FD6 cmp esi,esp 00132FD8 call __RTC_CheckEsp (01311DBh) } 00132FDD pop edi 00132FDE pop esi 00132FDF pop ebx 00132FE0 add esp,0D8h 00132FE6 cmp ebp,esp 00132FE8 call __RTC_CheckEsp (01311DBh) 00132FED mov esp,ebp 00132FEF pop ebp 00132FF0 ret 8 ;由于在传参的时候使用个两个寄存器帮助传参,用栈传参只用了两个,故ret 8 void main() { 00131C60 push ebp 00131C61 mov ebp,esp 00131C63 sub esp,0C0h 00131C69 push ebx 00131C6A push esi 00131C6B push edi 00131C6C lea edi,[ebp-0C0h] 00131C72 mov ecx,30h 00131C77 mov eax,0CCCCCCCCh 00131C7C rep stos dword ptr es:[edi] showfast(1, 2, 3, 4); ;看到这里 00131C7E push 4 ;用栈传递参数3和4 00131C80 push 3 00131C82 mov edx,2 ;用edx传递第二个参数2 00131C87 mov ecx,1 ;用ecx传递第一个参数1 00131C8C call showfast (01311E5h) } 00131C91 xor eax,eax 00131C93 pop edi 00131C94 pop esi 00131C95 pop ebx 00131C96 add esp,0C0h 00131C9C cmp ebp,esp 00131C9E call __RTC_CheckEsp (01311DBh) 00131CA3 mov esp,ebp 00131CA5 pop ebp } 00131CA6 ret
上面的注释已经很明白了,另外需要注意,在使用ebp相对寻址定位参数3和4时,为什么不是从ebp+4开始的,原因是在调用函数时,会将该call的下一条指令的地址压入栈中,所以定位从ebp+8开始。