函数的工作借助于栈。
栈在内存中是一块特殊的存储空间,它的存储原则是“先进后出”,最先被存储的数据最后被释放。
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
当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开始。