C/C++ 函数调用栈过程

1. 预备知识 程序内存的分配

一个C/C++程序占用的内存大约可以分为以下几个部分

  • 栈区(stack) 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区(heap) 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
  • 全局/静态区(static) 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量、未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
  • 文字常量区 常量字符串就是放在这里的。程序结束后由系统释放。

在函数体中定义的变量通常是在栈上,用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上。在所有函数体外定义的是全局量,加了static修饰符后不管在哪里都存放在全局区(静态区),在所有函数体外定义的static变量表示在该文件中有效,不能extern到别的文件用;在函数体内定义的static表示只在该函数体内有效。另外,函数中的”adgfdf”这样的字符串存放在常量区。

2.从一个经典的示例程序开始

直接给出这个程序

1
2
3
4
5
6
7
8
9
10
11
int func(int a, int b,int c)
{
int d;
d = 5;
return a + b + c + d;
}
int main(void)
{
int res = func(2, 3, 4);
return 0;
}

对于这个程序的主函数,其反汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int main(void)
{
013416D0 55 push ebp
013416D1 8B EC mov ebp,esp
013416D3 81 EC CC 00 00 00 sub esp,0CCh
013416D9 53 push ebx
013416DA 56 push esi
013416DB 57 push edi
013416DC 8D BD 34 FF FF FF lea edi,[ebp+FFFFFF34h]
013416E2 B9 33 00 00 00 mov ecx,33h
013416E7 B8 CC CC CC CC mov eax,0CCCCCCCCh
013416EC F3 AB rep stos dword ptr es:[edi]
int res = func(2, 3, 4);
013416EE 6A 04 push 4
int res = func(2, 3, 4);
013416F0 6A 03 push 3
013416F2 6A 02 push 2
013416F4 E8 4A FC FF FF call 01341343
013416F9 83 C4 0C add esp,0Ch
013416FC 89 45 F8 mov dword ptr [ebp-8],eax
return 0;
013416FF 33 C0 xor eax,eax
}
01341701 5F pop edi
01341702 5E pop esi
01341703 5B pop ebx
01341704 81 C4 CC 00 00 00 add esp,0CCh
0134170A 3B EC cmp ebp,esp
0134170C E8 F8 F9 FF FF call 01341109
01341711 8B E5 mov esp,ebp
01341713 5D pop ebp
01341714 C3 ret

从汇编代码可以看到这个程序的执行过程,18行的013416F4 E8 4A FC FF FF call 01341343便是调用func这个函数。
在上面代码中可以看到类似eax ebx ecx这种东西,不熟悉汇编的朋友可能会比较迷糊,其实这都是X86体系上CPU的通用寄存器的名称

  • EAX是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。
  • EBX 是”基地址”(base)寄存器, 在内存寻址时存放基地址。
  • ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
  • EDX 则总是被用来放整数除法产生的余数。
  • ESI/EDI分别叫做”源/目标索引寄存器”(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串。
  • EBP是”基址指针”(BASE POINTER), 它最经常被用作高级语言函数调用的”框架指针”(frame pointer).

3. 函数调用过程分析

3.1 函数参数的入栈

对于应用于intel系列处理器的程序,其函数调用栈地址是往低地址生长,所以越先压入栈的数据其地址越高
在函数调用的过程中有两个寄存器尤为重要 EBP和ESP
在刚开始调用函数的时候,先将其参数按照从右至左的顺序入栈,所以对于本程序而言,入栈顺序是4,3,2。

|            |
|------------|       ^高地址
|     4      |
|------------|
|     3      |
|------------|
|     2      |
|------------|<---ESP
|            |       ^低地址

3.2 函数的调用过程

接下来调用函数func,在调用函数的过程中,我们再次反汇编可以得到func的汇编代码如下
在call指令的时候相当于进行了如下两步

  • push 返回地址
  • jmp 函数入口地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int func(int a, int b,int c)
{
01341690 55 push ebp
01341691 8B EC mov ebp,esp
01341693 81 EC CC 00 00 00 sub esp,0CCh
01341699 53 push ebx
0134169A 56 push esi
0134169B 57 push edi
0134169C 8D BD 34 FF FF FF lea edi,[ebp+FFFFFF34h]
013416A2 B9 33 00 00 00 mov ecx,33h
013416A7 B8 CC CC CC CC mov eax,0CCCCCCCCh
013416AC F3 AB rep stos dword ptr es:[edi]
int d;
d = 5;
013416AE C7 45 F8 05 00 00 00 mov dword ptr [ebp-8],5
return a + b + c + d;
013416B5 8B 45 08 mov eax,dword ptr [ebp+8]
013416B8 03 45 0C add eax,dword ptr [ebp+0Ch]
013416BB 03 45 10 add eax,dword ptr [ebp+10h]
013416BE 03 45 F8 add eax,dword ptr [ebp-8]
}
013416C1 5F pop edi
013416C2 5E pop esi
013416C3 5B pop ebx
013416C4 8B E5 mov esp,ebp
013416C6 5D pop ebp
013416C7 C3 ret

在调用过程中压入返回地址与EBP,将ESP的值赋给EBP,然后ESP值减少,栈增大。

|            |
|------------|       ^高地址
|     4      |
|------------|
|     3      |<---EBP+12
|------------|
|     2      |<---EBP+8
|------------|
|  返回地址   |      
|------------|
|    EBP     |
|------------|<----EBP
|    EBX     |
|------------|       ^低地址
| 局部变量... |
               <----ESP

在进行运算的时候,由EBP作为基地址加偏移量可以得到变量,例如EBP+8可以得到2,EBP+12可以得到3,EBP-8可以得到局部变量。

在函数调用结束之后,便将使用的寄存器弹出

1
2
3
4
5
6
013416C1 5F pop edi
013416C2 5E pop esi
013416C3 5B pop ebx
013416C4 8B E5 mov esp,ebp
013416C6 5D pop ebp
013416C7 C3 ret

|            |                        |            |        ^高地址
|------------|                        |------------|
|     4      |                        |     4      |
|------------|                        |------------|
|     3      |               ====>    |     3      |  
|------------|                        |------------|
|     2      |                        |     2      |
|------------|                        |------------|
|  返回地址   |                        |  返回地址   |   
|------------|                        |------------|<----ESP
|    EBP     |                        |            |
|------------|<----EBP/ESP
|            |                                              ^低地址

最后使用ret指令将栈顶保存的地址弹入EIP,从而让程序返回到返回地址。

####3.3 值的返回
函数计算的返回值是存在EAX中,在主函数的汇编代码中,call调用之后,将EAX的值给了EBP-8的内存,这里便是将返回值付给了一个主函数的局部变量。

013416F4 E8 4A FC FF FF       call        01341343  
013416F9 83 C4 0C             add         esp,0Ch  
013416FC 89 45 F8             mov         dword ptr [ebp-8],eax  

至此一个完整的函数调用过程便结束勒。

4.总结

函数栈的调用,关键有如下几点:

  • 栈的增长由高地址往低地址的方向
  • ESP为栈帧的栈顶,用以增长栈
  • EBP为调用时栈帧的栈底地址,以偏移量得到变量值
  • 函数参数入栈从右至左
  • 返回值一般存在EAX中
  • call和ret两条指令实际上是调用和返回的过程