2.对main函数中的代码进行分析
接下来,我们正式进入对代码进行分析了~~
🔔mov dword ptr [ebp-8],0Ah:mov指令,将0Ah的值(0Ah转换十进制是10),赋给ebp-8指向的位置,此时ebp-8就是为a变量开辟的空间,值为10。(那么,我们在这里是不是就可以知道,如果局部变量没有初始化,那么它的值就是一个随机值,只不过在这里表示的是0xcccccccc),0xcccccccc如果打印成文本就是“烫烫烫”。
📌图形展示:
我们再往下调试一步,
🔔mov dword ptr [ebp-14h],14h:将14h(14转换成二进制是20),赋给ebp-14h指向的位置,ebp-14h指向的空间就是为变量b开辟的一块空间,值为20
📌图形展示:
🔔mov dword ptr [ebp-20h],0 :将0(0转换成二进制是0),赋给ebp-20h指向的位置,ebp-20h指向的空间就是为变量b开辟的一块空间,值为0
📌图形展示:
到此,我们应该就明确了局部变量是怎么创建和初始化的吧~,接下来我们继续往下看,
来到调用Add函数的部分
🔔mov eax,dword ptr [ebp-14h] 又是mov指令,将ebp-14h的值,也就是b的值给到eax,鼠标悬停到eax上面,我们可以看到eax的值为0x00000014 ,eax的值就是20
🔔push eax 对eax进行压栈操作,esp往上走一步
🔔mov ecx,dword ptr [ebp-8] 又是mov指令,将ebp-8的值,也就是a的值给到ecx,鼠标悬停到ecx上面,我们可以看到ecx的值为0x0000000a ,ecx的值就是10
🔔push ecx 对ecx进行压栈操作,esp再往上走一步
📌图形展示:
走到这里的时候,想必大部分读者都会认为这是函数的传递参数吧,答案的确是的,那么后面的结果如何呢?我们继续往下看,
🔔call 00C210E1 接下来就是调用Add函数,此时需要按 「F11」键
调用Add函数指令之后,我们再次观察esp减少了,变成0x008ffaab 这个地址里面放进了00c21450这个值,这个值恰好是call指令下面一条指令的地址,为什么呢,其实这里就是Add函数调用结束需要回到call指令下一条继续执行,所以需要记录call下面的一条指令。
📌图形展示:
3.探究Add函数栈帧的创建
继续「F11」之后,进入Add函数,此时就是准备为Add函数创建函数栈帧空间
🔔push ebp:对ebp进行压栈操作,把指向main函数的ebp寄存器压入栈顶
🔔mov ebp,esp:把esp寄存器中的值移动到ebp,此时由原来ebp指向main函数栈帧空间移动到esp指向的空间位置,
📌图形展示:
🔔sub esp,0CCh:将esp减去0CCh,esp寄存器此时再次向下增长,此时esp与ebp之间的空间就是为Add函数预开辟好的栈帧,
📌图形展示:
🔔push ebx、push esi、push edi:三次压栈操作,和main函数开辟栈帧时一样,这里就不多说了,直接上图~
📌图形展示: 🔔push ebx、push esi、push edi:三次压栈操作,和main函数开辟栈帧时一样,这里就不多说了,直接上图~
🔔 接下来lea指令、mov指令、rep stos指令,是让Add函数的空间都初始化为0CCCCCCCC这样的值
📌图形展示:
🔔mov dword ptr [ebp-8],8:接下来就可以给局部变量z赋予空间了,mov指令就是将0赋值给ebp-8指向的空间里,
📌图形展示:
z = x + y,这个代码怎么分析呢?难道我们会再次赋予两个空间给x和y吗?其实不然,我们继续往下看
🔔mov eax,dword ptr ebp+8:将ebp+8空间里的值移动到eax当中去。ebp+8得到的地址值增大了,我们在图形中往下寻找,找到ebp+8指向的位置,咦,不就是我们之前将ecx寄存器进行压栈操作压入main函数栈帧上面的吗,ecx寄存器里放的就是10啊,那此时eax里面的值放的就是10
🔔add eax,dword ptr ebp+0Ch:ebp+0Ch的值就是ebp+12,将ebp+12所指向的空间里的值,值为20加到eax寄存器中,寄存器中的值就是20了。
🔔mov dword ptr [ebp-8],eax:该指令将eax的值放到ebp-8的位置,即将30赋予给局部变量Z空间里。
所以到这里我们知道,形参并不会在函数内部进行创建,逻辑其实是调用Add函数时,将a和b的值进行了压栈操作,然后进入到Add函数里面时,寄存器就会找到对应压栈时压的值,ebp+8、ebp+12 里的值就是a、b的一份临时拷贝,也就对应了形参x、y值。所以我们就能通透理解形参是实参的一份临时拷贝,修改形参并不会影响实参!
📌图形展示:
那么看到这里,怎么将z的值进行返回呢?我们继续往下看
🔔 mov eax,dword ptr [ebp-8]:这一操作指令的意思是将ebp-8空间里的值,也就是z的值,赋值给eax寄存器中,因为出函数作用域,局部变量z会被销毁!而eax寄存器中的数据是不会立马销毁。
ok,以上Add函数执行完毕,我们接下来执行返回操作了,也就是函数栈帧逐步销毁的过程了。
三、函数栈帧的销毁过程
🔔pop 出Add函数之后,有三次pop出栈操作,将edi、esi、ebx寄存器中的数据从栈顶弹出
📌图形展示:
🔔mov esp,ebp:将ebp寄存器中的值赋值到esp寄存器中,esp指向的位置就是ebp寄存器指向的位置
🔔pop ebp: ebp此时指向的位置是在为main函数开辟函数栈帧空间时压栈的ebp,在Add函数调用完毕之后,回到main函数的栈帧空间,那么main函数栈帧的栈底在哪里呢?这是对当前的ebp寄存器进行pop出栈操作,此时就返回到了main函数栈帧的栈底
🔽此时esp寄存器和ebp寄存器又正式开始维护main函数的栈帧空间
🔔接下来就是ret指令 ret就是返回呀,没错,根据00C21450这个地址返回,也就是当时调用Add函数call指令的一条指令的地址,将这个地址pop一下,就回到了call指令的下一条继续执行
📌图形展示:
🔔add esp,8: 此时是esp的地址加8,地址增大,空间减小,相当于当时形参x、y两个变量的空间由操作系统回收了,
🔔mov dword ptr [ebp-20h],eax: 这条指令的意思就是将eax的值赋给ebp-20h所指向的空间,我们观察图,可以看到ebp-20h不就是为局部变量c开辟的一块空间吗?c的值就是当初出Add函数作用域时,存放到eax存储器中的数据,这个值就是30!
📌图形展示:
这样整个Add函数销毁的过程就很清晰啦,main函数的栈帧销毁过程就不再赘述了~