0
点赞
收藏
分享

微信扫一扫

C语言函数栈帧

序言

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

知识点储备

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

函数的概念

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

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

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

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

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

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

C语言函数栈帧_入栈_02

可以看出局部变量存储在栈上且栈空间是沿着向低地址方向开辟的

相关的 寄存器

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

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

相关的 汇编语言

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

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

函数栈帧

这里为了便于理解,我们这么看栈的空间,我们就多画些图片

C语言函数栈帧_入栈_03

我们知道 main函数也是一个函数,它也是能够被调用,所以main函数也会形成栈帧。

C语言函数栈帧_数据_04

样例代码

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

转到反汇编,打开寄存器

C语言函数栈帧_数据_05

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

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

C语言函数栈帧_数据_06

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

C语言函数栈帧_数据_07

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

C语言函数栈帧_寄存器_08

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

第二步

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

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

eax是一个临时的寄存器,保留临时数据,常用于返回值

C语言函数栈帧_寄存器_09

00821E7D  push        eax

push命令将eax的值放入栈中,同时栈顶的位置发生变化,变化的大小是4个字节,因为y是int型

C语言函数栈帧_入栈_10

push之后的栈顶

C语言函数栈帧_入栈_11

C语言函数栈帧_数据_12

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

把ebp-8(也就是x) 赋值给ecx

C语言函数栈帧_寄存器_13

00821E81  push        ecx

和上面的一样,将ecx的值压入栈内,栈顶的位置发生变化

C语言函数栈帧_入栈_14

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

调用函数

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

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

00821E82  call        008211E5

C语言函数栈帧_数据_15

C语言函数栈帧_寄存器_16

jump命令 通过修改eip,转入目标函数,进行调用

jmp前

C语言函数栈帧_入栈_17

jmp后

C语言函数栈帧_寄存器_18

现在我们总算进入了MyAdd()函数了,画一下我们的栈帧图

C语言函数栈帧_数据_19

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

样例代码

int MyAdd(int a, int b)
{
int c = 0
c = a + b;
return c;
}

int main()
{
int x = 0xA;
int y = 0xB;
int z = 0;

z = MyAdd(x, y);
printf("z = %d\n", z);
return 0;
}

今天的汇编语言

int MyAdd(int a, int b)
{
001E2EC0 push ebp
001E2EC1 mov ebp,esp
001E2EC3 sub esp,0CCh
001E2EC9 push ebx
001E2ECA push esi
001E2ECB push edi
001E2ECC lea edi,[ebp-0Ch]
001E2ECF mov ecx,3
001E2ED4 mov eax,0CCCCCCCCh
001E2ED9 rep stos dword ptr es:[edi]
001E2EDB mov ecx,1EC003h
001E2EE0 call 001E130C
int c = 0;
001E2EE5 mov dword ptr [ebp-8],0
c =a + b;
001E2EEC mov eax,dword ptr [ebp+8]
001E2EEF add eax,dword ptr [ebp+0Ch]
001E2EF2 mov dword ptr [ebp-8],eax
return c;
001E2EF5 mov eax,dword ptr [ebp-8]
}
001E2EF8 pop edi
001E2EF9 pop esi
001E2EFA pop ebx
001E2EFB add esp,0CCh
001E2F01 cmp ebp,esp
001E2F03 call 001E1235
001E2F08 mov esp,ebp
001E2F0A pop ebp
001E2F0B ret

我们的栈帧图

C语言函数栈帧_入栈_20

MyAdd函数栈帧的形成

第一步

00821740  push        ebp

这条命令是将ebp(也就是栈底)的内容压入栈中,同时栈顶也发生变化

C语言函数栈帧_入栈_21

C语言函数栈帧_寄存器_22

第二步

mov:数据转移指令

00821741  mov         ebp,esp

该命令的意思是将esp的内容覆盖到ebp中

  • esp的内容直接将ebp的内容给覆盖
  • 该过程没有通过内存,直接通过CPU
  • C语言函数栈帧_寄存器_23

C语言函数栈帧_数据_24

那么我们可能会发出疑惑,那栈底怎么办,是不是找不回来了?实际上不是的,上一步我们不是把栈底的内容给保存了吗!!!

第三步

sub:减法命令

00821743  sub         esp,0CCh
//0CCh 的大小和你定义的函数的规模有关

该命令的意思是esp减去一定的值,结果放在esp中

C语言函数栈帧_数据_25

到这里我们已近形成了MyAdd()的栈帧了

C语言函数栈帧_入栈_26

第四步

int c = 0;
001E2EE5 mov dword ptr [ebp-8],0
//在ebp-8处在开辟一个空间,将c的值放进去

这和main的变量开辟一样

C语言函数栈帧_寄存器_27

第五步

c =a + b;
001E2EEC mov eax,dword ptr [ebp+8]
001E2EEF add eax,dword ptr [ebp+0Ch]
001E2EF2 mov dword ptr [ebp-8],eax

一条一条分析

001E2EEC  mov         eax,dword ptr [ebp+8]

把ebp+8放在eax中那么ebp+8是多少呢?答案就是我们的x值的拷贝

C语言函数栈帧_寄存器_28

C语言函数栈帧_入栈_29

同理

001E2EEF  add         eax,dword ptr [ebp+0Ch]

这个命令是将ebp+0Ch的内容和eax加起来放到 eax中ebp+0Ch就是y值的拷贝

C语言函数栈帧_数据_30

C语言函数栈帧_数据_31

001E2EF2  mov         dword ptr [ebp-8],eax

这条命令是将eax写入到ebp-8,也就是c中

C语言函数栈帧_数据_32

C语言函数栈帧_入栈_33

准备返回

第一步

001E2EF5  mov         eax,dword ptr [ebp-8]

保存返回值

C语言函数栈帧_数据_34

第二步

001E2F08  mov         esp,ebp

把ebp 覆盖到 esp这一步也可以称为“释放栈帧”

C语言函数栈帧_入栈_35

C语言函数栈帧_寄存器_36

第三步

pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变

001E2F0A  pop         ebp

“弹栈”将main函数的栈底放在ebp中,esp内容改变

C语言函数栈帧_入栈_37

第四步

ret:恢复返回地址,压入eip,类似pop eip命令

001E2F0B  ret

将之前call的下一个地址放在eip,esp内容改变

C语言函数栈帧_数据_38

第五步

释放临时拷贝的变量

001E1E87  add         esp,8

意思是esp+8放在esp中

C语言函数栈帧_数据_39

现在我们已经返回到MyAdd执行之前了

第六步

001E1E8A  mov         dword ptr [ebp-20h],eax

接收返回值将eax的值放到ebp-20(也就是z)

返回的本质

  • 返回到main的栈帧
  • 返回到对应的代码处

总结

  • 函数的的栈帧是由编译器决定的
  • push进去的变量的空间是连续的
  • 0CCh 的大小和你定义的函数的规模有关

为什么函数的的栈帧是由编译器决定的?我们C语言中很多数据类型,这就是的编译器有能力知道所有类型变量的大小。

有关栈帧的知识大概都说的差不多了,后面我会看看是否还要补充一些知识。

举报

相关推荐

0 条评论