C语言代码函数栈帧的创建与销毁(修炼内功)

简介: 目录在前期的学习中我们可能有很多困惑例如:局部变量是怎么创建的为什么局部变量的值是随机值函数是怎么样传参的 传参的顺序是什么形参和实参的关系是什么函数调用是怎么做的函数掉调用结束后怎么返回的这篇博客我们来修炼自己的内功,掌握好这篇博客的大部分知识就已经很不错了我们用到VS2013这个编译器,目的是为了看到更详细的函数封装内容 提示不要使用太过高级的编译器,因为越高级的编译器越不容易观察。同时这里需要注意的是在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,不是完全相同的,具体细节取决于编译器

一、基础知识

1.1 什么是栈区?

C/C++程序内存分配的几个区域:

//1. 栈区(stack):

在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

//2. 堆区(heap):

一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。

//3. 数据段(静态区)(static):

存放全局变量、静态数据。程序结束后由系统释放。

//4. 代码段:

存放函数体(类成员函数和全局函数)的二进制代码

接下来,补充一下栈的知识,了解到这就可以了,足够使用了

栈区的使用是从高地址到低地址

栈区的使用遵循先进后出,后进先出

栈区的放置是从高地址往低地址放置:push 是压栈

删除是从低往高删除:pop 是出栈


1.2 寄存器

这里简单介绍一些寄存器,其它的先不要过多理解

常见寄存器有eax、ebx、ecx、edx,这四个都当做通用寄存器,保留临时数据,ebp和esp较为特殊

**eax “累加器” 它是很多加法乘法指令的缺省寄存器。

ebx "基地址"寄存器, 在内存寻址时存放基地址。

ecx 计数器,是重复(REP)前缀指令和LOOP指令的内定计数器。

edx 总是被用来放整数除法产生的余数。

esi 源索引寄存器

edi

目标索引寄存器

ebp (栈底指针)“基址指针”,存放的是地址,用来维护函数栈帧

esp (栈顶指针)专门用作堆栈指针,存放的是地址,用来维护函数栈帧

使用的测试代码


#include<stdio.h>
int Add(int x, int y)
{
  int z = 0;
  z = x + y;
  return z;
}
int main()
{
  int a = 10;
  int b = 20;
  int c = 0;
  c = Add(a, b);
  printf("%d\n", c);
  return 0;
}

接下来还有一些汇编代码的含义:

mov:mov是数据传送指令(move的缩写),用于将一个数据从源地址传送到目标地址

sub:减法,subtraction的缩写

lea:Load effective address的缩写,取有效地址

call:用于调用其他函数

add:加法

pop:出栈

push:入栈或压栈

二、函数栈帧的创建和销毁的过程

_tmainCRTStartup函数(调用main函数)栈帧的创建

先了解 main 函数是被谁调用的,按 F10 或 F11 进入调试模式,F10是逐过程,F11是逐语句,打开堆栈

f9c87bcf4a6d41d3a595b7bae0b75811.png

这时我们按住F10调试起来,右击鼠标转到反汇编就会呈现下图样式

d04bdddf5d714aac816e5b8e9237c863.png


_tmainCRTStartup函数(调用main函数)栈帧的创建

按F10,按到return 0 时再按一次,调用栈堆会出现以下内容

这时再看堆栈窗口发现 main 函数被 __tmainCRTStartup() 调用

42193f524ef442dd8a71ccbd13e00438.png


main函数栈帧的创建

push        ebp  //在栈顶开辟ebp寄存器对应的空间
 mov         ebp,esp  //将esp的值传入ebp中(即将ebp指针移动到原本esp指向的位置)
 sub         esp,0E4h  //将esp的内容减去0E4h(将esp移动到原esp-0E4h的位置)
 push        ebx  //在栈顶放入ebx
 push        esi  //在栈顶放入esi
 push        edi  //在栈顶放入edi

此时进入main函数(也就是程序调试开始),首先要 push ebp 进行压栈,ebp 在 __tmainCRTStartup() 上面压栈

观察esp ebp 地址的变化,在调试的监视里面查看,push ebp 之后,esp 指向的位置也随之改变 (地址减小)

看下图

5761f204d1f041fd9d2c94549638a0c5.png

