超详细 函数栈帧(利用反汇编窥探底层原理)+ 建议收藏

Posted IT莫扎特

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超详细 函数栈帧(利用反汇编窥探底层原理)+ 建议收藏相关的知识,希望对你有一定的参考价值。

前言

学习函数栈帧之前我们得了解一下什么是寄存器,因为关于函数栈帧的知识是需要了解寄存器的知识做一个内容铺垫的,每次测试采用的环境是在VS2017下

寄存器

寄存器与函数栈帧的关系
ebp,esp这两个寄存器存放的是地址,这两个地址是用来维护函数栈帧的,简单了解一下寄存器呢有六个

本次测试代码

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x,int y) 
{
	int z = x + y;
	return z;
}

int main()
{
	int a = 10;
	int b = 20;
	int ret = 0;

	ret = Add(a,b);
	printf("%d", ret);
	return 0;
}

首先我们得明白每一次调用函数都会为该函数创建一个函数栈帧的,那么这块空间由谁来维护呢,前面我们讲到函数栈帧是两个寄存器esp 和 ebp
来维护的,下面看内存图

通常我们把ebp 呢我们称之为栈底指针,esp
呢我们又称之为栈顶指针,为什么有栈顶指针和栈底指针的说法呢?我们都知道栈的使用习惯是先使用高地址空间,再使用低地址空间,每次函数调用压栈的时候,我的两个指针都会指向这块空间,
每次开辟空间的时候,在栈中是不是总是向上使用的,那么栈顶指针是不是就总指向这块空间的头,而栈底指针就是指向这块空间的底,

主函数是被谁调用的?调用逻辑是什么

接下来先从main函数开始开刀,我们都知道写好的代码都要放到主函数中去运行,然道主函数就这么牛?总是得经过他得同意?答案并不是,其实main函数的上头也有老大调用main,接下来我们通过调试观察他的调用逻辑,有猎奇心理的小伙伴请跟着来

这里我们等待主函数返回到被调用处,去寻找他的源头

在这里我们把调用堆栈打开就行

在这个地方我们和直观的就看到了主函数被调用处,另外要说明的是在VS2017上好像不能观察到主函数的调用逻辑,博主目前采用的是VC2010学习版,回到主题

不知道大家观察到没有,这里的__tmainCRTStartup()其实就是我的mainret函数,其实很明显在我们的调用堆栈中就可以看出

在这里我们选中这一行双击跳转到mainret函数的被调用处

我们再往上翻

原来是这个函数调用了我们的__tmainCRTStartup(),从而函数的调用逻辑我们就搞明白了,

我们再梳理一遍调用逻辑

主函数栈帧的创建

既然我们知道了这一点,来么就可以再完善一下刚刚的内存图

直到这里大家把对函数栈帧的理解开始建立一个大致的轮廓就行,接下来我们通过反汇编观察里面的秘密

下面就是整个程序的部分汇编代码

之前说过每调用一次函数都会为这个函数分配一个栈帧,main函数是由__tmainCRTStartup()函数调用的,那么__tmainCRTStartup这个函数的栈帧就已经创建好了,

回到反汇编,看这一行的 push 一个叫 ebp 的寄存器进来

栈顶指针,栈底指针

push进来一个元素那么我的esp是不是就得指向这个元素,因为esp是我的栈顶指针嘛

接下来,来到调试窗口,这行指令还没执行前esp的地址是0x00b5f964

这一行一执行来到了下一行会发现此时的esp的地址已经发生了变化由原来的地址(0x00b5f964)变化到了(0x00b5f960)差了4字节,这说明了其实就是压入了一个元素进去,因为此时的esp指向的是压入栈中的那一块空间,因为栈的使用习惯是先使用高地址再使用低地址,由原来的地址(0x00b5f964)减去4字节得到的就是(0x00b5f960),此时的esp指向的就是(0x00b5f960)这还不足以证明吗?是不是就对应了上面的这张内存图

函数栈帧创建的预备工作

接下来再看mov这一条指令,这条指令的作用是将esp赋值给我的ebp

很明显我的esp和ebp此时指向的是同一块空间,因为是同一个地址嘛

如果要用内存图来描述的话,这张就最通俗了

回到汇编代码,接下来程序该执行sub这条指令了,这条指令的作用就是让esp 减去 0E4h
这样子esp就会指向新的空间了,再啰嗦一句就是栈的使用习惯是先使用高地址再使用低地址,很明显esp
指向一个比他还小的地址,那么肯定是又开辟了一块空间

很明显此时的esp 又指向了一块新的空间,因为他的地址发生了变化

此时的内存布局是这样子的

所以主函数空间的大小就是ebp指针减去esp指针的大小,因为在一块连续的空间里地址 - 地址得到的结果就是这块空间的大小

接着回到汇编代码,可以看到程序又压进去3个值,这个我们先不用关心,知道内存图就可以

此时的栈顶指针发生变化,直到这里大家对栈顶指针对应每一次压栈都会指向这一块新空间这里已经很熟悉了吧

当我们程序来到这一行看到了一个 lea,那么这个lea是个什么意思呢?lea的全称是(load effective
address)意思就是加载有效地址,相当于给edi这个寄存器里面放入一个地址

为了便于大家观察这博主调整一下编译器,显示符号名一勾上便于后我们观察

这行汇编代码 edi,[ebp-0E4h] 意思是什么呢?就是用ebp寄存器里此时保存的地址减去0E4h得到的新的地址赋值给我的edi,

