透过汇编另眼看世界之多继承下的虚函数函数调用(二)

转载 2007年10月15日 13:58:00
文揭示多继承下的虚函数调用

 

在我的前一篇文章"透过汇编另眼看世界之函数调用"中,我们通过汇编了解了虚函数调用的全部过程。在本文中我将分析多继承的情况下虚函数调用的情况。

首先还是写一些简单的代码作为本文分析的例子代码:

//the abstract base class
class IBase ...{
public:
    
virtual void func1() = 0;
    
virtual void func2() = 0;
}
;

class IDerive1 : public IBase ...{
public:
    
//virtual functions inherited from IBase 
    virtual void func1() = 0;
    
virtual void func2() = 0;

    
//new virtual function
    virtual void foobar() = 0;
}
;

class IDerive2 : public IBase ...{
public:
    
//virtual functions inherited from IBase 
    virtual void func1() = 0;
    
virtual void func2() = 0;

    
//new virtual function
    virtual void callMe() = 0;
}
;

class CMyObject : public IDerive1, public IDerive2 ...{
public:
    
//virtual functions inherited from IBase 
    virtual void func1();
    
virtual void func2();

    
//virtual function inherited from IDerive1
    virtual void foobar();

    
//virtual function inherited from IDerive2
    virtual void callMe();

public:
    CMyObject(): m_iValue(
0...{}
private:
    
int m_iValue;
}
;
/**//////////////////////
//ingore the definitions of all the virtual functions in CMyObject class
int _tmain(int argc, _TCHAR* argv[])
...{
  CMyObject obj;
 
  
//retreive the IDerive1 interface from the object
  IDerive1* pDerive1 = (IDerive1*)&obj;
  pDerive1
->func2();
  pDerive1
->foobar();
 
  
//retreive the IDerive2 interface from the object
  IDerive2* pDerive2 = (IDerive2*)&obj;
  pDerive2
->func2();
  pDerive2
->callMe();
 
  
//retreive the IDerive2 interface from the IDerive1 interface
  pDerive2 = (IDerive2*)pDerive1;
  pDerive2
->func2();
  pDerive2
->callMe();
 
  
return 0;
}

 

这里我采用的是和COM中使用的多继承类似的继承关系。IDerive1和IDerive2都继承自同一个抽象基类IBase,而且IDerive1和IDerive2本身还是抽象基类,CMyObject类多继承自IDerive1和IDerive2。

熟悉COM的朋友很自然的就会想到IBase就是COM中的IUnkown接口,而IDerive1和IDerive2就是COM中其他接口和自定义接口,而CMyObject就是COM中的"组件(Component)"。 之所以这样设计的原因是熟悉COM的朋友对这样的类的层次关系会感到很舒服,而且这样的多继承层次关系也是比较简单的,便于分析。

在分析汇编代码之前,我们还需要了解多继承下类对象的内存分布情况。多继承下的类对象的内存分布情况比较复杂,这也是为什么很多人说"不要随便使用多继承"。本文虽然使用了多继承,但是类对象的内存分布情况还是相对比较简单和容易控制的,两个基类都是抽象类,他们没有数据成员,只有一个虚指针,而类对象本身也只有一个int型的成员变量。对于CMyObject对象的内存分布情况,下面是我用VS2002调试器查看CMyObject对象的内存分布情况的截图:CMyObject对象的内存分布

 下面是我根据上面的截图,并结合我自己对这部分内容的理解,画了一个简图:

Pointer CMyObject vTable for IDerive1
Pointer CMyObject vTable for IDerive2
m_iValue




 

下面就继续我们的汇编分析。在这里我并不想分析所有的汇编代码,原因之一就是有些汇编代码和前一篇文章的代码是一样的,这里就不用罗嗦了。另一个原因就是我只关心和本文主题有关的内容,那些和本文的主题没有太多联系的内容就不会出现在我的讨论中。

 一。派生类指针到基类指针的转化。由CMyObject指针到IDerive1指针和IDerive2指针转化的汇编代码略有不同:

