在网上看到一个非常热的帖子,里面是这样的一个问题:

在打印的时候发现pFun的地址和 &(Base::f)的地址竟然不一样太奇怪了?经过一番深入研究,终于把这个问题弄明白了。下面就来一步步进行剖析。

根据VC的虚函数的布局机制,上述的布局如下:

然后我们再细细的分析第一种方式:

Fun pFun = (Fun)*((int*)*(int*)(d)+0);

d是一个类对象的地址。而在32位机上指针的大小是4字节,因此*(int*)(&d)取得的是vfptr,即虚表的地址。从而*((int*)*(int*)(&d)+0)是虚表的第1项,也就是Base::f()的地址。事实上我们得到了验证,程序运行结果如下:

这说明虚表的第一项确实是虚函数的地址,上面的VC虚函数的布局也确实木有问题。

但是,接下来就引发了一个问题,为什么&(Base::F)和PFun的值会不一样呢?既然PFun的值是虚函数f的地址,那&(Base::f)又是什么呢?带着这个问题,我们进行了反汇编。

printf("&(Base::f): 0x%x \n", &(Base::f));

00401068  mov         edi,dword ptr [__imp__printf (4020D4h)]

0040106E  push        offset Base::`vcall'{0}' (4013A0h)

00401073  push        offset string "&(Base::f): 0x%x \n" (40214Ch)

00401078  call        edi

printf("&(Base::g): 0x%x \n", &(Base::g));

0040107A  push        offset Base::`vcall'{4}' (4013B0h)

0040107F  push        offset string "&(Base::g): 0x%x \n" (402160h)

00401084  call        edi

那么从上面我们可以清楚的看到:

Base::f 对应于Base::`vcall'{0}' (4013A0h)

Base::g对应于Base::`vcall'{4}' (4013B0h)

那么Base::`vcall'{0}'和Base::`vcall'{4}'到底是什么呢,继续进行反汇编分析

Base::`vcall'{0}':

004013A0  mov         eax,dword ptr [ecx]

004013A2  jmp         dword ptr [eax]

......

Base::`vcall'{4}':

004013B0  mov         eax,dword ptr [ecx]

004013B2  jmp         dword ptr [eax+4]

第一句中, 由于ecx是this指针, 而在VC中一般虚表指针是类的第一个成员, 所以它是把vfptr, 也就是虚表的地址存到了eax中. 第二句

相当于取了虚表的某一项。对于Base::f跳转到Base::`vcall'{0}',取了虚表的第1项;对于Base::g跳转到Base::`vcall'{4}',取了虚表第2项。由此都能够正确的获得虚函数的地址。

由此我们可以看出,vc对此的解决方法是由编译器加入了一系列的内部函数"vcall". 一个类中的每个虚函数都有一个唯一与之对应的vcall函数,通过特定的vcall函数跳转到虚函数表中特定的表项。

更深一步的进行讨论,考虑多态的情况,将代码改写如下:

打印的时候表现出来了多态的性质:

分析可知原因如下:

这是因为类Derive的虚函数表的各项对应的值进行了改写(rewritting),原来指向Based::f()的地址变成了指向Derive::f(),原来指向Based::g()的地址现在编变成了指向Derive::g()。

反汇编代码如下:

printf("&(Derive::f): 0x%x \n", &(Derive::f));

00401086  push        offset Base::`vcall'{0}' (4013B0h)

0040108B  push        offset string "&(Derive::f): 0x%x \n" (40217Ch)

00401090  call        esi

printf("&(Derive::g): 0x%x \n", &(Derive::g));

00401092  push        offset Base::`vcall'{4}' (4013C0h)

00401097  push        offset string "&(Derive::g): 0x%x \n" (402194h)

0040109C
 call        esi

因此虽然此时Derive::f依然对应Base::`vcall'{0}',而 Derive::g依然对应Base::`vcall'{4}',但是由于每个类有一个虚函数表,因此跳转到的虚表的位置也发生了改变,同时因为进行了改写,虚表中的每个slot项的值也不一样。

 

稍微总结一下:

在VC中有两种方法调用虚函数,一种是通过虚表,另外一种是通过vcall thunk的方式

通过虚表的方式

base *d = new Derive;

d->f();

004115FA  mov         eax,dword ptr [d] 

              004115FD  mov         edx,dword ptr [eax] 

              004115FF  mov         esi,esp 

              00411601  mov         ecx,dword ptr [d] 

              00411604  mov         eax,dword ptr [edx] 

              00411606  call        eax  

              00411608  cmp         esi,esp 

              0041160A  call        @ILT+470(__RTC_CheckEsp) (4111DBh)

这种方式的应用环境是通过类对象的指针或引用来调用虚函数

通过vcall thunk的方式:

typedef void (Base::* func1)( void );

base *d = new Derive;

func1 pFun1 = &Base::f;

(d->*pFun1)();