先看最开始的几个步骤

push:先在栈顶压一个ebp

move:把esp的值给了ebp(将地址传给ebp)

sub:给esp减去一个0E4h(八进制位数)这个值,减去一个0E4h的值后值变小了(地址变小了),那么此时ebp就指向了栈顶低地址

lea:load effective address(加载有效地址),ebp-0E4h就是再main函数的栈顶,为什么呢,这是因为我们前几个步骤在算的时候,esp在减去了一个0E4h之后就已经到达了栈顶,前面又把esp的值(地址)赋值给了ebp,所以理所当然的到达了栈顶

9fac77c2b9024f70b12b915b7b603a0a.png

再将ebx esi edi分别压栈压在栈顶

大家可以边调式边看我给大家写的步骤,我如果一边写一边画图会很不方便,大家可以先跟着我的步骤先自己尝试着画一下,我在博客的最后给大家画了完整的图供大家参考

再看下面几个步骤,这里大家再最后几个步骤理解起来有点困难,我这里用通俗易懂的语句给大家解释一下,这里的eax存放的是0CCCCCCCCh这个值,从edi开始向下的ecx里面放的值全部放成CCCCCCCC这个值,这里再强调一遍word–2个字节,dword–4个字节,希望大家能够理解!!!

最上面是低地址,最下面是高地址

859c7a6d077142218b1687511ac77adb.png

这就是为什么我们再VS编译器上面经常打印处烫烫烫烫,就是因为最开始没有在main函数里面定义变量的时候里面放的都是随机值,当然在不同的编译器上面效果不同

ecfeb54337d24bd0bc4cc244dc310bec.png


main函数内执行有效代码

mov         ecx,0ECC003h  
call        00EC131B  
  int a = 10;
mov         dword ptr [ebp-8],0Ah  
  int b = 20;
mov         dword ptr [ebp-14h],14h  
  int c = 0;
mov         dword ptr [ebp-20h],0  

mov这个意思我们上面已经讲到了,比如a,将14h(十六进制数也就是20,自己可以计算一下,116加上416的0次方),将20这个数字放到ebp-8这个地址里面,dword–4个字节,见下面我画的图大家就能理解

mov eax,dword ptr [ebp-14h] ,把 ebp-14h 的值0000 0014(十进制是20)放到 eax 里去

push eax ,压栈 eax(20),esp指向的位置也随之改变 (地址减小)

mov ecx,dword ptr [ebp-8] ,把 ebp-8 的值0000000a(十进制是10) 放到 ecx 里去

push ecx ,压栈 ecx(10),esp指向的位置也随之改变 (地址减小)

步骤演示图

1ad4ba549dee444b965e5042ba6d6efe.png


接下来为call 指令,按下F11,此时就正式进入Add函数内部 并为其开辟栈帧


Add函数栈帧的创建

按 F11,进入到 Add 函数 ,该add 函数地址不一定与main 函数地址相连,但是add 函数的地址一定在main 函数地址上面


 call        00EC131B  

call 指令调用 Add 函数,这里逐语句(F11)执行,发现这里竟然存储着下一条指令的地址,事实上 call 指令把下一条指令的地址压栈了(为了 Add 函数结束后能找回来),esp 地址也跟着变化

de47d65e9f3c4ae0aba0663600e06080.png

280d399bd9b34a13891ea0437460e6a6.png


进入 Add 函数前,会先为 Add 函数开辟函数栈帧,这这些操作跟先前main函数开辟函数栈帧操作一样,所以这里就不细谈了


push    ebp//将ebp上移
mov     ebp,esp//将esp内容放入ebp(移动ebp)
sub     esp,0CCh//esp-0CCh(为Add开辟空间)
push    ebx//在栈顶放入ebx
push    esi//在栈顶放入esi
push    edi//在栈顶放入edi
lea      edi,[ebp-0Ch]//ebp-0Ch的空间  
mov      ecx,3//3存入ecx  
mov      eax,0CCCCCCCCh//存入eax  
rep stos  dword ptr es:[edi]//esp往下0ch的空间进行赋值

首先,push ebp把ebp压栈到栈顶,再mov把esp赋给ebp,再sub,把esp-去0CCh,此步骤就是在为Add函数开辟空间,接着进行三次push,同main函数那样,同理,依旧是赋值为CCCCCCCC,详细过程不再赘述,跟上文main函数一样,如图所示:

