前面兩篇 PE文件結構詳解(四)PE導入表 和 PE文件結構詳解(五)延遲導入表 介紹了PE文件中比較常用的兩種導入方式,不知道大家有沒有注意到,在調用導入函數時系統生成的代碼是像下面這樣的:
在這裏,IE的iexplorer.exe導入了Kernel32.dll的GetCommandLineA函數,可以看到這是個間接call,00401004這個地址的內存裏保存了目的地址,根據圖中顯示的符號信息可知,00401004這個地址是存在於iexplorer.exe模塊中的,實際上也就是一項IAT的地址。這個是IE6的exe中的例子,當然在dll中如果導入其他dll中的函數,結果也是一樣的。這樣就有一個問題,代碼裏call的地址是一個模塊內的地址,而且是一個VA,那麼如果模塊基地址發生了變化,這個地址豈不是就無效了?這個問題如何解決?
答案是:Windows使用重定位機制保證以上代碼無論模塊加載到哪個基址都能正確被調用。聽起來很神奇,是怎麼做到的呢?其實原理並不很複雜,這個過程分三步:
1.編譯的時候由編譯器識別出哪些項使用了模塊內的直接VA,比如push一個全局變量、函數地址,這些指令的操作數在模塊加載的時候就需要被重定位。
2.鏈接器生成PE文件的時候將編譯器識別的重定位的項紀錄在一張表裏,這張表就是重定位表,保存在DataDirectory中,序號是 IMAGE_DIRECTORY_ENTRY_BASERELOC。
3.PE文件加載時,PE加載器分析重定位表,將其中每一項按照現在的模塊基址進行重定位。
以上三步,前兩部涉及到了編譯和鏈接的知識,跟本文的關係不大,我們直接看第三步,這一步符合本系列的特徵。
在查看重定位表的定義前,我們先了解一下他的存儲方式,有助於後面的理解。按照常規思路,每個重定位項應該是一個DWORD,裏面保存需要重定位的RVA,這樣只需要簡單操作便能找到需要重定位的項。然而,Windows並沒有這樣設計,原因是這樣存放太佔用空間了,試想一下,加入一個文件有n個重定位項,那麼就需要佔用4*n個字節。所以Windows採用了分組的方式,按照重定位項所在的頁面分組,每組保存一個頁面其實地址的RVA,頁內的每項重定位項使用一個WORD保存重定位項在頁內的偏移,這樣就大大縮小了重定位表的大小。
有了上面的概念,我們現在可以來看一下基址重定位表的定義了:
- typedef struct _IMAGE_BASE_RELOCATION {
- DWORD VirtualAddress;
- DWORD SizeOfBlock;
- // WORD TypeOffset[1];
- } IMAGE_BASE_RELOCATION;
- typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
SizeOfBlock:表示該分組保存了幾項重定位項。
TypeOffset:這個域有兩個含義,大家都知道,頁內偏移用12位就可以表示,剩下的高4位用來表示重定位的類型。而事實上,Windows只用了一種類型IMAGE_REL_BASED_HIGHLOW 數值是 3。
好了,有了以上知識,相信大家可以很容易的寫出自己修正重定位表的代碼,不如自己做個練習驗證一下吧。
本文 by evil.eagle轉載的時候請註明出處。http://blog.csdn.net/evileagle/article/details/12886949
最後,還是總結一下,哪些項目需要被重定位呢?
1.代碼中使用全局變量的指令,因爲全局變量一定是模塊內的地址,而且使用全局變量的語句在編譯後會產生一條引用全局變量基地址的指令。
2.將模塊函數指針賦值給變量或作爲參數傳遞,因爲賦值或傳遞參數是會產生mov和push指令,這些指令需要直接地址。
3.C++中的構造函數和析構函數賦值虛函數表指針,虛函數表中的每一項本身就是重定位項,爲什麼呢?大家自己考慮一下吧,不難哦~