前言:
1.请使用vs2013调试,我使用vs2019被恶心到了,封装严重,不利于观察。
2.函数栈帧:函数就是程序,程序就需要空间来运行,所以我们要为他分配空间,分配的空间用ebp esp维护,前者在底后者在顶,至于具体什么是ebp esp,我只能说他们是32-bit的寄存器,具体请看:
esp is the stack pointer. ebp is for a stack frame so that when you enter a function, ebp can get a copy of esp at that point. Everything already on the stack, the return address, passed-in parameters, etc. and things that are global for that function (local variables) will now be a static distance away from the stack frame pointer for the duration of the function. esp is now free to wander about as the compiler desires and can be used when nesting to other functions (each needs to preserve the ebp naturally).
It is a lazy way to manage the stack. It makes compiler debugging and understanding compiler-generated code easier, but it uses a register that could have been otherwise general-purpose.
//https://stackoverflow.com/questions/21718397/what-are-the-esp-and-the-ebp-registers
翻译并理解一下就是:
ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向内存中的栈区的最上面一个栈帧的栈顶。
EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向内存中的栈区的一个栈帧的底部。
这句话很抽象,你就记着:一个函数中可以一直调用其他的函数,形成一种嵌套的关系,但是一个CPU只有一个ESP EBP,所以他俩必须保存当前正在执行代码的栈帧。
当你进入一个函数时,ebp原来的值push到栈上,然后ebp被用esp的值覆盖掉,这相当于
ebp
会保存esp
的值,然后此时esp会进行SUB操作,也就是跳到更低的地址去,相当于开了空间。这种方式被认为是一种相对懒惰的栈管理方式。它虽然简化了编译器调试和理解编译器生成的代码的过程,但也使用了一个本可以作为通用目的的寄存器。
如果你还要详细了解,那就要看看汇编语言课程了,也可以看看这个链接。
4.vs的反汇编代码中显示的是十六进制,比如A代表十进制的10
5.dword ptr [ebp-14h]这一句相当于将ebp中的值减去20也就是5个框,即上移5个框,dword操作的是四字节量级,也就是一个框。
6.用文章来演示这个动态的过程稍显累赘,应该直接调试来的直观。
7.每次pop都是从esp那里pop,人家是栈顶指针,不要以为哪里有数据你就从哪里pop。
8.每push一次,esp的值都要减,相当于图中的往上移。
9.
esp是32-bit的register(寄存器),也就是四个字节,我们用一个框代表四个字节,esp指向箭头这里,表明它里面的值放的是8ca040h
10.图示:
写好源代码:
int Add(int x, int y) {int z = 0;z = x + y;return z;
}
int main()
{int a = 10;int b = 20;int c = 0;c = Add(a, b);printf("%d\n",c);return 0;
}
观察方法:
main函数也是被其他函数调用的,所以esp ebp 早早已经有了值,所以我们进入main函数前栈区的函数栈帧是这样的:
esp ebp存储的是调用我们main函数的那个函数(VS中有两层调用,mainCRTStartup()和另一个函数 )的栈帧的顶、底地址。
按下F11,启动程序,转到反汇编,去掉符号名(红框)。
我们看到在正式执行我们的main函数代码之前,程序还做了不少事情,比如将之前(调用main将函数的那个函数)的ebp在栈上存起来(其实就是push到栈上即可),然后将ebp esp更新为计算的main函数的栈帧大小,也就是为main函数开辟栈帧并用0CCCCCCCh刷新赋值。
执行完后,此时栈区结构变为:
然后到了下述三行代码:
执行完上述代码将:
然后到了:
此时执行前两行后:
再执行第3-4行:
注意这几步都是Add这一句引发的动作,他要两个参数,我们得给他准备,相当于把 a b(实参)的值拷贝(假设叫做a' b'(形参的值))了一份放在了栈上,我们可以预料到Add函数中会通过偏移来找到 a' b'。
注意:这就是我们常说的形参是实参的拷贝,你修改后者不影响前者的本质体现。
此时我们的代码到了:
好了,我们准备执行最后一行call,按下F11:
我们发现栈上被push了一个值,定情一看,竟然是call的下一个语句的地址:
即:00C21450,那说明call这个汇编就做了这个事。看起来我们待会儿要用这个地址,也就是去执行这个地址的语句,确实是这样,我们从call走,回来时不能再原地踏步了,确实应该执行下一句,人家已经提前存储起来了,厉害。
call也将代码带到了Add函数这里:
我们可以看到,int z = 0是Add的第一行代码,但是在这之前我们仍然做了不少事,为它分配栈帧.....
我们一直执行到int z = 0之前;栈区结构如下(用0CCCCCCh刷新我没有标出):
然后,一直执行完:
到了这里:
看起来他把30这个加后的结果放在了eax寄存器里。
来到这里:
三个pop就是把这三个值分别放在对应的寄存器中。这不重要。
然后来到了倒数第三行, 一口气执行两行,剩下最后一行ret了:
执行ret其实本质是pop栈顶的一个值,然后把程序跳到那个值对应的语句去:
所以我们的代码来到了:
也就是call的下一个语句,我们早早就存起来它的地址了。
此时我们执行红框,让esp朝着高地址偏移两个框(8字节),然后把eax也就是我们要的30放在[ebp-20h]也就是c那里去。
看起来是:我们是先把形参抛掉,然后把装在eax寄存器中的我们要的结果赋值给c。
其他的语句不再重要了。