55f0f2f9add34b139889cf6ab6508c41.png

84f9aff37972457ea41f16cfbeb6a37d.png

c2aa58a0703e4cb5829741180649efb7.png


Add函数内执行有效代码

 mov         ecx,0ECC003h  
 call        00EC131B  
  int z = 0;
 mov         dword ptr [ebp-8],0  //把 0 初始化到 ebp-8 的位置

2e446bca6c764ac3b4cfbb69dedbeb99.png


  z = x + y;
 mov         eax,dword ptr [ebp+8]  //把 ebp+8 的值 10 放到 eax 里
 add         eax,dword ptr [ebp+0Ch]  //把 ebp+0ch 的值 20 和 eax 的值 10 相加
 mov         dword ptr [ebp-8],eax  //把 eax 的值 30 放到 ebp-8(z) 里去
  return z;
 mov         eax,dword ptr [ebp-8]  //把 ebp-8 的值 30 放到 eax 里去

8042acb06a9c41d09ca5ea80c4f496f6.png


Add函数栈帧的销毁

mov eax,dword ptr [ebp-8] ,把ebp-8的值(30)放到eax里头去

pop edi ,出栈,释放为edi创建的栈区,地址开始增大

pop esi ,出栈,释放为esi创建的栈区,地址继续增大

pop ebx ,出栈,释放为exb创建的栈区,地址继续增大

执行一次pop,esp就会往高地址处移动一次

此时我的Add函数内部的CCCCCCCC这一些随机值就没有用了

mov将ebp的值赋值给esp,esp就指向高地址处了,ebp前面的Add函数的函数栈帧就销毁了,希望大家能够理解

然后我们再pop(出栈)一下,ebp就走了,就回到了main函数的低地址,此时我们的esp就来代替edp的位置,希望大家能够理解

a7121173366946c8a1548ac38b74c87c.png

最后我们看ret,我们此时回到了低地址处00C21450,这里面的逻辑其实是非常严谨的,我既要走出去也要走回来,我们接着从call指令处开始执行

然后执行add+8就是往高地址处走两步,dword是8个字节,地址往后+2个,这样就把我们形参中a,b给销毁了

e27b7212ed2141c293533b990584b185.png

248af4e9be9e43f09ef7a63283347893.png

mov esp,ebp ,ebp的值赋给esp,此时esp和ebp依旧相同

pop ebp ,弹出ebp,并将ebp所指向的main函数的起始地址赋值给了ebp指针,esp指针向高位移动,esp和ebp重新开始维护main函数的栈区空间

ret ,返回到main函数,在执行 ret 指令时,esp指针就指向了栈顶存放的call指令的下一条指令的地址

此时Add函数的栈帧算是真正销毁

939b3c8a74c54344b37360edbf5c1187.png

main函数代码继续执行

285c2747df6e4f69b8661a6cfa3228fd.png

mov dword ptr [ebp-20h],eax ,把eax的值放到ebp-20h上,而eax就是我们出Add函数时计算的和

接下来就是打印值和 main函数函数栈帧销毁,都与上面类似


所需反汇编代码总览

Add 函数


int Add(int x, int y)
{
 push        ebp  
 mov         ebp,esp  
 sub         esp,0CCh  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp-0Ch]  
 mov         ecx,3  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 mov         ecx,0ECC003h  
 call        00EC131B  
  int z = 0;
 mov         dword ptr [ebp-8],0  
  z = x + y;
 mov         eax,dword ptr [ebp+8]  
 add         eax,dword ptr [ebp+0Ch]  
 mov         dword ptr [ebp-8],eax  
  return z;
 mov         eax,dword ptr [ebp-8]  
}
 pop         edi  
 pop         esi  
 pop         ebx  
 add         esp,0CCh  
 cmp         ebp,esp  
 call        00EC1244  
 mov         esp,ebp  
 pop         ebp  
 ret  

main函数


