目录
演示用代码:
提示:后文讲解时后缀为h的指的是16进制表示
疑惑1:自定义函数、库函数都是在main函数内部调用,那么是什么调用了main函数呢?
疑惑2:如何观察ebp、esp等寄存器的运行?
疑惑3:ebp、esp等究竟是如何运作的?
疑惑4:push操作到底指的是什么意思?
疑惑5:数据是如何存入内存的?
疑惑6:从main函数到main函数所调用的函数,究竟发生了什么?
疑惑7:被调用函数内部发生了什么?
疑惑8:在被调用函数内部,是如何进行运算的?
疑惑9:函数return以后,会发生什么?
演示用代码:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int a, int b)
{int z = 0;z = a + b;return z;
}int main()
{int a = 10;int b = 20;int c = 0;c = Add(a, b);printf("%d\n", c);return 0;
}
提示:后文讲解时后缀为h的指的是16进制表示
前期学习时,常有一下疑惑:
- 局部变量如何创建?
- 为啥局部变量的不完全初始化值是个随机值?
- 函数如何传参?
- 传参顺序如何?
- 函数调用在计算机底底层是如何做到的?
- 函数调用结束后如何返回的?
注意:在不同编译器中,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。本文使用的是vs2013。
要理解函数栈帧,就必须要理解 esp、ebp这两个寄存器。这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。
每一个函数调用,都要在栈区创建一块空间。
在栈区的高地址、低地址之间会为main函数开辟一块空间,这块空间就称为main函数的函数栈帧。esp、ebp会随着函数的调用,维护不同的函数栈帧。如果调用到了其他自定义函数(假设存在一个Add函数),那么就从维护main函数变为了维护Add函数。
esp通常叫做栈顶指针,ebp通常称为栈底指针。栈区的使用习惯是先使用高地址,再使用低地址;如果再使用空间,是从esp往上使用空间,因此被称之为栈顶指针。
疑惑1:自定义函数、库函数都是在main函数内部调用,那么是什么调用了main函数呢?
在vs2013中,是其他函数调用了main函数。
main函数是由 __tmainCRTStartup 调用,__tmainCRTStartup 是由 mainCRTStartup 调用的。(即如下图所示)
因此,内存的栈区中应该先是在高地址处为 __tmainCRTStartup 和 mainCRTStartup 开辟空间;然后再调用到main函数,为其在较低地址处开辟空间;最后在main函数中调用其他函数,在低地址处为他们开辟空间。
疑惑2:如何观察ebp、esp等寄存器的运行?
在vs2013的代码编写处,鼠标右键弹出功能框,点击转到反汇编那一项,在反汇编窗口查看。
关闭反汇编窗口中的显示符号名功能,从本来的 a、b、c 变为和ebp相关的地址。(ebp - 8 还是个地址,详细内容请关注指针板块)
疑惑3:ebp、esp等究竟是如何运作的?
当我们使用了__tmainCRTStartup 函数以后,内存布局如上图所示;进入main函数以后,先是进行了ebp的push操作(压栈操作),通过push操作,会在已经被使用的的内存空间上再开辟一块空间,用来存放ebp寄存器,esp也会随之上移,具体情况如下图示。
具体的esp的地址变化,也可以通过调试页面的监视、内存窗口进行查看,最后会发现在经过push以后,esp的地址下降了4位(从高位到低位的变化)
mov就是把esp的值给到ebp,最后esp、ebp指向了同一个内存地址;sub行意为esp减去0E4h,esp指向了新的内存地址,然后esp与ebp之间的内存空间就是main函数的栈帧;而最后,开辟的main函数栈帧大小即为0E4h的大小。
在此之后,又push了ebx、esi和edi三个寄存器,esp也继续往低地址处移动。
接下来,就进行了lea操作。lea全称为 load effective address ,即加载有效地址。具体就是把后面的[ebp+FFFFFF1Ch](重新打开显示符号名功能,会发现+FFFFFF1Ch就是-0E4h)这个地址,给到edi;也就是把ebx底部、main函数栈帧顶部的那一个地址给到edi。
随后,通过 mov 操作和 rep stos 操作的搭配使用,进行了内存存放数据的修改。具体就是从edi所指位置开始,从低地址往高地址把 39h 个 dword(双字,即4个字节)全部改成 0CCCCCCCCh 这一内容。具体效果如下图所示。
疑惑4:push操作到底指的是什么意思?
push:压栈操作,往栈顶(栈区的顶部,即栈区当前的最低地址处)存放数据。
pop:出栈操作,从栈顶拿出数据。
push和pop 与 数据结构中的栈实现是一样的,可以协同理解。
疑惑5:数据是如何存入内存的?
int a = 10下面的这条语句是int a = 10 的汇编语句,即是把 0A(16进制表示的10)存入到 ebp所在位置 -8 的位置。具体效果如下图所示。(可以通过内存窗口,自行查看操作效果)
这一步操作也解释了,为什么当变量创建后,如果没有赋初值那么打印结果就是一串随机值;就是因为内存中存放的还是CCCCCCC这样的数据,打印的内容也就是CCCCCCCC。而后续的 int b = 20、int c = 0 所完成的汇编操作都是类似的,只是存放的数据不同而已。
疑惑6:从main函数到main函数所调用的函数,究竟发生了什么?
相信聪明的读者已经发现了,在int a = 10那条语句之前的所有汇编语句,好像都还没有什么作用。(用了,但又没有用doge)
通过汇编语言,我们不难发现:先是把ebp-14h地址的内容(即b=20)给到了eax,然后将eax寄存器压栈;又把ebp-8地址的内容(即a=10)给到了ecx,然后对ecx寄存器压栈。随着压栈操作,esp也往低地址移动了8个字节(2个寄存器)。
call为调用语句,是把call指令下一条指令的地址信息压栈。(esp继续随之移动)
随后再推进一步,就进入了Add函数。(即会发生从下图1到下图2的区别)
图1
图2
疑惑7:被调用函数内部发生了什么?
经过了上述一系列操作以后,终于结束了main函数的栈区内涵讲解;换言之,目前为止的所有内存都是main函数的栈帧(除CRTStart…那个函数以外)
进入add函数以后,先在栈顶push了ebp(此处push的是维护main函数的ebp寄存器,该寄存器指向了main函数下面的ebp寄存器);然后通过mov,让esp、ebp指向同一块内存空间(即存放维护main函数的ebp寄存器);然后esp又往低地址处移动了0CCh个地址,此时esp、ebp两指针间的区域就是Add函数的函数栈帧。最后,通过和main函数相同的办法,让Add函数的函数栈帧存放的是0CCCCCCCCh,并把 int z=0存入Add函数的函数栈帧。
疑惑8:在被调用函数内部,是如何进行运算的?
在上文中的图内我们能看到,z = x + y 语句应该先是把ebp+8 的数据10存入到eax寄存器中;然后再把 ebp+0Ch 的数据20通过控制器和加法器(计组的知识)加到eax当中。最后再把算出来的结果30放入到ebp-8位置的寄存器中。不难发现,实际上函数的参数并未在函数中创建,而是通过压栈操作放到Add函数栈帧的栈底的下面位置,具体如下图所示。这也就是说形参是实参的一份临时拷贝。
为防止z出了函数被销毁,因此还是得把ebp-8所在位置的寄存器中的数据存到eax当中。
疑惑9:函数return以后,会发生什么?
由图不难看出,先是将edi、esi和ebx删除;当Add函数没必要存在时,直接将ebp指针赋给esp指针;然后把ebp寄存器(刚刚存在Add函数栈底下面的的)pop弹出,此时Add函数的函数栈帧随同ebp寄存器全都出栈了,同时ebp指针直接就回到了另一个ebp寄存器所在的位置(在main函数栈帧的下面)。
最后的ret就代表着从目前地址处弹出,即从00C21450(以本博客的例子为参考)弹回,最后回到了下图所示的main函数反汇编页面。
调用完Add函数,add esp,8 就相当于把刚刚形参变量的空间给释放了;最后把eax中存放的30给到ebp-20h所指向的栈帧区域之中。整个Add的调用结束了,同时main函数栈帧中已经存放了30这一数据。