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

本文涉及的产品
云解析DNS,个人版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1

什么是函数栈帧❓

    我们在写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 命令



必备知识

每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。

这块空间的维护是使用了2个寄存器: esp 和 ebp , ebp 记录的是栈底的地址, esp 记录的是栈顶的地址。

第2点如图所示:


24adec6a38a648fda25f2482e76516a3.png


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


演示代码:

#include <stdio.h>
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;
}


大体思路:

       每一个函数调用,都要在栈区创建一个空间


8bc604bee0a340f2b43080fc4f33c71e.png

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


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



ba5e18c599114f6c8a2e342b3aca0e53.png

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


dac1aa82d4624ba280415ecbd19c35de.png

在VS2013中,main函数也是被其他函数调用的,调用逻辑如下:


4086dcd168954633a9b02a51498b9dae.png


反汇编代码:

右击鼠标,打开反汇编


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就维护当前的栈帧


20caf7627e744cca9b3f4c4e75a0028a.png


       ①执行push操作


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


49d47fa2d3134e53a0bf4870d338f24a.png


       怎么证明ebp压栈成功?


3a8425a5b9d14595a908a375b72be82c.png

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

051f42ae1ead411e8c2e1680b5fc536d.png



       ②接下来执行mov指令,就是把esp的值赋值给ebp

cae5481b77f44cc7bf74be3c8506004c.png


如下图:


fae8f153bc034236ab7c07ffe55b42f7.png


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


0eaf956fc2114b8792ac75094c55cdf3.png


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


2.函数栈帧的创建

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


b39552290c9549c6bf3c534f61483cf0.png


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


1c965fd7b16a4ec6ada5d75e27d5f428.png


通过监视可以看一看


f5be9651a24b4afb8fa8509868f70ccf.png


画出图如下:


9c7dcd3f323d42359b62f8f64298a9f7.png


接下来看这四条指令:


2966d1c515fc428fb574073b583948b3.png


①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,图解在下面:


e47479258bd149ddb5dbfab91dd57fae.png




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


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

https://developer.aliyun.com/article/1456965?spm=a2c6h.13148508.setting.30.2e124f0eZtymxB

相关文章
|
6天前
|
C语言 图形学 C++
|
3天前
|
自然语言处理 C语言 C++
程序与技术分享:C++写一个简单的解析器(分析C语言)
程序与技术分享:C++写一个简单的解析器(分析C语言)
|
3天前
|
程序员 编译器 C语言
详解C语言入门程序:HelloWorld.c
详解C语言入门程序:HelloWorld.c
8 0
|
3天前
|
机器学习/深度学习 C语言 Windows
程序与技术分享:C语言学生宿舍管理系统代码(可运行)
程序与技术分享:C语言学生宿舍管理系统代码(可运行)
|
6天前
|
程序员 C语言 C++
【C语言】:柔性数组和C/C++中程序内存区域划分
【C语言】:柔性数组和C/C++中程序内存区域划分
9 0
|
2月前
|
存储 算法 数据处理
C语言中的顺序结构程序
C语言中的顺序结构程序
21 1
|
24天前
|
程序员 C语言 C++
C语言学习记录——动态内存习题(经典的笔试题)、C/C++中程序内存区域划分
C语言学习记录——动态内存习题(经典的笔试题)、C/C++中程序内存区域划分
47 0
|
2月前
|
C语言
c语言循环设计程序结构
c语言循环设计程序结构
19 0
|
2月前
|
程序员 C语言
使用指针变量作为函数参数的C语言程序实例
使用指针变量作为函数参数的C语言程序实例
25 0
|
2月前
|
C语言
循环的应用--猜数字游戏、关机程序【c语言篇】
循环的应用--猜数字游戏、关机程序【c语言篇】
36 0