硅基计划 课外拓展 函数栈帧创建&销毁

环境:VS2013


为什么选择这个环境,因为低版本编译器便于观察,高版本往往会有很多优化,而且不同编译器的函数栈帧有所区别,但大体逻辑上一致


 一、寄存器

有很多类型寄存器,比如eax,ebx,scx,edx等等

还有两个比较特殊的:esp  栈顶指针 和 ebp  栈底指针 ,用来维护函数栈帧

并且每一个函数的调用,都会在栈区上开辟空间

二、函数举例

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函数分析(转到反汇编分析)

1.main函数调用

当你观察main函数反汇编代码时候,你会发现main函数被一个函数调用,被谁呢?

你发现是一个叫starmain........的函数,而你如果进入这个函数

你会发现这个函数又被一个函数调用,是谁呢?你发现是一个mainstarup函数

则我们再调用main函数的时候,main已经先被其他两个函数调用了

我们通过F10转到反汇编继续观察,记得关闭显示符号名,这样便于观察

因此我们就知道,在调用main函数时候,其他两个函数的栈帧已经创建好

而你的return0返回到mainret函数中

2.压栈

我们要开始准备创建main函数的函数栈帧了

进行压栈操作,压入栈底指针ebp,此时esp位置发生变化

接下来,我们再使用move操作让esp变成ebp

此时我们sep再减去一个值,可以看到对应我们图中的0E4h

那栈顶指针esp跑哪去了呢,我们可以看到esp减去了一个值

这个值根据不同编译器略有差异

我们来看看这个值对应的是什么地方

通过下面两张图对于,我们发现这个位置还是非常大的,因此此时我们的esp位置向上移动

ebp开始接管原来esp的位置

接着我们再在栈顶压入三个元素

压入第一个元素ebx时,esp栈顶指针位置就往上走,指到ebx顶端

接着压入第二个元素esi时,同理esp值再次变化,esp栈顶指针位置继续往上,指到esi顶端

压入第三个元素edi时,同理esp值再次变化,esp栈顶指针位置继续往上走,指到edi顶端

此时停下来,对应编译器的三条push指令

3.加载有效地址

进而我们执行下一个操作

lea(load effective adress) 加载有效地址,我们给edi放入了一个地址

我们通过move,把这个地址的值是ebp减去一个0E4h

我们把39h这个值存入ecx中,把0CCCCCCCCh这个值存入eax中,这些值之后会用到

4.初始化 

接着我们开始执行初始化操作

可以看到我们通过的dword指令将edi向下的一片区域初始化的改为eax的内容

即0CCCCCCCCh,一直到栈底指针ebp结束,这个初始化空间还是很大的

 我们此时才把main函数的函数栈帧开辟完毕,开始执行我们的第一条代码

假设每一行是4个字节,我们可以看到第一条指令时把变量a的值存入main函数的一块区域

同理变量b和变量c分别存入一块区域

假设你不初始化的话,存入的是随机值,打印出来就是乱码的字符(经典:烫烫烫烫.....)

所以,初始化很重要,对吧

5.调用add函数之前操作

接下来我们发现再调用add函数之前,还要进行一些列操作

 我们先把变量b的值放入eax中,再push进行压栈,此时esp位置往上走

同理我们再把另外个变量a放入ecx中,再push压栈,esp位置再往上走

这个过程就是传参

我们还发现,下面有个call指令,这个是干嘛的呢,我们可以看到后边有个地址02710E6h

这个是标记用的,最后我们再来解释

这时候esp就继续往上走了一大块,为add函数栈帧创建做准备



四、add函数分析

好,此时我们进入了add函数,前面过程与add函数类似

创建函数栈帧,压入来自main的ebp,三次pushebp变化,再初始化,开始执行计算

 1.形参创建分析

你会发下,我形参x和y怎么没见他们创建呢

诶,你之前可能没有发现,其实这两个变量是在add后函数栈帧的ebp+8和ebp+12中的

我们执行move指令就可以把变量拿过来,我们再把这两个变量的值相加,放在ebp-8中

即放入你之前规定的c变量中

说白了就是函数调用时传参之后形参x,y压了栈,但不是在函数内创建

这样就可以说形参是实参的一份临时拷贝也不为过

因此你修改形参并不会影响实参,形参只有在函数调用的时候才会传给函数对应的值

2.函数结果返回 

好,我们开始执行返回操作

我们看到上图中最后一条指令时move,结尾跟了个寄存器eax

目的是把函数的结果的值放入一个全局寄存器eax中

以便在函数栈帧销毁的时候,函数返回结果不会消失

 我们开始执行三条弹出pop指令,每次弹出esp位置都会增大,往下移(往回走)

此时我们esp位置变成了刚开始的esp位置

此时ebp再往下走(往开始地方,即高地址方向),回收空间

在add栈顶弹出之前,main函数栈底ebp回到了最开始的位置

我们的esp再重新指向原来main函数栈顶

还记得之前的call函数吗

我们就要回到那个位置继续执行我们的主函数代码,即之前的ecx位置

现在函数调用完了,形参x和y没有用了,此时形参x和y销毁

esp再往下走,再三次弹出pop:edi,esi,ebx

回到最最最开始的位置,此时我们再把事先存在eax中函数结果的值放入C中

完毕


整个基本流程就完成了,还是挺复杂的


声明

作者基础知识有限,本文只是阐述作者看法,并非完全正确,如果有错误,欢迎指出

友好交流,请勿上升到人身攻击


END

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值