今天下午写篇博客吧,分析分析c语言中函数调用的本质,首先我们知道c语言中函数的本质就是一段代码,但是给这段代码起了一个名字,这个名字就是他的的这段代码的开始地址
这也是函数名的本质,其实也就是汇编中的标号。下面我们会接触到一些东西 比如 eip 就是我们常常说的程序计数器,还有ebp和esp (这里是俩个指针,记得我们以前学8086也就一个sp堆栈指针)分别为EBP是指向栈底的指针,在过程调用中不变,又称为帧指针。ESP指向栈顶,程序执行时移动,ESP减小分配空间,ESP增大释放空间,ESP又称为栈指针。当然现在不理解没关系(在堆栈中变量分布是从高地址到低地址分布)。
好了我们开始正式话题吧:
1.先看图,下面我先贴出一个调用代码。
# include <stdio.h> int fun(int a, int b) { int c = 0; c= a + b; return c; } int main(void) { int a = 1; int b = 3; fun(a,b); return 0; }
反汇编后的代码
--- 汇编代码----------------------------------------------------------------------- __CxxUnhandledExceptionFilter: 00A51113 jmp __CxxUnhandledExceptionFilter (0A525E0h) ___CxxSetUnhandledExceptionFilter: 00A51118 jmp __CxxSetUnhandledExceptionFilter (0A52660h) [email protected]: 00A5111D jmp [email protected] (0A53BB0h) 、 _fun: ;注意了 fun在这里 00A51122 jmp fun (0A513C0h) ;可以看出fun还要跳转 这次跳到了0A513C0h __unlock: 00A51127 jmp __unlock (0A536F4h) [email protected]: 00A5112C jmp [email protected] (0A53BB6h) @[email protected]: 00A51131 jmp _RTC_CheckStackVars2 (0A51490h) ___set_app_type: 00A51136 jmp ___set_app_type (0A5269Eh) --- 被调函数的真正地址 ----------------------------------------- 1: # include <stdio.h> 2: 3: int fun(int a, int b) 4: { 00A513C0 push ebp ;压栈 ebp 保护ebp 00A513C1 mov ebp,esp ;将现在的esp地址给ebp换句话说ebp现在指向了这里 ;其实也就是栈帧的最下面 00A513C3 sub esp,0CCh 00A513C9 push ebx 00A513CA push esi 00A513CB push edi 00A513CC lea edi,[ebp-0CCh] 00A513D2 mov ecx,33h 00A513D7 mov eax,0CCCCCCCCh 00A513DC rep stos dword ptr es:[edi] 5: int c = 0; 00A513DE mov dword ptr [c],0 6: c= a + b; 00A513E5 mov eax,dword ptr [a] 00A513E8 add eax,dword ptr [b] 00A513EB mov dword ptr [c],eax 7: return c; 00A513EE mov eax,dword ptr [c] 8: 9: } 00A513F1 pop edi 00A513F2 pop esi 00A513F3 pop ebx 00A513F4 mov esp,ebp 00A513F6 pop ebp 00A513F7 ret --- 主调函数 ----------------------------------------------------------------------- 10: int main(void) 11: { 00A51A11 mov ebp,esp 00A51A13 sub esp,0D8h 00A51A19 push ebx 00A51A1A push esi 00A51A1B push edi 00A51A1C lea edi,[ebp-0D8h] 00A51A22 mov ecx,36h 00A51A27 mov eax,0CCCCCCCCh 00A51A2C rep stos dword ptr es:[edi] 12: int a = 1; 00A51A2E mov dword ptr [a],1 ;定义变量a 13: int b = 3; 00A51A35 mov dword ptr [b],3 ;定义变量b 14: fun(a,b); 00A51A3C mov eax,dword ptr [b] ;把变量b给eax 00A51A3F push eax ;eax压栈 也就b压栈 00A51A40 mov ecx,dword ptr [a] ;同上 00A51A43 push ecx 00A51A44 call _fun (0A51122h) ;汇编开始调用,在汇编中函数名前面加下划线当标号处理 ;地址是0A51122h,现在我们去哪里 00A51A49 add esp,8 15: 16: 17: return 0; 00A51A4C xor eax,eax 18: } 00A51A4E pop edi 00A51A4F pop esi 00A51A50 pop ebx 00A51A51 add esp,0D8h 00A51A57 cmp ebp,esp 00A51A59 call __RTC_CheckEsp (0A5113Bh) 00A51A5E mov esp,ebp 00A51A60 pop ebp 00A51A61 ret --- 无源文件 -----------------------------------------------------------------------
2.是不是看上面的已经懵逼了,没关系了,我来介绍一下
上面我是在vs中进行了反汇编,原本准备gcc下搞,后来懒得折腾了。先讲一下函数调用的过程,函数调用的时候其实也就是汇编中的地址的跳转,汇编中的跳转源于标号地址。其实这个也好理解,不知道地址,你让我如何找你。但是在找的开始,我们需要先记录一下回家地址,当前的一些寄存器状态(这是因为调用到里面也可能用到这些寄存器)注意还要压入一些函数调用参数。来张图我们看看
我们从上面的图可以看到,函数调用的时候依次压栈从右到左。压栈完毕调用call。call的作用有俩个,就是压栈返回值,然后修改程序计数器eip,实现程序跳转到被调函数。接着压栈ebp里面的内容(是什么我们先不讲)然后将esp赋值给ebp。也就是ebp里面的内容被改变,变为现在的esp内容,esp不就是栈顶,也就是说现在都指向了栈顶,然后压栈结束了或者可能换有一些其他的参数,比如我们递归调用,那下面就是下一个函数的参数,返回地址等等等。现在我们讨论的是ebp的作用是什么:那就是ebp指向了一个堆栈中一个栈帧的底部。而esp指向了顶部。我们可以利用ebp的偏移实现,局部变量和参数的访问。下面我们要讨论的就是如何返回。其实就是参数依次出栈,最后老ebp弹出到现在ebp。ebp指后到上一次的栈帧底部。但我们问一下参数是如何出栈的,难道是弹出,吗?弹出还有什么用,因为局部变量用完后就没用了呀,也没必要弹出给寄存器,其实是ebp将值赋给esp,esp由以前的栈顶指向栈底也就是ebp的地方。然后老ebp弹出到ebp。ebp归为到以前的ebp。esp再减4。esp回到返回地址处,然后在修改eip返回。然后esp再减4,回到新的栈顶。而返回的指令源于ret。
其实这个过程也不是很难,就是繁琐。需要对着栈图分析。需要理解局部变量的抛弃源于ebp对esp的修改。