透过汇编另眼看世界之多继承下的虚函数函数调用
在我的前一篇文章"透過匯編另眼看世界之函數(shù)調(diào)用"中,我們通過匯編了解了虛函數(shù)調(diào)用的全部過程。在本文中我將分析多繼承的情況下虛函數(shù)調(diào)用的情況。
首先還是寫一些簡單的代碼作為本文分析的例子代碼:
//the?abstract?base?classclass?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中使用的多繼承類似的繼承關(guān)系。IDerive1和IDerive2都繼承自同一個抽象基類IBase,而且IDerive1和IDerive2本身還是抽象基類,CMyObject類多繼承自IDerive1和IDerive2。
熟悉COM的朋友很自然的就會想到IBase就是COM中的IUnkown接口,而IDerive1和IDerive2就是COM中其他接口和自定義接口,而CMyObject就是COM中的"組件(Component)"。?之所以這樣設計的原因是熟悉COM的朋友對這樣的類的層次關(guān)系會感到很舒服,而且這樣的多繼承層次關(guān)系也是比較簡單的,便于分析。
在分析匯編代碼之前,我們還需要了解多繼承下類對象的內(nèi)存分布情況。多繼承下的類對象的內(nèi)存分布情況比較復雜,這也是為什么很多人說"不要隨便使用多繼承"。本文雖然使用了多繼承,但是類對象的內(nèi)存分布情況還是相對比較簡單和容易控制的,兩個基類都是抽象類,他們沒有數(shù)據(jù)成員,只有一個虛指針,而類對象本身也只有一個int型的成員變量。對于CMyObject對象的內(nèi)存分布情況,下面是我用VS2002調(diào)試器查看CMyObject對象的內(nèi)存分布情況的截圖:
?下面是我根據(jù)上面的截圖,并結(jié)合我自己對這部分內(nèi)容的理解,畫了一個簡圖:
| Pointer CMyObject vTable for IDerive1 |
| Pointer CMyObject vTable for IDerive2 |
| m_iValue |
?
下面就繼續(xù)我們的匯編分析。在這里我并不想分析所有的匯編代碼,原因之一就是有些匯編代碼和前一篇文章的代碼是一樣的,這里就不用羅嗦了。另一個原因就是我只關(guān)心和本文主題有關(guān)的內(nèi)容,那些和本文的主題沒有太多聯(lián)系的內(nèi)容就不會出現(xiàn)在我的討論中。
?一。派生類指針到基類指針的轉(zhuǎn)化。由CMyObject指針到IDerive1指針和IDerive2指針轉(zhuǎn)化的匯編代碼略有不同:
??;?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
通過比較我們發(fā)現(xiàn),當CMyObject類指針轉(zhuǎn)化成第二個基類IDerive2指針的時候,除了判斷了CMyObject類指針是否為空外,更重要的是,IDerive2指針的值是在CMyObject類指針值的基礎(chǔ)上多加了4個字節(jié)(一個指針的大小?)。仔細想像,這個不難理解:在多繼承的情況下,派生類對象的內(nèi)存分布是按照基類在派生類中聲明的順序來排列的,在本文中,按照聲明順序,obj的內(nèi)存分布應該也是基類IDerive1的數(shù)據(jù)成員,然后是IDerive2的數(shù)據(jù)成員,最后才是CMyObject的數(shù)據(jù)成員。由于IDerive1在最前面,而且只有一個虛指針,所以在指針轉(zhuǎn)化的過程中,IDerive1的指針值和CMyObject的指針值是一樣的,而IDerive2的指針值就要在CMyObject指針值的基礎(chǔ)上加4。
二。基類指針之間轉(zhuǎn)化。下面是由IDerive1指針轉(zhuǎn)化的IDerive2指針的匯編代碼:
????;pDerive2?=?(IDerive2*)pDerive1;????mov????eax,?DWORD?PTR?_pDerive1$[ebp]
????mov????DWORD?PTR?_pDerive2$[ebp],?eax
?感到奇怪的是,這里的轉(zhuǎn)化直接將IDerive1的指針賦給了IDerive2的指針。這樣的轉(zhuǎn)化合理么?根據(jù)上面的分析,我們知道IDerive1的地址和IDerive2的值應該是不相等的,它們之間差4個字節(jié),可是為什么這里編譯器卻將他們設為相等? 在這種情況下虛函數(shù)能正常調(diào)用么? 往下看看在說。
三。派生類的虛表。我奇怪的發(fā)現(xiàn),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'}
更奇怪的是,通過"反修飾"虛表的虛函數(shù)的符號名,我也得到兩套不同的符號名:
?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"是編譯器插入的一小段代碼,可以用來實現(xiàn)一些特殊的功能,例如在Win32環(huán)境下調(diào)用Win16 API,那在多繼承下的虛函數(shù)調(diào)用中,"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(一個指針的大小?),然后跳轉(zhuǎn)到另外一個函數(shù)。為什么是ECX?為什么是減4?ECX在虛函數(shù)調(diào)用的過程中不是存放this指針的寄存器么?結(jié)合著本文中的類的繼承層次關(guān)系,我開始慢慢的明白了"thunk"的任務。在多繼承的情況下,各基類指針的值應該是不一樣的,只有第一個基類的指針值和派生類類對象的首地址是一致的,其他的基類指針和派生類對象的首地址存在一個偏移。假如多個基類也都從一個共同的基類繼承而來,理論上說我們可以通過任何一個基類指針去調(diào)用這個共同基類的虛函數(shù),這個虛函數(shù)調(diào)用會被解析到派生類的虛函數(shù)實現(xiàn),而且派生類也只能有一個虛函數(shù)實現(xiàn)。為了使通過任何一個基類指針調(diào)用的虛函數(shù)都調(diào)用同一個函數(shù),我們只需要將這樣的虛函數(shù)調(diào)用"轉(zhuǎn)化"到通過第一個基類指針來調(diào)用就可以了,而在第一個基類的虛表中存放虛函數(shù)的實現(xiàn)。這個轉(zhuǎn)化的過程就是由"thunk"來完成的:它首先將基類指針調(diào)整到第一個基類的地址,也就是派生類對象的首地址,然后調(diào)用相應的虛函數(shù)。
有了這樣的分析,我們就可以畫出虛表的大致情況:
| &CMyObject::func1() |
| &CMyObject::func2()? |
| &CMyObject::foobar() |
?
?
CMyObject vTable for IDerive2
| &thunk for CMyObject::func1() |
| &thunk for CMyObject::func2() |
| &CMyObject::callme() |
接著再回到基類指針之間轉(zhuǎn)化的那個問題:
????pDerive2?=?(IDerive2*)pDerive1;????pDerive2->func2();
????pDerive2->callMe();
此時通過pDerive2能夠獲得虛表的應該是IDerive1的虛表,所以調(diào)用func2()的時候,應該沒有thunk發(fā)生的。而調(diào)用callMe()的時候?qū)嶋H上調(diào)用的是foobar(),應該它在IDerive1虛表中偏移量和callMe()在IDerive2虛表中的偏移量是一樣的。嗚!!!,這個是個錯誤么?是個Bug么?我也不知道。
11/04/2006 于家中
V1.1
還是基類指針之間轉(zhuǎn)化的問題
根據(jù)網(wǎng)友sting的回復,我也明白了這里為什么轉(zhuǎn)化不成功的原因。由于IDerive1和IDerive2之間并沒有什么繼承關(guān)系(雖然他們是另一個派生類的基類),編譯器就把他們當作兩個"毫無關(guān)系"的類,在轉(zhuǎn)化的過程中只能進行簡單的賦值,這樣的轉(zhuǎn)化形式在C++被定義為reinterpret_cast。
這里有兩個方法進行正確的轉(zhuǎn)化:
1。先將一個基類轉(zhuǎn)化到派生類,然后通過派生類再轉(zhuǎn)化到另一個基類。相應的代碼可以是這樣的:pDerive1 = static_cast<IDerive1*>( static_cast<CMyObject*>(pDerive2) );
pDerive2 = static_cast<IDerive2*>( static_cast<CMyObject*>(pDerive1) );
2。使用dynamic_cast來轉(zhuǎn)化。要想使dynamic_cast能夠正常的工作,我們需要開啟"運行時類型標識(RTTI)"。運行時類型標識為處于同一個繼承鏈上的所有類建立了一張"關(guān)系網(wǎng)",這樣任何兩個類之間就有了"千絲萬縷"的關(guān)系,這樣就為他們之間的直接轉(zhuǎn)化提供了可能。相應的代碼可以時這樣的:
pDerive1 = dynamic_cast<IDerive1*>(pDerive2);
pDerive2 = dynamic_cast<IDerive2*>(pDerive1);
11/11/2006 于家中??
今天是11月11日,光棍節(jié) 。雖然我不是光棍,但是正和女朋冷戰(zhàn)中 ,希望早日結(jié)束冷戰(zhàn) 。
附注:
1。在VS2002中,我們可以通過下面的方式開啟運行時類型標識:
? 在解決方案資源管理器中選擇Project --> C/C++ --> 語言 -->? 啟用運行時類型信息,選擇"是" --> 確定
總結(jié)
以上是生活随笔為你收集整理的透过汇编另眼看世界之多继承下的虚函数函数调用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: boost中unordered_map的
- 下一篇: 透过汇编另眼看世界之函数调用