C语言函数调用过程

*** 本文是《老码识途》第一章的读书笔记 ***

函数调用

例子代码如下所示:

int Add(int x, int y) {
    int sum;
    sum = x + y;
    return sum;
}

void main() {
    int z;
    z = Add(1, 2);
    printf("z=%d\n", z);
}

下面分析一下 Add函数的调用过程。

首先断点在z = Add(1, 2);处, 反汇编如下所示:

    int z;
    z = Add(1, 2);
002C141E 6A 02                push        2  
002C1420 6A 01                push        1  
002C1422 E8 60 FC FF FF       call        002C1087  
002C1427 83 C4 08             add         esp,8  
002C142A 89 45 F8             mov         dword ptr [ebp-8],eax

首先压入参数1和2:

002C141E 6A 02                push        2  
002C1420 6A 01                push        1  

通过观察ESP可以看到参数从右到左依次入栈,ESP往低内存方向移动8字节:

ESP=0025FCCC
...
0x0025FCAA  00 00 78 4c 33 00 bc fc 25 00 a9 fe aa 0f 78 4c 33 00 c8 fc 25 00 3d 5a b2 0f *** 01 00 00 00 02 00 00 00 ***
0x0025FCCC  00 00 00 00

然后执行:

002C1422 E8 60 FC FF FF       call        002C1087  

call指令执行时,首先压入call指令的返回地址,即add esp,8的地址002C1427:

0x0025FCAA  00 00 78 4c 33 00 bc fc 25 00 a9 fe aa 0f 78 4c 33 00 c8 fc 25 00 *** 27 14 2c 00 *** 01 00 00 00 02 00 00 00

然后跳转到02C1087。02C1087处为jmp语句,跳转到Add函数入口地址002C13C0:

int Add(int x, int y) {
002C13C0 55                   push        ebp  
002C13C1 8B EC                mov         ebp,esp  
002C13C3 81 EC CC 00 00 00    sub         esp,0CCh  
002C13C9 53                   push        ebx  
002C13CA 56                   push        esi  
002C13CB 57                   push        edi  
002C13CC 8D BD 34 FF FF FF    lea         edi,[ebp+FFFFFF34h]  
002C13D2 B9 33 00 00 00       mov         ecx,33h  
002C13D7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
002C13DC F3 AB                rep stos    dword ptr es:[edi]  
    int sum;
    sum = x + y;
002C13DE 8B 45 08             mov         eax,dword ptr [ebp+8]  
002C13E1 03 45 0C             add         eax,dword ptr [ebp+0Ch]  
002C13E4 89 45 F8             mov         dword ptr [ebp-8],eax  
    return sum;
002C13E7 8B 45 F8             mov         eax,dword ptr [ebp-8]  
}
002C13EA 5F                   pop         edi  
002C13EB 5E                   pop         esi  
002C13EC 5B                   pop         ebx  
002C13ED 8B E5                mov         esp,ebp  
002C13EF 5D                   pop         ebp  
002C13F0 C3                   ret  

获取参数

目前为止,栈上的情况如下图所示,从上往下内存地址从高到低:

          +----------------+
          |       2        |
          +----------------+
          |       1        |
          +----------------+
ESP       | return address |
 +------> +----------------+

此时参数可由ESP + 4,ESP + 8获得。但是由于程序执行时ESP会变化,为了方便定位栈上的数据,引入EBP(Extended Base Pointer,扩展基址指针寄存器),保存进入函数时ESP的值(可以说,ESP是堆栈指针,EBP是基址指针(段指针))。
由于函数可以嵌套调用,所以在进入函数时必须将EBP的旧值保存起来,以防覆盖EBP导致函数返回后无法恢复EBP。这里通过将EBP压入栈来保存旧值。如:

002C13C0 55                   push        ebp  
002C13C1 8B EC                mov         ebp,esp  
...
002C13EF 5D                   pop         ebp  

所以在函数开头有如下代码:

