深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)

本章的内容:

  • 什么是函数栈帧?
  • 理解函数栈帧能解决什么问题?
  • 函数栈帧的创建和销毁解析

本文放到 --> 该专栏内:http://t.csdnimg.cn/poMzA

目录

什么是函数栈帧❓

理解函数栈帧能解决什么问题呢?

函数栈帧的创建和销毁解析

预备知识

什么是栈?

认识相关寄存器和汇编指令

相关寄存器

相关汇编命令

必备知识

演示代码:

大体思路:

反汇编代码:

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

2.函数栈帧的创建

3.main函数中的核心代码

call指令

4.Add函数栈帧的创建

5.Add函数中的核心代码

6.Add函数栈帧的销毁 

总结:


什么是函数栈帧❓

     我们在写C 语言代码的时候,经常会把一个独立的功能抽象为函数,所以 C程序是以函数为基本
单位的。那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问
题都和函数栈帧有关系。
        
        函数栈帧(stack frame) 就是函数调用过程中在程序的调用栈(call stack)所开辟的空间 这些空间是用来存放:
  • 函数参数和函数返回值
  • 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
  • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)

理解函数栈帧能解决什么问题呢?

理解函数栈帧有什么用呢?
只要理解了函数栈帧的创建和销毁,以下问题就能够很好的额理解了:
  • 局部变量是如何创建的?
  • 为什么局部变量不初始化内容是随机的?
  • 函数调用时参数时如何传递的?传参的顺序是怎样的?
  • 形参和实参的关系是什么呢?
  • 函数调用结束后是如何返回的?
让我们一起走进函数栈帧的创建和销毁的过程中。

函数栈帧的创建和销毁解析

预备知识

什么是栈?

        栈(stack )是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
        在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push ),也可以将已经压入栈中的数据弹出(出栈,pop ),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out FIFO )。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。
        在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
        
        在经典的操作系统中, 栈总是向下增长(由高地址向低地址)的
        在我们常见的i386 或者 x86-64 下,栈顶由成为 esp 的寄存器进行定位的。

认识相关寄存器和汇编指令