  ; IDerive1* pDerive1 = (IDerive1*)&obj     
  lea    eax, DWORD PTR _obj$[ebp] 
  mov    DWORD PTR _pDerive1$[ebp], eax
  ; IDerive2* pDerive2 = (IDerive2*)&obj
  lea    eax, DWORD PTR _obj$[ebp]
  test    eax, eax
  je    SHORT $L1774
  lea    ecx, DWORD PTR _obj$[ebp
+4
]
  mov    DWORD PTR tv73[ebp], ecx
  jmp    SHORT $L1775
$L1774:
  mov    DWORD PTR tv73[ebp], 
0

$L1775:
  mov    edx, DWORD PTR tv73[ebp]
  mov    DWORD PTR _pDerive2$[ebp], edx

通过比较我们发现,当CMyObject类指针转化成第二个基类IDerive2指针的时候,除了判断了CMyObject类指针是否为空外,更重要的是,IDerive2指针的值是在CMyObject类指针值的基础上多加了4个字节(一个指针的大小?)。仔细想像,这个不难理解:在多继承的情况下,派生类对象的内存分布是按照基类在派生类中声明的顺序来排列的,在本文中,按照声明顺序,obj的内存分布应该也是基类IDerive1的数据成员,然后是IDerive2的数据成员,最后才是CMyObject的数据成员。由于IDerive1在最前面,而且只有一个虚指针,所以在指针转化的过程中,IDerive1的指针值和CMyObject的指针值是一样的,而IDerive2的指针值就要在CMyObject指针值的基础上加4。

二。基类指针之间转化。下面是由IDerive1指针转化的IDerive2指针的汇编代码:

    ;pDerive2 = (IDerive2*)pDerive1;
    mov    eax, DWORD PTR _pDerive1$[ebp]
    mov    DWORD PTR _pDerive2$[ebp], eax

 感到奇怪的是,这里的转化直接将IDerive1的指针赋给了IDerive2的指针。这样的转化合理么?根据上面的分析,我们知道IDerive1的地址和IDerive2的值应该是不相等的,它们之间差4个字节,可是为什么这里编译器却将他们设为相等? 在这种情况下虚函数能正常调用么? 往下看看在说。

三。派生类的虚表。我奇怪的发现,CMyObject有两个虚表:

CONST    SEGMENT
??_7CMyObject@@6BIDerive1@@@ DD FLAT:?func1@CMyObject@@UAEXXZ ; CMyObject::`vftable
'
    DD    FLAT:?
func2@CMyObject@@UAEXXZ
    DD    FLAT:
?
foobar@CMyObject@@UAEXXZ
CONST    ENDS
CONST    SEGMENT
??_7CMyObject@@6BIDerive2@@@ DD FLAT:?func1@CMyObject@@W3AEXXZ ; CMyObject::`vftable'
    DD    FLAT:?func2@CMyObject@@W3AEXXZ
    DD    FLAT:
?callMe@CMyObject@@UAEXXZ
CONST    ENDS

起初我还以为他们是一样的,但是通过undname.exe对虚表的符号名进行"反修饰",却得到了两个不同的符号名:
??_7CMyObject@@6BIDerive1@@@     const CMyObject::`vftable'{for `IDerive1'}
??_7CMyObject@@6BIDerive2@@@     const CMyObject::`vftable'{for `IDerive2'}

更奇怪的是,通过"反修饰"虚表的虚函数的符号名,我也得到两套不同的符号名:
?func1@CMyObject@@UAEXXZ          public: virtual void __thiscall CMyObject::func1(void)
?func2@CMyObject@@UAEXXZ          public: virtual void __thiscall CMyObject::func2(void)
?foobar@CMyObject@@UAEXXZ        public: virtual void __thiscall CMyObject::foobar(void)

?func1@CMyObject@@W3AEXXZ       [thunk]:public: virtual void __thiscall CMyObject::func1`adjustor{4}' (void)
?func2@CMyObject@@W3AEXXZ       [thunk]:public: virtual void __thiscall CMyObject::func2`adjustor{4}' (void)
?callMe@CMyObject@@UAEXXZ        public: virtual void __thiscall CMyObject::callMe(void)

当我看到"[thunk]"的时候突然就意识到:难道这就是江湖上传说的中的"thunk"? 传说中"thunk"是编译器插入的一小段代码,可以用来实现一些特殊的功能,例如在Win32环境下调用Win16 API,那在多继承下的虚函数调用中,"thunk"又起着什么作用呢?我在汇编代码中找到了"thunk"的代码:

?func1@CMyObject@@W3AEXXZ PROC NEAR            ; CMyObject::func1, COMDAT
    sub    ecx, 
4

