午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

C++虛函數(shù)調(diào)用的反匯編解析

 astrotycoon 2015-08-27

C++虛函數(shù)調(diào)用的反匯編解析

   

 

作者:阮建輝

 

虛函數(shù)的調(diào)用如何能實(shí)現(xiàn)其“虛”?作為C++多態(tài)的表現(xiàn)手段,估計很多人對其實(shí)現(xiàn)機(jī)制感興趣。大約一般的教科書就說到這個C++強(qiáng)大機(jī)制的時候,就是教大家怎么用,何時用,而不會去探究一下這個虛函數(shù)的真正實(shí)現(xiàn)細(xì)節(jié)。(當(dāng)然,因?yàn)椴煌木幾g器廠家,可能對虛函數(shù)有自己的實(shí)現(xiàn),呵呵,這就算是虛函數(shù)對于編譯器的“多態(tài)”了:)。 作為編譯型語言,C++編譯的最后結(jié)果就是一堆匯編指令了(這里不同于.NETCLR)。今天,我就來揭開它的神秘面紗,從匯編的層面來看看虛函數(shù)到底怎么實(shí)現(xiàn)的。讓大家對虛函數(shù)的實(shí)現(xiàn)不僅知其然,更知其所以然。(本文程序環(huán)境為:PC + Windows XP Pro + Visual C++6.0,文中所得出來的結(jié)果和反映的編譯器策略也只針對VC6.0的編譯器)

 

先看一段簡單代碼:

Code Segment:

Line01:  #include <stdio.h>

Line02:

Line03:  class Base {

Line04:  public:

Line05:      void __stdcall Output() {

Line06:          printf("Class Base/n");

Line07:      }

Line08:  };

Line09: 

Line10:  class Derive : public Base {

Line11:  public:

Line12:      void __stdcall Output() {

Line13:          printf("Class Derive/n");

Line14:      }

Line15:  };

Line16: 

Line17:  void Test(Base *p) {

Line18:      p->Output();

Line19:  }

Line20: 

Line21:  int __cdecl main(int argc, char* argv[]) {

Line22:      Derive obj;

Line23:      Test(&obj);

Line24:      return 0;

Line25:  }   

 

程序的運(yùn)行結(jié)果將是:

Class Base

 

那么將Base類的Output函數(shù)聲明(Line05)更改為:

