目录
接下来我们就要分析main函数的核心代码了,小伙伴们准备好了吗
这时候我们就跳转到add函数了。开始观察add函数的反汇编代码了
刚刚不小心取消了调试,导致后面的地址与前面不对,实在不好意思,但不影响大家理解,我们接着继续讲。
前言
在我们前期的学习中可能会遇到一些无法解决的问题,如以下几个问题。
1.局部变量是怎么创建的?
2.为什么局部变量的值是随机值?
3.形参和实参是什么关系?
4.函数调用是怎么做的?
还有等等问题
这些问题其实是与函数栈帧的创建与销毁有很大关系,当你看完我这篇文章,保证能对你理解这些问题有很大帮助。
一.函数栈帧定义
函数栈帧就是函数调用过程中在程序的调用栈所开辟的空间,这些空间用来存放函数参数和返回值;临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量);保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。
注意:在不同的编译器,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。我这是用vs2022来讲明函数栈帧的。
1.栈的定义
在计算机系统中栈是一个具有以上属性的动态内存区域。先入栈的后出栈。程序可以将数据从栈顶弹出,;压栈操作使得栈增大,而弹出操作使得栈减小,在操作系统中,栈总是向下增长的。
二.相关寄存器和汇编指令
接下来我们要认识一下相关寄存器及汇编指令,没有以下知识,会使我们学习函数栈帧变得十分的困难,直接上图了,下面这图就解释了相关的内容。
这些词语就是作用与函数栈帧中的一些,认识一下就行,后面还会讲到,这边看一下,留个印象。
三.函数栈帧是如何创建和销毁的呢
1.初步知识
1.每一次函数调用,都要为本次函数调用开辟空间,就是函数栈在帧的空间。
2.这块空间维护时使用了两个寄存器:esp和ebp,ebp记录的是栈底的地址,esp记录的是栈顶的地址。
看下面代码
我们可以画个图来表示这main函数内容存放在栈中
这开辟出的空间大概就是这样,接下来就来讲解如何把代码实现。
2.函数的调用堆栈
函数的调用堆栈是反馈函数的调用逻辑的看,我们可以观察到,;main函数调用之前,是由invoke_main函数调用的。看下图
我们从这可以知道invoke_main函数应该会有自己的栈帧,main函数和add函数也会维护自己的栈帧,每个函数栈帧都有自己的ebp和esp来维护栈帧空间。
3.准备环境
为了使我们更加清晰的观看汇编代码,我们要排除一些编译器附加的代码,如何操作看下图。
把支持仅我的代码调试改成否就可以了。
4.转到反汇编
调试到main函数的第一行,转到反汇编,如下图所示操作
注意:vs编译器每次调试都会为程序重新分配内存,课件中的反汇编代码是一次调试代码过程的数据,每次调试略有差异。
反汇编后就是如下图所示了
上面的这串反汇编代码是在x64上运行的,它是用rbp和rsp来表示的,其原理大同小异,这次我们讲解是以x86运行的及x32,遇到上面这种情况不用惊慌,把其改成x86就行哈。下面是x86的反汇编代码,如下图
5.函数栈帧的创建
我们来一步步拆解代码
1.009418B0 push ebp
上面这串代码表示把ebp寄存器的值进行压栈,由上面所讲,ebp为栈底寄存器,此时ebp中存放的是invoke_main函数栈帧的ebp,esp-4;为什么是esp-4呢,因为是一个int型,所以需要栈顶寄存器esp-4. 如下图所示
2009418B1 mov ebp,esp move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp,这么说你们可能不明白,就是把ebp放到esp的位置,如下图。
3.009418B3 sub esp,0E4h sub是减法命令,会让esp中的地址减去一个16进制数字0xe4h,产生新的esp,此时esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一块空间将储存main函数的局部变量,临时数据以及调试信息等。如下图所示
4.009418B9 push ebx 此时与1的操作形式相同,将寄存器ebx的值压栈,同时esp又要-4,如下图
5.009418BA push esi同上,将寄存器esi的值压栈,esp-4
6.009418BB push edi
同上,将寄存器edi的值压栈,esp-1
注意 :上面三条寄存器的值在栈区,这三个寄存器的在函数执行后可能会被修改,所以先保存寄存器原来的值,以便在退出函数时修复。图如下。
接下来的代码是在初始化main函数的栈帧空间
7,009418BC lea edi,[ebp-24h] 在这里我首先来解释一下lea的意思,lea及load effective address,这串代码及把ebp-24h的地址,放到edi中
8.009418BF mov ecx,9 把9放到ecx中
9.009418C4 mov eax,0CCCCCCCCh 把 0CCCCCCCCh放在eax中
10 .009418C9 rep stos dword ptr es:[edi] 将从edp-0x2h到ebp这一段的内存的每个字节都初始化0xccccccccc,最后如下图
一个小知识
在以前的时候,有时候我们会输出烫烫烫这个结果,很多小伙伴不知道这是为什么,十分的疑惑,现在看到这里了,就可以解开这个疑惑了。看以下代码
此时输出的是好多个烫,之所以输出烫这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间,每个字节都被初始化为0xcc,而arr数组为初始化,而0xcc的汉字编码就是烫,所以最后0xcc被当作文本就是烫。
接下来我们就要分析main函数的核心代码了,小伙伴们准备好了吗
11. int a = 10;
009418D5 mov dword ptr [a],0Ah 此时就是将0Ah存储到a的地址处,
此时及如下图
那么为什么为ebp-8呢,通过内存可知,此时a的地址是 0x004FFC60,ebp的地址是0x004FFC68,a为ebp-8.
12.int b = 20;
009418DC mov dword ptr [b],14h 同上,将20存放到b的地址处,我们可以看看b的地址为0x004FFC54,大家不要看这是54,ebp就是是68就是减14,此时是16进制的数字,故他们相差20,此时b的位置如下图所示
13.int c = 0;
009418E3 mov dword ptr [c],0 将0存储到c的地址处,同上,此时c的地址为0x004FFC48,与b相差12,故c的位置如下图
接下来就是调用add函数了
在调用add函数时的传参,其实就是把参数push传到 栈帧空间中
14.009418EA mov eax,dword ptr [b] 传递b,将b处放的20传到b的寄存器中。
15.009418ED push eax 将b的值压栈,此时esp-4
16.009418EE mov ecx,dword ptr [a] 传递a,将10放在a寄存器中
17.009418F1 push ecx 将a的值压栈,esp-4,此时如下图所示
接下来就进入调用函数阶段了
18.009418F2 call _add (09413B6h) 此时call指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令下一条指令的地方,继续往后执行,此时按f11进入add函数内部,位置及如下图所示。
这时候我们就跳转到add函数了。开始观察add函数的反汇编代码了
add函数初始化内容
1.00F34070 55 push ebp 将main函数栈帧的ebp保存,esp-4.
2.00F34071 8B EC mov ebp,esp 将main函数的esp赋值给新的ebp,ebp现在是add函数的ebp
3.00F34073 81 EC CC 00 00 00 sub esp,0CCh 给esp-0xcc,求出add函数的esp
4.00F34079 53 push ebx 将ebx的值压栈,esp-4
5.00F3407A 56 push esi 将 esi的值压栈,esp-4
6.00F3407B 57 push edi 将edi的值压栈,esp-4
7.00F3407C 8D 7D F4 lea edi,[ebp-0Ch] 把ebp-0Ch的地址放在edi中
8.00F3407F B9 03 00 00 00 mov ecx,3 把3放在ecx中
9.00F34084 B8 CC CC CC CC mov eax,0CCCCCCCCh 把0 CCCCCCCCh 放在eax中。
10.00F34089 F3 AB rep stos dword ptr es:[edi] 把从edi开始到ebp中间所有内容初始化为ccccccccc,如下图所示
对add函数中的调用了
11.int d = 0;
00F34095 C7 45 F8 00 00 00 00 mov dword ptr [ebp-8],0 将0放在ebp-8的地址处,就是创建d。如下图所示
12. d = a + b;
00F3409C 8B 45 08 mov eax,dword ptr [ebp+8] 将ebp+8的地址处的数字存储到eax中,由上图可知,及将a=10储存到eax中
13.00F3409F 03 45 0C add eax,dword ptr [ebp+0Ch] 将ebp+0ch,及12处的地址处的数字加到eax中,及加上b=20.
14.00F340A2 89 45 F8 mov dword ptr [ebp-8],eax 将eax的结果保存到ebp-8的地址处,其实就是放到d中
讲到这,我们就可以解释清楚形参和实参直接的关系了
函数在进行传值调用时,形参其实是实参的一份临时拷贝,对形参的修改不会影响实参,它是将其值进行压栈,然后了解他们的位置后直接调用的,并不会影响实参
15.return d;
00F340A5 8B 45 F8 mov eax,dword ptr [ebp-8] 把ebp-8地址处的值凡在eax中,其实就是把d的值储存到eax中,通过eax寄存器带回计算的结果,做函数的返回值。
接下来就是弹出了
16.00F340A8 5F pop edi
00F340A9 5E pop esi
00F340AA 5B pop ebx 弹出这三给寄存器,使esp指向add,如下图
17.00F340B8 8B E5 mov esp,ebp 把ebp的地址赋给esp,相当于回收了add函数的栈。
18.00F340BA 5D pop ebp ebp被弹出一格,因为它指向main函数,故它重新回到main函数中ebp的位置,如下图,而esp也就指向call下一条指令,如下图
刚刚不小心取消了调试,导致后面的地址与前面不对,实在不好意思,但不影响大家理解,我们接着继续讲。
在这里我们回到main函数
大家可以看到c=add(a,b)后面有这么多个指令,实际上call前面的是不执行的,add函数结束后它会直接跳到 call下一条指令,所以我们现在就直接从add开始。
1.00F318F7 83 C4 08 add esp,8 esp直接加8,由上图可知,esp就是跳过了main函数中压栈的a和b。此时如下图
2.00F318FA 89 45 E0 mov dword ptr [ebp-20h],eax 将eax中的值,存档到ebp-0x20的地址处,其实就是存储到c的变量中。
接着就是直接打印,输出了,这就不过多讲解了。
总结
本篇文章到这里就结束了,我也给大家完整的演示了函数栈帧的创建与销毁,如果这篇文章对你有帮助的话,就点个免费的赞和关注吧。