    jmp    
?func1@CMyObject@@UAEXXZ                  ; CMyObject::func1
?func1@CMyObject@@W3AEXXZ ENDP                    ; CMyObject::func1
?func2@CMyObject@@W3AEXXZ PROC NEAR   ; CMyObject::func2, COMDAT
   sub ecx, 4
   jmp ?func2@CMyObject@@UAEXXZ  ; CMyObject::func2
?func2@CMyObject@@W3AEXXZ ENDP    ; CMyObject::func2

 由上面汇编代码可以看出,"thunk"代码并不是那么神秘,它只是简单的将寄存器的值减4(一个指针的大小?),然后跳转到另外一个函数。为什么是ECX?为什么是减4?ECX在虚函数调用的过程中不是存放this指针的寄存器么?结合着本文中的类的继承层次关系,我开始慢慢的明白了"thunk"的任务。在多继承的情况下,各基类指针的值应该是不一样的,只有第一个基类的指针值和派生类类对象的首地址是一致的,其他的基类指针和派生类对象的首地址存在一个偏移。假如多个基类也都从一个共同的基类继承而来,理论上说我们可以通过任何一个基类指针去调用这个共同基类的虚函数,这个虚函数调用会被解析到派生类的虚函数实现,而且派生类也只能有一个虚函数实现。为了使通过任何一个基类指针调用的虚函数都调用同一个函数,我们只需要将这样的虚函数调用"转化"到通过第一个基类指针来调用就可以了,而在第一个基类的虚表中存放虚函数的实现。这个转化的过程就是由"thunk"来完成的:它首先将基类指针调整到第一个基类的地址,也就是派生类对象的首地址,然后调用相应的虚函数。

有了这样的分析,我们就可以画出虚表的大致情况:

CMyObject vTable for IDerive1

&CMyObject::func1()
&CMyObject::func2()
&CMyObject::foobar()

 

 


CMyObject vTable for IDerive2

&thunk for CMyObject::func1()
&thunk for CMyObject::func2()
&CMyObject::callme()



接着再回到基类指针之间转化的那个问题:

    pDerive2 = (IDerive2*)pDerive1;
    pDerive2
->
func2();
    pDerive2
->callMe();

此时通过pDerive2能够获得虚表的应该是IDerive1的虚表,所以调用func2()的时候,应该没有thunk发生的。而调用callMe()的时候实际上调用的是foobar(),应该它在IDerive1虚表中偏移量和callMe()在IDerive2虚表中的偏移量是一样的。呜!!!,这个是个错误么?是个Bug么?我也不知道。

11/04/2006 于家中

V1.1

还是基类指针之间转化的问题


根据网友sting的回复,我也明白了这里为什么转化不成功的原因。由于IDerive1和IDerive2之间并没有什么继承关系(虽然他们是另一个派生类的基类),编译器就把他们当作两个"毫无关系"的类,在转化的过程中只能进行简单的赋值,这样的转化形式在C++被定义为reinterpret_cast。

这里有两个方法进行正确的转化:

1。先将一个基类转化到派生类,然后通过派生类再转化到另一个基类。相应的代码可以是这样的:
pDerive1 = static_cast<IDerive1*>( static_cast<CMyObject*>(pDerive2) );
pDerive2 = static_cast<IDerive2*>( static_cast<CMyObject*>(pDerive1) );

2。使用dynamic_cast来转化。要想使dynamic_cast能够正常的工作,我们需要开启"运行时类型标识(RTTI)"。运行时类型标识为处于同一个继承链上的所有类建立了一张"关系网",这样任何两个类之间就有了"千丝万缕"的关系,这样就为他们之间的直接转化提供了可能。相应的代码可以时这样的:
pDerive1 = dynamic_cast<IDerive1*>(pDerive2);
pDerive2 = dynamic_cast<IDerive2*>(pDerive1);
 

透过汇编另眼看世界之多继承下的虚函数函数调用

在我的前一篇文章"透过汇编另眼看世界之函数调用"中,我们通过汇编了解了虚函数调用的全部过程。在本文中我将分析多继承的情况下虚函数调用的情况。 首先还是写一些简单的代码作为本文分析的例子代码: ...
  • liujiayu2
  • liujiayu2
  • 2015年12月11日 15:29
  • 254

透过汇编另眼看世界之函数调用

在我的另外一篇文章中 ,我提到了要通过汇编语言来分析虚函数调用的真相。我们现在就开始踏上这次艰辛却非常有意思的旅程。其他闲话少说,直接进入主题。本文中使用的C++代码: #include "st...
  • liujiayu2
  • liujiayu2
  • 2015年12月11日 15:30
  • 229

透过汇编另眼看世界之类成员函数指针

原文转自 透过汇编另眼看世界之类成员函数指针 开发者在线 Builder.com.cn 更新时间:2008-03-27作者:A Programming Bug 来源:CSDN 本文关键词: ...
  • whizchen
  • whizchen
  • 2014年07月04日 22:21
  • 454

c++多重继承下虚函数的this指针问题

今天偶然发现一个很有意思的问题,在vc编译器想检查有没有内存泄露,于是在一个类的构造函数和析构函数各下一个断点,追踪特定分配出来的一个对象实例有没有析构。却发现无论如何都没有析构,但是使用vld内存检...
  • river_mumu
  • river_mumu
  • 2013年02月25日 12:02
  • 865

透过汇编另眼看世界之DLL导出函数调用

前言:我一直对DLL技术充满好奇,一方面是因为我对DLL的导入/导出机制还不是特别的了解,另一面是因为我发现:DLL技术在Windows平台下占有重要的地位,几乎所有的Win32 API都是以导出函数...
  • liujiayu2
  • liujiayu2
  • 2015年12月11日 15:31
  • 215

透过汇编另眼看世界之DLL导出函数调用

透过汇编另眼看世界之DLL导出函数调用 前言:我一直对DLL技术充满好奇,一方面是因为我对DLL的导入/导出机制还不是特别的了解,另一面是因为我发现:DLL技术在Windows平台下占有...
  • linuxheik
  • linuxheik
  • 2013年04月18日 14:49
  • 574

子类继承父类,重写纯虚函数和虚函数时注意

今天遇到这么个问题,程序中定义了一个借口类,成员函数全是纯虚函数,我写的子类继承了这个接口类,但是报错提示说:无法实例化抽象类! 原因是这样的:子类继承父类时,必须重写父类的纯虚函数,函数名、返回类...
  • GK_2014
  • GK_2014
  • 2015年04月20日 21:51
  • 4346

通过虚函数继承,父类调用子类中函数

在父类中添加虚函数,父类中调用这个虚函数,子类继承父类后,子类实现的虚函数就会...
  • chinabinlang
  • chinabinlang
  • 2014年07月09日 18:56
  • 5015

C++虚继承(九) --- 构造函数调用顺序的实用之处

虚拟继承是C++语言中一个非常重要但是又比较生僻的存在,它的定义非常简单,但是对于理解C++的继承机制却是非常有用的。笔者最近学习过程中发现对C++的虚拟继承不是很明朗,故在这里对虚继承做个小结。 ...
  • liujiayu2
  • liujiayu2
  • 2016年03月18日 17:46
  • 285

继承多态与虚函数及对类的理解

B是A的子类,子类会继承父类的public方法,子类对象可以调用父类的public接口,但是子类和父类函数重名时,父类的方法会被子类覆盖(隐藏),子类调用和父类同名同参函数或者重载父类的函数后不可以直...
  • qq_35956442
  • qq_35956442
  • 2016年11月17日 13:41
  • 325
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:透过汇编另眼看世界之多继承下的虚函数函数调用(二)
举报原因:
原因补充:

(最多只允许输入30个字)