浅谈函数的调用过程(系统帧栈的调用过程)

前言

栈帧的定义:“栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。”其实通俗易懂的说法
就是:栈帧就是存储在用户栈上的每一次函数调用涉及的相关信息,就是存放函数的一个临时的空间。它是一种系
统调用的栈。在栈结构中,每一个函数都有他自己的EBP(栈底寄存器)和ESP(栈顶寄存器),栈帧即为ebp和esp之
间所表示的空间。但是栈帧又是什么原理?它又是如何实现的呢?下面将一一叙述

在介绍具体调用过程之前先对相关的几个名词做一些解释

1.帧栈: 即栈底寄存器(ebp)到栈顶寄存器(esp)所表示的空间。
2.ebp:栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个帧栈的栈顶,即栈顶寄存器。
3.ebp:基址指针寄存器,即栈底寄存器。
4.call:用于保存当前指令的下一条指令并跳转到目标函数。
5.push:入栈
6.pop:出栈
5.ptr:相当于指针
6.mov:类似于辅助操作
7.add:加法操作
8.sub:减法操作
9.ecx:计数器(counter),是重复(REP)前缀指令和LOOP指令的内定计数器
10.eax:是”累加器“,他是很多加法乘法指令的缺省寄存器
11.esi/edi分别叫做”源/目标索引寄存器”
12.ret:使得出栈一次,并将出栈的内容当作地址,将程序执行跳转到该地址处。

举一个栗子来演示程序具体的调用过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>

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

int main() //main函数也是会被调用的,用它的函数是mainCRTStartup(),且在调用时开辟了空间
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("ret = %d\n", ret);
system("pause");
return 0;
}

在vs6.0环境下调试调用堆栈可以发现main函数在__tmainCRTStartup函数中调用,而__tmainCRTStartup 函数是在 mainCRTStartup 被调用的。
图片无法正确显示
我们知道每一次函数调用都是一个过程。这个过程我们通常称之为: 函数的调用过程。这个过程要为函数开辟栈空间, 用于本次函数的调用中临时变量的保存、 现场保护。 这块栈空间我们称之为函数栈帧。
而栈帧的维护我们必须了解ebp和esp两个寄存器。 在函数调用的过程中这两个寄存器存放了维护这个栈的栈底和栈顶指针。比如:调用main函数, 我们为main函数分配栈帧空间, 那么栈帧维护如下:
图片无法正常显示

观察调用过程我们需要用到反汇编代码,如下:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
1:    // 42.cpp : Defines the entry point for the console application.
2: //
3:
4: #include "stdafx.h"
5:
6:
7: //#include<stdio.h>
8:
9: int Add(int x, int y)
10: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,44h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
11: int z = 0;
00401038 mov dword ptr [ebp-4],0
12: z = x + y;
0040103F mov eax,dword ptr [ebp+8]
00401042 add eax,dword ptr [ebp+0Ch]
00401045 mov dword ptr [ebp-4],eax
13: return z;
00401048 mov eax,dword ptr [ebp-4]
14: }
0040104B pop edi
0040104C pop esi
0040104D pop ebx
0040104E mov esp,ebp
00401050 pop ebp
00401051 ret

15:
16: int main()
17: {
00401060 push ebp
00401061 mov ebp,esp
00401063 sub esp,4Ch
00401066 push ebx
00401067 push esi
00401068 push edi
00401069 lea edi,[ebp-4Ch]
0040106C mov ecx,13h
00401071 mov eax,0CCCCCCCCh
00401076 rep stos dword ptr [edi]
18: int a = 10;
00401078 mov dword ptr [ebp-4],0Ah
19: int b = 20;
0040107F mov dword ptr [ebp-8],14h
20: int ret = Add(a, b);
00401086 mov eax,dword ptr [ebp-8]
00401089 push eax
0040108A mov ecx,dword ptr [ebp-4]
0040108D push ecx
0040108E call @ILT+0(Add) (00401005) //call命令的作用:1.保存当前正在运行时指令的下一条指令的地址到栈结构中 2.跳转到指定的函数入口(jmp)
00401093 add esp,8
00401096 mov dword ptr [ebp-0Ch],eax
21: printf("ret = %d\n", ret);
00401099 mov edx,dword ptr [ebp-0Ch]
0040109C push edx
0040109D push offset string "ret = %d\n" (0042201c)
004010A2 call printf (004010e0)
004010A7 add esp,8
22:
23: return 0;
004010AA xor eax,eax
24: }
004010AC pop edi
004010AD pop esi
004010AE pop ebx
004010AF add esp,4Ch
004010B2 cmp ebp,esp
004010B4 call __chkesp (00401160)
004010B9 mov esp,ebp
004010BB pop ebp
004010BC ret

具体分析调试过程

1.从main函数出发 (以下均为调试过程中转入反汇编的代码)

图片无法正常显示
过程:
1.首先是main函数调用前的准备工作(4、5、6行)
2.将ebp压栈处理
3.将esp的值赋给ebp,产生新的ebp;
4.给esp减去0E4H(为main函数预开辟空间);
5.push ebx、esi、edi;
6.lea指令,加载有效地址;
7.初始化预开辟的空间为0xcccccccc;
8.创建变量a与b。

执行call指令,跳转入函数Add
图片无法正常显示

2.Add函数的调用过程

图片无法正常显示
过程:
1.首先将main()函数ebp压栈处理,保存指向main()函数栈帧底部的ebp的地址(方便函数返回之后的现场恢复),此时esp指向新的栈顶位置;
2.将esp的值赋给ebp,产生新的ebp,即Add()函数栈帧的ebp;
3.给esp减去0E4H(为Add()函数预开辟空间);
4.push ebx、esi、edi;
5.lea指令,加载有效地址;
6.初始化预开辟的空间为0xcccccccc;
7.创建变量z;
8.获取形参的a和b再相加,将结果存储到z中;
9.将结果存储到eax寄存器,通过寄存器带回函数的返回值。
剩下的就是是函数返回部分:
图片无法正常显示
1.pop3次,edi、esi、ebx依次出栈,esp向下移动
2.将ebp赋给esp,使esp指向ebp指向的地方
3.ebp 出栈,将出栈的内容给ebp(即main()函数ebp),回到main()函数的栈帧;
4.ret 指令,出栈一次,并将出栈的内容当做地址,并跳转到该地址处(pop+jmp)
返回主函数:
图片无法正常显示
这便是一个函数的完整调用过程,下图为整个调用过程图
图片无法正常显示