分享
 
 
 

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

王朝other·作者佚名  2007-05-10
窄屏简体版  字體: |||超大  

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

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

//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对象的内存分布情况的截图:

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

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);

11/11/2006 于家中

今天是11月11日,光棍节。虽然我不是光棍,但是正和女朋冷战中,希望早日结束冷战。

附注:

1。在VS2002中,我们可以通过下面的方式开启运行时类型标识:

在解决方案资源管理器中选择Project --> C/C++ --> 语言 --> 启用运行时类型信息,选择"是" --> 确定

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有