int main()
{
 push        ebp  
 mov         ebp,esp  
 sub         esp,0E4h  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp-24h]  
 mov         ecx,9  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 mov         ecx,0ECC003h  
 call        00EC131B  
  int a = 10;
 mov         dword ptr [ebp-8],0Ah  
  int b = 20;
 mov         dword ptr [ebp-14h],14h  
  int c = 0;
 mov         dword ptr [ebp-20h],0  
  c = Add(a, b);
 mov         eax,dword ptr [ebp-14h]  
 push        eax  
 mov         ecx,dword ptr [ebp-8]  
 push        ecx  
 call        00EC10B4  
 add         esp,8  
 mov         dword ptr [ebp-20h],eax  
  printf("%d\n", c);
 mov         eax,dword ptr [ebp-20h]  
 push        eax  
 push        0EC7B30h  
 call        00EC10D2  
 add         esp,8  
  return 0;
 xor         eax,eax  
}
 pop         edi  
 pop         esi  
 pop         ebx  
 add         esp,0E4h  
 cmp         ebp,esp  
 call        00EC1244  
 mov         esp,ebp  
 pop         ebp  
 ret  

这次我们明显感觉难度大了起来,这正是我们修炼内功,变强的过程


相关文章
|
3月前
|
NoSQL 编译器 程序员
【C语言】揭秘GCC:从平凡到卓越的编译艺术,一场代码与效率的激情碰撞,探索那些不为人知的秘密武器,让你的程序瞬间提速百倍!
【8月更文挑战第20天】GCC,GNU Compiler Collection,是GNU项目中的开源编译器集合,支持C、C++等多种语言。作为C语言程序员的重要工具,GCC具备跨平台性、高度可配置性及丰富的优化选项等特点。通过简单示例,如编译“Hello, GCC!”程序 (`gcc -o hello hello.c`),展示了GCC的基础用法及不同优化级别(`-O0`, `-O1`, `-O3`)对性能的影响。GCC还支持生成调试信息(`-g`),便于使用GDB等工具进行调试。尽管有如Microsoft Visual C++、Clang等竞品,GCC仍因其灵活性和强大的功能被广泛采用。
122 1
|
3月前
|
存储 C语言
【C语言】基础刷题训练4(含全面分析和代码改进示例)
【C语言】基础刷题训练4(含全面分析和代码改进示例)
|
1月前
|
存储 搜索推荐 C语言
深入C语言指针,使代码更加灵活(二)
深入C语言指针,使代码更加灵活(二)
|
1月前
|
存储 程序员 编译器
深入C语言指针,使代码更加灵活(一)
深入C语言指针,使代码更加灵活(一)
|
1月前
|
C语言
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
深入C语言指针,使代码更加灵活(三)
|
2月前
|
安全 C语言
在C语言中,正确使用运算符能提升代码的可读性和效率
在C语言中,运算符的使用需要注意优先级、结合性、自增自减的形式、逻辑运算的短路特性、位运算的类型、条件运算的可读性、类型转换以及使用括号来明确运算顺序。掌握这些注意事项可以帮助编写出更安全和高效的代码。
49 4
|
1月前
|
C语言
C语言练习题代码
C语言练习题代码
|
2月前
|
存储 算法 C语言
数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解
本文详细介绍了单链表的理论知识,涵盖单链表的定义、优点与缺点,并通过示例代码讲解了单链表的初始化、插入、删除、查找等核心操作。文中还具体分析了按位序插入、指定节点前后插入、按位序删除及按值查找等算法实现,并提供了尾插法和头插法建立单链表的方法,帮助读者深入理解单链表的基本原理与应用技巧。
507 6
|
2月前
|
存储 C语言 C++
数据结构基础详解(C语言) 顺序表:顺序表静态分配和动态分配增删改查基本操作的基本介绍及c语言代码实现
本文介绍了顺序表的定义及其在C/C++中的实现方法。顺序表通过连续存储空间实现线性表,使逻辑上相邻的元素在物理位置上也相邻。文章详细描述了静态分配与动态分配两种方式下的顺序表定义、初始化、插入、删除、查找等基本操作,并提供了具体代码示例。静态分配方式下顺序表的长度固定,而动态分配则可根据需求调整大小。此外,还总结了顺序表的优点,如随机访问效率高、存储密度大,以及缺点,如扩展不便和插入删除操作成本高等特点。
193 5
|
2月前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
365 3