PE文件格式系列譯文之一----
【翻譯】「PE文件格式」1.9版 完整譯文(附註釋)
=========================================================
原著:Bernd. Luevelsmeyer
翻譯:ah007
[注意:本譯文的全部大小標題序號都是譯者添加,以方便你們閱讀。圓圈內的數字是註釋的編號,其中註釋②譯自微軟的《PECOFF規範》,其它譯自網絡。----譯者]
1、前言(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」(開發者網絡)中有所描述。
2、總覽(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
| |
+-------------------+
3、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」----譯者)。
4、文件頭(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。
5、相對虛擬地址(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處找到。
6、可選頭(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。
7、節目錄(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 | |
| | <--------------+
+-------------------+
每一個節都有一個節頭,而且每一個數據目錄都會指向其中的一個節(幾個數據目錄有可能指向同一個節,並且也可能有的節沒有數據目錄指向它們)。
8、節的原始數據(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(映象基址)」的優先載入地址;你也能知道你真正載入的地址。若是它們相同,你什麼也不用作。若是它們不一樣,你需計算出實際基址-優先基址的差並加上重定位位置的值(有符號,可能爲負值),此值你可經過上面講述的方法找到。
9、致謝(Acknowledgments)
---------------------------
感謝David Binette的調試和校讀。(剩下的錯誤所有都是個人。)
也感謝wotsit.org網站讓我將此文放到他們的網站上。
10、版權(Copyright)
---------------------
本文的版權屬於B. Luevelsmeyer,1999年。它是免費的,你能夠任意的使用,但後果自負。它含有錯誤並不完整,特此警告。
11、Bug報告(Bug reports)
----------------------------
Bug報告(或其餘建議)請發送至:bernd.luevelsmeyer@iplan.heitec.net
12、版本(Versions)
----------------------
你可在文件的頂部找到當前的版本號。
1998-04-06
第一次公開發表
1998-07-29
將映象文件版本和子系統版本中錯誤的「byte」改成「word」
更正「棧只限於1 MB」的錯誤(實際上沒有上限)
更正一些輸入錯誤
1999-03-15
更正輸出目錄的描述,原來很是不全
調整輸入目錄的描述,原來說的不清
更正輸入錯誤併爲其它節改了一些詞句
十3、參考文獻(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這個詞的含義。後在網上找到了它的英語解釋,卻找不到它對應的漢語譯法,現根據它的意思,姑且譯之。各位勿笑,還請高手指點。)
(英文參見:[url]http://www.webopedia.com/TERM/T/thunk.html)[/url]
?MSB
「Most Significant Bit」的首字母縮寫,意爲「最重要的位」。在一個二進制的數字中,它就是最左邊的那一位,也是最重要的那一位。