函数栈帧详解(1)

序言

这个模块临近C语言的边界,学起来需要一定的时间,不过当我们知道这些知识,在C语言函数这块我们看到的不仅仅是表象了,可以真正了解函数是怎么调用的。不过我的能力有限,下面的的知识若是不当,还请各位斧正。

知识点储备

  • 初步了解函数( 这里的所说的函数我们默认为自定义函数)
  • 了解C程序地址空间
  • 基本的寄存器
  • 知道一些汇编语言

函数的概念

函数大家应该都很熟悉了,这里就不细说了。
我们看看就行

ret_type fun_name(para1, * )
{
    statement;  //语句项
}

ret_type  返回类型
fun_name  函数名
para1     函数参数  

C程序地址空间(重点记忆)

我们一直说 :“全局变量的生命周期是所在的整个程序”、“static修饰的变量的生命周期变长了”、以及“最重要的临时变量出函数就要被销毁”。不过我们要知道这是因为什么。
在C语言中我们所创建的每一个变量都会有自己空间的的存储类别,就比如汽车一般不会停在高楼那样,每一个事物都会有自己的集合。

函数栈帧详解(1)_第1张图片

看一下代码,来验证一下

#include                                                                             
#include

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;
}

函数栈帧详解(1)_第2张图片
可以看出局部变量存储在栈上且栈空间是沿着向低地址方向开辟的

相关的 寄存器

函数的调用与CPU中的寄存器有很大关系,下面有一些基本知识

  • eax:通用寄存器,保留临时数据,常用于返回值
  • ebx:通用寄存器,保留临时数据
  • ebp:栈底寄存器
  • esp:栈顶寄存器
  • eip:指令寄存器,保存当前指令的下一条指令的地址,衡量走到了那一步

相关的 汇编语言

  • mov:数据转移指令
  • push:数据入栈,同时esp栈顶寄存器也要发生改变
  • pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
  • sub:减法命令
  • add:加法命令
  • call:函数调用,1. 压入返回地址 2. 转入目标函数
  • jump:通过修改eip,转入目标函数,进行调用
  • ret:恢复返回地址,压入eip,类似pop eip命令

看了这么多知识,我们一定会感到很是枯燥,觉得这和函数栈帧一点关系都没有,不要着急,下面就开始我们正式的内容。

函数栈帧

这里为了便于理解,我们这么看栈的空间,我们就多画些图片
函数栈帧详解(1)_第3张图片

我们知道 main函数也是一个函数,它也是能够被调用,所以main函数也会形成栈帧。
函数栈帧详解(1)_第4张图片

样例代码

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;
}

转到反汇编,打开寄存器
函数栈帧详解(1)_第5张图片

我将汇编代码复制下来,我们一步一步分析这些东西

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的值放进去

函数栈帧详解(1)_第6张图片

	int y = 0xB;
    01011E6C  mov      dword ptr [ebp-14h],0Bh  
                       //在ebp-14处在开辟一个空间,将y的值放进去

函数栈帧详解(1)_第7张图片

		int z = 0;
        00821E73  mov   dword ptr [ebp-20h],0  
                       //在ebp-20处在开辟一个空间,将z的值放进去 

函数栈帧详解(1)_第8张图片

可以看出,x、y、z 的空间是不连续的 ,这是VS保护机制, 防止一些程序员猜测对应的地址。

第二步

00821E7A  mov         eax,dword ptr [ebp-14h]  

把ebp-14(也就是y) 赋值给eax

eax是一个临时的寄存器,保留临时数据,常用于返回值
函数栈帧详解(1)_第9张图片

00821E7D  push        eax  

push命令将eax的值放入栈中,同时栈顶的位置发生变化,变化的大小是4个字节,因为y是int型
函数栈帧详解(1)_第10张图片
push之后的栈顶
函数栈帧详解(1)_第11张图片
函数栈帧详解(1)_第12张图片

00821E7E  mov         ecx,dword ptr [ebp-8]  

把ebp-8(也就是x) 赋值给ecx
函数栈帧详解(1)_第13张图片

00821E81  push        ecx  

和上面的一样,将ecx的值压入栈内,栈顶的位置发生变化
函数栈帧详解(1)_第14张图片

结论

  • 临时变量的形成(实参的临时拷贝)在函数调前就完成了
  • 形参实例化的顺序是从右向左依次形成的
  • 形参的空间是紧邻的

调用函数

这里先说一下call命令的作用

  • 压入返回地址 (最重要的)
  • 转入目标函数
    压入返回地址 ,压入谁?为什么要压入?
    压入谁? 压入的是下一条命令的地址
    为什么要压入?根本原因是函数调用完毕,可能就需要返回
00821E82  call        008211E5

函数栈帧详解(1)_第15张图片
函数栈帧详解(1)_第16张图片
jump命令 通过修改eip,转入目标函数,进行调用

jmp前函数栈帧详解(1)_第17张图片
jmp后
函数栈帧详解(1)_第18张图片
现在我们总算进入了MyAdd()函数了,
画一下我们的栈帧图
函数栈帧详解(1)_第19张图片

由于篇幅有限,我们先说到这里,下一篇接着说MyAdd()函数内部的事情。

你可能感兴趣的:(c语言)