相关寄存器
寄存器名称                                      简介
eax 通用寄存器,保留临时数据,常用于返回值
ebx 通用寄存器,保留临时数据
ebp 栈底寄存器(Stack bottom
esp 栈顶寄存器 (stack top
eip 指令寄存器,保存当前指令下一条指令的地址
相关汇编命令
汇编命令 解释
mov
数据转移指令(赋值)
push
数据入栈,同时 esp栈顶寄存器 也要发生改变
pop
数据弹出至指定位置,同时 esp栈顶寄存器 也要发生改变
sub
减法命令
add
加法命令
call
函数调用, 1 . 压入返回地址 2. 转入目标函数
jump
通过修改 eip ,转入目标函数,进行调用
ret
恢复返回地址,压入 eip ,类似 pop eip 命令

必备知识

  1. 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。
  2. 这块空间的维护是使用了2个寄存器: esp ebp ebp 记录的是栈底的地址, esp 记录的是栈顶的地址。

第2点如图所示:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第1张图片

        3.函数栈帧的创建和销毁过程,在不同的编译器下创建和销毁是略有差异的,但是大体逻辑是相差不大的,当编译器越高级的时候,函数栈帧的封装越不容易看,所以编译器的环境采用vs2013

演示代码:

#include 
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 3;
    int b = 5;
    int ret = 0;
    ret = Add(a, b);
    printf("%d\n", ret);
    return 0;
}

大体思路:

        每一个函数调用,都要在栈区创建一个空间
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第2张图片

        由于栈区使用内存的时候,每一次函数调用都要在栈区上分配空间,是先使用高地址,再使用低地址

打开调试窗口,接着打开调用堆栈

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第3张图片

从调用堆栈看到,原来main函数也被调用了,那么它是被谁调用呢?

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第4张图片 在VS2013中,main函数也是被其他函数调用的,调用逻辑如下:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第5张图片

反汇编代码:

右击鼠标,打开反汇编 

int main()
{
//函数栈帧的创建
00BE1820 push ebp
00BE1821 mov ebp,esp
00BE1823 sub esp,0E4h
00BE1829 push ebx
00BE182A push esi
00BE182B push edi
00BE182C lea edi,[ebp-24h]
00BE182F mov ecx,9
00BE1834 mov eax,0CCCCCCCCh
00BE1839 rep stos dword ptr es:[edi]
//main函数中的核心代码
    int a = 3;
00BE183B mov dword ptr [ebp-8],3
    int b = 5;
00BE1842 mov dword ptr [ebp-14h],5
    int ret = 0;
00BE1849 mov dword ptr [ebp-20h],0
    ret = Add(a, b);
00BE1850 mov eax,dword ptr [ebp-14h]
00BE1853 push eax
00BE1854 mov ecx,dword ptr [ebp-8]
00BE1857 push ecx
00BE1858 call 00BE10B4
00BE185D add esp,8
00BE1860 mov dword ptr [ebp-20h],eax
    printf("%d\n", ret);
00BE1863 mov eax,dword ptr [ebp-20h]
00BE1866 push eax
00BE1867 push 0BE7B30h
00BE186C call 00BE10D2
00BE1871 add esp,8
    return 0;
00BE1874 xor eax,eax
}

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

  我们已经知道了,main函数也是被调用的,画出函数栈帧图详解一波:

        栈空间的使用是,由高地址到低地址,而main函数是被_tmainCRTStartup的,所以esp与ebp就维护当前的栈帧

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第6张图片

        ①执行push操作

        这时候F10按一下, 执行一下push让ebp这个地址压栈

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第7张图片

        怎么证明ebp压栈成功?深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第8张图片

        所以说,esp这个栈顶指针指向了ebp这个压栈的值:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第9张图片

        ②接下来执行mov指令,就是把esp的值赋值给ebp深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第10张图片

如下图: 

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第11张图片

        ③然后执行sub指令,让esp减去0E4h,换成二进制就是228,,整体流程下图:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第12张图片

        当①②执行完后,其实_tmainCRTStartup栈帧的空间已经开辟完毕,当③执行完后,调用了main函数,此时esp、ebp就预开辟好了一块空间给main函数,并维护该栈帧,如下图

2.函数栈帧的创建

        接着上文的内容,画出该图:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第13张图片

        接着依次push三个寄存器ebx,esi,edi的值入栈中,esp往低地址处移动

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第14张图片

通过监视可以看一看

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第15张图片

画出图如下: 

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第16张图片

接下来看这四条指令:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第17张图片

①lea edi, [ebp+FFFFFF1Ch]

解析:

[ebp+FFFFFF1Ch]显示符号名去掉,也就是[ebp-0E4h] (也就是和[esp - OE4h]是同一个位置)

lea - 加载有效地址,即将[ebp-0E4h]的地址加载到edi寄存器中,[ebp-0E4h] - 指向ebp(基准指针寄存器)上减去0E4h(232)个字节位置的内存单元

②mov ecx,39h(准确的次数)

解析:

将立即数 39h 复制到 ecx 寄存器中,使 ecx 寄存器的内容变为 39h(十进制的57)。

③mov  eax,0cccccccCh

解析:

这条指令将立即数 0cccccccCh 复制到 eax 寄存器中,使 eax 寄存器的内容变为 0cccccccCh

 ④rep stos dword ptr es : [edi]

解析: 

  • rep 是重复前缀,用于指示指令要重复执行多次,执行的次数由 ecx 寄存器中的计数值决定。
  •  stos 是字符串存储 (Store String) 的缩写,用于将数据存储到字符串中。
  • dword ptr 指明操作数的大小为双字(32位),用于指示要存储的数据的大小。
  • es:[edi] 是目标操作数,表示将数据存储到以 es 寄存器为段地址,edi 寄存器为偏移地址的内存位置。

        第④点整体来看:该指令的作用是将 eax 寄存器中的值重复写入到以 es:[edi] 为起始地址的内存位置。执行次数由 ecx 寄存器中的计数值确定。

       

        整体①②③④来看:

        要把edi这个位置开始(也就是[ebp-0E4h]的地址),向下空间的ecx(次数)放的39h这个值,这么多个dword(4个字节)的数据全部都改成0CCCCCCCCh,图解在下面:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第18张图片

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第19张图片

         到这,main函数的开辟已经执行完了。

3.main函数中的核心代码

接下来执行以下三句代码:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第20张图片

以a为例子,观察下图:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第21张图片

        可得出以下图解:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第22张图片

然后接下来执行以下指令:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第23张图片

首先来看前两条指令: 

  • mov 是一个指令,用于将数据从一个位置复制到另一个位置。
  • eax 是一个32位的寄存器,用于存储通用数据。
  • dword ptr 是一个修饰符,用于指示后面的操作数应该被视为32位的双字(即4个字节)。
  • [ebp-14h] 是一个内存引用,它指向位于基址指针 ebp 减去 14h(20个字节)的位置。基址指针 ebp 是一个用于存储局部变量和函数参数的寄存器。

综上所述,这行代码的作用是将位于 ebp-14h 地址处的32位数据加载到 eax 寄存器中

  • push 是一个指令,用于将数据压入堆栈中。
  • eax 是之前加载了数据的寄存器。

综上所述,这行代码的作用是将 eax 寄存器中的值(20)压入堆栈中

所以,后两条指令同理③④

        将位于 ebp-8 地址处的32位数据加载到 ecx 寄存器中,将 ecx 寄存器中的值(10)压入堆栈中

图解:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第24张图片

call指令

函数调用过程

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第25张图片

  • call 是一个指令,用于调用一个函数或子程序。它的作用是将当前指令的下一条指令的地址(返回地址)压入堆栈,并跳转到指定的函数或子程序的地址执行。

按f11,通过call指令就会进入Add函数里面去了(并未真正进入,还要再按一次f11)

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第26张图片

     call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行

因此,call  的作用是将当前指令的下一条指令的地址压入堆栈(00C21450),并跳转到地址为 00C210E1 的函数或子程序的入口点执行

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第27张图片

4.Add函数栈帧的创建

        再按下f11,这时候才是真正来到add函数内,前面那一堆汇编代码跟main函数栈帧创建逻辑是一样的。

反汇编代码:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第28张图片

前提说明 

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第29张图片

图解: 

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第30张图片

5.Add函数中的核心代码

反汇编代码:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第31张图片①:将值0(初始化z)存储到位于内存中的地址 ebp-8 处的双字(32位)数据中。

②将位于内存中地址 ebp+8 处的双字(32位)数据(当前位置的值为10)加载到寄存器 eax 中。

③将位于内存中地址 ebp+0Ch 处的双字(32位)数据(当前位置的值为20)与寄存器 eax 中的值相加,并将结果存储(两数相加的结果为30)回 eax 寄存器中。

④位于当前堆栈帧中相对于基址寄存器 ebp 偏移 8 字节的内存位置的值(当前值为30)复制到寄存器 eax 中

图解:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第32张图片

6.Add函数栈帧的销毁 

代码:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第33张图片

 这句代码的意思是: 

        将位于 ebp-8 地址处的32位数据(值为30)加载到寄存器 eax 中,因为函数出去之后,值就销毁了,但是如果放在寄存器eax内就安全了,相当于用了一个全局的寄存器把返回值保存起来,回到主函数main再用。

 然后pop三次,把三个寄存器的地址分别弹出:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第34张图片

接着 mov esp,ebp,就是把ebp当前地址赋值给esp:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第35张图片

接着pop ebp,此时ebp回到main函数函数栈帧的栈底:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第36张图片

        说明此时Add函数已经销毁了。

此时最重要的一条指令来了:

        当pop ebp之后,只是让我们找到了esp和ebp的栈帧空间,但是当我回到main函数的时候,还应该从call指令的下一调指令的地址开始执行,所以此时恰好栈顶上就放着这个地址

        这个ret指令return返回的时候这个指令其实就是从栈顶弹出了call指令下一条指令的地址,然后跳那去了,接着F10走一下,回来main函数内:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第37张图片

        存这个地址(00C21450)就是当函数调用完之后还能回来,从call指令的下一条指令的地址开始执行。

 所以图解是这样的:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第38张图片

关于形参变量空间的释放:

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第39张图片

 返回值是怎么带回来:先把值委托到eax寄存器内,接着回到main函数内部赋值

深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)_第40张图片

        经过esp+8之后,关于x和y两个形参空间的变量就已经销毁,还给操作系统了。

 关于main函数的销毁跟上述Add函数的销毁逻辑相似,也不累赘地列举了。

