详解如何实现C++虚函数调用汇编代码

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

在排查软件异常或崩溃时,我们时常要借助汇编代码的上下文去辅助分析问题。读懂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

现在我们就来详细解读一下这段实现虚函数调用的汇编代码。

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这个首地址,就是去调用这个虚函数了。要注意区分数据段地址和代码段的地址。

到此这篇关于详解如何实现C++虚函数调用汇编代码的文章就介绍到这了,更多相关C++ 虚函数内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++虚函数表深入研究

    目录 探索虚函数表结构 继承基类重写虚函数 多基类继承 虚函数表 寻找被覆盖的虚函数 总结 面向对象的编程语言有3大特性:封装.继承和多态.C++是面向对象的语言(与C语言主要区别),所以C++也拥有多态的特性. C++中多态分为两种:静态多态和动态多态. 静态多态为编译器在编译期间就可以根据函数名和参数等信息确定调用某个函数.静态多态主要体现为函数重载和运算符重载. 函数重载即类中定义多个同名成员函数,函数参数类型.参数个数和返回值不完全相同,编译器编译后这些同名函数的函数名会不一样,也就是说

  • c++虚函数与虚函数表原理

    目录 1.什么是虚函数? 2.虚函数会影响类的内存 3.了解虚函数表--->通过虚函数表的指针去访问数据 4.虚函数声明 1.什么是虚函数? 用virtual 修饰的成员函数叫虚函数 小知识: 没有虚构造函数        不写虚函数,没有默认的虚函数 普通函数不影响类的内存: class MM { public: void print() { cout << "普通函数"<< endl; //普通函数不影响类的内存<--->普通函数存在另一段

  • C++虚函数表和虚析构介绍

    目录 1.虚函数表 2.虚析构 1.虚函数表 虚函数表是C++实现多态的基础,多态是面向对象的三大特性之一,多态有利于提高代码的可读性,便于后期代码的扩展和维护.我们都知道多态的实现是基于虚函数表,那么虚函数表是什么时候创建的呢?虚函数表是怎么实现多态的功能的呢? 首先应该明确多态也称为动态多态,他是在程序运行时候确定函数地址的,也就是程序在运行时,如果类成员函数加了virtual关键字,就会建立一个虚函数指针(vfptr)指针指向一个虚函数表,这个虚函数表就保存了虚函数的地址,子类继承父类也自

  • 聊一聊C++虚函数表的问题

    之前只是看过C++虚函数表相关介绍,今天有空就来写代码研究一下. 面向对象的编程语言有3大特性:封装.继承和多态.C++是面向对象的语言(与C语言主要区别),所以C++也拥有多态的特性. C++中多态分为两种:静态多态和动态多态. 静态多态为编译器在编译期间就可以根据函数名和参数等信息确定调用某个函数.静态多态主要体现为函数重载和运算符重载. 函数重载即类中定义多个同名成员函数,函数参数类型.参数个数和返回值不完全相同,编译器编译后这些同名函数的函数名会不一样,也就是说编译期间就确定了调用某个函

  • 关于C++虚函数与静态、动态绑定的问题

    覆盖:如果派生类中的方法,和基类继承来的某个方法,返回值.函数名.参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数,它们之间成为覆盖关系:也就是说派生类会在自己虚函数表中将从基类继承来的虚函数进行替换,替换成派生类自己的. 静态绑定:编译时期的多态,通过函数的重载以及模板来实现,也就是说调用函数的地址在编译时期我们就可以确定,在汇编代码层次,呈现的就是 call 函数名: 动态绑定:运行时期的多态,通过派生类重写基类的虚函数来实现.在汇编代码层次,呈现

  • C++虚函数注意事项

    目录 一.虚函数注意事项 1.构造函数 2.析构函数 3.友元 4.没有重新定义 5.重新定义将隐藏方法 文章转自公众号:Coder梁(ID:Coder_LT) 一.虚函数注意事项 在之前的文章当中,我们已经讨论了虚函数的使用方法,也对它的原理进行了简单的介绍. 这里简单做一个总结: 在基类的方法声明中使用关键字virtual可以声明虚函数 加上了virtual关键字的函数在基类以及派生类和派生类再派生出来的类中都是虚的 在调用虚函数时,程序将会根据对象的类型执行对应的方法而非引用或指针的类型

  • 详解如何实现C++虚函数调用汇编代码

    虚函数(代码段地址)被存放在虚函数表中,调用虚函数的流程是这样子的:先获取虚函数表的首地址,然后根据目标虚函数在虚函数表的位置(offset偏移)取出虚函数表中的虚函数地址,最后去call这个虚函数(地址),就完成虚函数的调用.这个虚函数调用的流程在汇编代码中可以最直观的反映出来. 在排查软件异常或崩溃时,我们时常要借助汇编代码的上下文去辅助分析问题.读懂C++虚函数调用的汇编代码实现,对于搞懂汇编代码的上下文时很有好处的.今天我们就来看看虚函数调用的汇编代码实现. 比如如下的C++代码: //

  • 详解C++中赋值,关系,函数调用运算符重载的实现

    目录 赋值运算符重载 类结构 问题的出现 具体实现 关系运算符重载 类结构 具体实现 函数调用运算符重载 类结构 具体实现 总结 赋值运算符重载 在C++中基本数据类型例如整型,可以实现连续赋值:a=b=c:而我们的对象的成员属性虽然可以相等,但是如果牵扯到堆地址,就会有深浅拷贝的问题存在.所以我们自己重载赋值运算符,实现连等的方法. 类结构 class Info { int* m_a; public: Info() { m_a = NULL; } Info(int a) { m_a = new

  • 详解微信小程序中的页面代码中的模板的封装

    详解微信小程序中的页面代码中的模板的封装 最近在进行微信小程序中的页面开发,其实在c++或者说是js中都会出现这种情况,就是相同的代码会反复出现,这就是进行一定的封装,封装的好处就是可以是程序中在于减少一定的代码量,并且可是使代码结构更加清晰.那今天所要记录的就是关于微信小程序中的页面的模板封装. 在微信小程序中的文件名都带有wxml等样式,在wxml中提供了模板,即可以在模板中定义代码片段,然后可以在页面中的不同位置进行调用,模板的定义: <templatename="products&

  • 详解Java Fibonacci Search斐波那契搜索算法代码实现

    一, 斐波那契搜索算法简述 斐波那契搜索(Fibonacci search) ,又称斐波那契查找,是区间中单峰函数的搜索技术. 斐波那契搜索采用分而治之的方法,其中我们按照斐波那契数列对元素进行不均等分割.此搜索需要对数组进行排序. 与二进制搜索不同,在二进制搜索中,我们将元素分成相等的两半以减小数组范围-在斐波那契搜索中,我们尝试使用加法或减法来获得较小的范围. 斐波那契数列的公式是: Fibo(N)=Fibo(N-1)+Fibo(N-2) 此系列的前两个数字是Fibo(0) = 0和Fibo

  • AngularJS框架中的双向数据绑定机制详解【减少需要重复的开发代码量】

    本文实例讲述了AngularJS框架双向数据绑定机制.分享给大家供大家参考,具体如下: 之前写的一篇<AngularJS入门示例之Hello World详解> ,介绍ng-model的时候提到:使用AngularJS的双向数据绑定机制,不需要我们编写繁琐的代码来实现同样的功能.现在我们看一个比较震撼的例子,看看angularJS是如何减少我们在前端开发中的繁琐劳动的.越是感受到框架功能的强大,越是能够激发学习的兴趣和动力. 假如我们有一个学生信息列表,包含学生的姓名.地址和年龄信息.假如这个数

  • 用十张图详解TensorFlow数据读取机制(附代码)

    在学习TensorFlow的过程中,有很多小伙伴反映读取数据这一块很难理解.确实这一块官方的教程比较简略,网上也找不到什么合适的学习材料.今天这篇文章就以图片的形式,用最简单的语言,为大家详细解释一下TensorFlow的数据读取机制,文章的最后还会给出实战代码以供参考. TensorFlow读取机制图解 首先需要思考的一个问题是,什么是数据读取?以图像数据为例,读取数据的过程可以用下图来表示: 假设我们的硬盘中有一个图片数据集0001.jpg,0002.jpg,0003.jpg--我们只需要把

  • 详解MongoDB和Spring整合的实例代码

    MongoDB现在用的非常非常多,如何和Spring整合也是经常碰到的问题. Spring提供了MongoTemplate这样一个模板类的实现方法,简化了具体操作. 下面讲一下具体实现: 添加依赖 <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-mongodb</artifactId> <version>1.10.3.RE

  • 详解C#实现MD5加密的示例代码

    C#实现MD5加密,具体如下: 方法一 首先,先简单介绍一下MD5 MD5的全称是message-digest algorithm 5(信息-摘要算法,在90年代初由mit laboratory for computer science和rsa data security inc的ronald l. rivest开发出来, 经md2.md3和md4发展而来. MD5具有很好的安全性(因为它具有不可逆的特征,加过密的密文经过解密后和加密前的东东相同的可能性极小) 引用 using System.S

  • 详解字典树Trie结构及其Python代码实现

    字典树(Trie)可以保存一些字符串->值的对应关系.基本上,它跟 Java 的 HashMap 功能相同,都是 key-value 映射,只不过 Trie 的 key 只能是字符串. Trie 的强大之处就在于它的时间复杂度.它的插入和查询时间复杂度都为 O(k) ,其中 k 为 key 的长度,与 Trie 中保存了多少个元素无关.Hash 表号称是 O(1) 的,但在计算 hash 的时候就肯定会是 O(k) ,而且还有碰撞之类的问题:Trie 的缺点是空间消耗很高. 至于Trie树的实现

  • linux多线程编程详解教程(线程通过信号量实现通信代码)

    线程分类 线程按照其调度者可以分为用户级线程和核心级线程两种. (1)用户级线程 用户级线程主要解决的是上下文切换的问题,它的调度算法和调度过程全部由用户自行选择决定,在运行时不需要特定的内核支持.在这里,操作系统往往会提供一个用户空间的线程库,该线程库提供了线程的创建.调度.撤销等功能,而内核仍然仅对进程进行管理.如果一个进程中的某一个线程调用了一个阻塞的系统调用,那么该进程包括该进程中的其他所有线程也同时被阻塞.这种用户级线程的主要缺点是在一个进程中的多个线程的调度中无法发挥多处理器的优势.

随机推荐