C++普通函数对应的汇编代码
谈到函数,一般首先要分析一下各种函数调用约定,比如_cdecl、 _stdcall等。这两种调用约定调用时都是最右侧的参数先进栈,栈最上面的就是函数的第一个参数。不同之处在于,_cdecl由调用者清理参数占用的栈空间,而_stdcall由被调用者清理参数占用的栈空间。很明显,对于接受可变参数的函数,如printf,被调用函数是无法知道到底有几个参数的,所以只能采用由调用者清理参数栈的方式。_stdcall调用方式生成的代码会小一点。
下面的分析采用VC++6.0进行。
一.函数内部的汇编代码
void func() { } int main() { func(); return 0; }
对应的汇编代码是:
1: void func() 2: { 00401020 push ebp 00401021 mov ebp,esp 00401023 sub esp,40h 00401026 push ebx 00401027 push esi 00401028 push edi 00401029 lea edi,[ebp-40h] 0040102C mov ecx,10h 00401031 mov eax,0CCCCCCCCh 00401036 rep stos dword ptr [edi] 3: } 00401038 pop edi 00401039 pop esi 0040103A pop ebx 0040103B mov esp,ebp 0040103D pop ebp 0040103E ret
5: int main() 6: { 00401050 push ebp 00401051 mov ebp,esp 00401053 sub esp,40h 00401056 push ebx 00401057 push esi 00401058 push edi 00401059 lea edi,[ebp-40h] 0040105C mov ecx,10h 00401061 mov eax,0CCCCCCCCh 00401066 rep stos dword ptr [edi] 7: func(); 00401068 call @ILT+0(func) (00401005) 8: return 0; 0040106D xor eax,eax 9: } 0040106F pop edi 00401070 pop esi 00401071 pop ebx 00401072 add esp,40h 00401075 cmp ebp,esp 00401077 call __chkesp (00401090) 0040107C mov esp,ebp 0040107E pop ebp 0040107F ret
一般来说,函数开头的代码如下:
push ebp //保存ebp mov ebp,esp //将esp的值送ebp,在函数内部可能还会使用push、pop等操作,这时esp的值会不断变化,如果采用esp来寻址局部变量或者参数的话,可能要不断修正偏移量,而采用ebp寻址变量就方便很多。当然,这样会浪费一个寄存器,编译器可以优化。 sub esp,40h //为局部变量分配空间,这里的40h大小是vc默认分配的。
而结尾代码如下:
mov esp,ebp //恢复esp,其实相当于把栈里局部变量的空间回收了 pop ebp ret
可以看到,即使函数没有定义任何局部变量,编译器仍然为我们非配了0x40大小的空间,并全部初始化0xcccccccc。有时候我们会见到烫烫€这样的字符串信息,就是这部分内存在作怪。
二 带参数和局部变量的函数
#include <cstdio> void func(int a,int b) { int m = a; int n = b; } int main() { func(16,32); return 0; }
反汇编代码:
main中调用func的代码是:
00401088 push 20h //32压栈 0040108A push 10h //16压栈 0040108C call @ILT+10(func) (0040100f) 00401091 add esp,8 //调用完毕后清理参数占用的栈空间,这里采用的是_cdecl调用约定。
再看func的代码:
2: void func(int a,int b) 3: { 00401020 push ebp 00401021 mov ebp,esp 00401023 sub esp,48h //默认分配0x40,现在有两个局部int类型变量,空间大小增大8 …… 4: int m = a; 00401038 mov eax,dword ptr [ebp+8] 0040103B mov dword ptr [ebp-4],eax 5: int n = b; 0040103E mov ecx,dword ptr [ebp+0Ch] 00401041 mov dword ptr [ebp-8],ecx 6: } …… 00401047 mov esp,ebp 00401049 pop ebp 0040104A ret
函数中通过ebp加偏移量的方式来寻址局部变量,很容易看出,栈布局如下图所示:
图中,地址从上到下增加。因为这里是near调用,所以没有保存ECS寄存器的内容。