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

分享

C++虛函數(shù)表與對(duì)象布局(轉(zhuǎn))

 昵稱2567208 2010-08-05
每個(gè)含有虛函數(shù)的類有一張?zhí)摵瘮?shù)表(vtbl),表中每一項(xiàng)指向一個(gè)虛函數(shù)的地址,實(shí)現(xiàn)上是一個(gè)函數(shù)指針的數(shù)組。

  虛函數(shù)表既有繼承性又有多態(tài)性。每個(gè)派生類的vtbl繼承了它各個(gè)基類的vtbl,如果基類vtbl中包含某一項(xiàng),則其派生類的vtbl中也將包含同樣的一項(xiàng),但是兩項(xiàng)的值可能不同。如果派生類重載(override)了該項(xiàng)對(duì)應(yīng)的虛函數(shù),則派生類vtbl的該項(xiàng)指向重載后的虛函數(shù),沒有重載的話,則沿用基類的值。

  在類對(duì)象的內(nèi)存布局中,首先是該類的vtbl指針,然后才是對(duì)象數(shù)據(jù)。在通過對(duì)象指針調(diào)用一個(gè)虛函數(shù)時(shí),編譯器生成的代碼將先獲取對(duì)象類的vtbl指針,然后調(diào)用vtbl中對(duì)應(yīng)的項(xiàng)。對(duì)于通過對(duì)象指針調(diào)用的情況,在編譯期間無法確定指針指向的是基類對(duì)象還是派生類對(duì)象,或者是哪個(gè)派生類的對(duì)象。但是在運(yùn)行期間執(zhí)行到調(diào)用語句時(shí),這一點(diǎn)已經(jīng)確定,編譯后的調(diào)用代碼能夠根據(jù)具體對(duì)象獲取正確的vtbl,調(diào)用正確的虛函數(shù),從而實(shí)現(xiàn)多態(tài)性。分析一下這里的思想所在,問題的實(shí)質(zhì)是這樣,對(duì)于發(fā)出虛函數(shù)調(diào)用的這個(gè)對(duì)象指針,在編譯期間缺乏更多的信息,而在運(yùn)行期間具備足夠的信息,但那時(shí)已不再進(jìn)行綁定了,怎么在二者之間作一個(gè)過渡呢?把綁定所需的信息用一種通用的數(shù)據(jù)結(jié)構(gòu)記錄下來,該數(shù)據(jù)結(jié)構(gòu)可以同對(duì)象指針相聯(lián)系,在編譯時(shí)只需要使用這個(gè)數(shù)據(jù)結(jié)構(gòu)進(jìn)行抽象的綁定,而在運(yùn)行期間將會(huì)得到真正的綁定。這個(gè)數(shù)據(jù)結(jié)構(gòu)就是vtbl??梢钥吹?,實(shí)現(xiàn)用戶所需的抽象和多態(tài)需要進(jìn)行后綁定,而編譯器又是通過抽象和多態(tài)而實(shí)現(xiàn)后綁定的。

  下面說一下多重繼承。多重繼承的兩個(gè)基類如果繼承了同一個(gè)類,則其派生類相當(dāng)于繼承了該類兩次,vtbl也繼承了兩次。對(duì)象布局中,該類的數(shù)據(jù)有兩份,vtbl指針有兩個(gè),分別指向兩次被繼承的vtbl。但派生類重載該類的虛函數(shù)時(shí)只能重載一次,那么重載后的函數(shù)地址將占據(jù)vtbl的哪個(gè)位置?通過寫程序測(cè)試,我覺得應(yīng)該是同時(shí)出現(xiàn)在所繼承的兩個(gè)vtbl的相應(yīng)位置,有待進(jìn)一步驗(yàn)證。

  說到虛函數(shù)機(jī)制,對(duì)象指針的類型轉(zhuǎn)換也是要弄清的,這里就不說了。還有一個(gè)this指針的問題,提一下。虛函數(shù)調(diào)用的時(shí)候也是需要傳遞this指針的,這沒什么奇怪,但是這時(shí)的this指針就隱含著一個(gè)問題,它要和實(shí)際調(diào)用的虛函數(shù)相一致,即this指針也要實(shí)現(xiàn)多態(tài)性。在多重繼承的情況下,這個(gè)問題不是那么簡(jiǎn)單的,請(qǐng)參考[《C++語言的設(shè)計(jì)和演化》p203]。

  C++虛函數(shù)表深度分析

  昨天聽完彭老師的C++的講座,感覺很不錯(cuò),但之后留了一個(gè)疑問,就是關(guān)于虛函數(shù)表的機(jī)制,課下和彭老師的討論似乎也沒能完全解惑,我的疑問主要就是:

  1:虛函數(shù)表到底是怎么工作的,for類,還是for對(duì)象

  2:如果for類,那么基類和派生類是共用一表,還是各有各的表(物理上)

  3:如果共用一表的話,總是后面的覆蓋前面的函數(shù)地址,那不是很容易出現(xiàn)混亂嗎?

  帶著這三個(gè)疑問,趁著熱呼勁,我搜了搜關(guān)于虛函數(shù)表的DASM的文章,當(dāng)然了,能搜到的幾篇都是for VC編譯器的

  初步得出了以前結(jié)論:

  1:虛表(虛函數(shù)表)是for類的

  2:基類和派生類是各有各的表,也就是說他們的物理地址是分開的,基類和派生類的虛表的唯一關(guān)聯(lián)是:當(dāng)派生類沒有實(shí)現(xiàn)

  基類虛函數(shù)的重載時(shí),派生類會(huì)直接把自己表的該函數(shù)地址值寫為基類的該函數(shù)地址值.

  3:任何一個(gè)有虛表的類,在實(shí)例化時(shí)不允許其虛表內(nèi)有項(xiàng)為空->純虛類不能初始化對(duì)象

  4:帶虛表的類在對(duì)象構(gòu)造函數(shù)中,會(huì)把一個(gè)指針指向該類虛表地址,我在這給它起個(gè)名字叫vp;

  5:僅對(duì)于VC和BC兩種編譯器論,如果該類帶有虛表,那么該類的對(duì)象的首地址就是虛表地址,也是this指針指向虛表

  下面我就用IDE Borland C++ Builder 6.0 sp4,編譯器版本Borland C++ 5.5,來驗(yàn)證一下:

  首先打開BCB6建立一個(gè)控制臺(tái)程序,寫上下面幾個(gè)備用類

  #include <conio.h>

  #include <stdio.h>

  #pragma hdrstop

  #pragma argsused

  class A

  {

  public:

  __stdcall A()

  {

  }

  virtual void __stdcall output()

  {

  printf("Class An");

  }

  virtual void __stdcall output2()

  {

  }

  };

  class B :public A

  {

  public:

  void __stdcall output()

  {

  printf("Class Bn");

  }

  };

  class C:public A

  {

  public :

  void __stdcall output()

  {

  printf("Class Cn");

  }

  };

  幾個(gè)類很簡(jiǎn)單,B和C是A的派生

  下面先寫一個(gè)引子主程序,用來驗(yàn)證虛表的存在:

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

  {

  B b;

  printf("%d",sizeof(b));

  }

  結(jié)果是8

  我把A類的兩個(gè)virtual都去掉后再運(yùn)行一次

  結(jié)果是4

  這說明了有virtual比沒virtual的對(duì)象多了32位,在win32中,32位正好是一個(gè)地址,那么這個(gè)地址就應(yīng)該指向的是虛表

  看來虛表果然存在,那么虛表指針是在對(duì)象什么時(shí)候生成的呢?我改一下main函數(shù)

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

  {

  A *pa;

  B b;

  C c;

  A a;

  pa=&b;

  pa->output();

  getch();

  return 0;

  }

  這應(yīng)該是一個(gè)很經(jīng)典的教科書上講多態(tài)的例子,如果有virtual輸出Class B,如果沒virtual輸出Class A

  現(xiàn)在看一下這段代碼的反編譯代碼,我把BCB6的full debug模式打開,在 B b; 處設(shè)斷點(diǎn)

  圖片

  我們可以看到在b執(zhí)行完基類的構(gòu)造函數(shù)后,執(zhí)行了

  mov edx,0x0040c114

  mov [ebp-0x0x],edx

  而這兩句話經(jīng)驗(yàn)證,在沒有virtual關(guān)鍵字時(shí)是沒有的,讓我們記住0x0040c114這個(gè)地址先

  [ebp-0x0x]是this指針,我們目前猜測(cè)這段話就是把虛表的地址寫入this指針

  我們?cè)倏碈 c;后的反編譯代碼

  mov eax,0x0040c0f8

  mov [ebp-0x14],eax

  看來不同的類具有不同的虛表地址,也就是不同的類的表從物理上是不同的

  我們現(xiàn)在來探討虛表工作的原理

  我們對(duì)比一下pa->output()在有沒有virtual修飾時(shí)候的區(qū)別

  mov eax,[ebp-0x04]

  push eax

  mov edx,[eax]

  call dword ptr [edx]

  這是有virtual的

  push dword ptr [ebp-0x04]

  call A::output();

  這是沒有virtual的

  我們分析一下asm代碼,可以得出虛表的過程,先把根據(jù)this地址得到虛表地址,然后由虛表項(xiàng)里存放的函數(shù)指針地址,訪問

  相應(yīng)的函數(shù),如果有多個(gè)虛函數(shù),且調(diào)用的是第N個(gè)虛函數(shù),那么上句call指令就會(huì)被更改為這樣的形式:call dword ptr

  [edx-4*(N-1)])

  一上是我們對(duì)dasm代碼做的一些推測(cè),一會(huì)兒我們還要進(jìn)一步驗(yàn)證這些

  我們仔細(xì)看反編譯的結(jié)果,發(fā)現(xiàn)在A a;的dasm結(jié)果中,好象沒有vp初始化的一步,我查了其他文獻(xiàn)針對(duì)VC編譯器的dasm結(jié)

  果,發(fā)現(xiàn)VC編譯器的dasm結(jié)果里是有初始化vp的一步的,類似

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

  我現(xiàn)在就得出這樣一個(gè)結(jié)論,在BC編譯器中很可能對(duì)于基類的對(duì)象構(gòu)造函數(shù)作出了這樣的優(yōu)化,就是默認(rèn)把this指針指向

  虛表地址,所以我們看不到這樣的dasm結(jié)果

  我還發(fā)現(xiàn),對(duì)于類的構(gòu)造函數(shù)處理,VC和BC的編譯器也是不一樣的

  如果我們?cè)陬惱锩鏇]有寫構(gòu)造函數(shù),VC會(huì)自動(dòng)為我們加一個(gè)構(gòu)造函數(shù),比如

  class Base {

  public:

  void __stdcall Output() {

  printf("Class Basen");

  }

  };

  我們得到這樣的dasm:

  004010D9 pop ecx

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

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

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

  可以看到自動(dòng)生成構(gòu)造函數(shù)地址

  但在BC中,我們沒有看到這樣的代碼

  當(dāng)我們把上面的A類里面的構(gòu)造函數(shù)刪去后,這是得到的A a;的dasm

  mov edx, 0x0040c0f0

  mov [ebp-0x04],ecx

  完全找不到構(gòu)造函數(shù)的影子,我猜測(cè)這也是編譯器對(duì)構(gòu)造函數(shù)所作出的優(yōu)化

  我這里不評(píng)價(jià)兩種編譯器在這問題上的優(yōu)次,我繼續(xù)回到正題,驗(yàn)證我們的結(jié)論的正確性

  因?yàn)榘凑瘴覀兊耐茰y(cè),0x0040c114就是虛表地址

  那么按照此理,我們通過訪問虛表地址的內(nèi)容里的第一個(gè)函數(shù)地址,就能訪問output函數(shù),而虛表的地址就是this地址,是這樣

  嗎,我再編了個(gè)main函數(shù)

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

  {

  A *pa;

  B b;

  C c;

  A a;

  //pa=&b;

  //pa->output();

  //printf("%d",sizeof(b));

  typedef void (__stdcall *PF)(void);

  void *pthis=&b;

  PF pf=(PF)(*(unsigned int*)pthis);

  printf("%x",pf);

  printf("n");

  pf=(PF)(*(unsigned int*)pf);

  pf();

  getch();

  return 0;

  }

  先來解釋一下這段代碼

  typedef void (__stdcall *PF)(void);

  聲明了配搭output的函數(shù)指針

  void *pthis=&b;

  用來得到b的this地址,它是指向虛表地址的

  PF pf=(PF)(*(unsigned int*)pthis);

  用來得到this地址的內(nèi)容,也就是虛表地址

  然后我們把虛表地址輸出

  pf=(PF)(*(unsigned int*)pf);

  用來得到虛表里第一項(xiàng)的內(nèi)容,也就是output的地址(表第一項(xiàng)目地址=表地址)

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

  我們來看結(jié)果

  成功了!!!

  雖然我們沒有在代碼里寫output();但執(zhí)行結(jié)果就是輸出了output的結(jié)果

  另外輸出的虛表地址就是0x0040c114,也就是我們最早推測(cè)的虛表地址!!!

  我把代碼改下一下,按照我們的推測(cè),如果把表第一項(xiàng)地址偏移32位,應(yīng)該就是表第二項(xiàng)地址,而第二項(xiàng)的內(nèi)容就應(yīng)該是

  output2的地址,驗(yàn)證一下:

  typedef void (__stdcall *PF)(void);

  void *pthis=&b;

  PF pf=(PF)(*(unsigned int*)pthis);

  printf("%x",pf);

  printf("n");

  pf=(PF)(*( (unsigned int*)pf-0x04 ) );

  pf();

  完全不出我們所料,輸出就是Class A output2

  到這里,應(yīng)該對(duì)虛表的機(jī)制很清楚了,每個(gè)類都有各的虛表,每個(gè)類生成的各對(duì)象分別把this指向類的虛表地址,如果本類沒

  有重載基類的虛函數(shù),那么虛表的該項(xiàng)會(huì)寫為基類的該項(xiàng)的內(nèi)容,在調(diào)用虛表的時(shí)候,會(huì)根據(jù)虛表地址做適當(dāng)?shù)钠埔缘玫?

  相應(yīng)的虛函數(shù)地址,再進(jìn)行調(diào)用.

  先分析到這,以后我會(huì)就修改虛表地址,以及如何應(yīng)用虛表做hook,繼續(xù)分析

  

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

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多