PE文件格式(四)

JMP DWORD PTR [XXXXXXXX]
指令處。這個 JMP 指令(譯註1)通過一個在 .idata 中的DWORD變量間接的轉移控制。 .idata 塊的DWORD包含操作系統函數入口的實際地址。在對這進行一會兒回想之後,我開始理解爲什麼DLL調用用這種方式來實現。通過一個位置傳送所有的對一個給定的DLL函數的調用,載入器不需要改變每個調用DLL的指令。所有的PE載入器必須做的是把目標函數的正確地址放到 .idata 的一個 DWORD 中。不需要改變任何call指令。在NE文件中就不同了,每個段都包含一個需要應用到這個段上的一個修正表。如果這個段把一個給定的DLL函數調用了20次,載入器必須把這個函數的地址寫入到這個段的每個調用指令中。PE方法的缺點是你不能用一個DLL函數的真實地址來初始化一個變量。比如,你要考慮這樣的情況:
  FARPROC pfnGetMessage = GetMessage;
將把GetMessage的地址存到變量 pfnGetMessage 中。在16位Windows中,這可以工作,但在Win32中不能。在Win32中,變量pfnGetMessage最終存儲的是我前面提到的JMP DWORD PTR [XXXXXXXX] 替換指示(譯註2)。如果你想通過函數指針調用一個函數,事情也會如你所預料的一樣。但是,如果你想讀取 GetMessage 開始的字節,你將不能如願(除非你自己做跟在 .idata 指針後的工作)。後面我將會返回到這個話題上--在導入表的討論中。
譯註1:英文 thunk,正統的計算機專業術語爲"形實轉換程序",類似宏(macro)替換,故我將它譯爲"替換指示",指在具體指令中xxxxxxxx 被替換,後面出現的替換指示同。
譯註2:現在的編譯器如VC6以上等等,產生的導入函數調用代碼不再是先來一個相對Call指令到 jmp [xxxx] 處,然後再到 xxxx 處(真正的導入函數入口),而是用了一種效率更高,也更容易讓人理解的方式:call [xxxx] 。以前用那種間接的方式多是爲兼容編譯器。但是現在仍有一些編譯器,如MASM,直到版本7.0,還是用前面那種間接的方式,從這裏也可以看出微軟對ASM的態度了。
雖然 Borland 可以讓編譯器輸出的代碼塊名爲 .text ,但它是選擇 NAME 作爲默認的段名。爲了確定PE文件中的塊名,Borland 的連接器(TLINK32.EXE)從OBJ文件中取出段名並把它截斷爲8字符(如果有必要)。
當塊名的不同只是一個小問題時,Borland  PE 文件怎樣鏈接到其它模塊就是一個重要的不同。就像我在 .text 的描述中提到的,所有到OBJ的調用通過一個JMP DWORD PTR [XXXXXXXX]替換指示。在微軟系統下,這條指令通過一個導入庫到達 .text 塊。因爲庫管理器(LIB32)當你鏈接外部DLL時才創建導入庫(和這個替換指示),連接器自己不需要"知道"怎樣生成這這個替換指示。導入庫實際上只不過是鏈接到這個PE文件的一些更多的代碼和數據。
Borland 處理導入函數的系統只是一個簡單的16位NE文件方式擴展。Borland 連接器使用的導入庫實際上只不過是一個函數名連同它所在的DLL名的列表。於是TLINK32就有責任確定外部DLL的修正,並生爲它成一個適當的JMP DWORD PTR [XXXXXXXX] 替換指示 。TLINK32把這個替換指示存儲在它創建的名爲 .icode 塊中。正像 .text 是默認的代碼塊,.data 塊是已初始化數據的歸宿。這些數據包含編譯時初始化的全局和靜態局部變量。它還包括文字字符串。連接器把從OBJ/LIB文件得來的所有 .data 塊組合到EXE文件的一個 .data 塊中。局部變量載入到一個線程的堆棧中,在 .data 或 .bss 中不佔空間。
.bss 塊是存儲未初始化的全局和靜態局部變量的地方。連接器把 OBJ/LIB 文件中的所有 .bss 塊鏈接到EXE文件的一個 .bss 塊中。在塊表中,.bss 塊的RawDataOffset 域置爲0 ,表示這個塊在文件中不佔用任何空間。TLINK 不產生這個塊。代替的,它擴展 DATA 塊的虛擬尺寸(virtual size)。
.CRT 塊是微軟 C/C++ 運行時庫利用的另一個已初始化數據的塊(從名字)。我不能理解爲什麼這些數據不放在 .data 中。(譯註)譯註:從CRT的字面意思看,應該是"C Run Time",即C運行時庫。
.rsrc 塊這個模塊的所有資源。在Windows NT的早期,16位RC.EXE輸出的RES文件是微軟的PE連接器不能識別的格式。CVTRES 程序把這種格式的RES文件轉換成COFF格式的OBJ文件,把資源數據放在 OBJ 的 .rsrc 塊中。連接器就可以把這個資源OBJ當作另一個OBJ來鏈接了,允許連接器"知道"關於資源的特殊東西。微軟最近發佈的更多連接器可以直接處理RES文件。
.idata 塊包含關於這個模塊從其它DLL導入的函數(和數據)的信息(譯註)。這個塊和NE文件的模塊引用表是等價的。一個關鍵的不同是PE文件導入的每個函數都明確的列在這個塊中。爲找到NE文件中的等價信息,你必須去挖掘這個段生鮮數據的結尾的重定位信息。
譯註:現在許多編譯器產生的EXE文件都沒有這個塊,然而ImportTable並不是沒有了,代替的,ImportTable僅由DataDirectory[1]指示,一般指向.text塊或.data塊中。
.edata 塊是這個PE文件導出到其它模塊的函數和數據的列表。它的NE文件等價物是條目表的聯合,駐留名錶,和非駐留名錶,和16位Windows不一樣,很少有理由從一個EXE文件導出一些東西,所以你通常只在DLL中看到 .edata 塊。當使用微軟的工具時,.edata 塊中的數據通過EXP文件來到PE文件中。換種方法,連接器不爲它自己生成這個信息。代替的,它依賴庫管理器(LIB32)來掃描OBJ文件,並創建EXP文件,連接器要把它要鏈接的模塊的列表加入其中。是的,好!這些麻煩的EXP文件實際上只是擴展名不同的OBJ文件而已。
.reloc 塊保持一個基本重定位表。基本重定位是一個對一條指令或已初始化的變量值的調整,如果載入器不能把這個文件載入到連接器假定的位置,這就是很重要的了。如果載入器能把這個映像載入到連接器建議(prefer)的基地址,載入器就完全忽略這個塊的重定位信息。如果你願意冒險,並且希望載入器可以始終把這個映像載入到假定的基址,你可以通過 /FIXED 選項告訴鏈接器去除這個信息。這樣可以在可執行文件中節省空間,但會導致這個可執行文件在其它的Win32實現中不能工作。比如,假定你爲Windows NT建立了一個EXE文件,並且把基址設爲 0x10000 。如果你讓連接器去除重定位信息,這個EXE文件在Windows95下將不能運行,因爲在這裏地址0x10000已被系統使用了。
注意編譯器生成的JMP和CALL指令是很重要的,首選它使用相對偏移量的版本,而非32位平坦段中的真實偏移量版本。如果映像需要被載入非連接器假定的基址處,這些指令不需要改變,因爲它使用的是相對尋址。結果就是,並不需要你想象的那麼多的重定位。重定位通常只需要使用指向一些數據的32位偏移。舉個例子,讓我們看一下,你有如下的全局變量聲明:
 int i;
 int *ptr = &i; 
