序言
这个模块临近C语言的边界,学起来需要一定的时间,不过当我们知道这些知识,在C语言函数这块我们看到的不仅仅是表象了,可以真正了解函数是怎么调用的。不过我的能力有限,下面的的知识若是不当,还请各位斧正。
知识点储备
- 初步了解函数( 这里的所说的函数我们默认为自定义函数)
- 了解C程序地址空间
- 基本的寄存器
- 知道一些汇编语言
函数的概念
函数大家应该都很熟悉了,这里就不细说了。
我们看看就行
ret_type fun_name(para1, * )
{
statement; //语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
C程序地址空间(重点记忆)
我们一直说 :“全局变量的生命周期是所在的整个程序”、“static修饰的变量的生命周期变长了”、以及“最重要的临时变量出函数就要被销毁”。不过我们要知道这是因为什么。
在C语言中我们所创建的每一个变量都会有自己空间的的存储类别,就比如汽车一般不会停在高楼那样,每一个事物都会有自己的集合。
看一下代码,来验证一下
#include<stdio.h>
#include<stdlib.h>
int g_val1 = 10;
int g_val2 = 10;
int g_val3;
int g_val4;
int main()
{
const char* str = "abcdef";
printf("code: %p\n", main);
printf("read only : %p\n", str);
printf("init g_val1 : %p\n", &g_val1);
printf("init g_val2 : %p\n", &g_val2);
printf("uninit g_val2 : %p\n", &g_val3);
printf("uninit g_val2 : %p\n", &g_val4);
char* p1 = (char*)malloc(sizeof(char*) * 10);
char* p2 = (char*)malloc(sizeof(char*) * 10);
printf("heap addr : %p\n", p1);
printf("heap addr : %p\n", p2);
printf("stack addr : %p\n", &str);
printf("stack addr : %p\n", &p1);
printf("stack addr : %p\n", &p2);
return 0;
}
可以看出局部变量存储在栈上且栈空间是沿着向低地址方向开辟的
相关的 寄存器
函数的调用与CPU中的寄存器有很大关系,下面有一些基本知识
- eax:通用寄存器,保留临时数据,常用于返回值
- ebx:通用寄存器,保留临时数据
- ebp:栈底寄存器
- esp:栈顶寄存器
- eip:指令寄存器,保存当前指令的下一条指令的地址,衡量走到了那一步
相关的 汇编语言
- mov:数据转移指令
- push:数据入栈,同时esp栈顶寄存器也要发生改变
- pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
- sub:减法命令
- add:加法命令
- call:函数调用,1. 压入返回地址 2. 转入目标函数
- jump:通过修改eip,转入目标函数,进行调用
- ret:恢复返回地址,压入eip,类似pop eip命令
看了这么多知识,我们一定会感到很是枯燥,觉得这和函数栈帧一点关系都没有,不要着急,下面就开始我们正式的内容。
函数栈帧
这里为了便于理解,我们这么看栈的空间,我们就多画些图片
我们知道 main函数也是一个函数,它也是能够被调用,所以main函数也会形成栈帧。
样例代码
int MyAdd(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int x = 0xA;
int y = 0xB;
int z = 0;
z = MyAdd(a, b);
printf("z = %d\n",z);
return 0;
}
转到反汇编,打开寄存器
我将汇编代码复制下来,我们一步一步分析这些东西
int main()
{
int main()
{
int main()
{
00821E40 push ebp
00821E41 mov ebp,esp
00821E43 sub esp,0E4h
00821E49 push ebx
00821E4A push esi
00821E4B push edi
00821E4C lea edi,[ebp-24h]
00821E4F mov ecx,9
00821E54 mov eax,0CCCCCCCCh
00821E59 rep stos dword ptr es:[edi]
00821E5B mov ecx,82C003h
00821E60 call 0082130C
int x = 0xA;
00821E65 mov dword ptr [ebp-8],0Ah
int y = 0xB;
00821E6C mov dword ptr [ebp-14h],0Bh
int z = 0;
00821E73 mov dword ptr [ebp-20h],0
z = MyAdd(x, y);
00821E7A mov eax,dword ptr [ebp-14h]
00821E7D push eax
00821E7E mov ecx,dword ptr [ebp-8]
00821E81 push ecx
00821E82 call 008211E5
00821E87 add esp,8
00821E8A mov dword ptr [ebp-20h],eax
printf("z = %d\n", z);
00821E8D mov eax,dword ptr [ebp-20h]
00821E90 push eax
00821E91 push 827BCCh
00821E96 call 008213A2
00821E9B add esp,8
return 0;
00821E9E xor eax,eax
}
00821EA0 pop edi
00821EA1 pop esi
00821EA2 pop ebx
00821EA3 add esp,0E4h
00821EA9 cmp ebp,esp
00821EAB call 00821235
00821EB0 mov esp,ebp
00821EB2 pop ebp
00821EB3 ret
- ebp指向栈底
- esp指向栈顶
- eip指向下一个即将执行的地址 还未执行
第一步
int x = 0xA;
01011E65 mov dword ptr [ebp-8],0Ah
//在ebp-8处在开辟一个空间,将x的值放进去
int y = 0xB;
01011E6C mov dword ptr [ebp-14h],0Bh
//在ebp-14处在开辟一个空间,将y的值放进去
int z = 0;
00821E73 mov dword ptr [ebp-20h],0
//在ebp-20处在开辟一个空间,将z的值放进去
可以看出,x、y、z 的空间是不连续的 ,这是VS保护机制, 防止一些程序员猜测对应的地址。
第二步
00821E7A mov eax,dword ptr [ebp-14h]
把ebp-14(也就是y) 赋值给eax
eax是一个临时的寄存器,保留临时数据,常用于返回值
00821E7D push eax
push命令将eax的值放入栈中,同时栈顶的位置发生变化,变化的大小是4个字节,因为y是int型
push之后的栈顶
00821E7E mov ecx,dword ptr [ebp-8]
把ebp-8(也就是x) 赋值给ecx
00821E81 push ecx
和上面的一样,将ecx的值压入栈内,栈顶的位置发生变化
结论
- 临时变量的形成(实参的临时拷贝)在函数调前就完成了
- 形参实例化的顺序是从右向左依次形成的
- 形参的空间是紧邻的
调用函数
这里先说一下call命令的作用
- 压入返回地址 (最重要的)
- 转入目标函数
压入返回地址 ,压入谁?为什么要压入?
压入谁? 压入的是下一条命令的地址
为什么要压入?根本原因是函数调用完毕,可能就需要返回
00821E82 call 008211E5
jump命令 通过修改eip,转入目标函数,进行调用
jmp前
jmp后
现在我们总算进入了MyAdd()函数了,
画一下我们的栈帧图
由于篇幅有限,我们先说到这里,下一篇接着说MyAdd()函数内部的事情。