第六步:把main函数里面的空间全初始化
执行完lea后,我们来看下面这三行汇编指令,这三行汇编指令放在一起只为了执行一件事情,所以把这三条指令放在一起看。(分别标上序号1 , 2 ,3如下图所示:)
序号1指令中的ecx是一个计数器,该指令完成的操作是把39h(39h是十六进制数,对应十进制数57)存到ecx里。
序号2指令完成的操作是把CCCCCCCC存到eax里。
序号3指令中rep的目的是重复其上面的指令,而重复的次数就是ecx存的值,stos的目的是把eax中的值拷贝到es:[edi]所指向的地址里面。一个word代表两个字节,dword其实就是double word的缩写,所以dword代表4个字节。因此,一次拷贝4个字节,重复57次,那最后就一共拷贝了4×57=228个字节
也就是说,在执行完这三条指令后edi所指向的地址(0x0037fcd0)和其后面的228个地址里面所存储的内容全被赋值为CCCCCCCC,其实这里0x0037fcd0后面的第228个地址就是ebp所指向的地址。也就是说从edi所指向的地址一直到ebp所指向的地址中间这一部分全被赋值成CCCCCCCC。
值得注意的是:在执行完这三条指令后,edi所指向的地址已经发生改变,此时的edi和edp指向同一地址。
第七步:main函数中变量的创建
1:变量a的创建
接着就来到了红框圈起来的指令了,mov dword ptr [ebp-8],0Ah的意思是:把0Ah(对应十进制的10)放在edp-8(8是8个字节的意思)这个地址里面。
可见执行完这条指令后,原本ebp-8这个地址所指向空间中的CCCCCCCC被替换成了0Ah(10),说明变量a已经创建完毕。
动图演示:
这里也说明了一个问题,如果只是对变量进行声明而不进行初始化赋值,那内存里面存的就是CCCCCCCC(随机值,只是vs2013放的是CCCCCCCC,其他的编译器可能是其他的值),这也就是为什么我们有时打印变量会出现“烫烫烫烫”的原因
2:变量b的创建
和上面一样,这里的mov dword ptr [ebp-14h],14h指令执行的操作就是把14h(对应十进制的20)存到ebp-14h这个地址所指向的空间里。 注意,这里出现的两个14h没有任何关联,如果让b等于其他任何一个整数,始终都是把这个整数存到ebp-14h所指向的这个空间里面。
可以看出在vs2013这个编译器上,变量a和b在内存中的存储地址相差了8个字节,至于相差几个字节这以取决于编译器,不同的编译器会有不同的效果。
3:变量c的创建
三:在main函数中调用Add函数:
1:传参
接下来就到了调用Add函数的时候了。
此时出现了两组mov和push。第一组中的mov eax,dword ptr [ebp-14h]意思是,把ebp-14h这个地址里面存放的值(也就是20)赋值给eax,然后push(压栈)eax。同理,第二组先把ebp-8这个地址里面存的值(也就是10)赋值给ecx,然后push(压栈)ecx。
经过这两次压栈后,esp指针指向的地址减小8个字节(一次减小4个字节,也就是1个整型)
动图演示:
以上就是函数调用时的传参过程
2:进入Add函数
在执行call指令之前,先看一下call指令下一条指令的地址,也就是add指令的地址003F1450
执行call指令时我们需要点击F11才能进入到Add函数的内部去一探究竟。
点击F11执行了call指令后,我们不但进入了Add函数的内部,还让esp指针上移了4个字节,为什么会上移呢?那一定是又元素压栈,因为只有这样才能让栈顶指针上移,那我们再来看一下压入的元素是什么呢?不难发现,压入的这个值就是call指令下一条指令add的地址003F1450。
那为什么要把call指令下一条指令的地址存起来呢?是因为在Add函数调用结束的时候,需要返回继续执行call指令的下一条指令,所以在执行call指令的时候要把call指令的下一条指令的地址存下来,在Add函数调用结束的时候,就能根据存的这个地址找到call指令的下一条指令,进而让程序继续进行。
接着再点击一次F11,就真真正正的进入到Add函数的内部了
蓝色的部分是不是特别眼熟,这部分和main函数前面那部分是一样的,就是为Add函数创建函数栈帧。
实际结果:
2.1:Add函数中变量z的创建
这里变量z的创建过程和main函数中变量a、b、c的创建过程一模一样,详细过程就不再进行赘述,忘了的下伙伴可以往上翻翻看看前面的介绍。这里就简单的用动画为大家演示一下:
2.2:Add函数中的求x+y的和
接着往下,终于来到了 z = x + y,要求两数和了:
求和分3条指令来完成
- 首先第一条指令mov eax,dword ptr [ebp+8]执行的结果是:把ebp+8这个地址里面存储的值放到eax里
- 不难发现ebp+8这个地址里面存的其实就是10(实参a和形参x的值)
接着第二条指令add eax,dword ptr [ebp+0Ch]的执行结果就是:把ebp+0Ch这个地址里面存储的值加到eax里面去,通过上图可以看出ebp+0Ch这个地址里面存的其实就是20(实参b和形参y的值),第二条指令执行完后eax里面存的值就变成了10和20的和,也就是30。
最后第三条指令mov dword ptr [ebp-8],eax执行的结果就是:把eax里面的值存到ebp-8这个地址里面(变量z的地址),也就是;z=30
通过上面的分析可以看出,在调用Add函数的过程中,并没有创建形参x和y,而是在执行call指令调用Add函数之前就已经进行了传参,当时先让eax等于20(也就是实参b的值)先压栈,再让ecx等于10(也就是实参a的值)进行压栈,实参是按照从右到左的顺序进行传递的。当在Add函数中需要用到形参x的时候,是返回去找到ecx取出ecx里面存的值,这个值就是形参x的值。而当Add函数中需要用到形参y的时候,是返回去找到eax取出eax里面存的值,这个值就是形参y的值。
通过这里我们还可以看出,形参实际上只是实参的一份拷贝,改变形参的值不会影响实参
3:return z
接下来,最重要的一步来了:就是把z=30返回到main函数里面,分成以下6条指令来完成:
第一条指令mov eax,dword ptr [ebp-8]的执行结果是:把ebp-8里面存的值(也就是30)存到eax里面,这里的eax是一个寄存器,它不会随着函数调用的结束而销毁掉。如果没有这条指令,在Add函数代用调用结束后的时候,ebp-8(变量z的地址)这个地址所指向的空间会被释放掉,这样的话30x+y的和就无处可寻。(上图序号1和2之间的 } 的右边表明Add函数调用结束)
接着标号2、3、4的这3条指令都执行的是pop(出栈)操作,比如pop edi的意思就是把栈顶数据弹出至edi这个寄存器里,然后栈顶指针下移,后面两个同理,动画演示如下:
通过实际的调试也可以看出没每执行一次pop指令,esp指针的值就加4. 这和每执行一次push指令,esp的值就减4形成呼应。
接着执行标号5的指令 mov esp,ebp,这条指令执行的结果就是把ebp存的地址给esp,也就是说,执行完这条指令后,esp和edp指向同一个地址空间。动画演示如下:
接着执行标号6的指令:pop ebp。这条指令的执行结果是:把栈顶数据弹出至edp里面进行储存。此时的栈顶数据是什么呢?通过动画不拿发现:此时的栈顶数据其实就是main函数栈底的地址呀,这样以来,执行完这条指令后edp就又指向main函数的栈底了,而此时栈顶存储的数据也变成了call指令下一条子陵的地址。动画演示如下:
最后执行ret指令**,ret指令会从栈顶弹出元素给IP,也就是下一条要执行的指令的地址**。此时的栈顶存储的就是call指令下一条指令的地址,这就是为什们当时要存call指令下一条指令的地址。是为了在Add函数调用结束后这个黄色的小箭头能够回到正确的地方继续执行接下来的指令
通过上面的调试动图可以看出:在执行完ret指令之后esp的值从个位上的8变成了个位上的c(c对应十进制下的12),二者相差4。说明确实把栈顶数据弹出了。
动画演示如下:
- 到这里return z的操作就顺利结束了
四:回到main函数:
此时黄色的小箭头成功地来到了main函数中call指令的下一条指令add,这完全归功于最初我们压栈的add指令的地址。
首先执行add esp,8这条指令,该指令的执行结果是让esp指针指向的地址加8,动画展示如下:
- 这条指令执行完就相当于释放了形参变量的存储空间
- 接着执行:mov dword ptr [ebp-20h],eax指令。执行结果是:把eax里面存的值(30)赋值给ebp-20h所指向的这块空间,其实就是变量c的存储空间
- 通过调试可以看出:在执行这条指令之前ebp-20h所指向的这块空间存的值是0,在执行完这条指令之后,ebp-20h所指向的这块空间里面存的就是1e(十进制下的30)。并且可以看出ebp-20h和&c的值相同,说明他们指向同一块空间——变量c的存储空间.动画演示如下:
- 现在我们就清楚函数的返回值是怎么带回来的了——返回值首先会放到寄存器里,当我们真的回到主调函数时,需要用到这个返回值的时候,去访问寄存器,从寄存器里面取值。
相信看到这里,你的心中一定有了最开始那几个问题的答案了!!!
到这里,函数栈帧的有关分享就结束啦,喜欢的话可以点赞、评论和收藏哟!