如果連接器假定一個0x10000的映像基址,變量i的地址將最終是一個特定值如0x12004 。在用來存放指針"ptr"的內存中,連接器將寫進0x12004 ,因爲這是變量 i 的地址。如果載入器由於某種原因決定把這個文件載入基址0x70000處,變量i的地址將是0x72004 。.reloc 塊是映像中的一些內存位置的列表,這些內存位置在連接時連接器假定的載入地址和實際需要的載入地址是不同的,這個因素需要考慮。
當你使用編譯器指令 __declspec(thread) 時,你定義的數據不在 .data 和 .bss 塊種。它最終在 .tls 塊中,這個塊指示"線程局部存儲",並且和Win32的TlsAlloc函數族相聯繫。處理 .tls 塊時,內存管理器設置頁表以便進程在任何時刻切換線程時,都有一個新的物理內存頁集映射到 .tls 塊的地址空間。這就允許線程內的全局變量。在大部分情況下,利用這種機制,比基於線程分配內存並把其指針存在一個 "TlsAlloc 過的"(注:原文TlsAlloc'ed)槽(注:原文Slot)中要容易的多。
不幸的是,有一點需要注意--必須深入研究.tls 塊和 __declspec(thread) 的變量。在WindowsNT 和Windows95 中,如果DLL是被載入庫動態載入的,這種線程局部存儲機制將不能在這個DLL中工作。然而在EXE中或一個隱含載入的DLL中,一切都工作正常。如果你不隱含鏈接到這個DLL ,但需要按線程的數據,你必須會到過去並使用 TlsAlloc 和 TlsGetValue 這種原始方式來設置線程動態內存分配。雖然 .rdata 塊通常在 .data 和 .bss 塊之間,你的程序一般看不見並使用這些塊中的數據。.rdata 塊至少在兩種東西中使用。第一,在微軟連接器生成的EXE中,.rdata 塊存放調試目錄,這隻在EXE文件中出現。(在 TLINK32 的 EXE 中,調試目錄在名爲 ".DEBUG"的塊中)。調試目錄是一個IMAGE_DEBUG_DIRECTORY結構數組。這些結構保持存儲在文件中的變量的類型,尺寸,和位置的調試信息。三種主要的調試信息類型顯示如下:CodeView?, COFF,和 FPO,表9顯示了PEDUMP輸出的一個典型的調試目錄。
表 7   一個典型的調試目錄
Type Size Address FilePtr Charactr TimeDate Version
COFF 000065C5 00000000 00009200 00000000 2CF8CF3D 0.00
??? 00000114 00000000 0000F7C8 00000000 2CF8CF3D 0.00
FPO 000004B0 00000000 0000F8DC 00000000 2CF8CF3D 0.00
CODEVIEW 0000B0B4 00000000 0000FD8C 00000000 2CF8CF3D 0.00

 

調試目錄不必在 .rdata 塊的開始找到。爲找到調試目錄表的開始,使用數據目錄的第七個條目(IMAGE_DIRECTORY_ENTRY_DEBUG)的RVA。數據目錄在文件的PE首部結尾部分。爲確定微軟連接器生成的調試目錄的條目數,用調試目錄的尺寸(在數據目錄條目的尺寸域)除以一個IMAGE_DEBUG_DIRECTORY結構的尺寸即可。TLINK32產生一個簡單的數目,通常是1 。PEDUMP示例程序描述了這一點。
.rdata 域的另一個有用的部分是"描述串"。如果你在程序的DEF文件中指定一個DESCRIPTION條目,這個指定的描述串就出現在 .rdata 塊中。在NE格式中,描述串總是非駐留名錶的第一個條目。描述串是用來保持一個描述這個文件的有用的文本串的。不幸的是,我還沒找到一條便捷的途徑來得到它。我看到有些描述串在PE文件的調試目錄之前,在另一些文件中它在調試目錄之後。我找不到得到這個描述串的一致的方法(或甚至這種方法根本就不存在)。
.debug$S 和 .debug$T 塊只出現在 OBJ 中。他們保存 CodeView 調試符號和類型信息。這些塊名是從以前16位編譯器($$SYMBOLS 和 $$TYPE)使用的段名繼承來的。.debug$T 塊的唯一用途是保持包含工程中所有OBJ的CodeView信息的PDB文件的路徑。連接器從PDB中讀取並且使用它來創建CodeView信息的組成部分,這些CodeView信息放置在PE文件的結尾。
.drectve 塊只出現在OBJ文件中。它包含用文本表示的連接器命令。比如,在我用微軟編譯器編譯的任一OBJ中,下面的字符串都出現在 .drectve 塊中:
 -defaultlib:LIBC -defaultlib:OLDNAMES
當你在程序中用 __declspec(export) 時,編譯器簡單的把等價的命令行輸出到 .drectve 塊中(例如:"-exprot:MyFunction")。
在玩弄 PEDUMP 的過程中,我不時的遇到其它塊。例如,在Window95的KERNEL32.DLL中,有LOCKCODE和LOCKDATA塊。大概這是一種特殊的頁處理方法,是爲了避免缺頁(譯註)。
譯註:缺頁,在頁式內存管理中,一條指令訪問的虛擬內存未映射到物理內存中,此時將發生缺頁中斷,關於缺頁中斷,請參閱操作系統相關書籍。
從這裏學到兩個教訓。第一:不要以爲有約束而只使用編譯器或彙編器提供的標準塊。如果由於某種原因你需要一個分開的塊,不要猶豫,自己去創建!在C/C++編譯器中,使用 #pragma code_seg 和 #pragma data_seg 。在彙編語言中,只不過是創建一個名字和和標準塊不同的32位的段(將成爲一個塊)。如果使用TLINK32 ,你必須使用一個不同的類,或者關掉代碼段包裝(packing)。其它要記住的東西是使用非標準塊名你將會更透徹的理解特殊PE文件的意圖和實現。

 

5 PE文件的導入表
前面,我描述了函數調用怎樣到一個外部DLL中而不直接調用這個DLL 。代替的,在執行體中的 .text 塊中(如果你用Borland C++ 就是 .icode 塊),CALL指令到達一條JMP DWORD PTR [XXXXXXXX]
指令處。JMP指令尋找的地址把控制轉移到實際的目標地址。PE文件的 .idata 會包含一些必要的信息,這些信息是載入器用來確定目標函數的地址以及在執行體映像中去修正他們的。
.idata 塊(或稱導入表,我更喜歡這樣叫)開始於一個IMAGE_IMPORT_DESCRIPTOR數組。每個DLL都有一個PE文件隱含鏈接上的IMAGE_IMPORT_DESCRIPTOR。沒有指定這個數組中結構的數目的域。代替的,這個數組的最後一個元素是一個全NULL的IMAGE_IMPORT_DESCRIPTOR 。IMAGE_IMPORT_DESCRIPTOR的格式顯示在表8 。
表 8  IMAGE_IMPORT_DESCRIPTOR Format
DWORD Characteristics
在一個時刻,這可能已是一個標誌集。然而,微軟改變了它的涵義並不再糊塗地升級WINNT.H 。這個月實際上是一個指向指針數組的偏移(RVA)。其中每個指針都指向一個IMAGE_IMPORT_BY_NAME結構。

 

DWORD TimeDateStamp
指示這個文件的創建時間。

 

DWORD ForwarderChain
這個域聯繫到前向鏈。前向鏈包括一個DLL函數向另一個DLL轉送引用。比如,在WindowsNT中,NTDLL.DLL就出現了的一些前向的它向KERNEL32.DLL導出的函數。應用程序可能以爲它調用的是NTDLL.DLL中的函數,但它最終調用的是KERNEL32.DLL中的函數。這個域還包含一個FirstThunk數組的索引(即刻描述)。用這個域索引得函數會前向引用到另一個DLL 。不幸的是,函數怎樣前向引用的格式沒有文檔,並且前向函數的例子也很難找。

 

DWORD Name
這是導入DLL的名字,指向以NULL結尾的ASCII字符串。通用例子是KERNEL32.DLL和USER32.DLL 。

 

PIMAGE_THUNK_DATA FirstThunk
這個域是指向IMAGE_THUNK_DATA聯合的偏移(RVA)。幾乎在任何情況下,這個域都解釋爲一個指向的IMAGE_IMPORT_BY_NAME結構的指針。如果這個域不是這些指針中的一個,那它就被當作一個將從這個被導入的DLL的導出序數值。如果你實際上可以從序數導入一個函數而不是從名字導入,從文檔看,這是不清楚的。
IMAGE_IMPORT_DESCRIPTOR 的一個重要部分是導入的DLL的名自和兩個IMAGE_IMPORT_BY_NAME指針數組。在EXE文件中,這兩個數組(由Characteristics域和FirstThunk域指向)是相互平行的,都是以NULL指針作爲數組的最後一個元素。兩個數組中的指針都指向 IMAGE_IMPORT_BY_NAME 結構。表3以圖形顯示了這種佈局。表12顯示了PEDUMP對一個導入表的輸出。

 

 
圖 3. 兩個平行的指針數組
表 9. 一個EXE文件的導入表
GDI32.dll
  Hint/Name Table: 00013064
  TimeDateStamp:   2C51B75B
  ForwarderChain:  FFFFFFFF
  First thunk RVA: 00013214
  Ordn  Name
    48  CreatePen
    57  CreateSolidBrush
    62  DeleteObject
   160  GetDeviceCaps
    //  Rest of table omitted...

 

  KERNEL32.dll
  Hint/Name Table: 0001309C
  TimeDateStamp:   2C4865A0
  ForwarderChain:  00000014
  First thunk RVA: 0001324C
  Ordn  Name
    83  ExitProcess
   137  GetCommandLineA
   179  GetEnvironmentStrings
   202  GetModuleHandleA
    //  Rest of table omitted...

 

  SHELL32.dll
  Hint/Name Table: 00013138
  TimeDateStamp:   2C41A383
  ForwarderChain:  FFFFFFFF
  First thunk RVA: 000132E8
Ordn  Name
    46  ShellAboutA

 

  USER32.dll
  Hint/Name Table: 00013140
  TimeDateStamp:   2C474EDF
  ForwarderChain:  FFFFFFFF
  First thunk RVA: 000132F0
  Ordn  Name
    10  BeginPaint
    35  CharUpperA
    39  CheckDlgButton
    40  CheckMenuItem
 
    //  Rest of table omitted...
PE文件的導入表的每一個函數有一個 IMAGE_IMPORT_BY_NAME 結構。IMAGE_IMPORT_BY_NAME結構非常簡單,看上去是這樣:
 WORD    Hint;
 BYTE    Name[?];
第一個域是導入函數的導出序數的最佳猜測。和NE文件不同,這個值不是必須正確的。於是,載入器指示把它當作一個進行二分查找的建議開始值。下一個是導入函數的名字的ASCIIZ字符串。
爲什麼有兩個平行的指針數組指向結構IMAGE_IMPORT_BY_NAME ?第一個數組(由Characteristics域指向的)單獨的留下來,並不被修改。經常被稱作提名錶。第二個數組(由FirstThunk域指向的)將被PE載入器覆蓋。載入器在這個數組中迭代每個指針,並查找每個IMAGE_IMPORT_BY_NAME結構指向的函數的地址。載入器然後用找到的函數地址覆蓋這個指向IMAGE_IMPORT_BY_NAME結構的指針。JMP DWORD PTR [XXXXXXXX] 替換指示中的 [XXXXXXXX] 表示 FirstThunk 數組的一個條目。因爲由載入器覆蓋的這個指針數組實際上保持所有導入函數的地址,叫做"導入地址表"。
對Borland用戶,上面的描述有點彆扭。由TLINK32產生的PE文件缺少其中一個數組。在這樣一個執行體中,IMAGE_IMPORT_DESCRIPTOR(提名數組)中Characteristics域的是0 。於是,僅有的由FirstThunk域(導入地址表)指向的數組在PE文件中就是必須的了。故事到這裏應該結束了,除非在我寫PEDUMP時深入一個有趣的問題中。在優化上無止境的探索,微軟在WindowsNT中"優化"了系統DLL(KERNEL32.DLL等等)的thunk數組。在這個優化中,這個數組中的指針不再指向IMAGE_IMPORT_BY_NAME結構,它們已經包含了導入函數的地址。換句話說,載入器不需要去查找函數的地址並用導入函數的地址覆蓋thunk數組(譯註)。對希望這個數組包含指向IMAGE_IMPORT_BY_NAME結構的指針的PEDump程序,這導致了一個問題。你可能正在思考,"但是,Matt ,爲什麼呢不順便使用提名錶數組?"這可能是一個完美的解決方案,除非提名錶數組在Borland文件中不存在。PEDUMP處理所有這些情況,但是代碼理所當然的就有些雜亂。
譯註: 這就是 Bound Import,關於Bound Import,請參閱:
Matt Pietrek "Inside Windows An In-Depth Look into the Win32 Portable Executable File Format, Part 2 " From MSDN Magazine March 2002 on Internet
URL :http://msdn.microsoft.com/msdnmag/issues/02/03/PE2/PE2.asp
因爲導入地址表在一個可寫的塊中,攔截一個EXE或DLL對另一個DLL的調用就相對容易。只需要修改適當地導入地址條目去指向希望攔截的函數。不需要修改調用者或被調者的任何代碼。
注意微軟產生的PE文件的導入表並不是完全被連接器同步的,這一點很有趣。所有對另一個DLL中的函數的調用的指令都在一個導入庫中。當你連接一個DLL時,庫管理器(LIB32.EXE或LIB.EXE)掃描將要被連接的OBJ文件並且創建一個導入庫。這個導入庫完全不同於16位NE文件連接器使用的導入庫。32位庫管理器產生的導入庫有一個.text塊和幾個.idata$塊。導入庫中的.text塊包含 JMP [XXXX] 的替換指示,這個替換指示在OBJ文件的符號表中有一個名字來存儲它。這個符號名對將從DLL中導出的所有函數名都是唯一的(例如:_Dispatch_Message@4)。導入庫中的一個.idata$塊包含一個從其中引用的替換指示(譯註:即JMP [XXXX]中的XXXX)。另一個.idata$塊有一個導入函數名之前的提示序號(hint ordinal)的空間。這兩個域就組成了IMAGE_IMPORT_BY_NAME結構。當你晚連接一個使用導入庫的PE文件時,導入庫的塊被加到連接器需要處理的在OBJ文件中的你的塊的列表中。一旦導入庫中的這個替換指示的名字和和要導入的函數名相同,連接器就假定這個替換指示就是這個導入函數,並修正對這個導入函數,使其指向這個替換指示。導入庫中的這個替換指示在本質上就被當作這個導入函數本身了。
<script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"> </script> 除了提供一個導入函數替換指示的代碼部分,導入庫還提供PE文件的.idata塊(或稱導入表)的片斷。這些片斷來自於庫管理器放入導入庫中的不同的.idata$塊。簡而言之,連接器實際上不知道出現在不同的OBJ文件中的導入函數和普通函數之間的不同。連接器只是按照它的邊框調整規則去建立並結合塊,於是,所有的事情就自然順理成章了。
6 術語
生鮮數據:原文"RawData",意指未加工過的數據,即原原本本從磁盤上讀入而未經過任何改動的數據。
替換指示:原文"thunk",本質上是一條指令,這條指令中有浮動的地址域。如文中的 jmp [xxxx],其中xxxx是一個浮動地址(floating address),或稱可重定位地址(relocatable address)。
OBJ文件:Object文件,即編譯器編譯產生的目標文件,這種文件只有在(和LIB)連接之後,才能形成可執行文件。
LIB文件:庫文件,這種文件中包含一些二進制的代碼(數據)及其符號,一般情況下,用到LIB中的哪個符號,連接器連接時,關於那個符號的二進制代碼(數據)纔會放入最終的執行體中。
RES文件:Widows資源文件,由RC.EXE編譯。
EXE文件:不用多說Windows下的可執行文件,這類文件一般有導入表(Import Table)。有少數這類文件有導出表(Export Table)。
DLL文件:Dinamic Link Library ,即動態連接庫,用來向其它執行體導出函數(或數據等)。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章