目录
二:__tmainCRTStartup函数与mainCRTSartup函数
一:寄存器:
eax ; ebx ; ecx edx
ebp ; esp
ebp 与 esp这两个寄存器中存放的是两个地址,这两个地址是用来维护函数栈帧的.
ebp:又叫栈底指针,主要是存放函数栈帧的底部地址
esp:又叫栈顶指针,主要是存放函数栈帧顶部的地址
栈中数据存放的特点:从高地址向低地址存放数据
寄存器ebp存放的是当前函数栈底的地址
寄存器esp存放的是当前函数栈顶的地址
当调用另一个函数的时候,esp与ebp也会相应的改变,esp去存放被调函数的栈顶的地址,ebp去存放被调函数的栈底的地址。
二:__tmainCRTStartup函数与mainCRTSartup函数
每一次函数的调用,都要在栈上开辟空间,在main函数中调用add函数,就会在栈上为add函数开辟一块空间,调用main函数时,也会为main函数开辟一块空间。
那谁调用的main函数呢?是__tmainCRTStartup这个函数调用的main函数,那又是谁调用的__tmainCRTStartup函数呢?是mainCRTSartup函数调用的__tmainCRTStartup。
![](https://img-blog.csdnimg.cn/cde60ef756504c598ad45299e23782bf.png)
![](https://img-blog.csdnimg.cn/c5a6a560599f4d6fa6bbc9dd48d64bd3.png)
![](https://img-blog.csdnimg.cn/5940717a427842b6b20889613795f97e.png)
![](https://img-blog.csdnimg.cn/a0ba16838cc64b668ac8c01bb2f7321f.png)
栈的使用特点是:由高地址向低地址
三:接下来是main函数栈帧的创建
说明:
压栈(push):向栈里存放数据(将数据存放在栈顶)
出栈(pop):从栈里删除数据(将数据从栈顶删除)
第一步是push ebp 压栈,将ebp压入栈中,esp也相应的自动向上移动,存放
栈顶的地址 这里(蓝括号里面)存放的是__tmainCRTStartup函数栈底的地址
之所以要将ebp(__tmainCRTStartup函数栈底的地址)压入栈顶,其目的是:当main函数调用完回来以后
可以找到__tmainCRTStartup函数的栈底,因为调用完main函数回来很容易找到__tmainCRTStartup函数
的栈顶,而栈底是比较难找的,所以干脆把栈底的地址给放到函数栈帧的上部,方便,main函数回来后
寻找栈底。
第二步是mov ebp esp意思是将esp的值赋值给ebp,即将__tmainCRTStartup函数栈顶的地址赋值给ebp,现在ebp指向__tmainCRTStartup函数栈顶
这是准备创建main函数的动作-》移动esp与ebp
第三步是sub esp 0e4h意思是esp-oe4h,即esp向上移动0e4h个类型单元
第4,5,6步是push ebx,push esi,push edi
esp向上移动3个类型单元
第七步是 lea edi ebp+0e4h
lea的意思是:load effective address 加载有效地址
这里 lea edi ebp+0e4h的意思是:将ebp+0e4h赋值给edi
将这个位置的地址存放在edi里面
第8步是 mov ecx 39h意思是:将39h这个数赋值给ecx,
即ecx里面存放着39h这个数值
第9步是mov eax 0CCCCCCCCh意思是,将0CCCCCCCCh这个数赋值给eax
即eax里面现在存着0CCCCCCCCh数值
第10步
从edi这个位置开始,向下esi个类型单元,都赋值成为ebx
即从A位置开始向下39h个类型单元,每个类型单元里面的数值都被赋值成为 0CCCCCCCCh
![](https://img-blog.csdnimg.cn/77426841fc7e4d64accf41361c90c61d.png)
四:接下来开始在main函数里面初始化局部变量
第11步
意思是将0A(10)存放在ebp-8这个地方
这是赋了初值,如果没有赋初值,那么ebp-8这个地方存放的是cccccccc,所以打印的是一个随机数
第12步
这里的意思是:将14h(20)存放在ebp-14h这个地方
将0(0)存放在ebp-20h这个地方
函数局部变量的创建方式:
首先先创建好函数的栈帧
然后在栈帧里面存放函数的局部变量
五:main函数中函数参数的传递
![](https://img-blog.csdnimg.cn/a7a68d5b27fb4020875a763f5c7220c4.png)
第13,14,15,16步
分别是:将ebp-14h里的内容(b==20)赋值给eax
将eax压入栈中
将ebp-8里面的内容(a==10)赋值给ecx
将ecx压入栈中
之后esp向上移动两个类型单元
第17,18,19步
call指令是调用函数,走到call的时候,会进入add函数内部,对add函数再开辟栈帧,然后存储add函数内的局部变量,但是在call的同时,会将call下面的语句的地址压到栈顶,因为在调用完add函数后会返回来继续往下执行,所以要记住下一行的地址。
六:创建add函数的栈帧,并进行局部变量的初始化
第20步,通过call的函数调用,进入add函数
创建add函数的栈帧,就不写了,其与main函数的栈帧的创建相同
创建add函数的局部变量:将0赋值给ebp-8
将ebp+8的内容(10)赋值给eax=10
Add eax,dword ptr [ebp+0ch] 的意思是:向eax里面再加上 [ebp+0ch]
就相当于eax=eax+ [ebp+0ch] ==30
因为 [ebp+0ch] = [ebp+12]=20
再将eax的值赋值给ebp-8
注意这里函数参数的传递,并没有在add函数里面创建参数,而是使用的main函数压栈的参数,
即将main函数里面的参数(要传的参数)压在main函数的栈的上部,方便被调函数获取
注意:参数调用的先后顺序是先a,后b
即add ( a , b )先左后右
之前说的形参是实参的临时拷贝,这句话是完全正确的,从内存图中就可以清楚的看到。
而且形参的变化不会影响实参
七:返回函数值
第21步,开始return z
首先将ebp-8的内容赋值给eax,注意eax是一个寄存器,他不会随着栈帧的销毁而消失,所以这个操作是很安全的。等跳出add函数后,add函数的栈帧就会被销毁,再将eax里的内容拿出来,赋值。
之后接连着3个pop,弹出栈顶的值,并将其赋值给edi,再弹出栈顶的值,并将其赋值给esi,再再弹出栈顶的值,并将其赋值给ebx,
之后将ebp赋值给esp,这样esp瞬间来到ebp的位置,最会pop 栈顶的内容赋值给ebp;
因为栈顶存放的内容是ebp-main函数栈底的地址(解释一下为什么栈顶存放的内容是ebp-main函数栈底的地址,因为add函数调用完回来以后,很容易找到main函数的栈顶,但是栈底是很难找到的,所以先把栈底的地址保存一份存放起来,方便后续函数调用完回来找到栈底),所以ebp又瞬间的指向了main函数的栈底
esp也向下移动一个类型单元
从而完成了add函数栈帧的销毁
最后:ret其实就是弹出栈顶的call指令下一条指令的地址;然后跳到那里去。
进而esp再向下移动一个类型单元
进入ret,即进入main函数,进而在main函数内操作
(call是进入函数,ret是出函数)
跳到了call指令的下一个指令,这就解释了为什么将call指令的下一条指令的地址放在栈的上部;
是回到main函数以后,还能再继续执行函数调用完以后的语句。
执行add esp,8意思是esp=esp+8
现在就销毁里两个形参
之后再执行mov dword ptr [ebp-20h],eax即将eax里面的值赋值给ebp-20h
函数的返回值是如何返回的,首先是先将要返回的内容存放再eax寄存器中,然后回来的时候再将eax里面的内容赋值给接收的变量
八:解决几个常见的问题
question1:
局部变量的创建:首先要先创建好函数的栈帧,然后在函数栈帧的基础上进行局部变量的创建。局部变量的创建是从函数的栈底开始往上创建的
每个局部变量创建的类型单元之间的距离间隔可能不同。
question2:
如果局部变量不初始化的化,那么内存里面存的是CCCCCCCCC这种编译器自己生成的东西,那么打印出来的值肯定是随机值
question3:
函数的传参并非在函数里面开辟空间存放形参,举个例子:
在main函数内部要向add函数传递形参add(a,b),
形参a,b是存放在main函数的栈帧的上部的,先存b,后存a.
然后add函数使用形参的时候,直接向下通过ebp来拿就可以了。
question4:
形参是实参的一分临时拷贝,这句话一点毛病都没有,通过内存就可以看到,实参与形参分别占有不同的内存空间,形参的改变不会影响实参。
question5:
将形参放在main函数的上部后,进行call指令,call指令进行的同时,也会将call指令的下一条指令压入栈顶。
call指令主要的是函数的调用,通过call指令调用add函数,
如何调用add函数呢?首先创建add函数的栈帧,然后再创建局部变量。
question6:
函数调用完成以后,首先将返回值存放在寄存器中(安全),然后将esp与ebp返回到main函数之前的位置(具体如何返回上面写的很详细)
最后ret跳出add函数,ret就是弹出栈顶的call指令的下一条指令的地址,然后跳到那里去。
所以就跳回到main函数指令中.继续接着call指令后面往后执行。
把每一件简单的事做好,就不简单了;把每一件平凡的事完成,就不平凡了!
与君共勉