我们都知道,可以在类当中定义函数,当然也可以利用关键字“virtual”定义虚函数。接下来,我们使用如下代码看看,在汇编层当中两者之间有什么区别呢?
代码当中定义了一个类,其中包含了函数c()以及虚函数d()。接下来,我们在“p->c();”位置下断点,然后转到汇编层来看看。我们看到如下代码:
首先,我们来看在调用c()函数的对应的汇编代码
我们来看一下,它call 指令后面跟了一个地址“0FD10FFh”。那么我们来跟踪一下这个地址。麻烦这个地址对应的指令是一个jmp指令。
接着,我们继续跟踪,发现,该跳转的地址就是我们类当中c()函数的地址。
总结来说:普通函数,是直接Call一个固定的地址。该固定的地址最终是指向了类调用函数的地址。
接下来我们来看看虚函数所对应的汇编代码,初次看,我们就发现,它对应的汇编代码行数上就比普通函数要多。那么,我们就慢慢来看。
我们主要关注“mov eax,dword ptr [p]”,"mov edx,dword ptr [eax]","mov eax,dword ptr [edx]","call eax"。首先它指针p当中的值放入到寄存器edx当中。最后在从寄存器当中将值取出放入寄存器eax 当中,并且执行call eax 指令。
总结来说:虚函数执行的call 指令后面并不是一个绝对的地址,而是寄存器eax , 其中eax 的值是什么地址,那么将执行什么函数。
我们在同一个类当中定义多个虚函数,那么在执行调用,在底层会是一个什么样的情况呢?我们使用下面案例来测试一下:
我们在调用虚函数位置使用断点,执行并且进入到“反汇编”窗口,详细代码如下:
我们来对比调用虚函数d(),e(),f() ,大体上的汇编代码是一样的,有区别的是在每次调用的第5行。熟悉汇编代码的人可能很快就知道这是利用定长偏移来寻址的一种方式(即:[ebp]表示某个地址A,[ebp+4]即表示A地址下一位地址。)。为了结论的严谨,这里我们耐心的追踪一下。
我们首先追踪一下第一个虚函数d(),首先会将指针p里面的内容放入寄存器EAX,然后将寄存器EAX当中的值作为地址来搜索内容并且放入寄存器EDX。
此时,EAX=1EFA20,EDX=E37BEC
直到执行到地址“00E34C44”在寄存器EDX的值为地址去两个字大小的内容放入寄存器EAX。
我们来看一下,此时EAX是指的哪个指令,它指向了一个jmp 指令(地址:00E3135C)。
继续执行跟踪跳转后的地址,最终我们来到了虚函数d()的代码位置。
刚刚我们已经分析了。在分析“p->d()”虚函数的时候,寄存器EDX当中存储的值作为地址寻址操作,最终定位的位置正是虚函数d()代码存储的位置。那么接下来我们看看虚函数e(),同样我们将代码执行到地址“00E34C5A”位置。此时我们发现EAX的值为00E313DE。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2021-4-13 15:37
被天象独行编辑
,原因: