VC下的函式位址

2021-04-09 08:51:44 字數 4207 閱讀 7934

vc下的函式位址

最近突然有一位同事問我關於虛擬繼承(virtual inheritance)的問題,我記得在《虛擬與多型》(繁體版,2023年)裡讀到過,也許當時讀的匆忙,一知半解的,所以現在也答不清楚。於是,我又拿起這本書重新讀了第二章c++物件模型。這一次我讀的仔細多了。在這章的結尾作者,侯捷老師留下了乙個關於函式位址的疑問。在網上搜了一下,沒有發現有人解答過這個問題,正好最近比較空,所以就下決心研究了一番。

1.從vtbl觀察到的virutal member function的位址。這個位址可以用程式的方法得到,也可以使用偵錯程式直接觀察得到。我使用後者。

2.在偵錯程式中直接把游標移到member function的名稱上,或者在watch視窗裡,直接輸入class::func(比如:a::func1),觀察所得。我使用後者。

3.在程式中直接取得member function的位址。

書中留下的問題是,對於同乙個函式,有時這三個位址不相同。準確地說,如果是virtual member function,這三個位址總是不同的。如果是non-virtual member function,2和3也不相同。

先說說我的實驗結果(所有實驗都是在vc6上做的):每乙個函式,不管是non-virtual member function,或是virtual member function,或是static member function,編譯器都會為它生成一組**,這組**的第一條指令的位置,就是函式的位址,姑且稱它為函式的實體地址(body address)。這就是使用第二種方法取得的位址。

但是,當程式的其他部分要呼叫某個函式的時候,編譯器生成的**不會直接使用函式的實體地址,而是使用乙個另乙個位址,姑且稱它為函式的符號位址(symbol address)。每當需要呼叫某個函式,無論是non-virtual member function,static function,或是virtual member function,編譯器生成的**都是去呼叫函式的符號位址。在這個位址裡,只有一條指令,就是跳轉到函式的實體地址。

其實,通過跟蹤我發現,編譯器在記憶體的某個位置生成了一張**(函式的入口表)。這個**的每一項就是乙個函式的符號位址,而**每一項裡的內容,就是一條跳轉指令,跳轉到相應的函式的實體地址。所以每一項裡都是「e9 xx xx xx xx」的形式。

採用這種間接的、**驅動的函式呼叫方法,我推測這與編譯器(compiler)和聯結器(linker)的實現方法有關。使用這種方法,編譯器在生成呼叫**的時候可以不知道函式的實體地址,先使用函式的符號位址。待到函式的實體被編譯後,在鏈結(link)過程時,再把函式的實體地址,以near jmp指令的形式寫入函式入口表中相應的項,這樣即使某個函式在多處被呼叫,最後也只需要修改一處即可。

當使用第一種方法檢視virtual member function的位址時,得到就是函式的符號位址。當使用第三種方法取得non-virtual member function的位址時,得到也是函式的符號位址。但是當使用該方法取得virtual member function的位址時,得到的卻是vcall thunk函式的符號位址,這裡仍然使用了間接的呼叫方法。

使用vcall thunk可以使編譯器在生成**時,無需關心function ptr指向的函式是non-virtual member function還是virtual member function,都使用相同的呼叫方法。這種方法的過程大致如此:

1.暫存器準備

2.從右向左依次把引數壓棧

3.this指標放入ecx暫存器

4.call 函式指標

對於virtual member function,函式指標指向的是vcall thunk函式的符號位址,由vcall thunk來呼叫真正的virtual member function。由於ecx中已經儲存了this指標,通過它可以得到vtbl,所以只要知道virtual member function在vtbl中的index,vcall thunk就可以呼叫這個virtual member function。而這個index資訊由編譯器直接放在vcall thunk的**中。所以,假設要取得a::say,b::tell和c::talk三個虛函式的位址,a、b、c三個類沒有任何關係,say和tell在vtbl中的index是0,talk在vtbl中的index是1。那麼編譯器只會生成兩個vcall thunk:vcall』}』和vcall』}』。顯然,其中0和4正好是index*4,至於flat的含義我不是很清楚。say和tell都會使用第乙個 thunk,talk會使用第二個thunk。因此,我們會發現,通過程式的方法取得的say和tell函式的位址總是相同的。