int Add(int x  , int y) {
002C13C0 55                   push        ebp  
002C13C1 8B EC                mov         ebp,esp

此时栈上的内存布局如下图所示:

          +----------------+
          |       2        |
          +----------------+
          |       1        |
          +----------------+
          | return address |
          +----------------+
ESP       |     ebp        |
 +------> +----------------+

取出1,2参数的代码如下所示:

    int sum;
    sum = x + y;
002C13DE 8B 45 08             mov         eax,dword ptr [ebp+8]  
002C13E1 03 45 0C             add         eax,dword ptr [ebp+0Ch]

其中ebp+8取出参数1,ebp+0Ch取出参数2(0C为十进制的12),然后计算结果放在EAX中。

初始化堆栈和分配局部变量

接下来有如下代码段,将esp下移0CC,然后push ebx,esi,edi这三个寄存器:

002C13C3 81 EC CC 00 00 00    sub         esp,0CCh  // 1
002C13C9 53                   push        ebx  
002C13CA 56                   push        esi  
002C13CB 57                   push        edi

其中语句1是为了给局部变量分配足够大的栈空间,然后再保存三个寄存器的值。局部变量利用ebp定位,存于ebp和OCCh之间。

ebx,esi,edi的作用如下所示来源

寄存器%ebx、%esi和%edi为被调函数保存寄存器(callee-saved registers),即被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。

然后运行:

002C13CC 8D BD 34 FF FF FF    lea         edi,[ebp+FFFFFF34h]  
002C13D2 B9 33 00 00 00       mov         ecx,33h  
002C13D7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
002C13DC F3 AB                rep stos    dword ptr es:[edi]  

首先,通过lea指令,将ebp+FFFFFF34h地址(栈帧的底部)写入edi。然后设置ecx和eax,最后运行rep stos语句。

rep stos dword ptr es:[edi]语句的意思是:将栈上从ebp+FFFFFF34h开始的位置向高地址方向的内存赋值eax(0xCCCCCCCC),次数重复ecx(0x33, 51)次。每运行一次edi的值会增加。注意0xCCCCCCCC代表着未被初始化(int3中断)。这样做的原因是防止分配好的局部变量空间中的代码被意外执行。

局部变量

示例代码中,x+y的结果保存在局部变量sum中,由如下代码可知,sum分配在栈上。

    int sum;
    sum = x + y;
002C13DE 8B 45 08             mov         eax,dword ptr [ebp+8]  
002C13E1 03 45 0C             add         eax,dword ptr [ebp+0Ch]
002C13E4 89 45 F8             mov         dword ptr [ebp-8],eax   <<==== 结果保存在局部变量sum中

目前栈的分配情况如下所示:

          +----------------+                
          |       2        |                
          +----------------+                
          |       1        |                
          +----------------+                
          | return address |                
          +----------------+                
          |      ebp       |                
          +----------------+                
          |       ?        |                
          +----------------+               
ESP       |      sum       |                
 +------> +----------------+                

“?”处的4字节是编译器为了防止溢出攻击而设置的。

返回值

函数返回处的代码如下:

002C13EA 5F                   pop         edi  
002C13EB 5E                   pop         esi  
002C13EC 5B                   pop         ebx  
002C13ED 8B E5                mov         esp,ebp  
002C13EF 5D                   pop         ebp  
002C13F0 C3                   ret  

函数返回时需要考虑两件事情:恢复栈和保存返回值。

恢复栈

首先,通过pop栈恢复edi,esi和ebx的值,然后将esp恢复到ebp处,然后pop ebp,将ebp恢复旧值。此时esp指向return address:

          +----------------+                
          |       2        |                
          +----------------+                
          |       1        |                
          +----------------+                
 ESP      | return address |                
  +------>+----------------+                
          |      ebp       |                
          +----------------+                
          |       ?        |                
          +----------------+               
          |      sum       |                
          +----------------+  

接下来运行ret指令。ret指令会将栈顶保存的地址压入指令寄存器EIP,相当于pop eip。运行后EIP和ESP都会有变化。

然后程序跳转到return address处,如下所示:

    int z;
    z = Add(1, 2);
002C141E 6A 02                push        2  
002C1420 6A 01                push        1  
002C1422 E8 60 FC FF FF       call        002C1087  
002C1427 83 C4 08             add         esp,8   // ret跳转到此处
002C142A 89 45 F8             mov         dword ptr [ebp-8],eax

其中add esp,8语句的目的是将1,2参数出栈,将栈恢复到函数调用之前的状态。接下来便可以从eax中取出返回值:

002C142A 89 45 F8             mov         dword ptr [ebp-8],eax

由于ebp已被恢复,故其中ebp-8即为临时变量z的地址

发表评论

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据