004115A9  mov         dword ptr [pFun1],offset Base::`vcall'{0}' (4110C3h) 

                004115B0  mov         esi,esp 

                004115B2  lea          ecx,[d] 

                004115B5  call          dword ptr [pFun1] 

                004115B8  cmp         esi,esp 

                004115BA  call        @ILT+460(__RTC_CheckEsp) (4111D1h)

【C/C++】概念: VC虚函数布局引发的问题的更多相关文章

  1. 从汇编层面深度剖析C++虚函数

    文章出处:http://blog.csdn.net/linyt/article/details/6336762 虚函数是C++语言实现运行时多态的唯一手段,因此掌握C++虚函数也成为C++程序员是否合 ...

  2. C++中的继承与虚函数各种概念

    虚继承与一般继承 虚继承和一般的继承不同,一般的继承,在目前大多数的C++编译器实现的对象模型中,派生类对象会直接包含基类对象的字段.而虚继承的情况,派生类对象不会直接包含基类对象的字段,而是通过一个 ...

  3. 从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数

    一.多态 多态性是面向对象程序设计的重要特征之一. 多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态的实现: 函数重载 运算符重载 模板 虚函数 (1).静态绑定与动态绑 ...

  4. C++虚函数及虚函数表解析

    一.背景知识(一些基本概念) 虚函数(Virtual Function):在基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数.纯虚函数(Pure Virtual Functio ...

  5. sdut 6-2 多态性与虚函数

    6-2 多态性与虚函数 nid=24#time" title="C.C++.go.haskell.lua.pascal Time Limit1000ms Memory Limit ...

  6. C++ 多态与虚函数

    1.多态的概念 由虚函数实现的动态多态性就是:同一类族中不同类的对象,对同一函数调用作出不同的响应. 先看下面这个简单的例子: #include<iostream> using std:: ...

  7. mfc 虚函数

    知识点 类虚函数概念 类虚函数定义virtual 一.虚函数 简单地说,那些被virtual关键字修饰的成员函数,就是虚函数. 二.虚函数定义 定义:在某基类中声明为 virtual 并在一个或多个派 ...

  8. C++ ------ 虚函数覆盖、重载

    在C++语言中,虚函数是非常重要的概念,虚函数是实现C++面向对象中多态性和继承性的基石.而多态性和继承性则是面向对象语言的精髓.掌握虚函数才算是真正掌握C++语言,而C++语言中虚函数的继承覆盖与函 ...

  9. C++开发系列-纯虚函数和抽象类

    概念 纯虚函数和抽象类 纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都实现该函数. 纯虚函数为各派生类提供了一个公共界面(接口的封装和设计.软件的模块功能的划分) 纯虚函数说明 ...

随机推荐

  1. 全网最详细的Ceph14.2.5集群部署及配置文件详解,快来看看吧! -- <2>

    部署Ceph集群 Ceph版本选择 Ceph版本来源介绍 Ceph 社区最新版本是 14,而 Ceph 12 是市面用的最广的稳定版本. 第一个 Ceph 版本是 0.1 ,要回溯到 2008 年 1 ...

  2. keuectl命令

    Kubernetes命令行 kubectl用于运行Kubernetes集群命令的管理工具 kubectl命令行语法 kubectl [command] [TYPE] [NAME] [flags] co ...

  3. 每日一问2:堆(heap)和栈(stack)的区别

    因为这里没有明确指出堆是指数据结构还是存储方式,所以两个尝试都回答一下. 一.堆和栈作为数据结构 1.堆(heap),也叫做优先队列(priority queue),队列中允许的操作是先进先出(FIF ...

  4. 最强常用开发库总结 - JSON库详解

    最强常用开发库总结 - JSON库详解 JSON应用非常广泛,对于Java常用的JSON库要完全掌握.@pdai JSON简介 JSON是什么 JSON 指的是 JavaScript 对象表示法(Ja ...

  5. Sample Codes之Query features from a FeatureLayer

    除了地图基本的放大缩小等功能,在webgis上的二次开发中,查询功能 通常作为需求的一部分需要我们去实现,今天就给大家详细的分析实例代码中的查询功能:Query features from a Fea ...

  6. 探究Dubbo的拓展机制: 下

    承接上篇, 本篇博文的主题就是认认真真捋一捋, 看一下 Dubbo是如何实现他的IOC / AOP / 以及Dubbo SPI这个拓展点的 总览: 本篇的话总体上分成两部分进行展开 第一点就是 Dub ...

  7. hdfs/hbase 程序利用Kerberos认证超过ticket_lifetime期限后异常

    问题描述 业务需要一个长期运行的程序,将上传的文件存放至HDFS,程序启动后,刚开始一切正常,执行一段时间(一般是一天,有的现场是三天),就会出现认证错误,用的JDK是1.8,hadoop-clien ...

  8. Apache Hudi使用问题汇总(一)

    1.如何写入Hudi数据集 通常,你会从源获取部分更新/插入,然后对Hudi数据集执行写入操作.如果从其他标准来源(如Kafka或tailf DFS)中提取数据,那么DeltaStreamer将会非常 ...

  9. 测试必备之Java知识(一)—— Java基础

    Java基础 Java如何运行的? 开发的java源代码,通过javac编译成为平台无关的字节码文件(class),然后通过JVM的解释器将字节码解释成对应的机器码 “一次编译,到处运行”的理解 说的 ...

  10. springboot 报错nested exception is java.lang.IllegalStateException: Failed to check the status of the service xxxService No provider available for the service

    spring: dubbo:#关闭所有服务的启动时检查:(没有提供者时报错) consumer: check: false timeout: 3000