几秒读懂C++虚函数调用的汇编代码实现

多态是C++中最重要的特性之一,对虚函数的调用在C++代码中是随处可见的。虚函数(代码段地址)被存放在虚函数表中,调用虚函数的流程是这样子的:先获取虚函数表的首地址,然后根据目标虚函数在虚函数表的位置(offset偏移)取出虚函数表中的虚函数地址,最后去call这个虚函数(地址),就完成虚函数的调用。这个虚函数调用的流程在汇编代码中可以最直观的反映出来。

图片[1]-几秒读懂C++虚函数调用的汇编代码实现-卡核

       在排查软件异常或崩溃时,我们时常要借助汇编代码的上下文去辅助分析问题。读懂C++虚函数调用的汇编代码实现,对于搞懂汇编代码的上下文时很有好处的。今天我们就来看看虚函数调用的汇编代码实现。

       比如如下的C++代码:

// 1、虚接口类中定义了纯虚接口
class IContactInterface
{
    //...

    virtual BOOL IsLoadFinish() == 0;  // 虚接口

    //...
}


// 2、子类中实现虚接口
class Contact : public IContactInterface
{
    //...

    BOOL IsLoadFinish(); 

    //...
}


// 3、new出子类的对象,存放到父类的指针变量中
IContactInterface* g_pContactPtr = new Contact;


// 4、获取子类对象的接口实现
IContactInterface* GetContactPtr()
{
    return g_pContactPtr;
} 


// 5、调用GetContactPtr获取子类的对象去调用虚函数IsLoadFinish
GetContactPtr()->IsLoadFinish();

上述C++代码片中,主要包含了以下几点:

1)定义了虚接口类IContactInterface,在类中有个IsLoadFinish虚函数;

2)定义了一个子类Contact,继承于IContactInterface接口类,并实现了虚函数IsLoadFinish;

3)new出一个Contact类的对象,赋值给父类IContactInterface的指针变量;

4)调用GetContactPtr接口获取IContactInterface指针变量,调用虚函数IsLoadFinish。

其中,GetContactPtr()->IsLoadFinish()这句虚函数调用的汇编代码如下所示:

.text:005103EB                 call    ds:__imp__GetContactPtr
.text:005103F1                 mov     [ebp+var_2AF4], eax
.text:005103F7                 mov     ecx, [ebp+var_2AF4]
.text:005103FD                 mov     edx, [ecx]
.text:005103FF                 mov     ecx, [ebp+var_2AF4]
.text:00510405                 mov     eax, [edx+78h]
.text:00510408                 call    eax

现在我们就来详细解读一下这段实现虚函数IsLoadFinish调用的汇编代码,虚函数的寻址过程如下所示:

45e2d06cf568488fa932d134c06d8ff2.png

1、汇编代码段1:

.text:005103EB                 call    ds:__imp__GetContactPtr
.text:005103F1                 mov     [ebp+var_2AF4], eax
.text:005103F7                 mov     ecx, [ebp+var_2AF4]

         调用GetContactPtr接口,返回IContactInterface*指针,其中存放的是子类Contact对象。GetContactPtr接口返回的IContactInterface*指针中的值,放到eax寄存器中。

在C++的汇编代码中,调用函数的返回值是放到eax寄存器中的。

       所以eax中存放的是子类Contact对象的地址。然后将eax中存放的子类Contact对象的地址,放到函数栈内存[ebp+var_2AF4]中。然后又将子类Contact对象的地址放到ecx寄存器中。

2、汇编代码段2:

.text:005103FD                 mov     edx, [ecx]

       代码运行至此,此时ecx中存放的就是子类Contact对象的地址。该句汇编代码是以ecx中的值作为内存地址,取出该地址中的值放到edx寄存器中。

       在C++中,C++类中如果有虚函数,则类中会掩藏一个虚函数指针成员变量,是排放在C++类所有数据成员的首位,所以C++对象的地址,就是虚函数表指针变量的内存首地址(是指针变量的地址,不是指针变量中的值),所以此处的[ecx]操作,是取出虚函数表指针变量中存放的内容,即虚函数表的首地址。所以edx寄存器中存放的是Contact对象中的虚函数表的首地址。

C++类中如果有虚函数,则该类中会掩藏一个用来存放虚函数表地址的虚函数表指针变量,该虚函数表指针变量放置在该C++对象所有数据成员的首位,所以C++对象的地址就是其虚函数指针变量的内存首地址。

3、汇编代码段3:

.text:005103FF                 mov     ecx, [ebp+var_2AF4]

       回到整段汇编代码最开始的地方,[ebp+var_2AF4]中存放的是子类Contact对象的地址。因为下面要调用Contact类对象的虚函数,调用时要将Contact类对象的地址传给被调用的函数,即this指针。C++汇编代码中,是通过ecx寄存器将类对象的地址传给被调用函数,所以此句代码是为下面调用类Contact的虚函数IsLoadFinish做准备的。

在调用C++类的函数时,是通过ecx寄存器传递C++类对象的地址的,即this指针的存放的C++对象的地址。

4、汇编代码段4:

.text:00510405                 mov     eax, [edx+78h]
.text:00510408                 call    eax

       接着上面,当前edx寄存器中存放的值是虚函数表的首地址(虚函数表指针中存放的值),78h是目标虚函数IsLoadFinish在虚函数中的偏移,所以对edx+78h地址取址,是读出虚函数表该位置存放的就是目标虚函数IsLoadFinish的地址(代码段地址),所以执行这行代码后,eax寄存器中存放的就是目标虚函数IsLoadFinish的地址,然后call eax就是调用虚函数IsLoadFinish了。

在C++中,函数名称其实就是该函数在代码段的首地址,去call这个首地址,就是去调用这个虚函数了。要注意区分数据段地址和代码段的地址。

© 版权声明
THE END
喜欢就支持一下吧
点赞2.1W+赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容