kuikuitage

  博客园  ::  :: 新随笔  :: 联系 ::  :: 管理
#include <stdio.h>

int Output(int a, int b) 
{
	int c = a + b;
	return c;
}

int Test(int a, int b)
{
	return Output(a, b);
}

int __cdecl main(int argc, char* argv[]) {
	int a = 10;
	int b = 20;
	Test(a, b);

	return 0;
}

直接打开反汇编,内存,寄存器和局部变量四个窗口
图1

这里先看看进main函数之前的栈基址ebp和栈顶esp
ebp = 0x0117FE2C,esp = 0x0117FE1C
即调用main函数的栈大小实际只有16个字节。
图2

push esp之后,ebp = 0x0117FE2C压栈,同时esp = 0x0117FE1C - 4 = 0x0117FE18
图3

mov ebp,esp之后,ebp变为0x0117FE18

sub esp,0xD8之后,即重新定位esp位置向低地址偏移0xD8长度,偏移后esp = 0x0117FD40

图4

这个时候esp = 0x0117FD40,经过三次push,esp = 0x0117FD34即原值-0xC
而ebp还是0x0117FE18
图5

这4条整理用于清栈空间,本应该是ebp(高地址)到esp(低地址),但由于在设定函数栈后,又压了寄存器,故而esp已经变了,这里通过ebp到ebp-0xD8后即栈空间大小,eax用于存放初始化值,ecx用于计数为应该是rep stos专用寄存器,这条指令重复ecx次数,将es:[edi]地址初始化为eax的值,同时es:[edi]值更新。功能就是初始化栈为全0xCCCCCCCC,0xCC为INT 3的指令码,如果取到了0xCCCCCCCC就进入调试模式。
图6

然后执行局部变量的定义,可看到对a, 和b顺序压栈,而且是从基址ebp开始的,至于中间为何空了部分字节,这个原因还不清楚。
图7

继续看到两个push即从右往左的顺序将b和a的值压栈了。esp -= 8,即esp = 0x0117FD34 - 0x8 = 0x0117FD2C
图8

在call Test函数之前,我们先注意一下,call之后的下一个指令的地址,即0x00C71779
图9

进入到Test函数像main函数一样
先对上一个函数栈的ebp进行压栈,但是在压栈之前我们看到栈中多了一条数据,这条命令不在反汇编代码之中,即push EIP(下一条将要执行的指令地址),是将跳转到Test函数时的main即call后面的那条指令的地址即0x00C71779压栈,esp更新为esp - 0x4 = 0x0117FD28,然后进入Test函数又压了main函数的ebp即0x0117FE18,压栈后esp - 0x4 = 0x0117FD24,然后mov ebp ,esp执行完,,Test函数的ebp = 0x0117FD24, 然后继续重新分配和初始化Test函数栈,和main中一样的流程,esp = 0x0117FD24 - 0xC0(函数栈大小) - 0xC(3个寄存器) = 0x0117FC58
图10

然后经过压b和a形参,esp = 0x0117FC58 - 0x8 = 0x0117FC50
执行call Output前压了EIP后, esp = 0x0117FC50 - 0x4 = 0x0117FC4C

图11

进Output后压了Test的ebp即0x0117FD24,此时esp - 4 = 0x0117FC48

图12

这张图是为了看之前压入的EIP的地址即0x00C7170B

图13

继续在Output中重新初始化Output的栈基址ebp和更新esp,ebp = 0x0117FC48,同时esp经过分配Output占空间和压寄存器 esp = 0x0117FC48 - 0xCC - 0xC(34) = 0x0‭117 FB70‬,但实际初始化Out栈为0xCC的是0x0117FC48到0x0117FC48-0xCC,即现在esp加34的位置。图中还没初始化完。

图14

这是初始化完的图,Output的ebp = 0x0117FC48,esp = 0x0‭117 FB70,图中指出了各指令对应操作的内存数据。

图15

可见运算的内存结果存放在栈基址ebp = 0x0117FC48开始后的某个位置

图16

前面我们是一层一层的进入函数,这里开始要一层层的退出函数了。此图是还没有执行前的
按照FILO先入后出,从esp = 0x0‭117 FB70位置开始弹栈,之后esp + 3*4 = 0x0‭117 FB7C
mov esp ebp,即将当前Output函数的栈基址,还原为栈顶即esp = ebp = 0x0117FC48位置, 再pop出ebp进入该函数前压入的上一个函数的栈基址,在这个位置进行了还原,EBP = 0x0117FD24,pop后esp + 4 = 0x0117FC4C,结果见图17寄存器值,看出是一致的

图17

这里还存在之前额外push进来的EIP,这里要pop,但反汇编代码里面没有出现。
EIP执行pop后esp = 0x0117FC4C + 4 = 0x0117FC50。这下才退出到上一层函数即Test

回到Test
图18

我们看到esp的寄存器值确实为0x0117FC50,ebp = 0x0117FD24证明还原是正确的。这里执行到了
add esp,8;为何要对esp加8,因为之前形参压栈占用了8个字节,这里还原后esp = 0x0117FC50+ 8= 0x0117FC58
后面的代码执行,都是恢复从main进Test时的现场保留信息
即三个pop恢复main调用Test时的寄存器值,同时esp + 3*4 = 0x0117FC58 + 0xC = 0x0117FC64,再加上Test栈大小0xD8即esp = 0x0117FC64 + 0xC0 = 0x0117FD24,然后pop出main的栈底ebp = 后,ebp = 0x0117Fe18,esp = 0x0117FD24 + 4 = 0x0117FD28,在退出Test前还要弹出main的执行Test前的EIP,之后esp = 0x0117FD28 + 4 = 0x0117FD2C,然后我们回到main的call Test下面

图19

图示执行前还原的esp = 0x0117FD2C,ebp = 0x0117Fe18,对照main的寄存器指示,完全一致。

图20

再把esp + 8 = 0x0117FD2C + 8 = 0x0117FD34,即之前main之前Test前的形参压栈去掉
再把main之前压栈保存的寄存器弹出esp + 3*4 = 0x0117FD34 + 0xC = 0x0117FD40
再加上main的函数栈esp + 0xD8 = 0x0117FD40 + 0xD8 = 0x0117FE18
再在esp位置pop出ebp即ebp = 0x0117FE2C, esp = 0x0117FE18 + 4 = 0x0117FE1C

对照图1,可见至此,函数栈还原到调用main之前的状态。

这里另外说一点,就是之前有个cmp ebp,esp,这个是VS的栈平衡策略,即栈在一层函数使用完毕,释放栈空间后esp应该是和进该函数前的基址ebp是一致的,这个逻辑细理一下就比较清楚了。

posted on 2020-02-22 19:43  kuikuitage  阅读(508)  评论(0编辑  收藏  举报