virtual void __stdcall Output() {

那么,很明顯地,程序的運(yùn)行結(jié)果將是:

Class Derive

 

Test函數(shù)這回算是認(rèn)清楚了這個指針是一個指向Derive類對象的指針,并且正確的調(diào)用了其Output函數(shù)。編譯器如何做到這一切的呢?我們來看看沒有“virtual”關(guān)鍵字和有“virtual”關(guān)鍵字,其最終的匯編代碼區(qū)別在那里。

 

(在講解下面的匯編代碼前,讓我們對匯編來一個簡單掃描。當(dāng)然,如果你對匯編已經(jīng)很熟練,那么goto到括號外面吧^_^。先說說上面的Output函數(shù)被聲明為__stdcall的調(diào)用方式:它表示函數(shù)調(diào)用時,參數(shù)從右到左進(jìn)行壓棧,函數(shù)調(diào)用完后由被調(diào)用者恢復(fù)堆棧指針esp。其它的調(diào)用方式在文中描述。所謂的C++this指針:也就是一個對象的初始地址。在函數(shù)執(zhí)行時,它的參數(shù)以及函數(shù)內(nèi)的變量將擁有如下所示的堆棧結(jié)構(gòu):

Picture1

(圖1

如上圖1所示,我們的參數(shù)和局部變量在匯編中都將以ebp加或者減多少來表示。你可能會有疑問了:有時候我的參數(shù)或者局部變量可能是一個很大的結(jié)構(gòu)體或者只是一個char,為什么這里ebp加減的都是4的倍數(shù)呢?恩,是這樣的,對于32位機(jī)器來說,采用4個字節(jié),也就是每次傳輸32位,能夠取得最佳的總線效率。如果你的參數(shù)或者局部變量比4個字節(jié)大,就會被拆成每次傳4個字節(jié);如果比4個字節(jié)小,那還是每次傳4個字節(jié)。再簡單解釋一下下面用到的匯編指令,這些指令都是見名知意的哦:

mov destination,source

source的值賦給destination。注意,下面經(jīng)常用到了“[xxx]”這樣的形式,“xxx”對應(yīng)某個寄存器加減某個數(shù),“[xxx]”表示是取“xxx”的值對應(yīng)的內(nèi)存單元的內(nèi)容。好比“xxx”是一把鑰匙,去打開一個抽屜,然后將抽屜里的東西取出來給別人,或者是把別人給的東西放到這個抽屜里;

lea destination,[source]

source的值賦給destination。注意,這個指令就是把sourcedestination,而不會去把source對應(yīng)的內(nèi)存單元的內(nèi)容賦給destination。好比是它就把鑰匙給別人了;

在調(diào)試時如果想查看反匯編的話,你應(yīng)該點(diǎn)擊圖2下排最右的按鈕。

Picture2
(圖2

其它指令我估計你從它的名字都能知道它是干什么的了,如果想知道其具體意思,這個應(yīng)該參考匯編手冊。:)

 

. 沒有virtual關(guān)鍵字時:

1  main函數(shù)的反匯編內(nèi)容:

Line22:       Derive obj;

Line23:       Test(&obj);

//如果你把斷點(diǎn)設(shè)置在22行,開始調(diào)試的時候VC會告訴你這是一個無效行,而把斷

//點(diǎn)自動移到下一行(Line23),這是因?yàn)榇a中沒有為Derive以及其基類定義構(gòu)造函

//數(shù),而且編譯器也沒有為它生成一個默認(rèn)的構(gòu)造函數(shù)的緣故,此行C++代碼不會生成

//任何可實(shí)際調(diào)用的匯編指令;

004010D8   lea         eax,[ebp-4]

//將對象obj的地址放入eax寄存器中;

004010DB   push        eax

//將參數(shù)入棧;

004010DC   call        @ILT+5(Test) (0040100a)

//調(diào)用Test函數(shù);

//這里@ILT+5就是跳轉(zhuǎn)到Test函數(shù)的的jmp指令的地址,一個模塊中所有的

//函數(shù)調(diào)用都會是象這樣@ILT+5*n,n表示這個模塊中的第n個函數(shù),而ILT的意思

//Import Lookup Table,程序調(diào)用函數(shù)的時候就是通過這個表來跳轉(zhuǎn)到相應(yīng)函數(shù)而執(zhí)

//行代碼的。

004010E1   add         esp,4

//調(diào)整堆棧指針,剛才調(diào)用了Test函數(shù),調(diào)用方式__cdecl, 由調(diào)用者來恢復(fù)堆棧指針;

2  Test函數(shù)的反匯編內(nèi)容:

Line18:       p->Output();

00401048   mov         eax,dword ptr [ebp+8]

//這里的[ebp+8]其實(shí)就是Test函數(shù)最左邊的參數(shù),就是上面main函數(shù)中壓棧的eax

//將參數(shù)的值(也就是上面的main函數(shù)中的obj對象的地址)放入eax寄存器中。

//注意:對于C++類的成員函數(shù),默認(rèn)的調(diào)用方式為“__thiscall”,這不是一個由程

//序員指定的關(guān)鍵字,它所表示的的函數(shù)調(diào)用,參數(shù)壓棧從右向左,而且使用ecx寄存

//器來保存this指針。這里我們的Output函數(shù)的調(diào)用方式為“__stdcall”,ecx寄存器

//并不被使用來保存this指針,所以得有額外的指令將this指針壓棧,如下句:

0040104B   push        eax

//eax入棧,也就是下面調(diào)用Output函數(shù)需要的this指針了;

0040104C   call        @ILT+0(Base::Output) (00401005)

//調(diào)用類的成員函數(shù),沒有任何懸念,老老實(shí)實(shí)地調(diào)用Base類的Output函數(shù);

 

. virtual關(guān)鍵字時:

1  main函數(shù)的反匯編內(nèi)容:

Line22:       Derive obj;

//在有virtual關(guān)鍵字的時候,把斷點(diǎn)設(shè)置在22行,調(diào)試時就會停在此處了。我們沒有

//Derive類或者它的基類聲明構(gòu)造函數(shù),這說明編譯器自動為類生成了一個構(gòu)造函

//數(shù),下面我們就可以看看編譯器自動生成的這個構(gòu)造函數(shù)干了什么;

00401088   lea         ecx,[ebp-4]

//將對象obj的地址放入ecx寄存器中,為什么呢?上面說了哦~

0040108B   call        @ILT+25(Derive::Derive) (0040101e)

//編譯器幫忙生成了一個構(gòu)造函數(shù),它在這里干了什么呢?等會再說吧,作個記號先://@_@1;上面要把obj的地址放入ecx中就是為這個函數(shù)調(diào)用做準(zhǔn)備的;

Line23:       Test(&obj);

//這個調(diào)用操作跟上面的沒有virtual關(guān)鍵字時是一樣的:

00401090   lea         eax,[ebp-4]

00401093   push        eax

00401094   call        @ILT+5(Test) (0040100a)

004010C9   add         esp,4

2  Test函數(shù)的反匯編內(nèi)容(跟上面的沒有virtual關(guān)鍵字時可是大不一樣哦):

Line18:       p->Output();

00401048   mov         eax,dword ptr [ebp+8]

//Test的第一個參數(shù)的值放入eax寄存器中,其實(shí)你應(yīng)該已經(jīng)知道了,這就是obj//地址了;

0040104B   mov         ecx,dword ptr [eax]

//喔噢,將eax寄存器中存的數(shù)對應(yīng)的地址的內(nèi)容取出來,你知道這是什么嗎?等會再//說,做個記號先: @_@2

0040104D   mov         esi,esp

//這個是用來做esp指針檢測的

0040104F   mov         edx,dword ptr [ebp+8]

//又把obj的地址存放到edx寄存器中,你該知道,其實(shí)就是this指針,而這個就是為  //調(diào)用類的成員函數(shù)做準(zhǔn)備的;

00401052   push        edx

//將對象指針(也就是this指針)入棧,為調(diào)用類的成員函數(shù)做準(zhǔn)備;

00401053   call        dword ptr [ecx]

//這個調(diào)用的就是類的成員函數(shù),你知道調(diào)用的哪個函數(shù)嗎?等會再說,做個記號先:

//@_@3

00401055   cmp         esi,esp

//比較esp指針的,要是不相同,下面的__chkesp函數(shù)將會讓程序進(jìn)入debug

00401057   call        __chkesp (00401110)

//檢測esp指針,處理可能出現(xiàn)的堆棧錯誤(如果出錯,將陷入debug)。

 

對一個C++類,如果它要呈現(xiàn)多態(tài)(一般的編譯器會將這個類以及它的基類中是否存在virtual關(guān)鍵字作為這個類是否要多態(tài)),那么類會有一個virtual table,而每一個實(shí)例(對象)都會有一個virtual pointer(以下簡稱vptr)指向該類的virtual function table,如圖3所示:

 

(下面右邊表格中的VFuncAddr應(yīng)該被理解為存放虛函數(shù)地址的內(nèi)存單元的地址才準(zhǔn)確。更準(zhǔn)確地說,應(yīng)該是跳轉(zhuǎn)到相應(yīng)函數(shù)的jmp指令的地址。)

Picture3

(圖3

先來分析我們的main函數(shù)中的Derive類的對象obj,看看它的內(nèi)存布局,由于沒有數(shù)據(jù)成員,它的大小為4個字節(jié),只有一個vptr,所以obj的地址也就是vptr的地址了。(之所以我這里舉例的類沒有數(shù)據(jù)成員,因?yàn)椴煌木幾g器將vptr放置的位置在對象內(nèi)存布局中有可能不一樣,當(dāng)然,一般不是放在對象的頭部,比如微軟編譯器;就是放在對象的尾部。不管哪種情況,對于這個例子,我這里的“obj的地址也就是vptr的地址”都是成立的。)

一個對象的vptr并不由程序員指定,而是由編譯器在編譯中指定好了的。那么現(xiàn)在讓我來分別解釋上文中標(biāo)記的@_@1 - @_@ 3

 

@_@1

也就是要解釋這里為什么編譯器會為我們生成一個默認(rèn)的構(gòu)造函數(shù),它是用來干什么的?還是讓我們從反匯編里尋找答案:

 

這是由編譯器默認(rèn)生成的Derive的構(gòu)造函數(shù)中選取出來的核心匯編片段:

004010D9   pop         ecx

//編譯器默認(rèn)生成的Derive的構(gòu)造函數(shù)的調(diào)用方式為__thiscall,所以ecx寄存器,如前

//所說,保存的就是this指針,也就是obj對象的地址,在這里也是vptr的地址了;

//我發(fā)現(xiàn)即使你把一個構(gòu)造函數(shù)聲明為__stdcall,它跟默認(rèn)的__thiscall的反匯編也是一

//樣的,這一點(diǎn)跟成員函數(shù)是不一樣的;

004010DA   mov         dword ptr [ebp-4],ecx

//對于__thiscall方式調(diào)用的類的成員函數(shù),第一個局部變量總是this指針,ebp-4就是

//函數(shù)的第一個局部變量的地址

004010DD   mov         ecx,dword ptr [ebp-4]

//因?yàn)橐{(diào)用基類的構(gòu)造函數(shù),所以又得把this指針賦給ecx寄存器了;

004010E0   call        @ILT+30(Base::Base) (00401023)

//執(zhí)行基類的構(gòu)造函數(shù);

004010E5   mov         eax,dword ptr [ebp-4]

//this指針放入eax寄存器;

004010E8   mov         dword ptr [eax],offset Derive::`vftable' (0042201c)

//將虛函數(shù)表的首地址放入this指針?biāo)赶虻牡刂?,也就是初始化?/span>vptr了;

 

大家看到了吧,編譯器生成一個默認(rèn)的構(gòu)造函數(shù),就是用來初始化vptr的;那么你大概也能想到其實(shí)Base的構(gòu)造函數(shù)做了什么了,不出你所料,它也是用來做初始化vptr的:

0040D769   pop         ecx

0040D76A   mov         dword ptr [ebp-4],ecx

0040D76D   mov         eax,dword ptr [ebp-4]

0040D770   mov         dword ptr [eax],offset Base::`vftable' (00422020)

不用再解釋了,跟Derive的構(gòu)造函數(shù)功能一樣,初始化vptr了。如果你自己聲明和定義了一個構(gòu)造函數(shù)的話,將先執(zhí)行這些初始化vptr的代碼后,再會來執(zhí)行你的代碼了。(如果你在構(gòu)造函數(shù)中有作為構(gòu)造函數(shù)的初始化列表形式出現(xiàn)的賦值代碼,那么將先執(zhí)行你的初始化列表中的賦值代碼,然后再執(zhí)行本類的vptr的初始化操作,再執(zhí)行構(gòu)造函數(shù)體內(nèi)的代碼)

 

@_@2 @_@ 3:

00401048   mov         eax,dword ptr [ebp+8]

0040104B   mov         ecx,dword ptr [eax]

這里前一條指令是將obj的地址存放入eax中,那么你該知道obj地址對應(yīng)的內(nèi)存單元的前四個字節(jié)其實(shí)就是vptr地址?而vptr地址所對應(yīng)的內(nèi)存單元的內(nèi)容其實(shí)就是vftable表格的起始地址,而vftable表格地址所對應(yīng)的內(nèi)存單元的內(nèi)容就是虛函數(shù)地址。用下圖更清楚地表示一下吧(如圖4,該圖表示地址和地址單元中的內(nèi)容對應(yīng)表。注意,右邊的vftable表中的地址,其實(shí)并不是真正的函數(shù)地址,而是跳轉(zhuǎn)到函數(shù)的jmp指令的地址,如0x0040EF12,并不是真正的Class::XXX函數(shù)的地址,而是跳轉(zhuǎn)到Class::XXX函數(shù)的jmp指令的地址)。這樣ecx其實(shí)就是存放Derive::Output函數(shù)地址的內(nèi)存單元的地址,然后調(diào)用:

0040104F   mov         edx,dword ptr [ebp+8]

00401052   push        edx

00401053   call        dword ptr [ecx]

就跳轉(zhuǎn)到相應(yīng)函數(shù)執(zhí)行該函數(shù)了。

(如果有多個虛函數(shù),且調(diào)用的是第N個虛函數(shù),那么上句call指令就會被更改為這樣的形式:call dword ptr [ecx+4*(N-1)]

 

上面的匯編是不是象這樣:我拿到一把鑰匙,打開一個抽屜,取出里面的東西,不過這個東西還是一把鑰匙,還得拿著這個鑰匙去打開另一個抽屜,取出里面真正的東西。^_^

Picture4

(圖4

知道了來龍去脈,別人這么調(diào)用用匯編能做到調(diào)用相應(yīng)的虛函數(shù),那么我如果要用C/C++,該怎么做呢?我想你應(yīng)該有眉目了吧。看看我是怎么干的(下面用一個C的函數(shù)指針調(diào)用了一個C++類的成員函數(shù),將一個C++類的成員函數(shù)轉(zhuǎn)換到一個C函數(shù),需要做這些:C函數(shù)的參數(shù)個數(shù)比相應(yīng)的C++類的成員函數(shù)多出一個,而且作為第一個參數(shù),而且它必須是類對象的地址):

Base類的Output函數(shù)聲明為virtual,然后將main函數(shù)更改為:

int __cdecl main(int argc, char* argv[]) {

    Derive obj;                                                               //對象還是要有一個的

    typedef void (__stdcall *PFUNC)(void*);              //聲明函數(shù)指針

void *pThis = &obj;                                                //取對象地址,作為this指針用

                                                                                 //對應(yīng)圖4是將0x0012ff24賦給pThis

PFUNC pFunc = (PFUNC)*(unsigned int*)pThis; //取這個地址的內(nèi)容,對應(yīng)圖4就應(yīng)

                                                                                 //該是取地址0x0012ff24的內(nèi)容為

                                                                                 //0x00400112

pFunc = (PFUNC)*(unsigned int*)pFunc;              //再取這個地址的內(nèi)容,對應(yīng)圖4

                                                                                 //應(yīng)該是取地址0x00400112的內(nèi)容為

                                                                                 //0x0040EF12,也就是函數(shù)地址了

    pFunc(pThis);                                                          //執(zhí)行函數(shù),將執(zhí)行Derive::Output

    return 0;

}

運(yùn)行一下,看看結(jié)果。我可沒有使用對象或者指向類的指針去調(diào)用函數(shù)哦。J

 

這回你該知道虛函數(shù)是怎么回事了吧?這里介紹的都是基于微軟VC++ 6.0編譯器對虛函數(shù)的實(shí)現(xiàn)手段。編譯器實(shí)現(xiàn)C++所使用的方法和策略,都是可以從其反匯編語句中一探究竟的。了解這些底層細(xì)節(jié),將會對提高你的C/C++代碼大有裨益!希望本文能對你有所幫助。任何問題或者指教,請mailto:tigger_211@sina.com。

。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多