这三行指令一执行完,到底干了什么事,真正能够产生效果的是这句指令 rep stos dword ptr
es:[edi],这句话的意思就是要把从edi这个位置开始向下走的39h次(ecx中的内容),每次将dword的空间初始化为0CCCCCCCCh(eax中的内容)
注意:(一个word占2字节 d就是双倍的意思,也就是4个字节)

这三行代码将主函数的空间全都初始化为0CCCCCCCCh

Add函数是怎么被调用的

以上汇编指令执行完,初步任务就已经完成了,main函数的栈帧的开辟就已经准备完了,接下来就可以执行有效代码了

程序走到这一步,开始执行我们的C语言代码 int a = 10;

这一行汇编代码的意思是将0Ah(十进制的10) 这个值,放到 [ebp-8]这个地址指向的这块空间处初始化这 dword(四字节空间)
黄色小块ebp-8这个地址指向的这块空间就是变量a所在的内存单元,在这个时候已经被初始化为10,额外插一句如果没有将10赋值给a
这块空间,那么这块空间不就还是(ccccc)了?答案:是的,每次开辟栈帧的时候系统都会给这块内存空间给随机值,随机值是什么值?就是这个(ccccc),每次如果程序员只定义局部变量但是没有给局部变量赋值,那么局部变量的随机值不是(烫烫烫烫)吗?只是针对局部变量,因为只有局部变量才会使用栈空间,那如果是全局变量呢?是不是就不会啊?因为全局变量没有用到栈的内存空间,

断点停在这一行表示还没执行这条语句,此时a的内容就是0xcccccc

c,那么执行这条语句出现的结果又是什么呢呢,让我们接着观察

变红的部分表示变量a已经初始化了,不再是随机值了,那么这条汇编代码是不是就验证了上面的解释,由于编译器采用的是小端存储(小端在左大端在右),所以在内存中是倒着存放的实际上应该是
0x 00 00 00 0a,对应的就是10

那当我们有了对上面这个代码的理解,在理解这条汇编代码是不是就简单多了啊,这里就不再啰嗦,下面我们直接看内存图

当我的 [ebp-14h] 一执行,其实就已经跳到
0x00B5F94C这个地址处了,也就是对应的变量b的地址处,在这个时候可以看到变量b也已经有初始值了,不再是随机值了


绿色箭头标识的是变量b,这种做法希望能便于大家理解,在这个地方呢我们可以观察到两个局部变量是差了2个整形的,下面是他们对应的地址,当然这个跟编译器有关,有的编译器是差1个整形,有的编译器是差2个整形,在liunx中差的是1个整形,而在VS2017中差两个整形,关于这个内容在我的这篇博客中有过解释,https://blog.csdn.net/m0_53421868/article/details/118726690?spm=1001.2014.3001.5501

接着往下看,这就不用多说了

这行语句一执行完ret在内存中也被初始化好了

并且我们也能观察到这3个局部变量都是差了2个整形

此时的内存布局,橙色箭头所指向的就是变量c所在的内存块

接下来就轮到函数调用了 mov一执行 eax的存放的就是变量b的地址,接着执行完push再压栈eax

此时此刻esp就指向了我的eax,明显esp发生了变化

将变量a的值存放到ecx寄存器中

ecx此时此刻存放的是10

push指令一执行

esp指向了新的地址

内存分布再发生变化

F11一按进入到了我的Add函数中,这是Add函数对应的汇编指令

push一执行完esp又指向了新压栈的一块空间

对应的内存图

mov一执行完,esp指针和ebp指针又指向同一个地方去了


当sub一执行完esp又指向另外一个地址处去了,那么这样子以来的目的是不是就又为了Add函数分配内存呢


再接着又是3次push压入3个寄存器


而以上这一块代码的执行又是为了Add函数栈帧的创建做准备

紧接着再把x 和 y变量存放到寄存器eax中,执行完add指令后,再将x + y的结果存放到eax中,最后将eax去初始化变量z

其实x变量和y变量并不是在函数栈帧中创建的而是在传参的时候就已经记录了他们的值

在函数调用完了之后,return z 会将变量z的值放入到寄存器中,返回的是寄存器的值,所以在变量z会销毁了之后并不影响函数的返回值

这3行代码会将edi、esi、ebx给弹栈并将esp逐渐往高地址去指向,

当再执行mov指令的时候,两个指针又重叠了

回看内存图,此时此刻这个Add函数栈帧就被销毁了

而当pop esp pop
ebp这两条指令一执行后,我的esp又回到最开始指向主函数的edi处去了,而我的ebp又指向到最开始的ebp处去了,请看下面的内存图

而函数一调用完就会回到call指令的下一条指令,回到主函数程序继续执行

当执行当add指令后esp + 8,这个过程会将原来的两个临时变量x和y给释放掉

最后将原本存放在eax寄存器中的值带回给我的ret

计算的结果就是30

以上的所有流程博主就交代完了,如果有什么不足的请指教,感谢

以上是关于超详细 函数栈帧(利用反汇编窥探底层原理)+ 建议收藏的主要内容,如果未能解决你的问题,请参考以下文章

Java 虚拟机原理线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器

图解C/C++语言底层:函数调用过程之函数栈帧的创建和销毁(上)

图解C/C++语言底层:函数调用过程之函数栈帧的创建和销毁(上)

函数栈帧的创建与销毁,带你了解代码底层原理

IDA反汇编工具的使用详解

C语言从入门到入土(进阶篇)函数栈帧的创建和销毁讲解(不看必后悔系列)(超详细)