vcall thunk的實現非常簡單,以vcall』}』為例:

004011a0 8b 01                mov         eax,dword ptr [ecx]

004011a2 ff 60 08             jmp         dword ptr [eax+4]

第一行**把vtbl的指標裝入eax;第二行**跳轉到index為1的virtual member function的符號位址處,eax+4正好是vtbl中的第二項。

接下來說說我的實驗過程:

定義兩個類:a和b,b從a派生而來: a

bclass a

virtual void vfunca()

virtual void vfuncb()

};class b:public a

virtual void vfuncb()};

接下來是main()函式,這裡我主要通過偵錯程式來觀察結果:

#34 int main(int argc, char* argv)

#35

在第53行處設定斷點,然後執行程式,當程式停在斷點處後,開啟watch視窗

圖一

我們發現第一行和第二行顯示的a::func1的位址是不一樣的。第一行顯示的是func1的實體地址,第二行顯示的是符號位址。

繼續觀察:

圖二

vtbl

pmvfx

class::func

a::vfunca

0x00401005

0x00401028

0x00401260

a::vfuncb

0x00401032

0x0040102d

0x004012c0

b::vfuncb

0x0040100a

0x0040102d

0x00401380

vtbl列顯示的是函式的符號位址,class::func列顯示的是函式的實體地址,a::vfunca,a::vfuncb,b::vfuncb三個函式各自有自己的符號位址和實體地址。pmvfx列顯示的是vcall thunk的位址,因為a::vfuncb和b::vfuncb在vtbl中的index都是1,所以它們使用相同的thunk:vcall 『}』。

接下來開啟disassembly視窗,跟蹤程式的呼叫過程:

首先,跟蹤一下non-virtual member function的呼叫:

004010bb 8b f4          mov         esi,esp         //暫存器準備

004010bd 8d 4d fc   lea         ecx,[ebp-4]          //this指標裝入ecx

004010c0 ff 55 f4      call        dword ptr [ebp-0ch]//呼叫pmf1中儲存的函式位址

位址0x00401005開始的地方就是一張函式入口表,其中0x0040100f就是a::func1的符號位址,其中儲存的5個位元組,是一條jmp指令,跳轉到a::func1的實體地址。可以和圖一做乙個比較。

繼續單步執行:

我們終於到達了a::func1的函式體內部。

接下來再用同樣的方法跟蹤一次virtual member function的呼叫過程:

如果比較一下這一次的彙編**和上一次呼叫的彙編**,我們發現它們並沒有什麼區別,這就是vcall thunk的用處,它使的編譯器在生成**時,不用關心function ptr指向的是乙個non-virtual member function,還是乙個virtual member function。

繼續跟蹤,進入call指令呼叫的函式:

即使在呼叫thunk函式,編譯器仍然使用的是入口表,間接呼叫的方法。繼續

第一行用來在eax中裝入vtbl的指標,第二行跳轉到vtbl中的第乙個位址。a::vfunca就是vtbl中的第乙個函式。繼續

再一次回到函式入口表。繼續

終於來到了a::vfunca()的函式體內部。

在VC下獲取原始MAC位址

在vc下獲取原始mac位址 許多windows的作業系統都支援修改mac位址的功能,因此當使用者修改了mac位址之後,在dos視窗下使用ipconfig all命令和getmac v命令得到的網絡卡位址資訊都是經使用者修改過的。修改mac位址的方法可以在網上查詢,這裡主要講述如何通過對驅動程式的操作...

VC下讀取PSD函式

hresult loadpsd lpstr strfilename 讀取psd檔案 頭四個位元組為 8bps char signature 5 signature 0 fgetc fppsd signature 1 fgetc fppsd signature 2 fgetc fppsd signat...

VC 寫shellcode 時函式位址去掉跳轉表

在預設debug release模式下函式位址不是最終的函式位址,而是e9 offset 的形式,這使得直接使用函式位址作為shellcode 起始位址時會出現問題。該怎麼修改編譯選項呢?在專案屬性中,選擇 配置屬性 c c 優化 全程式優化 選擇 是 gl 這樣就去掉了函式跳轉表。但是這個開關會和...