C开发基础--函数调用栈

发现有一些问题几乎是全部的新人都会遇到,并且也常由于缺少一些基本的知识而无从下手。函数调用栈的内容就是其中之一。因而花点时间把之前写的内容整理出来。数据结构

程序在运行期间,内存中有一块区域,用来实现程序的函数调用机制。这块区域是一块LIFO的数据结构区域,咱们能够叫函数栈(调用栈)。每一个未退出的函数都会在函数栈中拥有一块数据区,咱们叫函数的栈帧。函数的调用栈帧中,保存了相应的函数的一些重要信息:函数中使用的局部变量,函数的参数,另外还有一些维护函数栈所须要的数据,好比EBP指针,函数的返回地址。以下图。咱们假设程序当前执行的函数是Z函数,那么在函数调用栈中就会存在相似像这样的结构(EBP所指向的实际上是“父函数”的调用栈帧,如何作到的后面会解释):函数

image

编译器把C/C++代码编译成汇编指令时,会生成一系列(颇有规则)的指令来支持函数调用的机制。当一个函数调用发生时(咱们假设是Z函数内调用了A函数):this

    1. 会执行零到多个PUSH指令(用于参数入栈),而后有执行一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动做。以后,IP(instruction point)指向要跳转的指令的地址(CALL指令的目标地址,也就是下一个函数)spa

    2. 大部分的本地编译器都会在每一个函数体以前插入相似以下指令:PUSH EBP; MOV EBP ESP;即,在程式执行到一个函数的真正函数体时,已有如下数据顺序入栈:参数,返回地址,EBP。(注意,EBP是如何做到指向上个函数调用栈帧的)debug

    3. 将栈顶指针进行上移,“空”出一块区域,用于临时地存放函数的局部变量。3d

这几步完成(也就是一个函数调用发生了),函数调用栈就会变成这个样子,函数调用结束的时候,相应的作“反向的动做”就能够了。指针

image

 

咱们用一段很是简单的真实代码看来:code

  1. int increase(int a) {
  2.  
  3.     int temp = 4;
  4.  
  5.     return a + 3;
  6. }
  7.  
  8. int main(int argc, char* const argv[])
  9. {
  10.     int sum = increase(3);
  11.  
  12.     return 0;
  13. }

在main函数中调用 increase 函数。用VS单步断点打开汇编模式,能够看到以下的代码blog

  1.     int sum = increase(3);
  2. 00D2561E  push        3  
  3. 00D25620  call        increase (0D2142Eh)  
  4. 00D25625  add         esp,4  
  5. 00D25628  mov         dword ptr [sum],eax  

对照前面的说明,咱们能够看到,调用函数前有 push 指令先把函数参数压栈。以后才真正的call increase 。而后咱们进入 increase 函数再看看函数体是什么样的。内存

  1. int increase(int a) {
  2. 000455C0  push        ebp  
  3. 000455C1  mov         ebp,esp  
  4. 000455C3  sub         esp,0CCh  
  5. 000455C9  push        ebx  
  6. 000455CA  push        esi  
  7. 000455CB  push        edi  
  8. 000455CC  lea         edi,[ebp-0CCh]  
  9. 000455D2  mov         ecx,33h  
  10. 000455D7  mov         eax,0CCCCCCCCh  
  11. 000455DC  rep stos    dword ptr es:[edi]  
  12.  
  13.     int temp = 4;
  14. 000455DE  mov         dword ptr [temp],4  
  15.  
  16.     return a + temp;
  17. 000455E5  mov         eax,dword ptr [a]  
  18. 000455E8  add         eax,dword ptr [temp]  
  19. }
  20. 000455EB  pop         edi  
  21. 000455EC  pop         esi  
  22. 000455ED  pop         ebx  
  23. 000455EE  mov         esp,ebp  
  24. 000455F0  pop         ebp  
  25. 000455F1  ret  

进入函数前,作的动做主要是保存各寄存器,注意“sub esp,0xcch”就是移动ESP,空出局部变量的“位置”,为何只有一个局部变量,却生成了这么大块区域呢?

Stackoverflow上有解释:

This extra space is generated by the /Zi compile option. Which enables Edit + Continue. The extra space is available for local variables that you might add when you edit code while debugging.

You are also seeing the effect of /RTC, it initializes all local variables to 0xcccccccc so that it is easier to diagnose problems due to forgetting to initialize variables. Of course none of this code is generated in the default Release configuration settings.

从这段简单的代码中,咱们能够知道函数调用大概是什么回事了。经过上面的内容,咱们仔细体会下ESP和EBP两个寄存器的变化,也就下面向个指令

013D55C0  push        ebp      // 构建新的调用帧
013D55C1  mov         ebp, esp

013D55EE  mov         esp, ebp // 恢复到原来的调用帧
013D55F0  pop         ebp

再加上参数,返回地址,局部变量的入栈出栈,经过这样一种统一的、并不复杂代码生成模式和数据结构,能够应对任意复杂的函数调用状况,极其灵活。我一直以为这是计算机科学中很是漂亮的一个创造,也是以简驭繁的一个经曲例子。

相关文章
相关标签/搜索