【翻譯】“PE文件格式”1.9版 完整譯文(附注釋)
=========================================================
原著:Bernd. Luevelsmeyer?????????????????????????????
翻譯:ah007
[注意:本譯文的所有大小標題序號都是譯者添加,以方便大家閱讀。圓圈內的數字是注釋的編號,其中注釋②譯自微軟的《PECOFF規范》,其它譯自網絡。----譯者]
一、前言(Preface)
------------------
PE(“portable executable”,可移植的可執行文件)文件格式,是微軟WindwosNT,Windows95和Win32子集①中的可執行的二進制文件的格式;在WindowsNT中,驅動程序也是這種格式。它還能被應用于各種目標文件②和庫文件中。
這種文件格式是由微軟設計的,并于1993年被TIS(tool interface standard,工具接口標準)委員會(由Microsoft,Intel,Borland,Watcom,IBM,等等組成)所批準,它明顯的基于COFF文件格式的許多知識。COFF(“common object file fromat”,通用目標文件格式)是應用于好幾種UNIX系統③和VMS④系統中的目標文件和可執行文件的格式。
Win32 SDK⑤中包含一個名叫<winnt.h>的頭文件,其中含有很多用于PE格式的#define和typedef定義。我將逐步地提到其中的很多結構成員名字和#define定義。
你也可能發現DLL文件“imagehelp.dll”很有用途,它是WindowNT的一部分,但其書面文件卻很缺乏。它的一些功用在“Developer Network”(開發者網絡)中有所描述。
?
二、總覽(General Layout)
-------------------------
在一個PE文件的開始處,我們會看到一個MS-DOS可執行體(英語叫“stub”,意為“根,存根”);它使任何PE文件都是一個有效的MS-DOS可執行文件。
在DOS-根之后是一個32位的簽名以及魔數0x00004550 (IMAGE_NT_SIGNATURE)(意為“NT簽名”,也就是PE簽名;十六進制數45和50分別代表ASCII碼字母E和P----譯者注)。
之后是文件頭(按COFF格式),用來說明該二進制文件將運行在何種機器之上、分幾個區段、鏈接的時間、是可執行文件還是DLL、等等。(本文中可執行文件和DLL文件的區別在于:DLL文件不能被啟動,但能被別的二進制文件使用,而一個二進制文件則不能鏈接到另一個可執行文件。)
那些之后,是可選頭(盡管它一直都存在,卻仍被稱作“可選”----因為COFF文件格式僅為庫文件使用一個“可選頭”,卻不為目標文件使用一個“可選頭”,這就是為什么它被稱為“可選”的原因)。它會告訴我們該二進制文件怎樣被載入的更多信息:開始的地址呀、保留的堆棧數呀、數據段的大小呀、等等。
可選頭的一個有趣的部分是尾部的“數據目錄”數組;這些目錄包含許多指向各“節”數據的指針。例如:如果一個二進制文件擁有一個輸出目錄,那么你就會在數組成員“IMAGE_DIRECTORY_ENTRY_EXPORT”(輸出目錄項)中找到一個指向那個目錄的指針,而該指針指向文件中的某節。
跟在各種頭后面我們就發現各個“節”了,它們都由“節頭”引導。本質上講,各節中的內容才是你執行一個程序真正需要的東西,所有頭和目錄這些東西只是為了幫助你找到它們。
每節都含有和對齊、包含什么樣的數據(如“已初始化數據”等等)、是否能共享等有關的一些標記,還有就是數據本身。大多數(并非所有)節都含有一個或多個可通過可選頭的“數據目錄”數組中的項來參見的目錄,如輸出函數目錄和基址重定位目錄等。無目錄形式的內容有:例如“可執行代碼”或“已初始化數據”等。
??? +-------------------+
??? | DOS-stub????????? |??? --DOS-頭
??? +-------------------+
??? | file-header?????? |??? --文件頭
??? +-------------------+
??? | optional header?? |??? --可選頭
??? |- - - - - - - - - -|
??? |?????????????????? |
??? | data directories? |??? --數據目錄
??? |?????????????????? |
??? +-------------------+
??? |?????????????????? |
??? | section headers?? |???? --節頭
??? |?????????????????? |
??? +-------------------+
??? |?????????????????? |
??? | section 1???????? |???? --節1
??? |?????????????????? |
??? +-------------------+
??? |?????????????????? |
??? | section 2???????? |???? --節2
??? |?????????????????? |
??? +-------------------+
??? |?????????????????? |
??? | ...?????????????? |
??? |?????????????????? |
??? +-------------------+
??? |?????????????????? |
??? | section n???????? |???? --節n
??? |?????????????????? |
??? +-------------------+
?
三、DOS-根和簽名(DOS-stub and Signature)
-----------------------------------------
DOS-根的概念很早從16位windows的可執行文件(當時是“NE”格式⑥)時就廣為人知了。根原來是用于OS/2⑦系統的可執行文件的,也用于自解壓檔案文件和其它的應用程序。對于PE文件來說,它是一個總是由大約100個字節所組成的和MS-DOS 2.0兼容的可執行體,用來輸出象“this program needs windows NT”之類的錯誤信息。
你可以通過確認DOS-頭部分是否為一個IMAGE_DOS_HEADER(DOS頭)結構來認出DOS-根,它的前兩個字節必須為連續的兩個字母“MZ”(有一個#define IMAGE_DOS_SIGNATURE的定義是針對這個WORD單元的)。
你可以通過跟在后面的簽名來將一個PE二進制文件和其它含有根的二進制文件區分開來,跟在后面的簽名可由頭成員'e_lfanew'(它是從字節偏移地址60處開始的,有32字節長)所設定的偏移地址找到。對于OS/2系統和Windows系統的二進制文件來說,簽名是一個16位的word單元;對于PE文件來說,它是一個按照8位字節邊界對齊的32位的longword單元,并且IMAGE_NT_SIGNATURE(NT簽名)的值已由#defined定義為0x00004550(即字母“PE/0/0”----譯者)。
?
四、文件頭(File Header)
-------------------------
要到達IMAGE_FILE_HEADER(文件頭)結構,請先確認DOS-頭“MZ”(起始的2個字節),然后找出DOS-根的頭部的成員“e_lfanew”,并從文件開始處跳過那么多的字節。在核實你在那里找到的簽名后,IMAGE_FILE_HEADER(文件頭)結構的文件頭就緊跟其后開始了,下面我們將從頭至尾的介紹其成員。
1)第一個成員是“Machine(機器)”,一個16位的值,用來指出該二進制文件預定運行于什么樣的系統。已知的合法的值有:
???? IMAGE_FILE_MACHINE_I386 (0x14c)
??????????? Intel 80386 處理器或更高
???? 0x014d
??????????? Intel 80386 處理器或更高
???? 0x014e
??????????? Intel 80386 處理器或更高
???? 0x0160?????????
???????????? R3000 (MIPS⑧)處理器,大尾⑨
???? IMAGE_FILE_MACHINE_R3000 (0x162)
???????????? R3000 (MIPS)處理器,小尾
???? IMAGE_FILE_MACHINE_R4000 (0x166)
???????????? R4000 (MIPS)處理器,小尾
???? IMAGE_FILE_MACHINE_R10000 (0x168)
???????????? R10000 (MIPS)處理器,小尾
???? IMAGE_FILE_MACHINE_ALPHA (0x184)
???????????? DEC Alpha AXP⑩處理器
???? IMAGE_FILE_MACHINE_POWERPC (0x1F0)
???????????? IBM Power PC,小尾
2)然后是“NumberOfSections(節數)”成員,16位的值。它是緊跟在頭后面的節的數目。我們以后將討論節的問題。
3)下一個成員是時間戳“TimeDateStamp”(32位),用來給出文件建立的時間。即使它的“官方”版本號沒有改變,你也可通過這個值來區分同一個文件的不同版本。(除了同一個文件的不同版本之間必須唯一,時間戳的格式沒有明文規定,但似乎是按照UTC時間“從1970年1月1日00:00:00算起的秒數值”----也就是大多數C語言編譯器給time_t標志使用的格式。)
這個時間戳是用來綁定各個輸入目錄的,我們稍后再討論它。
警告:有一些鏈接器往往將時間戳設為荒唐的值,而不是如前所述的time_t格式的鏈接時間。
4-5)成員“PointerToSymbolTable(符號表指針)”和成員“NumberOfSymbols(符號數)”(都是32位)都用于調試信息的。我不知道該怎樣去解讀它,并且我發現該指針的值總為0。
6)成員“SizeOfOptionalHeader(可選頭大小)”(16位)只是“IMAGE_OPTIONAL_HEADER(可選頭)”項的大小,你能用它去驗證PE文件結構的正確性。
7)成員“Characteristics(特性)”是一個16位的,由許多標志位形成的集合組成,但大多數標志位只對目標文件和庫文件有效。具體如下:
??? 位0 IMAGE_FILE_RELOCS_STRIPPED(重定位被剝離文件) 表示如果文件中沒有重定位信息,該位置1,這就表明各節的重定位信息都在它們各自的節中;可執行文件不使用該位,它們的重定位信息放在下面將要描述的“base relocation”(基址重定位)目錄中。
??? 位1 IMAGE_FILE_EXECUTABLE_IMAGE(可執行映象文件) 表示如果文件是一個可執行文件,也即不是目標文件或者庫文件時,置1。如果鏈接器嘗試創建一個可執行文件,卻因為一些原因失敗了,并保存映像以便下次例如增量鏈接時使用,此時此標志位也可能置1。
??? 位2 IMAGE_FILE_LINE_NUMS_STRIPPED(行數被剝離文件) 表示如果行數信息被剝除,此位置1;此位也不用于可執行文件。
??? 位3 IMAGE_FILE_LOCAL_SYMS_STRIPPED(本地符號被剝離文件) 表示如果文件中沒有關于本地符號的信息時,此位置1(此位也不用于可執行文件)。
??? 位4 IMAGE_FILE_AGGRESIVE_WS_TRIM(強行工作集修剪文件) 表示如果操作系統被假定為:通過將正在運行的進程(它所使用的內存數量)強行的頁清除來修剪它的工作集時,此位置1。如果一進程是大部分時間處于等待,且一天中僅被喚醒一次的演示性的應用程序之類時,此位也應該被置1。
???
??? 位7 IMAGE_FILE_BYTES_REVERSED_LO(低字節變換文件)和 位15IMAGE_FILE_BYTES_REVERSED_HI(高字節變換文件) 表示如果一文件的字節序不是機器所預期的形式,因此它在讀入前必須調換字節時,此位置1。這樣做對可執行文件是不可靠的(操作系統期望可執行文件都已經被正確地按字節排整齊了)。
??? 位8 IMAGE_FILE_32BIT_MACHINE(32位機器文件) 表示如果使用的機器被期望為32位的機器時,此位置1。現在的應用程序總將此位置1;NT5系統可能工作不同。
??? 位9 IMAGE_FILE_DEBUG_STRIPPED(調試信息被剝離文件) 表示如果文件中沒有調試信息,此位置1。此位可執行文件不用。按照其它信息([6])(這里指的是參考書目中的第[6]種----譯者注),此位被稱作“恒定”,并且當一個映象文件只有在被裝入優先的裝入地址才能運行(亦即:此文件不可重定位)時,此位置1。
??
??? 位10 IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP(移動介質文件從交換文件運行) 表示如果一個應用程序不可以從可移動的介質,如軟盤或CD-ROM上運行時,此位置1。在這種情況下,建議操作系統將文件復制到交換文件并從那里執行。
??? 位11 IMAGE_FILE_NET_RUN_FROM_SWAP(網絡文件從交換文件運行) 表示如果一個應用程序不可以從網絡上運行時,此位置1。在這種情況下,建議操作系統將文件復制到交換文件并從那里執行。
??? 位12 IMAGE_FILE_SYSTEM(系統文件) 表示如果文件是一個象驅動程序那樣的系統文件,此位置1。此位可執行文件不用;我所見過的所有NT系統的驅動程序也不用。
??? 位13 IMAGE_FILE_DLL(DLL文件) 表示如果文件是一個DLL文件時,此位置1。
??? 位14 IMAGE_FILE_UP_SYSTEM_ONLY(僅但處理器系統的文件) 表示如果文件不設計運行在多處理器系統上(也就是說,因為此文件嚴格地依賴單一處理器的一些方式工作,所以它會發生沖突)時,此位置1。
?
五、相對虛擬地址(Relative Virtual Addresses)
---------------------------------------------
PE格式大量地使用所謂的RVA(相對虛擬地址)。一個RVA,亦即一個“Relative Virtual Addresses(相對虛擬地址)”,是在你不知道基地址時,被用來描述一個內存地址的。它是需要加上基地址才能獲得線性地址的數值。基地址就是PE映象文件被裝入內存的地址,并且可能會隨著一次又一次的調用而變化。
例如:假若一個可執行文件被裝入的地址是0x400000,并且從RVA 0x1560處開始執行,那么有效的執行開始處將位于0x401560地址處。假若它被裝入的地址為0x100000,那么執行開始處就位于0x101560地址處。
因為PE-文件中的各部分(各節)不需要像已載入的映象文件那樣對齊,事情變得復雜起來。例如,文件中的各節常按照512(十六進制的0x200----譯者注)字節邊界對齊,而已載入的映象文件則可能按照4096(十六進制的0x1000----譯者注)字節邊界對齊。參見下面的“SectionAlignment(節對齊)”和“FileAlignment(文件對齊)”。
因此,為了在PE文件中找到一個特定RVA地址的信息,你得按照文件已被載入時的那樣來計算偏移量,但要按照文件的偏移量來跳過。
試舉一例,假若你已知道執行開始處位于RVA 0x1560地址處,并且想從那里開始的代碼處反匯編。為了從文件中找到這個地址,你得先查明在RAM(內存)中各節是按照4096字節對齊的,并且“.code”節是從RVA 0x1000地址處開始,有16384字節長;然后你才知道RVA 0x1560地址位于此節的偏移量0x560處。你還要查明在文件中那節是按512字節邊界對齊,且“.code”節在文件中從偏移量0x800處開始,然后你就知道在文件中代碼的執行開始處就在0x800+0x560=0xd60字節處。
然后你反匯編它并發現訪問一個變量的線性地址位于0x1051d0處。二進制文件的線性地址在裝入時將被重定位,并常被假定使用的是優先載入地址。因為你已查明優先載入地址為0x100000,因此我們可開始處理RVA 0x51d0了。因數據節開始于RVA 0x5000處,且有2048字節長,所以它處于數據節中。又因數據節在文件中開始于偏移量0x4800處,所以該變量就可以在文件中的0x4800+0x51d0-0x5000=0x49d0處找到。
?
六、可選頭(Optional Header)
----------------------------
緊跟在文件頭后面的就是IMAGE_OPTIONAL_HEADER(盡管它名叫“可選頭”,它卻一直都在那里)。它包含有怎樣去準確處理PE文件的信息。我們也將從頭至尾的介紹其成員。
1)第一個16位的word單元叫“Magic(魔數)”,就我目前所觀察過的PE文件而言,它的值總是0x010b。
2-3)下面2個字節是創建此文件的鏈接器的版本(‘MajorLinkerVersion’,“鏈接器主版本號”和‘MinorLinkerVersion’,“鏈接器小版本號”)。這兩個值又是不可靠的,并不能總是正確地反映鏈接器的版本號。(有好幾個鏈接器根本就不設置這個值。)況且,你可想象一下,你連使用的是“什么”鏈接器都不知道,知道它的版本號又有什么作用呢?
4-6)下面3個longword(每個32位)分別用來設定可執行代碼的大小(“SizeOfCode”)、已初始化數據的大小(“SizeOfInitializedData”,所謂的“數據段”)、以及未初始化數據的大小(“SizeOfUninitializedData”,所謂的“bss段”)。這些值也是不可靠的(例如:數據段實際上可能會被編譯器或者鏈接器分成好幾段),并且你可以通過查看可選頭后面的各個“節”來獲得更準確的大小。
7)下一個32位值是RVA。這個RVA是代碼入口點的偏移量(‘AddressOfEntryPoint’,“入口點地址”)。執行將從這里開始,它可以是:例如DLL文件的LibMain()的地址,或者一個程序的開始代碼(這里相應的叫main())的地址,或者驅動程序的DriverEntry()的地址。如果你敢于“手工”裝載映象文件,那么在你完成所有的修正和重定位后,你可以從這個地址開始執行你的進程。
8-9)下兩個32位值分別是可執行代碼的偏移值(‘BaseOfCode’,“代碼基址”)和已初始化數據的偏移值(‘BaseOfData’,“數據基址”),兩個都是RVA,并且兩個對我們來說都沒有多少意義,因為你可以通過查看可選頭后面的各個“節”來獲得更可靠的信息。
未初始化的數據沒有偏移量,正因為它沒有初始化,所以在映象文件中提供這些數據是沒有用處的。
10)下一項是個32位值,提供整個二進制文件包括所有頭的優先(線性)載入地址(‘ImageBase’,“映象文件基址”)。這是一個文件已被鏈接器重定位后的地址(總是64 KB的倍數)。如果二進制文件事實上能被載入這個地址,那么加載器就不用再重定位文件了,也就節省了一些載入時間。
優先載入地址在另一個映象文件已被先載入那個地址(“地址沖突”,在當你載入好幾個全部按照鏈接器的缺省值重定位的DLL文件時經常發生)時,或者該內存已被用于其它目的(堆棧、malloc()、未初始化數據、或不管是什么)時,就不能用了。在這些情況下,映象文件必須被載人其它的地址,并且需要重定位(參見下面的“重定位目錄”)。如果是一個DLL文件,這么做還會產生其它問題,因為此時的“綁定輸入”已不再有效,所以使用DLL的二進制文件必須被修正----參見下面的“輸入目錄”一節。
11-12)下兩個32位值分別是RAM中的“SectionAlignment”(當映象文件已被載入后,意為“節對齊”)和文件中的“FileAlignment”(文件對齊),它們都是PE文件的各節的對齊值。這兩個值通常都是32,或者是:FileAlignment為512,SectionAlignment為4096。節會在以后討論。
13-14)下2個16位word單元都是預期的操作系統版本信息(MajorOperatingSystemVersion,“操作系統主版本號”)和(MinorOperatingSystemVersion,“操作系統小版本號”)[它們都使用微軟自己書面確定的名字]。這個版本信息應該為操作系統的版本號(如NT 或 Win95),而不是子系統的版本信息(如Win32)。版本信息常常被不提供或者錯誤提供。很明顯的,加載器并不使用它們。
15-16)下2個16位word單元都是本二進制文件的版本信息('MajorImageVersion'“映象文件主版本號”和
'MinorImageVersion'“映象文件小版本號”)。很多鏈接器不正確地設定這個信息,許多程序員也懶得提供這些,因此即便存在這樣的信息,你最好也不要信賴它。
17-18)下2個16位word單元都是預期的子系統版本信息('MajorSubsystemVersion'“子系統主版本號”和'MinorSubsystemVersion'“子系統小版本號”)。此信息應該為Win32或POSIX的版本信息,因為很明顯的,16位程序或OS/2程序都不是PE格式的。子系統版本應該被正確的提供,因為它“會”被檢驗和使用:
如果一個應用程序是一個Win32-GUI應用程序并運行于NT4系統之上,而且子系統版本“不是”4.0的話,那么對話框就不會是以3D形式顯示,并且一些其它的特征也只會按“老式”的方式工作,因為此應用程序預期是在NT 3.51系統上運行的,而NT 3.51系統上只有程序管理器而沒有瀏覽器、等等,于是NT 4.0系統就盡可能地仿照那個系統的行為來運行程序。
19)然后,我們便碰到32位的“Win32VersionValue”(Win32版本值)。我不清楚它有什么作用。在我所觀察過的PE文件中,它全部都為0。
20)下一個是32位值,給出映象文件將要使用的內存數量,單位為字節(‘SizeOfImage’,“映象文件大小”)。如果是按照“SectionAlignment”對齊的,它就是所有頭和節的長度的總和。它提示加載器,為了載入映象文件需要多少頁。
21)下一個是32位值,給出所有頭的總長度,包括數據目錄和節頭(‘SizeOfHeaders’,“頭的大小”)。同時,它也是從文件的開頭到第一節的原始數據的偏移量。
22)然后,我們發現一個32位的校驗和(“CheckSum”)。這個校驗和,對于當前的NT版本,只在映象文件是NT驅動程序時才校驗(如果校驗和不正確,驅動就將裝載失敗)。對于其他的二進制文件形式,校驗和不需提供并且可能為0。計算校驗和的算法是微軟的私產,他們不會告訴你的。但是,Win32 SDK的好幾個工具都會計算和/或補正一個有效的校驗和,而且imagehelp.dll中的CheckSumMappedFile()函數也會做同樣的工作。
使用校驗和的目的是為了防止載入無論如何都會沖突的、已損壞的二進制文件----況且一個沖突的驅動程序會導致一個BSOD錯誤,因此最好根本就不載入這樣的壞文件。
23)然后,就到了一個16位的word單元“Subsystem”(子系統),用來說明映象文件應運行于什么樣的NT子系統之上:
??????? IMAGE_SUBSYSTEM_NATIVE (1)
???????????? 二進制文件不需要子系統。用于驅動程序。
???????
??????? IMAGE_SUBSYSTEM_WINDOWS_GUI (2)
???????????? 映象文件是一個Win32二進制圖象文件。(它還是能用AllocConsole()打開一個控制臺界面,但在開始時卻不能自動地打開。)
????????????
??????? IMAGE_SUBSYSTEM_WINDOWS_CUI (3)
???????????? 二進制文件是一個Win32控制臺界面二進制文件。(它將在開始時按照缺省值打開一個控制臺,或者繼承其父程序的控制臺。)
??????? IMAGE_SUBSYSTEM_OS2_CUI (5)
????????????? 二進制文件是一個OS/2控制臺界面二進制文件。(OS/2控制臺界面二進制文件是OS/2格式,因此此值在PE文件中很少使用。)
??????? IMAGE_SUBSYSTEM_POSIX_CUI (7)
????????????? 二進制文件使用POSIX控制臺子系統。
Windows 95的二進制文件總是使用Win32子系統,因此它的二進制文件的合法值只有2和3;我不知道windows 95的“原”二進制文件是否可能(會有其它值----譯者添加,僅供參考)。
24)下一個是16位的值,指明,如果是DLL文件,何時調用DLL文件的入口點(‘DllCharacteristics’,“DLL特性”)。此值似乎不用;很明顯地,DLL文件總是被通報所有的情況。
???????? 如果位0被置1,DLL文件被通知進程附加(亦即DLL載入)。
???????? 如果位1被置1,DLL文件被通知線程附加(亦即線程終止)。
???????? 如果位2被置1,DLL文件被通知線程附加(亦即線程創建)。
???????? 如果位3被置1,DLL文件被通知進程附加(亦即DLL卸載)。
25-28)下4個32位值分別是:保留棧的大小(SizeOfStackReserve)、初始時指定棧大小(SizeOfStackCommit)、保留堆的大小(SizeOfHeapReserve)和指定堆大小(SizeOfHeapCommit)。
“保留的”數量是保留給特定目的的地址空間(不是真正的RAM);在程序開始時,“指定的”數量是指在RAM中實際分配的大小。如果需要的話,“指定的”值也是指定的堆或棧用來增加的數量。(有資料說,不管“SizeOfStackCommit”的值是多少,棧都是按頁增加的。我沒有驗證過。)
因此,舉例來說,如一個程序的保留堆有1 MB,指定堆為64 KB,那么啟動時堆的大小為64 KB,并且保證可以擴大到1 MB。堆將按64 KB一塊來增加。
“堆”在本文中是指主要(缺省)堆。如果它愿意的話,一個進程可創建很多堆。
棧是指第一個線程的棧(啟動main()的那個)。進程可以創建很多線程,每個線程都有自己的棧。
DLL文件沒有自己的堆或棧,所以它們的映象文件忽略這些值。我不知道驅動程序是否有它們自己的堆或棧,但我認為它們沒有。
29)堆和棧的這些描述之后,我們就發現一個32位的“LoaderFlags(加載器標志)”,我沒有找到它的任何有用的描述。我只發現一篇時新的關于設置此標志位的短文,說設置此標志位會在映象文件載入后自動地調用一個斷點或者調試器;可似乎不正確。
30)接著我們會發現32位的“NumberOfRvaAndSizes(Rva數和大小)”,它是緊隨其后的目錄的有效項的數目。我已發現此值不可靠;你也許希望用常量IMAGE_NUMBEROF_DIRECTORY_ENTRIES(映象文件目錄項數目)來代替它,或者用它們中的較小者。
NumberOfRvaAndSizes之后是一個IMAGE_NUMBEROF_DIRECTORY_ENTRIES (16)(映象文件目錄項數目)個IMAGE_DATA_DIRECTORY(映象文件數據目錄)數組。這些目錄中的每一個目錄都描述了一個特定的、位于目錄項后面的某一節中的信息的位置(32位的RVA,叫“VirtualAddress(虛擬地址)”)和大小(也是32位,叫“Size(大小)”)。
例如,安全目錄能在索引4中給定的RVA處發現并具有索引4中給定的大小。
稍后我將討論我知道其結構的目錄。
已定義的目錄及索引有:
??? IMAGE_DIRECTORY_ENTRY_EXPORT (0)
??????? 輸出符號目錄;大多用于DLL文件。
??????? 后面介紹。
???
??? IMAGE_DIRECTORY_ENTRY_IMPORT (1)
??????? 輸入符號目錄;參見后面。
???
??? IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
??????? 資源目錄。后面介紹。
???
??? IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
??????? 異常目錄 - 結構和用途不詳。
???
??? IMAGE_DIRECTORY_ENTRY_SECURITY (4)
??????? 安全目錄 - 結構和用途不詳。
???
??? IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
??????? 基址重定位表 - 參見后面。
???
??? IMAGE_DIRECTORY_ENTRY_DEBUG (6)
??????? 調試目錄 - 內容編譯器相關。此外, 許多編譯器將編譯信息填入代碼節,并不為此創建一個單獨的節。
???
??? IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)
??????? 描述字符串 - 一些隨意的版權信息之類。
???
??? IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
??????? 機器值 (MIPS GP) - 結構和用途不詳。
???
??? IMAGE_DIRECTORY_ENTRY_TLS (9)
??????? 線程級局部存儲目錄 - 結構不詳;包含聲明為“__declspec(thread)”的變量, 也就是每線程的全局變量。
???
??? IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)
??????? 載入配置目錄 - 結構和用途不詳。
???
??? IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)
??????? 綁定輸入目錄 - 參見輸入目錄的描述。
???
??? IMAGE_DIRECTORY_ENTRY_IAT (12)
??????? 輸入地址表 - 參見輸入目錄的描述。
試舉一例,如果我們在索引7中發現2個longword:0x12000 和 33,并且載入地址為0x10000,那么我們就知道版權信息數據位于地址0x10000+0x12000(在哪個節都有可能)處,并且版權信息有33字節長。
如果二進制文件中沒有使用特殊類型的目錄,Size(大小)和VirtualAddress(虛擬地址)的值就都為0。
七、節目錄(Section directories)
---------------------------------
節由兩個主要部分組成:首先,是一個節描述(IMAGE_SECTION_HEADER[意為“節頭”]類型的),然后是原始的節數據。因此,我們會在數據目錄后發現一“NumberOfSections”個節頭組成的數組,它們按照各節的RVA排序。
節頭包括:
1)一個IMAGE_SIZEOF_SHORT_NAME (8)(意為“短名的大小”)個字節的數組,形成節的名稱(ASCII形式)。如果所有的8位都被用光,該字符串就沒有0結束符!典型的名稱象“.data”或“.text”或“.bss”形式。開頭的“.”不是必須的,名稱也可能為“CODE”或“IAT”或類似的形式。
請注意:并不是所有的名稱都和節中的內容相關。一個名叫“.code”的節可能包含也可能不包含可執行代碼;它還可能只包含輸入地址表;它也可能包含代碼“和”地址表“和”未初始化數據。要找到節中的信息,你必須通過可選頭的數據目錄來查詢它。既不要過分相信它的名稱,也不要以為節的原始數據會從節的開頭就開始。
2)IMAGE_SECTION_HEADER(“節頭”)的下一個成員是一個32位的、“PhysicalAddress(物理地址)”和“VirtualSize(虛擬大小)”組成的共用體。在目標文件中,它是內容重定位到的地址;在可執行文件中,它是內容的大小。事實上,此域似乎沒被使用;因為有的鏈接器輸入大小,有的鏈接器輸入地址,我還發現有一個鏈接器輸入0,而所有的可執行文件都運行如風。
3)下一個成員是“VirtualAddress(虛擬地址)”,是一個32位的值,用來保存載入RAM(內存)后,節中數據的RVA。
4)然后,我們到了32位的“SizeOfRawData”(意味“原始數據大小”),它表示節中數據被大約到下一個“FileAlignment”的整數倍時節的大小。
5)下一個是“PointerToRawData”(意味“原始數據指針”,32位),它特別有用,因為它是從文件的開頭到節中數據的偏移量。如果它為0,那么節的數據就不包含在文件中,并且要在載入時才定。
6-9)然后,我們得到“PointerToRelocations”(意味“重定位指針”,32位)和“PointerToLinenumbers”(意味“行數指針”,也是32位),以及“NumberOfRelocations”(意味“重定位數”,16位)和“NumberOfLinenumbers”(意味“行數數”,也是16位)。所以這些都是只用于目標文件的信息。可執行文件擁有一個特殊的基址重定位目錄,并且行數信息(如果真的存在的話)通常包含在有一個特殊目的的調試段中或者別的什么地方。
10)節頭的最后一個成員是32位的“Characteristics”(意味“特性”),它是一串描述節的內存如何被處理的標志:
??? 如果位5 IMAGE_SCN_CNT_CODE(含有代碼的節)被置1,表示節中包含可執行代碼。
???
??? 如果位6 IMAGE_SCN_CNT_INITIALIZED_DATA(含有初始化數據的節)被置1,表示節中包含執行開始前即取得已定義值的數據。換言之:文件中節的數據就是有意義的。
???
??? 如果位7 IMAGE_SCN_CNT_UNINITIALIZED_DATA(含有未初始化數據的節)被置1, 表示節中包含未初始化數據,并需于執行開始前被初始化為全0。這通常是BSS節。
??? 如果位9 IMAGE_SCN_LNK_INFO(鏈接器信息節)被置1, 表示節中不包含映象數據,只有一些注釋、描述或者其他的文檔。這些信息是目標文件的一部分,并有可能是提供給鏈接器的信息,比如需要哪些庫文件。
??? 如果位11 IMAGE_SCN_LNK_REMOVE(鏈接可刪除節)被置1,表示數據是目標文件的、被預定于可執行文件被鏈接后丟棄掉的節的一部分。常和位9連用。
??? 如果位12 IMAGE_SCN_LNK_COMDAT(鏈接通用塊節)被置1, 表示節中包含“common block data”(通用塊數據),也即某種形式的打包函數。
??? 如果位15 IMAGE_SCN_MEM_FARDATA(內存遠程數據節)被置1,表示我們擁有遠程數據----意味著什么。此位的含義不明。
??? 如果位17 IMAGE_SCN_MEM_PURGEABLE(內存可清除節)被置1, 表示節中的數據可清除----但我認為它和“可丟棄”不是一回事,可丟棄擁有自己的標志位,參見后面。同樣,它也明顯的不是用來指示16位信息的,因為它也有一個IMAGE_SCN_MEM_16BIT定義。此位的含義不明。
??? 如果位18 IMAGE_SCN_MEM_LOCKED(內存被鎖節)被置1, 表示節不應該被從內存中移除?抑或表明沒有重定位信息?此位的含義不明。
??? 如果位19 IMAGE_SCN_MEM_PRELOAD(內存預載入節)被置1,表示節在執行開始前應該被頁載入?此位的含義不明。
??? 位20至23 指定我沒有找到信息的對齊。諸如#defines IMAGE_SCN_ALIGN_16BYTES之類。我曾經見過的唯一值為0,是16位的缺省對齊。 我懷疑它們是庫之類文件的目標對齊。
??? 如果位24 IMAGE_SCN_LNK_NRELOC_OVFL(鏈接擴展重定位節)被置1,表示節中包含一些我不知道的擴展重定位。
??? 如果位25 IMAGE_SCN_MEM_DISCARDABLE(內存可丟棄節)被置1,表示節中的數據在進程啟動后就不需要了。它是,舉例來說,含有重定位信息的情況。我曾經見過它也用于只執行一次的驅動和服務程序的啟動例程,還用于輸入目錄。
??? 如果位26 IMAGE_SCN_MEM_NOT_CACHED(內存不緩存節)被置1,表示節中的數據不應該被緩存。不要問我為什么不。這是不是意味著關掉2級緩存?
??? 如果位27 IMAGE_SCN_MEM_NOT_PAGED(內存不可頁換出節)被置1,表示節中的數據不應該頁換出。它對驅動程序有意義。
??? 如果位28 IMAGE_SCN_MEM_SHARED(內存共享節)被置1,表示節中的數據在映象文件的所有正在運行的實例中共享。如果它是,例如DLL文件的未初始化數據,那么DLL的所有正在運行的實例程序在任何時候都將擁有相同的變量內容。
注意:只有第一個實例的節被初始化。
含有代碼的節總是被共享寫時拷貝(copy-on-write)(亦即:如果重定位必不可少,那么共享就不工作)。(譯注:“寫時拷貝”的譯法也許根本就是錯誤的,但我一時找不到更準確的翻譯,也不清楚其具體含義,只能以此充數了。希望知情著指點。)
??? 如果位29 IMAGE_SCN_MEM_EXECUTE(內存可執行節)被置1,表示進程對節的內存有“執行”的存取權限。
???
??? 如果位30 IMAGE_SCN_MEM_READ(內存可讀節)被置1,表示進程對節的內存有“讀”的存取權限。
???
??? 如果位31 IMAGE_SCN_MEM_WRITE(內存可寫節)被置1,表示進程對節的內存有“寫”的存取權限。
在節頭之后,我們就會發現節本身。在文件中,它們按照“FileAlignment”(文件對齊)的字節數對齊(也就是說,在可選頭之后和每個節的數據之后將要填充一些字節)并按照它們的RVA排序。在載入后(內存中),? 它們按照“SectionAlignment”(節對齊)的字節數對齊。
試舉一例,如果可選頭在文件的偏移量981處結束,“FileAlignment”(文件對齊)的值為512,那么第一個節將于1024字節處開始。注意:你可通過“PointerToRawData”(原始數據指針)或者“VirtualAddress”(虛擬地址)的值來找到各節,因此實際上根本沒必要在對齊上小題大做。
試畫映象文件的全圖如下:
??? +-------------------+
??? |???? DOS-根??????? |
??? +-------------------+
??? |????? 文件頭?????? |
??? +-------------------+
??? |????? 可選頭?????? |
??? |- - - - - - - - - -|
??? |?????????????????? |----------------+
??? |???? 數據目錄????? |??????????????? |
??? |?????????????????? |??????????????? |
??? |?? (指向節中?????? |-------------+? |
??? |???? 目錄的RVA)??? |???????????? |? |
??? |?????????????????? |---------+?? |? |
??? |?????????????????? |???????? |?? |? |
??? +-------------------+???????? |?? |? |
??? |?????????????????? |-----+?? |?? |? |
??? |?????? 節頭??????? |???? |?? |?? |? |
??? |???? (指向節?????? |--+? |?? |?? |? |
??? |???? 邊界的RVA)??? |? |? |?? |?? |? |
??? +-------------------+<-+? |?? |?? |? |
??? |?????????????????? |???? | <-+?? |? |
??? |????? 節數據 1???? |???? |?????? |? |
??? |?????????????????? |???? | <-----+? |
??? +-------------------+<----+????????? |
??? |?????????????????? |??????????????? |
??? |????? 節數據 2???? |??????????????? |
??? |?????????????????? | <--------------+
??? +-------------------+
每個節都有一個節頭,并且每個數據目錄都會指向其中的一個節(幾個數據目錄有可能指向同一個節,而且也可能有的節沒有數據目錄指向它們)。
?
八、節的原始數據(Sections' raw data)
--------------------------------------
1.概述(general)
-------
所有的節在載入內存后都按“SectionAlignment”(節對齊)對齊,在文件中則以“FileAlignment”(文件對齊)對齊。節由節頭中的相關項來描述:在文件中你可通過“PointerToRawData”(原始數據指針)來找到,在內存中你可通過“VirtualAddress”(虛擬地址)來找到;長度由“SizeOfRawData”(原始數據長度)決定。
根據節中包含的內容,可分為好幾種節。大多數(并非所有)情況下,節中至少由一個數據目錄,并在可選頭的數據目錄數組中有一個指針指向它。
2.代碼節(code section)
------------------------
首先,我將提到代碼節。此節,至少,要將“IMAGE_SCN_CNT_CODE”(含有代碼節)、“IMAGE_SCN_MEM_EXECUTE”(內存可執行節)和“IMAGE_SCN_MEM_READ”(內存可讀節)等標志位設為1,并且“AddressOfEntryPoint”(入口點地址)將指向節中的某個地方,指向開發者希望首先執行的那個函數的開始處。
“BaseOfCode”(代碼基址)通常指向這一節的開始處,但是,如果一些非代碼字節被放在代碼之前的話,它也可能指向節中靠后的某個地方。
通常,除了可執行代碼外,本節沒有別的東東,并且通常只有一個代碼節,但是不要太迷信這一點。
典型的節名有“.text”、“.code”、“AUTO”之類。
3.數據節(data section)
------------------------
我們要討論的下一件事情就是已初始化變量;本節包含的是已初始化的靜態變量(象“static int i = 5;”)。它將,至少,使“IMAGE_SCN_CNT_INITIALIZED_DATA”(含有已初始化數據節)、“IMAGE_SCN_MEM_READ”(內存可讀節)和“IMAGE_SCN_MEM_WRITE”(內存可寫節)等標志位被置為1。
一些鏈接器可能會將常量放在沒有可寫標志位的它們自己的節中。如果有一部分數據可共享,或者有其它的特定情況,那么可能會有更多的節,且它們的合適的標志位會被設置。
不管是一節,還是多節,它們都將處于從“BaseOfData”(數據基址)到“BaseOfData”+“SizeOfInitializedData”(數據基址+已初始化數據的大小)的范圍之內。
典型的名稱有“.data”、“.idata”、“DATA”、等等。
4.BSS節(bss section)
----------------------
其后就是未初始化的數據(一些象“static int k;”之類的靜態變量);本節十分象已初始化的數據,但它的“PointerToRawData”(文件偏移量)卻為0,表明它的內容不存儲在文件中;并且“IMAGE_SCN_CNT_UNINITIALIZED_DATA”(含有未初始化數據節)而不是“IMAGE_SCN_CNT_INITIALIZED_DATA”(含有已初始化數據節)標志位被置為1,表明在載入時它的內容應該被置為0。這就意味著,在文件中只有節頭,沒有節身;節身將由加載器創建,并全部為0字節。
它的長度由“SizeOfUninitializedData”(未初始化數據大小)確定。
典型的名稱有“.bss”、“BSS”之類。
有些節數據“沒有”被數據目錄指向。它們的內容和結構是由編譯器而不是鏈接器提供。
(棧段和堆段不是二進制文件中的節,它們是由加載器根據可選頭中的棧大小和堆大小項來創建的。)
5.版權(copyright)
-------------------
為了從一個簡單的目錄節開始講解,讓我們來看一看數據目錄“IMAGE_DIRECTORY_ENTRY_COPYRIGHT”(版權目錄項)項。它的內容是一個版權信息或ASCII形式的描述字符串(不是以0結尾的),象“Gonkulator control application, copyright (c) 1848 Hugendubel & Cie”這樣。這個字符串,通常,是用命令行或者描述文件提供給鏈接器的。
這個字符串在運行時并不需要,并可能被丟棄。它是不可寫的;事實上,應用程序根本不需要存取它。因此,如果已有一個可丟棄的、非可寫的節存在,鏈接器就會找到它;如果沒有,就創建一個(命名為“.descr”之類)。然后它就將那個字符串填入該節中并讓版權目錄項指針指向這個字符串。“IMAGE_SCN_CNT_INITIALIZED_DATA”(含有已初始化數據節)標志位應置為1。
6.輸出符號(exported symbols)
------------------------------
(注意:本文的1993年03月12日之前的各個版本中,輸出目錄的描述有誤。文中沒有描述中轉、只以序數輸出、或者使用好幾個名稱輸出等內容。)
下一件最簡單的事情是輸出目錄,是由“IMAGE_DIRECTORY_ENTRY_EXPORT”(輸出目錄項)指向的。它是一個典型的在DLL中常見到的目錄;包含一些輸出函數的入口點(以及輸出對象等的地址)。當然可執行文件也可能擁有輸出符號但一般沒有。
包含它們的節應該有“已初始化數據的”和“可讀的”特性。這樣的節應該是不可丟棄的,因為在運行時,進程有可能調用“GetProcAddress()”來尋找一個函數的入口點。如果單獨成節的話,本節通常被稱作“.edata”;更常見的是,它被并入象“已初始化數據”之類的節中。
輸出表(“IMAGE_EXPORT_DIRECTORY”)的結構由一個頭和輸出數據,也就是:符號名稱、它們的序號和它們的入口點偏移量等構成。
1)首先,我們有一個沒被使用并通常為0的、32位的“Characteristics”(特性)。
2)然后是一個32位的“TimeDateStamp”(時間日期戳),大概是提供此表被創建的time_t格式的時間;天呀,它的值并不總是有效(有些鏈接器將它設置為0)。
3-4)往后我們看到2個16位的、有關版本信息的word單元(“MajorVersion”和“MinorVersion”,含義分別為‘主版本號’和‘小版本號’),同樣,它們很多地被設為0。
5)下一個東東是32位的“Name”(名稱);它是一個指向以0結尾的ASCII字符串為DLL名稱的RVA。(為防DLL被改名時的錯誤,名稱是必須的----參見輸入目錄中的“綁定”部分。)
6)然后是32位的“Base”(基址)。稍后我們再討論。
7)下一個32位值是輸出條目的總數(“NumberOfFunctions”,意為‘函數數’)。除了它們的序數外,各條目還可能使用一個或多個名稱來輸出。接下來的一個32位數字是輸出名稱的總數(“NumberOfNames”,意為‘名字數’)。
在大多數情況下,每一個輸出條目都準確的有一個相應的名稱,并且將用這個名稱來使用它;但是一個條目可能擁有好幾個相關聯的名稱(那樣它們的每一個名稱都可訪問);或者它也可能沒有名稱,此時它只能以它的序數來訪問。無名輸出項(只有序數)的使用是不鼓勵的,因為此時輸出DLL的所有版本都必須使用相同的序數法,而這會造成維護的問題。
8)下一個32位值“AddressOfFunctions”(函數地址)是指向輸出條目列表的RVA。它指向一個32位值的“NumberOfFunctions”(函數數)數組,數組的每一項都是一個指向輸出函數或變量的RVA。
關于此列表有兩個怪事:第一,這樣一個輸出的RVA竟可能會為0,在此情況下,此值沒被使用。第二,如果一RVA指向含有輸出目錄的節,那么它就是一個中轉輸出。一個中轉輸出就是指指向另一個二進制文件中的輸出項的指針;如果使用了它,就可用另一個二進制文件中的被指向的輸出項來代替使用。此時的RVA指向,正如已提到的,輸出目錄的節中,指向一個以以零結尾的字符串組成的、被指向的DLL的名稱和一個用點分開的輸出項的名稱,象“otherdll.exportname”這樣,或者是DLL的名稱和輸出序數,象“otherdll.#19”這樣。
現在到了解釋輸出序數的時候了。一個輸出項的序數就是函數地址數組中的索引值加上上面提到的“Base”(基址)的值的和。在大多數情況下,“Base”(基址)的值為1,這就意味著第一個輸出項的序數為1,第二個輸出項的序數為2,以此類推。
9-10)“AddressOfFunctions”(函數地址)RVA之后,我們發現二個RVA,一個指向符號名稱的32位RVA的數組“AddressOfNames”(名字的地址),另一個指向16位序數“AddressOfNameOrdinals”(名字序數的地址)的數組。兩個數組都有“NumberOfNames”(名字數)個元素。
符號名稱可能會全部丟失,此時“AddressOfNames”(名字的地址)為0;否則,被指向的數組并行運行,這意味著它們的每個索引中的元素共同擁有。“AddressOfNames”(名字的地址)數組由以0結尾的輸出名稱的RVA組成;這些名稱以一個分類的列表排列(即:數組的第一個成員是按照字母順序排列的最小的名稱的RVA;這使當按名稱查找一個輸出符號時,搜索的效率更高。)
根據PE規范,“AddressOfNameOrdinals”(名字序數的地址)數組每個名稱擁有一個相應的序數,然而,我發現這個數組卻將實際的索引包含到“AddressOfFunctions”(函數地址)數組中去。
我將畫一個有關這三個表的圖:
??????? 函數地址
??????????? |
??????????? |
??????????? |
??????????? v
??? 帶序數‘基址’的輸出RVA
??? 帶序數‘基址+1’的輸出RVA
???? ...
??? 帶序數‘基址+函數數-1’的輸出RVA
?
?????? 名字地址?????????????????????? 名字序數地址
?????????? |?????????????????????????????? |
?????????? |?????????????????????????????? |
?????????? |?????????????????????????????? |
?????????? v?????????????????????????????? v
????? 第一個名字的RVA????? <->??? 第一個名字的輸出索引
????? 第二個名字的RVA????? <->??? 第二個名字的輸出索引
?????? ...???????????????????????????? ...
??? 第‘名字數’個名字的RVA? <->? 第‘名字數’個名字的輸出索引
舉一些例子是適宜的。
為按序數找到一個輸出符號,先減去“Base”(基址)值以得到索引值,再根據“AddressOfFunctions”(函數地址)的RVA得到輸出項數組,并用索引值去找到數組中的輸出RVA。如果結果沒有指向輸出節中,你就完了。否則,它就指向那里的一個描述輸出DLL和(輸出項)名稱或序數的字符串,之后你就得在那里查找中轉輸出。
為按名稱找到一個輸出符號,先跟隨“AddressOfNames”(名字的地址)的RVA(如果是0就沒有名稱)找到輸出名稱的RVA數組。在列表中搜尋你要找的名稱。用該名稱在“AddressOfNameOrdinals”(名字序數的地址)數組中的索引,得到和找到的名稱相應的16位數字。根據PE規范,這是一個序數,你需先減去“Base”(基址)值以得到輸出索引值;但依據我的經驗,這就是輸出索引值,你不需要再減了。使用輸出索引值,你就能在“AddressOfFunctions”(函數地址)數組中找到輸出RVA了,要么是輸出RVA本身,要么是一個描述中轉輸出的字符串的RVA。
7.輸入符號(imported symbols)
------------------------------
當編譯器發現一個對別的可執行文件(大多數是DLL文件)中的函數調用時,在最簡單化的情況下,它會對此情況一無所知,只是簡單地輸出一個對那個符號的正常調用指令。鏈接器不得不修正那個符號的地址,就象它為任何其它的外部符號所做的那樣。
鏈接器使用一個輸入庫來查找從哪個DLL文件輸入了哪個符號,并為所有的輸入符號都建立存根,每個存根包含一個跳轉指令;存根就是實際的調用目標。這些跳轉指令實際上將跳往從所謂的輸入地址表中提取的一個地址。在更復雜的應用程序(使用“__declspec(dllimport)”時)中,編譯器會知道函數是輸入的,并直接輸出一個位于輸入地址表中的地址的調用,繞過跳轉。
不管怎樣,DLL文件中的函數地址總是必要的,并將于應用程序載入時,由加載器從輸出DLL文件的輸出目錄中提供。加載器知道哪個庫中的哪些符號需要被查找以及哪些地址需要通過搜索輸入目錄來修正。
我最好給你一個例子。有或無__declspec(dllimport)的調用如下所示:
??? 源文件:
??????? int symbol(char *);
??????? __declspec(dllimport) int symbol2(char*);
??????? void foo(void)
??????? {
??????????? int i=symbol("bar");
??????????? int j=symbol2("baz");
??????? }
??? 匯編:
??????? ...
??????? call _symbol???????????????? ; 沒有declspec(dllimport)的
??????? ...
??????? call [__imp__symbol2]??????? ; 含有declspec(dllimport)的
??????? ...
在第一種(沒有__declspec(dllimport))情況下,編譯器不知道“_symbol”位于一個DLL文件中,因此鏈接器必須要提供“_symbol”函數。因為此函數不存在,它就為輸入符號提供一個存根函數,即一個間接跳轉。所有輸入存根的集合被稱為“轉移區”(有時也叫做“跳板”,因為你跳到那里的目的是為了跳到別的地方)。
典型地,此轉移區位于代碼節中(它不是輸入目錄的一部分)。每一個函數存根都是一個跳往DLL文件中的實際函數的跳轉。轉移區的形式象這樣:
??? _symbol:??????? jmp? [__imp__symbol]
??? _other_symbol:? jmp? [__imp__other__symbol]
??? ...
這意味著:如果你不指定“__declspec(dllimport)”來使用輸入符號,那么鏈接器將會為它們產生一個由間接跳轉所組成的轉移區。如果你真指定了“__declspec(dllimport)”,那么編譯器就會自己做間接(跳轉),轉移區也就不需要了。(這也意味著:如果你輸入的是變量或其它東西,你就必須指定“__declspec(dllimport)”,因為一個具有jmp指令的存根只合適于函數。)
不管怎樣,符號“x”的地址都被存在“__imp_x”的存儲單元。所有這樣的存儲單元一起形成所謂的“輸入地址表”,此表是由被用到的各DLL文件中的輸入庫提供給鏈接器的。輸入地址表就是由下面這種形式的一組地址組成的:
?? __imp__symbol:?? 0xdeadbeef
?? __imp__symbol2:? 0x40100
?? __imp__symbol3:? 0x300100
?? ...
這個輸入地址表是輸入目錄的一部分,并且被IMAGE_DIRECTORY_ENTRY_IAT(輸入地址表目錄項)目錄指針所指向(盡管有些鏈接器不設置此目錄項,程序也能運行;很明顯地,這是因為加載器不使用IMAGE_DIRECTORY_ENTRY_IAT(輸入地址表目錄項)目錄也能解決輸入問題)。
這些地址并不被鏈接器所知;鏈接器只插入一些偽地址(函數名稱的RVA;參見后面的更多信息),這些偽地址會在載入時被加載器用輸出DLL文件中的輸出目錄來修正。輸入地址表,以及它是怎樣被加載器找到的,將會在本章的后面被詳細講述。
注意:這個介紹是針對C語言規范的;有些別的應用程序構建環境是不使用輸入庫的,盡管它們都需要建立一個輸入地址表,用來讓它們的程序訪問輸入對象和函數。C語言編譯器往往使用輸入庫,因為無論如何講,這都有利于它們----它們的鏈接器使用好庫。別的環境使用的是例如:一個列出需要的DLL文件名稱和函數名稱的描述文件(比如“模塊定義文件”),或者一個源文件中的聲明形式的列表等。
這就是程序的代碼如何使用輸入函數的;現在我們再來看看輸入目錄是如何建立以便加載器使用的。
輸入目錄應該存在于是“已初始化數據”并且“可讀”的節中。
輸入目錄是一個多IMAGE_IMPORT_DESCRIPTOR(輸入描述結構)的數組,每個被使用的DLL文件都有一個。(它們的)列表由一個全部用0填充的IMAGE_IMPORT_DESCRIPTOR(輸入地址表目錄項)結構作為結束。
一個IMAGE_IMPORT_DESCRIPTOR(輸入地址表目錄項)是一個擁有下列成員的結構體:
??? OriginalFirstThunk(原始第一個換長)(漢譯的說明見注釋)
??????? 它是一個RVA(32位),指向一個以0結尾的、由IMAGE_THUNK_DATA(換長數據)的RVA構成的數組,其每個IMAGE_THUNK_DATA(換長數據)元素都描述一個函數。此數組永不改變。
??? TimeDateStamp(時間日期戳)
??????? 它是一個具有好幾個目的的32位的時間戳。讓我們先假設時間戳為0,一些高級的情況將在以后處理。
??? ForwarderChain(中轉鏈)
??????? 它是輸入函數列表中第一個中轉的、32位的索引。中轉也是高級的東東。對初學者先將所有位設為-1。
???????
??? Name(名稱)
??????? 它是一個DLL文件的名稱(0結尾的ASCII碼字符串)的、32位的RVA。
???????
??? FirstThunk(第一換長)
??????? 它也是一個RVA(32位),指向一個0結尾的、由IMAGE_THUNK_DATA(換長數據)的RVA構成的數組,其每個IMAGE_THUNK_DATA(換長數據)元素都描述一個函數。此數組是輸入地址表的一部分,并且可以改變。
因此,數組中的每個IMAGE_IMPORT_DESCRIPTOR(輸入描述結構)結構體都給出輸出DLL文件的名稱,并且,除了中轉和時間日期戳,它還給出2個指向IMAGE_THUNK_DATA(換長數據)的數組的RVA,都是32位。(每個數組的最后一個成員都全部填充為0字節,以標志結尾。)
目前看來,每個IMAGE_THUNK_DATA(換長數據)都是一個RVA,指向一個描述輸入函數的IMAGE_IMPORT_BY_NAME(輸入名字)項。
現在,有趣的是兩個數組并行運行,也就是說:它們指向同一組IMAGE_IMPORT_BY_NAME(輸入名字)。
沒有必要失望,我將再畫一圖。這里是IMAGE_IMPORT_DESCRIPTOR(輸入描述結構)的關鍵內容:
????? 原始第一個換長??????? 第一個換長
??????????? |??????????????????? |
??????????? |??????????????????? |
??????????? |??????????????????? |
??????????? V??????????????????? V
??????????? 0-->??? 函數1???? <--0
??????????? 1-->??? 函數2???? <--1
??????????? 2-->??? 函數3???? <--2
??????????? 3-->??? foo?????? <--3
??????????? 4-->??? mumpitz?? <--4
??????????? 5-->??? knuff???? <--5
??????????? 6-->0??????????? 0<--6????? /* 最后的RVA是0! */
圖當中的名字就是尚未討論的IMAGE_IMPORT_BY_NAME(輸入名字)。每一個都是一個16位的數字(一個提示)跟著一些數量未定的字節,它們都是以0結尾的、輸入符號的ASCII碼名字。
提示就是指向輸出DLL文件名字表的索引(參見上面的輸出目錄)。那個索引中的名字將被一一嘗試,如果沒有相符的,再使用二進制搜索來尋找名字。
(有些鏈接器不愿意查找正確的提示,總是只簡單的將其指定為1,或者其它的隨意數字。這并無大害,只是使解決名字的第一次嘗試總是失敗,并迫使每個名字都使用二進制搜索來進行。)
總結一下:如果你想從“knurr”DLL中查找輸入函數“foo”的信息,第一步你先找到數據目錄中的IMAGE_DIRECTORY_ENTRY_IMPORT(輸入目錄項)項,得到一個RVA,再在原始節數據中找到那個地址,現在你就得到一個IMAGE_IMPORT_DESCRIPTOR(輸入描述結構)數組了。通過查看根據它們的“名稱”被指向的字符串,得到和“knurr”DLL有關的這個數組的成員(即一個輸入描述結構)。在你找到正確的IMAGE_IMPORT_DESCRIPTOR(輸入描述結構)后,順著它的“OriginalFirstThunk”(原始第一個換長)得到被指向的IMAGE_THUNK_DATA(換長數據)數組;再通過查詢RVA找到“foo”函數。
好了,為什么我們有“兩”列指向IMAGE_IMPORT_BY_NAME(輸入名字)的指針呢?這是因為在運行時,應用程序不需要輸入函數的名字,只需要地址。在這里輸入地址表又出現了。加載器將從相關的DLL文件的輸出目錄中查找每一個輸入符號,并用DLL文件入口點的線性地址替換“FirstThunk”( 第一個換長)列表中的IMAGE_THUNK_DATA(換長數據)元素(到現在之前它還是指向IMAGE_IMPORT_BY_NAME(輸入名字)的)。
請記住帶有象“__imp__symbol”標簽的地址列表;被數據目錄IMAGE_DIRECTORY_ENTRY_IAT(輸入地址表目錄項)所指向的輸入地址表,就是被“FirstThunk”( 第一個換長)所指向的列表。[在從好幾個DLL文件輸入的情況下,輸入地址表是包含所有DLL文件的“FirstThunk”( 第一個換長)數組。目錄項IMAGE_DIRECTORY_ENTRY_IAT(輸入地址表目錄項)可能會丟失,但輸入(函數)仍能工作良好。]
“OriginalFirstThunk”( 原始第一個換長)數組保持不變,因此你總能通過“OriginalFirstThunk”( 原始第一個換長)列表查找原始的輸入名字列表。
現在輸入已經被用正確的線性地址修正,如下所示:
?????? 原始第一個換長??????? 第一個換長
??????????? |??????????????????? |
??????????? |??????????????????? |
??????????? |??????????????????? |
??????????? V??????????????????? V
??????????? 0-->??? 函數1??????? 0-->? 輸出函數1
??????????? 1-->??? 函數2??????? 1-->? 輸出函數2
??????????? 2-->??? 函數3??????? 2-->? 輸出函數3
??????????? 3-->??? foo????????? 3-->? 輸出函數foo
??????????? 4-->??? mumpitz????? 4-->? 輸出函數mumpitz
??????????? 5-->??? knuff??????? 5-->? 輸出函數knuff
??????????? 6-->0??????????? 0<--6
這是簡單情況下的基本結構。現在我們將要學習輸入目錄中的需細講的東西。
第一,當數組中IMAGE_THUNK_DATA元(換長數據)素的IMAGE_ORDINAL_FLAG(序數標志)位(也是:MSB,參見注釋)被置1時,表示列表中沒有符號的名字信息,符號只以序數輸入。你可通過查看IMAGE_THUNK_DATA(換長數據)中的低地址word來得到序數。
通過序數輸入是不鼓勵的,通過名字輸入會更安全,因為如果輸出DLL文件不是預期的版本時輸出序數可能會改變。
第二,有所謂的“綁定輸入”。
請思考一下加載器的工作:當它想執行的一個二進制文件需要一個DLL中的函數時,加載器會載入該DLL,找到它的輸出目錄,查找函數的RVA并計算函數的入口點。然后用這樣找到的地址修正“FirstThunk”( 第一個換長)列表。
假設程序員很聰明,給DLL文件提供的唯一優先載入地址不會發生沖突,那么我們就能認為函數的入口點將總是相同的。它們在鏈接時能被算出并被補進“FirstThunk”( 第一個換長)列表中,這就是“綁定輸入”所發生的一切。(“綁定”工具就是干這個的,它是Win32 SDK的一部分。)???????????
當然,你得慎重:用戶的DLL可能是不同的版本,或者DLL必須重定位,這些都會使先前修正的“FirstThunk”( 第一個換長)列表不再有效;此時,加載器仍能查尋“OriginalFirstThunk”( 原始第一個換長)列表,找出輸入符號并重新補正“FirstThunk”( 第一個換長)列表。加載器知道這是必須的,當:1)輸出DLL文件的版本不符,或2)輸出DLL文件需要重定位時。
確定有沒有重定位表對加載器來說不是問題,但該怎樣找出版本的不同呢?這時IMAGE_IMPORT_DESCRIPTOR(輸入描述結構)的“時間戳”就派上用場了。如果它是0,表明輸入列表沒有被綁定,加載器總是要修復入口點。否則的話,輸入被綁定,“時間戳”必須要和“文件頭”中的輸出DLL文件的“時間戳”相符;如果不符的話,加載器就認為該二進制文件被綁到一個“錯誤”的DLL文件上并重新補正輸入列表。
這里有另外一個有關輸入列表中的“中轉”的怪事。一個DLL文件能輸出一個不定義在本DLL文件中卻需從另一個DLL文件中輸入的符號;這樣的符號據說就是被中轉的(參見上面的輸出目錄描述)。
現在,很明顯的,你不能通過查看那個實際上并不包含入口點信息的DLL文件的時間戳來確定一個符號的入口點是否有效。因此,出于安全的原因,中轉符號的入口點必須總是被修正。在二進制文件的輸入列表中,中轉符號的輸入必須被找出,以便加載器能補正它們。
這一點可通過“ForwarderChain”(中轉鏈)來做到。它是一個指向換長列表中的索引值;被索引位置的輸入就是一個中轉輸出,并且此位置的“FirstThunk”( 第一個換長)列表中的內容就是“下一個”中轉輸入的索引值,以此類推,直到索引值為-1,就表明已沒有其他的中轉了。如果根本就沒有中轉,那么“ForwarderChain”(中轉鏈)的值本身就為-1。
這就是所謂的“老式”綁定。
至此,我們應該總結一下我們目前已掌握的情況 :-)
OK,我將認為你已找到了IMAGE_DIRECTORY_ENTRY_IMPORT(輸入目錄項)并且已根據它找到了它的輸入目錄,位于某個節中。現在你已處于IMAGE_IMPORT_DESCRIPTOR(輸入描述結構)數組的開頭了,此類數組的最后一個將以全0字節填充。
要讀懂一個IMAGE_IMPORT_DESCRIPTOR(輸入描述結構),你得先查看它的“名字”項,根據它的RVA,你就能找到輸出DLL文件的名字。下一步你得確定輸入是否是綁定的;如果輸入是綁定的,“時間戳”就會是非“0”的。如果它們是綁定的,現在就是你通過比較“時間戳”來檢查DLL文件的版本是否相符的好機會了。
現在你根據“OriginalFirstThunk”( 原始第一個換長)的RVA來到了IMAGE_THUNK_DATA(換長數據)數組;過完這些數組(它是0結尾的),它的每個成員都將是一個IMAGE_IMPORT_BY_NAME(輸入名字)的RVA(除非它的高位被置1,此時你找不到名字只有序數)。根據那個RVA,并跳過2字節(即‘提示’),現在你就得到一個以0結尾的字符串,這就是輸入函數的名字。
在綁定輸入時要找到提供的入口點,先根據“FirstThunk”( 第一個換長)平行的來到“OriginalFirstThunk”( 原始第一個換長)數組;數組成員就是入口點的線性地址(暫時不考慮中轉的話題)。
還有一件我到現在都沒有提及的事情:明顯地有些鏈接器在構建輸入目錄時會產生bug(我就發現一個還在被一個Borland C鏈接器使用的bug)。這些鏈接器把IMAGE_IMPORT_DESCRIPTOR(輸入描述結構)中的“OriginalFirstThunk”( 原始第一個換長)設為0,并只建立“FirstThunk”( 第一個換長)。很明顯的,這樣的輸入目錄不能被綁定(否則重修輸入的必須信息就會丟失----你根本找不到函數名字)。在這種情況下,你得根據“FirstThunk”( 第一個換長)數組來取得輸入符號名字,你將永遠得不到預先補正的入口地址。我已發現一個TIS文件(參考書目[6]),講述一個在某種程度上和此bug兼容的輸入目錄,因此那個文件可能就是該bug的起源。
TIS文件規定:
??? IMPORT FLAGS(輸入標志)
??? TIME/DATE STAMP(時間/日期戳)
??? MAJOR VERSION - MINOR VERSION(主版本號 - 小版本號)
??? NAME RVA(名字的RVA)
??? IMPORT LOOKUP TABLE RVA(輸入查詢表的RVA)
??? IMPORT ADDRESS TABLE RVA(輸入地址表的RVA)
而別處使用的對應結構是:
??? OriginalFirstThunk( 原始第一個換長)
??? TimeDateStamp(時間日期戳)
??? ForwarderChain(中轉鏈)
??? Name(名字)
??? FirstThunk(第一個換長)
最后一個關于輸入目錄的需要細講的就是所謂的“新式”綁定(在參考書目[3]中講述),它也可以由“綁定”工具來處理。當使用這種方式時,“時間日期戳”的所有位被置為1,并且沒有中轉鏈;此時所有輸入符號的地址都將被補正,而不管它們是不是中轉的。盡管如此,你還是需要知道DLL的版本,并且你還是需要將序數符號從中轉符號中區分開來。為了達到這個目的,IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT(綁定輸入目錄項)目錄被創建了。就我所見,它將不被放在節中,而是被放在頭中,處于節頭之后第一節之前。(咳,這不是我的發明,我只是講述它而已!)
這個目錄告訴你,每一個已使用的DLL文件的中轉輸出是從哪些別的DLL文件中來的。
結構是IMAGE_BOUND_IMPORT_DESCRIPTOR(綁定輸入描述結構)形式的,包括(按這個順序):
一個32位數字,“時間戳”。
一個16位數字,“OffsetModuleName(模塊名字偏移量)”,是從目錄開頭到以0結尾的DLL文件名的偏移量;
一個16位數字,“NumberOfModuleForwarderRefs(模塊中轉參考的數字)”,給出這個DLL文件為它的中轉使用的DLL文件數。
緊隨這個結構之后你會發現“NumberOfModuleForwarderRefs(模塊中轉參考的數字)”結構,告訴你這個DLL文件的中轉所來自的DLL文件的名稱和版本。這些結構就是“IMAGE_BOUND_FORWARDER_REF(綁定中轉參考)”結構的:
一個32位的數字“時間日期戳”(TimeDateStamp);
一個16位的數字“模塊名稱偏移量”(OffsetModuleName),它就是從目錄開頭到中轉來自的那個DLL文件的0結尾的名字處的偏移量;
一個16位的未使用單元。
跟在“IMAGE_BOUND_FORWARDER_REF(綁定中轉參考)”后的是下一個“IMAGE_BOUND_IMPORT_DESCRIPTOR(綁定輸入描述結構)”,以此類推;列表最終以一個全部為0位的IMAGE_BOUND_IMPORT_DESCRIPTOR(綁定輸入描述結構)結束。
我對由此(描述)造成的不便表示歉意,但這就是它看起來的樣子:-)
現在,如果你有一個新的綁定輸入目錄,你得載入所有的DLL文件,并使用目錄指針IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT(綁定輸入目錄項)找到IMAGE_BOUND_IMPORT_DESCRIPTOR(綁定輸入描述結構),掃描整個IMAGE_BOUND_IMPORT_DESCRIPTOR(綁定輸入描述結構),并檢查被載入的DLL文件的“時間日期戳”和這個目錄中提供的是否相符。如果不符,就將輸入目錄中“第一換長”(FirstThunk)中的錯誤全部修改過來。
?
8.資源(resources)
-------------------
資源,比如對話框、菜單、圖標等等,都存儲在IMAGE_DIRECTORY_ENTRY_RESOURCE(“資源目錄項”)指向的數據目錄中。它們處于一個至少“IMAGE_SCN_CNT_INITIALIZED_DATA(已初始化數據內容節)”和“IMAGE_SCN_MEM_READ(內存可讀節)”標志位都被置為1的節中。
資源的基礎是“資源目錄”(IMAGE_RESOURCE_DIRECTORY);它包含好幾個“資源目錄項”(IMAGE_RESOURCE_DIRECTORY_ENTRY),其中的每一項反過來又可能指向一個“資源目錄”。按照這種方式,你就得到一個以“資源目錄項”為樹葉的“資源目錄”樹;它們的樹葉指向實際的資源數據。
在實際使用中,情況會稍微簡單些。一般你不會遇到不可能理清的特別復雜的樹的。
通常,它的層次結構是這樣的:一個目錄作為根。它指向很多目錄,每種資源類型都有一個。這些目錄又指向子目錄,每個子目錄都有一個名字或者ID號并指向這個資源所提供的各種語言的目錄;每種語言你都能找到一個資源項,資源項最終指向(具體的)數據。(注意:多語言資源不能在Win95上運行。即使程序有好幾種語言,Win95也總是使用相同的資源----我沒有查出是哪一種,但我猜測肯定是它最先碰到的那種。多語言資源在NT系統上可以運行。)
沒有指針的樹大致象這樣:
?????????????????????????? ( 根 )
????????????????????????????? |
???????????? +----------------+------------------+
???????????? |??????????????? |????????????????? |
??????????? 菜單?????????? 對話框????????????? 圖標
???????????? |??????????????? |????????????????? |
?????? +-----+-----+??????? +-+----+?????????? +-+----+----+
?????? |?????????? |??????? |????? |?????????? |????? |??? |
??? "main"????? "popup"?? 0x10?? "maindlg"??? 0x100 0x110 0x120
?????? |??????????? |?????? |????? |?????????? |????? |??? |
?? +---+-+????????? |?????? |????? |?????????? |????? |??? |
?? |???? |???? default?? english?? default??? def.?? def.? def.
german english
一個“資源目錄項”(IMAGE_RESOURCE_DIRECTORY)包含:
32位未使用標志,叫做“特征”(Characteristics);
32位“時間日期戳”(同樣按常用的time_t表示法),告訴你資源被創建的時間(如果此項被設置的話);
16位“主版本號”(MajorVersion)和16位“小版本號”(MinorVersion),以允許你據此維護資源的幾個版本;
16位“已命名項目數”(NumberOfNamedEntries)和另一個16位的“ID項目數”(NumberOfIdEntries)。
緊隨此結構后的是“已命名項目數”+“ID項目數”兩結構體,它們都是“資源目錄項”格式,都以名字開頭。它們可能指向下一個“資源目錄”或者指向實際的資源數據。
一個“資源目錄項”由下面組成:
32位單元提供你它所描述的資源的ID或者是目錄;
32位的到數據的偏移量或者是到下一個子目錄的偏移量。
ID的含義取決于樹中的層次;ID可能是一個數字(如果最高位為0)也可能是一個名字(如果最高位為1)。如果是一個名字,它的低31位就是從資源節原始數據的開始到這個名字(名字有16位長并由unicode的寬字符而不是0結尾符作為結束)的偏移量。
如果你位于根目錄之中,且如果ID是一個數字的話,那么它指的就是下面的一種資源類型:
??? 1: 光標
??? 2: 位圖
??? 3: 圖標
??? 4: 菜單
??? 5: 對話框
??? 6: 字串表
??? 7: 字體目錄
??? 8: 字體
??? 9: 快捷鍵
??? 10: 未格式化資源數據
??? 11: 信息表
??? 12: 組光標
??? 14: 組圖標
??? 16: 版本信息
任何其它數字都是用戶自定義的。任何有類型名的資源類型也是用戶自定義的。
如果你處于(樹的)下一層當中,此時ID一定是一個數字,且就是資源的一個特例的語言ID號;例如,你可以(同時)擁有澳大利亞英語、加拿大法語和瑞士德語等本地化形式的對話框,并且它們分享同一個資源ID。系統會根據線程的地點來選擇要使用的對話框,反過來地點又反映了用戶的“區域設置”。(如果資源找不到線程地點,系統將先使用一個中性的子語言資源作為地點,比如它將尋找標準法語而不是用戶所擁有的加拿大法語;如果它還是找不到,就使用最小語言ID號的那個實例。必須注意,所有這些只工作于NT系統之上的。)
為便于辨認語言ID,使用宏PRIMARYLANGID()(意為“主語言ID”)和SUBLANGID()(意為“子語言ID”)將它分開為主語言ID和子語言ID,分別使用它的0-9位和10-15位。這些值定義在“winresrc.h”文件中。
語言資源只支持快捷鍵、對話框、菜單、資源數據或字符串等;其它資源類型必須為LANG_NEUTRAL/SUBLANG_NEUTRAL(中性語言/中性子語言)。
要確定資源目錄的下一層是不是另一個目錄,你可查看它的偏移量的最高位。如果它是1,剩下的31位就是從資源節原始數據的開始到下一層目錄的偏移量,還是按“資源目錄”后接“資源目錄項”的格式。如果高位為0,它就是從資源節原始數據的開始到資源的原始數據描述,即一個資源數據項的偏移量。資源的原始數據描述包含32位的“OffsetToData”(到數據的偏移量)(指的是到原始數據的偏移量,從資源節原始數據的開頭算起),32位的數據的“Size”(大小),32位的“CodePage”(代碼頁)和一個未使用的32位單元。
(不鼓勵使用代碼頁,你應該使用“語言”的特性來支持多地域。)
原始數據格式依賴于資源類型;詳細的介紹可在微軟的SDK文檔中找到。注意:除了用戶自定義資源,資源中的任何字符串總是按UNICODE格式,明顯的,用戶自定義的資源按的是開發者選定的格式。
?
9.重定位(relocations)
-----------------------
我將要描述的最后一個數據目錄是基址重定位目錄。它是由可選頭數據目錄中的IMAGE_DIRECTORY_ENTRY_BASERELOC(基址重定位目錄項)項來指向的。典型的,它包含在自己的節中,名字象“.reloc”這樣,并且IMAGE_SCN_CNT_INITIALIZED_DATA(已初始化數據內容節)、 IMAGE_SCN_MEM_DISCARDABLE(內存可丟棄節)和IMAGE_SCN_MEM_READ(內存可讀節)等標志位被置1。
如果映象文件不能被加載到可選頭中提到的優先載入地址“ImageBase”(映象基址)時,重定位數據對加載器來說就是必須的。此時,鏈接器所提供的固定地址就不再有效,并且加載器將不得不對靜態變量、字符串文字等使用的絕對地址進行修正。
所謂重定位目錄就是一些連續的塊,每一塊都包含4K映象文件的重定位信息。塊由一個“IMAGE_BASE_RELOCATION(基址重定位)”結構體開始,這個結構體包含一個32位的“VirtualAddress(虛擬地址)”項和一個32位的“SizeOfBlock(塊大小)”項。跟在它們后面的就是塊的實際重定位數據,每一條都是16位的。
“VirtualAddress(虛擬地址)”就是重定位所在塊需要應用的基本的RVA;“SizeOfBlock(塊大小)”就是整個塊的字節大小;跟在后面的重定位的數目是:('SizeOfBlock'-sizeof(IMAGE_BASE_RELOCATION))/2個。當你碰到一個“VirtualAddress(虛擬地址)”值為0的“IMAGE_BASE_RELOCATION(基址重定位)”結構體時,重定位信息就結束了。
每一個16位的重定位信息由低12位的重定位位置和高4位的重定位類型組成。要得到重定位的RVA,你需要用這個12位的位置加上“IMAGE_BASE_RELOCATION(基址重定位)”中的“VirtualAddress(虛擬地址)”。類型是下面之一:
??? IMAGE_REL_BASED_ABSOLUTE (0)
??????? 這種不需操作;用于將塊按32位邊界對齊。位置應該為0。
??? IMAGE_REL_BASED_HIGH (1)
??????? 重定位的高16位必須被用于被偏移量所指向的那個16位的WORD單元,此WORD是一個32位的DWORD的高位WORD。
??? IMAGE_REL_BASED_LOW (2)
??????? 重定位的低16位必須被用于被偏移量所指向的那個16位的WORD單元,此WORD是一個32位的DWORD的低位WORD。
??? IMAGE_REL_BASED_HIGHLOW (3)
??????? 重定位的全部32位必須應用于上面所說的全部32位。這種(和不需操作的第“0”種)是我在二進制文件種實際發現的僅有的重定位類型。
??? IMAGE_REL_BASED_HIGHADJ (4)
??????? 這是一種復雜的。請自己參閱(參考文獻[6]),并努力弄懂它的意思:“高調整。這種修正要求一個全32位值。高16位定位于偏移量處,低16位定位在下一個數組元素(此數組元素包括在大小的域中)的偏移量處。它們兩個需要被連成一個有符號的變量。加上32位的增量。然后加上0x8000并將有符號變量的高16位存儲在偏移量處的16位域中。”
??? IMAGE_REL_BASED_MIPS_JMPADDR (5)
??????? 不清楚
??? IMAGE_REL_BASED_SECTION (6)
??????? 不清楚
??? IMAGE_REL_BASED_REL32 (7)
??????? 不清楚
舉一個例子,如果你發現重定位信息是
??? 0x00004000????? (32位, 開始的RVA)
??? 0x00000010????? (32位, 塊的大小)
??? 0x3012????????? (16位的重定位數據)
??? 0x3080????????? (16位的重定位數據)
??? 0x30f6????????? (16位的重定位數據)
??? 0x0000????????? (16位的重定位數據)
??? 0x00000000????? (下一塊的RVA)
??? 0xff341234
你知道第一塊描述的重定位開始于RVA 0x4000處,有16字節長。因為頭用掉了8字節,并且一個重定位要用2字節,所以塊中計有(16-8)/2=4個重定位。第一個重定位被應用于0x4012處的DWORD,第二個于0x4080處的DWORD,第三個于0x40f6處的DWORD。最后一個不需操作。
下一塊的RVA是0,列表結束。
好,你怎么處理一個重定位呢?
你能知道映象文件“被”重定位到可選頭“ImageBase(映象基址)”的優先載入地址;你也能知道你真正載入的地址。如果它們相同,你什么也不用做。如果它們不同,你需計算出實際基址-優先基址的差并加上重定位位置的值(有符號,可能為負值),此值你可通過上面講述的方法找到。
?
九、致謝(Acknowledgments)
---------------------------
感謝David Binette的調試和校讀。(剩下的錯誤全部都是我的。)
也感謝wotsit.org網站讓我將此文放到他們的網站上。
?
十、版權(Copyright)
---------------------
本文的版權屬于B. Luevelsmeyer,1999年。它是免費的,你可以任意的使用,但后果自負。它含有錯誤并不完整,特此警告。
?
十一、Bug報告(Bug reports)
----------------------------
Bug報告(或其他建議)請發送至:bernd.luevelsmeyer@iplan.heitec.net
?
十二、版本(Versions)
----------------------
你可在文件的頂部找到當前的版本號。
1998-04-06
? 第一次公開發表
1998-07-29
? 將映象文件版本和子系統版本中錯誤的“byte”改為“word”
? 更正“棧只限于1 MB”的錯誤(實際上沒有上限)
? 更正一些輸入錯誤
1999-03-15
? 更正輸出目錄的描述,原來非常不全
? 調整輸入目錄的描述,原來講的不清
? 更正輸入錯誤并為其它節改了一些詞句
?
十三、參考文獻(Literature)
----------------------------
[1]
"Peering Inside the PE: A Tour of the Win32 Portable Executable File
Format" (M. Pietrek), in: Microsoft Systems Journal 3/1994
[2]
"Why to Use _declspec(dllimport) & _declspec(dllexport) In Code", MS
Knowledge Base Q132044
[3]《Windows 問與答》
"Windows Q&A" (M. Pietrek), in: Microsoft Systems Journal 8/1995
[4]《編寫多語言資源》
"Writing Multiple-Language Resources", MS Knowledge Base Q89866
[5]
"The Portable Executable File Format from Top to Bottom" (Randy Kath),
in: Microsoft Developer Network
[6]《Windows下TIS格式規范1.0版》
Tool Interface Standard (TIS) Formats Specification for Windows Version
1.0 (Intel Order Number 241597, Intel Corporation 1993)
?
附錄(Appendix: hello world):
-------------------------------
在這個附錄中我將給大家展示一下怎樣手工建立一個程序。因為我不會DEC Alpha的,本例將使用Intel匯編語言。
本程序相當于
??? #include <stdio.h>
??? int main(void)
??? {
??????? puts(hello,world);
??????? return 0;
??? }
首先,我使用Win32函數來翻譯它以取代C運行時庫:
??? #define STD_OUTPUT_HANDLE -11UL
??? #define hello "hello, world\n"
??? __declspec(dllimport) unsigned long __stdcall
??? GetStdHandle(unsigned long hdl);
??? __declspec(dllimport) unsigned long __stdcall
??? WriteConsoleA(unsigned long hConsoleOutput,
??????????????????? const void *buffer,
??????????????????? unsigned long chrs,
??????????????????? unsigned long *written,
??????????????????? unsigned long unused
??????????????????? );
??? static unsigned long written;
??? void startup(void)
??? {
??????? WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE),hello,sizeof(hello)-1,&written,0);
??????? return;
??? }
現在我將笨拙的將它匯編出來:
??? startup:
??????????????? ; WriteConsole()的參數, 反向的
??? 6A 00???????????????????? push????? 0x00000000
??? 68 ?? ?? ?? ????????????? push????? offset _written
??? 6A 0D???????????????????? push????? 0x0000000d
??? 68 ?? ?? ?? ????????????? push????? offset hello
??????????????? ; GetStdHandle()的參數
??? 6A F5???????????????????? push????? 0xfffffff5
??? 2E FF 15 ?? ?? ?? ??????? call????? dword ptr cs:__imp__GetStdHandle@4
??????????????? ; 結果是WriteConsole()的參數
??? 50??????????????????????? push????? eax
??? 2E FF 15 ?? ?? ?? ??????? call????? dword ptr cs:__imp__WriteConsoleA@20
??? C3??????????????????????? ret??????
??? hello:
??? 68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A?? "hello, world\n"
??? _written:
??? 00 00 00 00
以上就是編譯的部分。任何人都能做到這點。從現在起讓我們扮演起鏈接器的角色,這會非常有趣 :-)
我需要先找出函數WriteConsoleA()和GetStdHandle()。碰巧它們都在“kernel32.dll”中。(這是“輸入庫”部分。)
現在我開始做可執行文件。問號代表待定的值;它們將在以后被修正。
首先是DOS-根,開始于0x0,有0x40字節長:
??? 00 | 4d 5a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
??? 10 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
??? 20 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
??? 30 | 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00
正如你所見到的,這不是真正的MS-DOS程序。它只是一個開始部分有“MZ”簽名的頭和緊跟在頭后面的e_lfanew指針,沒有任何代碼。這是因為它并非打算運行于MS-DOS之上;它之所以在這里只是因為規范的需要。
然后是PE簽名,開始于0x40,有0x4字節長:
??????? 50 45 00 00
現在到了文件頭,開始于0x44,有0x14字節長:
??? Machine???????????????????? 4c 01?????? ; i386
??? NumberOfSections??????????? 02 00?????? ; 代碼段和數據段
??? TimeDateStamp?????????????? 00 00 00 00 ; 誰管它?
??? PointerToSymbolTable??????? 00 00 00 00 ; 未用
??? NumberOfSymbols???????????? 00 00 00 00 ; 未用
??? SizeOfOptionalHeader??????? e0 00?????? ; 常量
??? Characteristics???????????? 02 01?????? ; 32位機器上的可執行文件
接著是可選頭,開始于0x58,有0x60字節長:
??? Magic?????????????????????? 0b 01?????? ; 常量
??? MajorLinkerVersion????????? 00????????? ; 我是 0.0 版:-)
??? MinorLinkerVersion????????? 00????????? ;
??? SizeOfCode????????????????? 20 00 00 00 ; 32字節代碼
??? SizeOfInitializedData?????? ?? ?? ?? ?? ; 待找出
??? SizeOfUninitializedData???? 00 00 00 00 ; 我們沒有BSS節
??? AddressOfEntryPoint???????? ?? ?? ?? ?? ; 待定
??? BaseOfCode????????????????? ?? ?? ?? ?? ; 待定
??? BaseOfData????????????????? ?? ?? ?? ?? ; 待定
??? ImageBase?????????????????? 00 00 10 00 ; 1 MB, 隨意選
??? SectionAlignment??????????? 20 00 00 00 ; 32字節對齊
??? FileAlignment?????????????? 20 00 00 00 ; 32字節對齊
??? MajorOperatingSystemVersion? 04 00????? ; NT 4.0
??? MinorOperatingSystemVersion? 00 00????? ;
??? MajorImageVersion?????????? 00 00?????? ;0.0版
??? MinorImageVersion?????????? 00 00?????? ;
??? MajorSubsystemVersion?????? 04 00?????? ; Win32 4.0
??? MinorSubsystemVersion?????? 00 00?????? ;
??? Win32VersionValue?????????? 00 00 00 00 ; 未使用?
??? SizeOfImage???????????????? ?? ?? ?? ?? ; 待定
??? SizeOfHeaders?????????????? ?? ?? ?? ?? ; 待定
??? CheckSum??????????????????? 00 00 00 00 ; 非驅動不用
??? Subsystem?????????????????? 03 00?????? ; Win32控制臺
??? DllCharacteristics????????? 00 00?????? ; 未用 (不是一個DLL)
??? SizeOfStackReserve????????? 00 00 10 00 ; 1 MB棧
??? SizeOfStackCommit?????????? 00 10 00 00 ; 開始時4 KB
??? SizeOfHeapReserve?????????? 00 00 10 00 ; 1 MB堆
??? SizeOfHeapCommit??????????? 00 10 00 00 ; 開始時4 KB
??? LoaderFlags???????????????? 00 00 00 00 ; 未知
??? NumberOfRvaAndSizes???????? 10 00 00 00 ; 常量
正如你所見,我計劃只用2個節,一個用于代碼,一個用于所有剩余的東西(數據、常量和輸入目錄等)。沒有重定位和象資源之類其它東西。我也不用BSS節并將變量“written”放入已初始化數據。文件和RAM中的節對齊都是一樣的(32字節);這將有助于使任務簡單,否則我就得來回地計算RVA很多次。
現在我們設置數據目錄,開始于0xb8字節,有 0x80字節長:
?????? 地址?????????? 大小
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_EXPORT (0)
??? ?? ?? ?? ????? ?? ?? ?? ?????????? ; IMAGE_DIRECTORY_ENTRY_IMPORT (1)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_SECURITY (4)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_DEBUG (6)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_TLS (9)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_IAT (12)
??? 00 00 00 00??? 00 00 00 00???????? ; 13
??? 00 00 00 00??? 00 00 00 00???????? ; 14
??? 00 00 00 00??? 00 00 00 00???????? ; 15
僅使用輸入目錄。
下一個使節頭。首先我們做代碼節的,代碼節將包含前面所編的匯編語句。它有32字節長,所以代碼節也就是這么長。節頭從0x138處開始,有0x28字節長:
??? Name??????????? 2e 63 6f 64 65 00 00 00???? ; ".code"的ASCII碼值
??? VirtualSize???????? 00 00 00 00???????????? ; 未用
??? VirtualAddress????? ?? ?? ?? ?????????????? ; 待定
??? SizeOfRawData?????? 20 00 00 00???????????? ; 代碼的大小
??? PointerToRawData??? ?? ?? ?? ?????????????? ; 待定
??? PointerToRelocations 00 00 00 00??????????? ; 未用
??? PointerToLinenumbers 00 00 00 00??????????? ; 未用
??? NumberOfRelocations? 00 00????????????????? ; 未用
??? NumberOfLinenumbers? 00 00????????????????? ; 未用
??? Characteristics???? 20 00 00 60???????????? ; 代碼節,可執行,可讀
第二節將包含數據。節頭開始于0x160處,有0x28字節長:
??? Name??????????? 2e 64 61 74 61 00 00 00???? ; ".data"的ASCII碼值
??? VirtualSize???????? 00 00 00 00???????????? ; 未用
??? VirtualAddress????? ?? ?? ?? ?????????????? ; 待定
??? SizeOfRawData?????? ?? ?? ?? ?????????????? ; 待定
??? PointerToRawData??? ?? ?? ?? ?????????????? ; 待定
??? PointerToRelocations 00 00 00 00??????????? ; 未用
??? PointerToLinenumbers 00 00 00 00??????????? ; 未用
??? NumberOfRelocations? 00 00????????????????? ; 未用
??? NumberOfLinenumbers? 00 00????????????????? ; 未用
??? Characteristics???? 40 00 00 c0???????????? ; 已初始化的,可讀,可寫
下一個字節位于0x188處,但節需要按32字節(的倍數)對齊(因為我是這樣選擇的),所以我們需要添一些(0)字節直到0x1a0處:
??? 00 00 00 00 00 00?????? ; 填充的
??? 00 00 00 00 00 00
??? 00 00 00 00 00 00
??? 00 00 00 00 00 00
現在第一節,就是上面所匯編的代碼節,“到”了。它開始于0x1a0處,有0x20字節長:
??? 6A 00??????????????????? ; push????? 0x00000000
??? 68 ?? ?? ?? ???????????? ; push????? offset _written
??? 6A 0D??????????????????? ; push????? 0x0000000d
??? 68 ?? ?? ?? ???????????? ; push????? offset hello_string
??? 6A F5??????????????????? ; push????? 0xfffffff5
??? 2E FF 15 ?? ?? ?? ?????? ; call????? dword ptr cs:__imp__GetStdHandle@4
??? 50?????????????????????? ; push????? eax
??? 2E FF 15 ?? ?? ?? ?????? ; call????? dword ptr cs:__imp__WriteConsoleA@20
??? C3?????????????????????? ; ret??????
因為這一節的長度(剛好32字節),在下一節(數據節)前我們不需要填充任何字節。下一節到了,從0x1c0處開始:
??? 68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A? ; "hello, world\n"的ASCII碼值
??? 00 00 00??????????????????????????????? ; 填充幾個0以和_written對齊
??? 00 00 00 00???????????????????????????? ; _written
現在剩下的只有輸入目錄了。本文件將從"kernel32.dll"庫中輸入2個函數,輸入目錄將從本節的變量后面立即開始。首先我們先將上面的數據按32字節對齊:
??? 00 00 00 00 00 00 00 00 00 00 00 00???? ; 填充的
在0x1e0處開始輸入描述(IMAGE_IMPORT_DESCRIPTOR):
??? OriginalFirstThunk????? ?? ?? ?? ?????? ; 待定
??? TimeDateStamp?????????? 00 00 00 00???? ; 未綁定
??? ForwarderChain????????? ff ff ff ff???? ; 無中轉
??? Name??????????????????? ?? ?? ?? ?????? ; 待定
??? FirstThunk????????????? ?? ?? ?? ?????? ; 待定
我們需要用一個0字節項來結束輸入目錄(我們現在位于0x1f4):
??? OriginalFirstThunk????? 00 00 00 00???? ; 結束符號
??? TimeDateStamp?????????? 00 00 00 00???? ;
??? ForwarderChain????????? 00 00 00 00???? ;
??? Name??????????????????? 00 00 00 00???? ;
??? FirstThunk????????????? 00 00 00 00???? ;
現在只剩下DLL名字,還有2個換長,以及換長數據和函數名字了。但現在我們真的很快就要完成了。
DLL名字,以0結尾,開始于0x208處:
??? 6b 65 72 6e 65 6c 33 32 2e 64 6c 6c 00? ; "kernel32.dll"的ASCII碼值
??? 00 00 00??????????????????????????????? ; 填充到32位邊界
原始第一個換長,開始于0x218處:
??? AddressOfData?? ?? ?? ?? ?????????????? ; "WriteConsoleA"函數名的RVA
??? AddressOfData?? ?? ?? ?? ?????????????? ; "GetStdHandle"函數名的RVA
??????????????????? 00 00 00 00???????????? ; 結束符號
第一個換長就是同樣的列表,開始于0x224處:
(__imp__WriteConsoleA@20, at 0x224)
??? AddressOfData?? ?? ?? ?? ?????????????? ; "WriteConsoleA"函數名的RVA
(__imp__GetStdHandle@4, at 0x228)
??? AddressOfData?? ?? ?? ?? ?????????????? ; "GetStdHandle"函數名的RVA
??????????????????? 00 00 00 00???????????? ; 結束符號
現在剩下的只有輸入名字(IMAGE_IMPORT_BY_NAME)形式的兩個函數名了。我們現處于0x230字節。
??? 01 00????????????????????????????????????? ; 序數,不需要正確
??? 57 72 69 74 65 43 6f 6e 73 6f 6c 65 41 00? ; "WriteConsoleA"的ASCII碼值
??? 02 00????????????????????????????????????? ; 序數,不需要正確
??? 47 65 74 53 74 64 48 61 6e 64 6c 65 00???? ; "GetStdHandle"的ASCII碼值
Ok, 這就全部結束了。下一個字節,我們并不真正需要,是0x24f。我們必須將節填充到0x260處:
??? 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; 填充的
??? 00
------------
我們已經完成了。因為我們已經知道了所有的字節偏移量,我們可以應用我們的修正到所有原先被用“??”符號標為“未知”的地址和大小了。
我將不強迫你一步一步地去讀它(很好懂的),只直接給出結果來:
------------
DOS-頭, 開始于0x0:
??? 00 | 4d 5a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
??? 10 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
??? 20 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
??? 30 | 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00
簽名, 開始于0x40:
??????? 50 45 00 00
文件頭, 開始于0x44:
??? Machine???????????????????? 4c 01?????? ; i386
??? NumberOfSections??????????? 02 00?????? ; 代碼和數據
??? TimeDateStamp?????????????? 00 00 00 00 ; 誰管它?
??? PointerToSymbolTable??????? 00 00 00 00 ; 未用
??? NumberOfSymbols???????????? 00 00 00 00 ; 未用
??? SizeOfOptionalHeader??????? e0 00?????? ; 常量
??? Characteristics???????????? 02 01?????? ; 可執行于32位機器上
可選頭, 開始于0x58:
??? Magic?????????????????????? 0b 01?????? ; 常量
??? MajorLinkerVersion????????? 00????????? ; 我是 0.0版 :-)
??? MinorLinkerVersion????????? 00????????? ;
??? SizeOfCode????????????????? 20 00 00 00 ; 32字節代碼
??? SizeOfInitializedData?????? a0 00 00 00 ; 數據節大小
??? SizeOfUninitializedData???? 00 00 00 00 ; 我們沒有 BSS節
??? AddressOfEntryPoint???????? a0 01 00 00 ; 代碼節的開始處
??? BaseOfCode????????????????? a0 01 00 00 ; 代碼節的RVA
??? BaseOfData????????????????? c0 01 00 00 ; 數據節的RVA
??? ImageBase?????????????????? 00 00 10 00 ; 1 MB, 任意選擇
??? SectionAlignment??????????? 20 00 00 00 ; 32字節對齊
??? FileAlignment?????????????? 20 00 00 00 ; 32字節對齊
??? MajorOperatingSystemVersion? 04 00????? ; NT 4.0
??? MinorOperatingSystemVersion? 00 00????? ;
??? MajorImageVersion?????????? 00 00?????? ; 0.0版本
??? MinorImageVersion?????????? 00 00?????? ;
??? MajorSubsystemVersion?????? 04 00?????? ; Win32 4.0
??? MinorSubsystemVersion?????? 00 00?????? ;
??? Win32VersionValue?????????? 00 00 00 00 ; 未用?
??? SizeOfImage???????????????? c0 00 00 00 ; 所有節大小的總數
??? SizeOfHeaders?????????????? a0 01 00 00 ; 第一節的偏移量
??? CheckSum??????????????????? 00 00 00 00 ; 非驅動程序不須用
??? Subsystem?????????????????? 03 00?????? ; Win32控制臺程序
??? DllCharacteristics????????? 00 00?????? ; 未用(不是一個DLL)
??? SizeOfStackReserve????????? 00 00 10 00 ; 1 MB 棧
??? SizeOfStackCommit?????????? 00 10 00 00 ; 開始時4 KB
??? SizeOfHeapReserve?????????? 00 00 10 00 ; 1 MB 堆
??? SizeOfHeapCommit??????????? 00 10 00 00 ; 開始時4 KB
??? LoaderFlags???????????????? 00 00 00 00 ; 未知
??? NumberOfRvaAndSizes???????? 10 00 00 00 ; 常量
數據目錄,開始于 0xb8:
????? 地址??????????? 大小
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_EXPORT (0)
??? e0 01 00 00??? 6f 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_IMPORT (1)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_SECURITY (4)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_DEBUG (6)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_TLS (9)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)
??? 00 00 00 00??? 00 00 00 00???????? ; IMAGE_DIRECTORY_ENTRY_IAT (12)
??? 00 00 00 00??? 00 00 00 00???????? ; 13
??? 00 00 00 00??? 00 00 00 00???????? ; 14
??? 00 00 00 00??? 00 00 00 00???????? ; 15
節頭(代碼節), 開始于0x138:
??? Name??????????? 2e 63 6f 64 65 00 00 00???? ; ".code"
??? VirtualSize???????? 00 00 00 00???????????? ; 未用
??? VirtualAddress????? a0 01 00 00???????????? ; 代碼節的RVA
??? SizeOfRawData?????? 20 00 00 00???????????? ; 代碼的大小
??? PointerToRawData??? a0 01 00 00???????????? ; 代碼節的文件偏移量
??? PointerToRelocations 00 00 00 00??????????? ; 未用
??? PointerToLinenumbers 00 00 00 00??????????? ; 未用
??? NumberOfRelocations? 00 00????????????????? ; 未用
??? NumberOfLinenumbers? 00 00????????????????? ; 未用
??? Characteristics???? 20 00 00 60???????????? ; 代碼節,可執行,可讀
節頭(數據節),開始于0x160:
??? Name??????????? 2e 64 61 74 61 00 00 00???? ; ".data"
??? VirtualSize???????? 00 00 00 00???????????? ; 未用
??? VirtualAddress????? c0 01 00 00???????????? ; 數據節的RVA
??? SizeOfRawData?????? a0 00 00 00???????????? ; 數據節的大小
??? PointerToRawData??? c0 01 00 00???????????? ; 數據節的文件偏移量
??? PointerToRelocations 00 00 00 00??????????? ; 未用
??? PointerToLinenumbers 00 00 00 00??????????? ; 未用
??? NumberOfRelocations? 00 00????????????????? ; 未用
??? NumberOfLinenumbers? 00 00????????????????? ; 未用
??? Characteristics???? 40 00 00 c0???????????? ; 已初始化,可讀,可寫
(填充)
??? 00 00 00 00 00 00?????? ; 填充的
??? 00 00 00 00 00 00
??? 00 00 00 00 00 00
??? 00 00 00 00 00 00
代碼節, 開始于0x1a0:
??? 6A 00??????????????????? ; push????? 0x00000000
??? 68 d0 01 10 00?????????? ; push????? offset _written
??? 6A 0D??????????????????? ; push????? 0x0000000d
??? 68 c0 01 10 00?????????? ; push????? offset hello_string
??? 6A F5??????????????????? ; push????? 0xfffffff5
??? 2E FF 15 28 02 10 00???? ; call????? dword ptr cs:__imp__GetStdHandle@4
??? 50?????????????????????? ; push????? eax
??? 2E FF 15 24 02 10 00???? ; call????? dword ptr cs:__imp__WriteConsoleA@20
??? C3?????????????????????? ; ret??????
數據節,開始于0x1c0:
??? 68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A? ; "hello, world\n"
??? 00 00 00??????????????????????????????? ; 填充到和_written對齊
??? 00 00 00 00???????????????????????????? ; _written
填充:
??? 00 00 00 00 00 00 00 00 00 00 00 00???? ; 填充的
輸入描述(IMAGE_IMPORT_DESCRIPTOR),開始于0x1e0:
??? OriginalFirstThunk????? 18 02 00 00???? ; 原始第一個換長的RVA
??? TimeDateStamp?????????? 00 00 00 00???? ; 未綁定
??? ForwarderChain????????? ff ff ff ff???? ; -1,無中轉
??? Name??????????????????? 08 02 00 00???? ; DLL名字的RVA
??? FirstThunk????????????? 24 02 00 00???? ; 第一個換長的RVA
結束標志(0x1f4):
??? OriginalFirstThunk????? 00 00 00 00???? ; 結束標志
??? TimeDateStamp?????????? 00 00 00 00???? ;
??? ForwarderChain????????? 00 00 00 00???? ;
??? Name??????????????????? 00 00 00 00???? ;
??? FirstThunk????????????? 00 00 00 00???? ;
DLL名字, 開始于0x208:
??? 6b 65 72 6e 65 6c 33 32 2e 64 6c 6c 00? ; "kernel32.dll"
??? 00 00 00??????????????????????????????? ; 填充到32位邊界
原始第一個換長, 開始于0x218:
??? AddressOfData?? 30 02 00 00???????????? ; 函數名"WriteConsoleA"的RVA
??? AddressOfData?? 40 02 00 00???????????? ; 函數名"GetStdHandle"的RVA
??????????????????? 00 00 00 00???????????? ; 結束標志
第一個換長,開始于0x224:
??? AddressOfData?? 30 02 00 00???????????? ; 函數名"WriteConsoleA"的RVA
??? AddressOfData?? 40 02 00 00???????????? ; 函數名"GetStdHandle"的RVA
??????????????????? 00 00 00 00???????????? ; 結束標志
輸入函數名稱(IMAGE_IMPORT_BY_NAME),開始于0x230:
??? 01 00????????????????????????????????????? ; 序數,不需要正確
??? 57 72 69 74 65 43 6f 6e 73 6f 6c 65 41 00? ; "WriteConsoleA"的ASCII碼值
IMAGE_IMPORT_BY_NAME,開始于0x240:
??? 02 00????????????????????????????????????? ; 序數,不需要正確
??? 47 65 74 53 74 64 48 61 6e 64 6c 65 00???? ; "GetStdHandle"的ASCII碼值
(填充)
??? 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; 填充的
??? 00
第一個未使用字節開始于: 0x260
--------------
噢,這個文件能在NT上卻不能在windows 95上運行。windows 95不能運行按32字節節對齊的應用程序,它要求節對齊為4 KB;并且很明顯的,文件對齊也應為512字節。因此要想在windows 95上運行,你得插入很多的0字節(為了對齊)并調整RVA。感謝D. Binette在windows 95上的(運行)試驗。
??????? -- 全文結束 --?????????
[譯后記]:
由于時間等因素,遺漏、重復、不準確甚至錯誤等情況在所難免,敬請各位批評、指正!另外,由于我保留了所有的英文術語(譯文就在后面),所以譯文看起來有點亂,請大家見諒!
本文的原文寫于1999年,由于時間關系,文中所說的有關公司、某某項目應用的操作系統范圍等等介紹可能已經不對或不準確了,請大家自己分析、鑒別。
最后再談點個人感想:
1)個人覺得本文的難點在于輸入符號(表)部分,而其精華乃在附錄之中。在學習前面的各種項目成員名稱、說明等的同時,如能對照后面的附錄來學習,將會起到事半功倍的效果。另外,文中所說的什么結構體、共用體之類術語都是針對編程而言,如果你并不想或不會編程的話,可以將其理解為一個將其它東西集合在一起的一個容器就行了。
2)原文發表于1998-1999年之間,而相應的中文譯文至今也難在網上搜尋得到,這對中國的破界來說不能說不是一個很大的遺憾!本文僅起拋磚引玉之用,希望能有更多、更好、更及時的國外類似資料出現在我們的網絡之上,以造福于我們這些廣大的菜鳥。
????????????????????????????????????????????????????????????????????? 沈忠平??? 2006.02 于和州
===========================
|“PE文件格式”1.9版注釋:|
===========================
①Win32s和Win32
Win32s是“WIN32 subset”的縮寫,它是一個可被加入到Windows 3.1和Windows for Workgroups系統中以使它們能夠運行32位應用程序的軟件包。正如它的名字所暗示的那樣,Win32s只是Windows 95和Windows NT系統中使用的Win32 API的一個子集。Win32s的主要功能就是在32位和16位內存地址間相互轉換,也就是一種被稱為換長的操作。
Win32是32位Windows(包括Windows NT,95, 98 和2000等)操作系統的編程接口(API)。當應用程序是按Win32 API編寫時,它們就具有16位API(Win16)所不具備的一些高級性能。一個按Win32編寫的程序能運行在所有的操作系統之上,除非這個程序要求特定的操作系統特性,而這些特性別的操作系統又沒有時。例如,Windows NT提供的安全特性Windows 95/98就沒有。一個為NT系統的這些特性編寫的程序就不能運行在其它的Windows系統之上。
?? 使用此API的程序??????????????????????? 能運行在...上
???? Win32????????????????????????????????? 95, 98, NT, 2000, XP
???? Win32s??????????????????????????? 3.1, 95, 98, NT, 2000, XP
???? Win32c???????????????????????????????? 95??
???? Win16??????????????????????? 3.0, 3.1, 95, 98, NT, 2000, XP
②目標文件(Object file? )和映象文件(Image file)
目標文件(Object file)指的是鏈接程序(鏈接器)的輸入文件。鏈接器輸出的是映象文件,映象文件反過來又是加載器的輸入文件。“object file”一詞未必含有任何和面向對象的編程有關的聯系。
映象文件(Image file)指的就是可執行文件:或者是.EXE,或者是.DLL。一個映象文件可被想象為“內存映象”。“映象文件”一詞常被用來代替“可執行文件”,因為后者有時被用來專指.EXE文件。
③UNIX
是一個很流行的多用戶、多任務的操作系統,由貝爾實驗室于上世紀70年代早期開發出來的。只有很少的程序員建立的UNIX系統本來是設計給他們這些程序員專用的、小巧的、靈活的系統。UNIX是用高級編程語言,就是C語言,編寫的第一批操作系統之一。這就意味著只要電腦上有C語言編譯器,UNIX就可以被虛擬地安裝到任何電腦上。天生的可移植性加上低廉的價格使得UNIX成為各大學的流行選擇。(因為反信用條款禁止貝爾實驗室將UNIX作為它的全權產品推向市場,所以UNIX的價格不貴。)
貝爾實驗室只發布它自己源語言形式的UNIX操作系統,所以任何獲得一份拷貝的人都可以按照自己的意愿來修改和定制它。到上世紀70年代末時,有好幾十種不同版本的UNIX運行在世界各地。(更多信息請參閱別的資料。)
④VMS
“Open Virtual Memory System”或僅VMS,是運行于VAX和Alpha系列電腦之上的高端電腦服務器操作系統的名字,現在用于使用英特爾Itanium CPU的Hewlett-Packard(惠普)系統之上。VAX和Alpha系列電腦由美國馬薩諸塞州Maynard市的數據設備(DEC)公司(現在由HP擁有)生產的。OpenVMS 是一個基于多用戶、多處理虛擬存儲的操作系統,設計用于時間共享、批處理和事項處理等。
⑤SDK
是“software development kit”(軟件開發工具箱)的縮寫,它是一個供程序員為特定平臺開發應用程序的編程包。典型的,一個SDK包含一個或多個API庫、各種編程工具和相關文檔等。
⑥Ne Format(New-style EXE Format的縮寫)
是一個早期Windows操作系統的可執行文件(.EXE),包含一個代碼和數據的集合或者一個代碼、數據和資源的集合。這種可執行文件也包括兩個頭:一個MS-DOS頭和一個Windows頭,和一些節。(具體參看其他資料)
⑦OS/2(IBM Operating System/2,IBM 操作系統/2)
操作系統/2(OS/2)最初是由 Microsoft 和 IBM 共同合作開發的一種應用于 PC 機的操作系統。現在只由 IBM 銷售、支持和管理。其設計目標是替換傳統的 DOS 操作系統。OS/2 與 DOS、Windows 都相兼容。換句話說,OS/2 操作系統可運行所有的 DOS 和 Windows 程序,但在 OS/2 下運行的某些特殊寫程序卻不能在 DOS 或 Windows 下運行。
OS/2 是一個32位的、為個人計算機而設計的、支持保護模式和多任務的操作系統。OS/2 系統中的圖形表示管理器(Presentation Manager)作為其圖形系統,主要負責管理窗口、字體及控件等。OS/2 系統頂部是 Workplace 命令解釋程序(WPS - 該內容在 OS/2 2.0中有具體介紹),WPS 以文檔為中心,允許用戶訪問文件和打印機,并可以啟動程序。WPS 遵循 IBM 的用戶界面標準,即“通用用戶訪問”。
OS/2 操作系統中包含一種系統對象模型(SOM),包括磁盤、文件夾、文件、程序對象及打印機等對象。SOM 允許應用程序間代碼共享,但這與編程語言無關。一種稱為 DSOM 的分布式版本支持不同計算機上對象間的相互通信。DSOM 建立在 CORBA 基礎上。SOM 類似于微軟的組件對象模型(Component Object Model),同時兩者相互競爭。目前人們對 SOM 和 DSOM 已停止深度開發。
OS/2 操作系統也包括一種叫做 OpenDoc 的混合文檔技術,它由 Apple 開發而成。但目前人們對 OpenDoc 也已停止深度開發。
由于 OS/2 存在市場局限性,IBM 公司已于2003年3月12日按照電子商務計劃停止了 OS/2 的發展市場。
?
⑧MIPS
MIPS是世界上很流行的一種RISC處理器。MIPS的意思是“無內部互鎖流水級的微處理器”(Microprocessor without interlocked piped stages),其機制是盡量利用軟件辦法避免流水線中的數據相關問題。它最早是在80年代初期由斯坦福(Stanford)大學Hennessy教授領導的研究小組研制出來的。MIPS公司的R系列就是在此基礎上開發的RISC工業產品的微處理器。這些系列產品為很多計算機公司采用構成各種工作站和計算機系統。如R3000、R4000、R10000等都是其生產的處理器。
MIPS技術公司是美國著名的芯片設計公司,它采用精簡指令系統計算結構(RISC)來設計芯片。和英特爾采用的復雜指令系統計算結構(CISC)相比,RISC具有設計更簡單、設計周期更短等優點,并可以應用更多先進的技術,開發更快的下一代處理器。MIPS是出現最早的商業RISC架構芯片之一,新的架構集成了所有原來MIPS指令集,并增加了許多更強大的功能。
⑨big-endian、Little-endian和endian?
Big-endian和Little-endian是用來表述一組有序的字節數存放在計算機內存中時的順序的術語。Big-endian(即“大端結束”或者“大尾”)是將高位字節(序列中最重要的值)先存放在低地址處的順序,而Little-endian(即“小端結束”或者“小尾”)是將低位字節(序列中最不重要的值)先存放在低地址處的順序。舉例來說,在使用Big-endian順序的計算機中,要存儲一個十六進制數4F52所需要的字節將會以4F52的形式存儲(比如4F存放在內存的1000位置,而52將會被存儲在1001位置)。而在使用Little-endian順序的系統中,存儲的形式將會是524F(52在地址1000處,4F在地址1001處)。IBM的370種大型機、大多數基于RISC的計算機以及Motorola的微處理器使用的是Big-endian順序,TCP/IP協議也是。而Intel的處理器和DEC公司的一些程序則使用的Little-endian方式。
“endian”這個詞出自《格列佛游記》。小人國的內戰就源于吃雞蛋時是究竟從大頭(Big-Endian)敲開還是從小頭(Little-Endian)敲開,由此曾發生過六次叛亂,其中一個皇帝送了命,另一個丟了王位。
我們一般將endian翻譯成“字節序”,將big endian和little endian稱作“大尾”和“小尾”。
⑩Alpha AXP
“DEC Alpha”,也被稱作“Alpha AXP”,是一個原來由美國數據設備公司(DEC)開發和制造的64位RISC微處理器(例如:DEC Alpha AXP 21064 微處理器),他們將它用在自己的工作站和服務器系列上。被設計作為VAX系列計算機的繼承者,Alpha AXP不但支持VMS操作系統,同時也支持Digital UNIX操作系統。后來的一些開放源碼操作系統也能運行于Alpha之上,著名的Linux和BSD UNIX操作系統特別支持。微軟直到Windows NT 4.0 SP6才支持這種處理器,但Windows 2000第2版之后就又不支持了。
UTC
是“Coordinated Universal Time”的縮寫,意為“協調通用時間”,它是綜合了只以地球的不停旋轉速率為基準的格林威治標準時間(Greenwich Mean Time)和高度精確的原子時間的一種時標。當原子時間和地球時間達到一秒的時差時,一個閏秒就被算進UTC時間中。UTC設計于1972年1月1日,并被國際度量衡局(International Bureau of Weights and Measures)于巴黎協調通過。跟格林威治標準時間一樣,UTC也被設定于0經度的本初子午線。
BSS
是“Block Started by Symbol”的縮寫,意為“以符號開始的塊”。BSS是Unix鏈接器產生的未初始化數據段。其他的段分別是包含程序代碼的“text”段和包含已初始化數據的“data”段。BSS段的變量只有名稱和大小卻沒有值。此名后來被許多文件格式使用,包括PE。
“以符號開始的塊”指的是編譯器處理未初始化數據的地方。BSS節不包含任何數據,只是簡單的維護開始和結束的地址,以便內存區能在運行時被有效地清零。BSS節在應用程序的二進制映象文件中并不存在,例如:
unsigned char var;??? // 分配到.bss節的8位未初始化變量
unsigned char var2 = 25;?? // 分配到.data節的8位已初始化變量
BSOD(blue screen of death,藍屏死機)
是運行在Windows環境下的計算機上出現的一個錯誤,甚至包括最早版本的Windows,比如Windows 3.0和3.1,在后來的Windows版本比如Microsoft Windows 95, Windows 98, Windows NT,和Windows 2000上仍能出現。它被開玩笑地稱為藍屏之死是因為錯誤發生時,屏幕變成藍色,電腦總是不能正常運轉并需要重新啟動。
POSIX
是“Portable Operating System Interface for UNIX”(UNIX可移植操作系統接口)的首字母縮寫,它是定義程序和操作系統之間的接口的一套IEEE和ISO標準。通過將他們的程序設計為符合POSIX標準,開發者就能獲得一些讓他們的程序可以容易地被移植到其他POSIX兼容的操作系統上的保證,主要包括大多數UNIX操作系統。POSIX標準目前由IEEE下叫做“Portable Applications Standards Committee”(PASC)(可移植的應用程序標準委員會)維護。
thunk
(動詞) 換長,變長;已經想到的,預先想到的
(指在個人電腦中,將一個16位內存地址轉換為一個32位的地址,或者相反。換長是必須的,因為英特爾的老16位微處理器使用一種叫分段內存的定址方式,而它的32位微處理器使用的卻是一個統一的地址空間。Window 95支持一種允許32位程序調用16位DLL的換長機制,叫統一換長。而另一方面,運行在Windows 3.x和Windows for Workgroup下的16位應用程序不能使用32位DLL,除非32位地址被轉換為16位地址。這就是Win32的功能,并被稱為通用換長。
根據民間傳說,thunk一詞是由一位Algol-60編程語言的開發者編出的,他在一天深夜意識到參數的數據類型是可以被編譯器稍先一點知道的。也就是說,到了編譯器處理參數的時候,它就已經想到了(thunked)數據類型了。該詞的含義近年來已變化很大了。)
(名詞)換長,變長(在一個分段內存地址空間和一個統一地址空間之間互相轉換的操作)
(我查遍書店中所有的大大小小的英漢和英英詞典,都沒有找到thunk這個詞的含義。后在網上找到了它的英語解釋,卻找不到它對應的漢語譯法,現根據它的意思,姑且譯之。各位勿笑,還請高手指點。)
(英文參見:http://www.webopedia.com/TERM/T/thunk.html)
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1475359