总结:

1.局部变量是如何创建的?

        首先为main函数分配栈帧空间,然后在栈帧空间内初始化一部分空间之后,给局部变量在该栈帧空间内分配一点空间

2.为什么局部变量不初始化内容是随机的?

        因为随机值是我们放进去的,如果局部变量给它们初始化,那就是把随机值覆盖了。

3.函数调用时参数时如何传递的?传参的顺序是怎样的?

        当我要调用那个函数的的时候,就已经push,push,把这两个参数从右向左开始压栈压进去,当我们进入形参函数Add的时候通过指针的偏移量找回来找到了形参

4.形参和实参的关系是什么呢?

        形参确实是我在压栈的时候开辟的空间,形参和实参只是值是相同的,空间是独立的,所以形参是实参的一份临时拷贝,改变形参不会影响实参

5.函数调用结束后是如何返回的?

        我们在调用之前就已经把call指令的下一条指令的地址压栈压进去了,当函数调用完要返回的时候,弹出ebp就能找到原始上一个函数调用的ebp,然后指针往下走的时候就能中找到esp的地址,接着跳转到call指令下一条指令的地址,返回值是通过寄存器的方式带回来的

        本文结束,感谢来访! 

你可能感兴趣的:(C初阶,c语言,笔记,汇编,汇编代码,修炼内功,编程语言)