<在不同的编译器下有所差异,可能会有细小区别>
1.寄存器
一般寄存器:AX、BX、CX、DX
AX:累积暂存器,BX:基底暂存器,CX:计数暂存器,DX:资料暂存器,
EAX、ECX、EDX、EBX:为ax,bx,cx,dx的延申。
堆叠、基底暂存器:BP、SP
BP:基底指标暂存器,SP:堆叠指标暂存器
EBP,ESP为BP,SP的延申。
ebp和esp这两个寄存器,存放的是地址。这两个地址是用来维护函数栈帧的
ebp和esp是如何维护函数栈帧的呢?
每一个函数调用,都要创建一块空间,而且都在栈区上。
假若是这么一段代码
#include<stdio.h>
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叫栈底指针。现在esp和ebp就是维护main函数的这块空间。当我要调用哪个函数,esp和ebp就会马上去维护。栈区的习惯是先使用高地址,再使用低地址。放数据也是从顶上,往下放数据。
我们一开始调试就发现,main函数被调用了。但是main函数,是被谁调用的呢?
我们可以得知,main函数也是被调用的,他的返回值,就回到mainret里面去了。从中也能看出,在vs2013中,main函数也是被其他函数调用的。
调用的main函数。他又是被调用的。
简单来说就是,mainCRTstartup调用了__tmainCRTstartup,__tmainCRTstartup调用了main函数。
所以说一开始,就分配了mainCRTstartup和__tmainCRTstartup的空间,然后main函数的空间,然后是Add函数的空间,大体的思路就是这样,接下来详解是如何做到的
1.
程序一开始时,内存的布局是这样的情景。栈区的使用是从高地址向低地址使用的
压栈是从栈顶放一个元素,出栈是从栈顶删除一个元素(push and top)
第一步,压栈,push ebp:
我就相当于放了个ebp进去,那么esp也会发生变化
esp就放到这里了。我们按F10可以观察到
变成了
就意味着,esp变小了,esp走到上面去了。
第二步:move,esp的值给ebp
ebp就跑到esp的位置上去了。
第三步:sub 给esp减去一个0E4H的八进制数,也就是228
可见,esp跑当上面来了。
其实这块区域,就是main函数的空间。
紧跟着,压栈了三个元素
→esp也同时发生了变化:
第二步,lea:load effective address(把地址加载给edi里面去)
接下来这三步是干什么呢
把39h存到ecx(计数暂存器)里面去,然后eax存入0ccccc,接下来edi就会执行39次
从edi开始,向下39h次双字节double word的数据全部初始化成0cccccch这样的数字:
接下来轮到我的语句了。
0Ah就是10,把10这个数字,放到ebp-8这个位置上去
这块空间,就是我放的a
如果我创建变量时,不给你默认初始化值,那么就放的就是cccccc这样的东西。这就是为什么要初始化的原因。
然后把14h这样的值,放到ebp-14h的位置上去。空了两个整形的位置
然后把0放到ebp-20h的位置上去。
这样一来,abc就创建完成
接下来调用add函数:
怎么传参?
他第一步是把ebp-14h,就是我们的b,放到eax里面去了,eax就是20
然后push eax,就是压栈,栈上又压了eax,就是把20放进去了:
然后重复刚刚的步骤,把ebp-8的值,也就是a,放给ecx,再压栈ecx
这两个动作就是在传参
接下来是调用函数,这个地址非常关键。
当我按F11跳转进入函数时就能发现:
我这个地方却放了这个我要执行的下一个地址,放到这里去了。
为什么要记住这个地址呢?
因为我执行完后我要回来,从这儿往下执行。
接下来才执行的我函数部分。
第一步:我把main函数的ebp压到上面去了,
第二步,把esp给ebp
ebp就应该代替了esp的位置了
第三步,给esp减了一个0CCH
其实这就是在为add函数开辟栈帧。
然后压栈了ebx,esi,edi,然后rep并把edi往下的所有内容,都初始化为cccccccccccc
然后终于开始我的计算了。
x和y从哪里来呢?
把ebp+8的值放到eax里去。可知ebp+8实际上就是我刚刚压栈压过来的值。
10加上ebp+0Ch这个值,eax实际上就是30
然后再把eax的值放到ebp-8里去,ebp-8就是我们创建的z。
整个过程我有创建参数吗?没有。是我在刚刚调用函数的时候,我传参的时候,就把形参压栈就压下去了,而且先传的b,再传的a。所以参数是从右向左传得到。我最后是找到我压栈传来的值。
他就会被认为我要的x和y。
所以说形参是实参的临时拷贝。改变形参,不会影响实参。
接下来是返回:
return z的意思就是把ebp-8的值放到eax里面去,寄存器可是不会因为z没有了而销毁的。
然后把edi esi ebx全部出栈,这三个东西的空间就被操作系统回收了。
然后把ebp,赋给esp,esp就应该指到ebp这个位置了。
然后pop ebp,就把ebp弹出去了,让ebp就回到了他最开始的位置了
esp也走到栈顶去了。这样就找到了我的空间。
最后一步ret。我们还有call指令的下一个指令的地址还存着的。我存他,就是为了回来,从下一个指令继续走下去
现在我就回来了,esp也走到这儿了:
然后形参也就没有用了
然后接下来add esp,8
就说明我回来了,形参也还给操作系统了。
然后再把eax放到ebp-20h,就是刚刚创建的c里面
为什么局部变量是随机数,函数是怎么传参等等,就迎刃而解了。