久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

PE文件結構

 xenophobe 2014-11-06

PE文件結構

現(xiàn)在讓我們開始挖掘PE文件的實際格式吧,。我要從文件的開頭處開始,描述存在于所有PE文件中的數(shù)據(jù)結構,。然后我會描述PE文件的節(jié)中具體的數(shù)據(jù)結構(例如導入表與資源),。下面我要討論的所有數(shù)據(jù)結構都被定義在WINNT.H文件中,除非我特別聲明,。
在許多情況下,,32位和64位數(shù)據(jù)結構都是成對出現(xiàn)的——例如IMAGE_NT_HEADERS32和IMAGE_NT_HEADERS64。這些結構幾乎總是一樣的,,除了相應的64位結構中一些域的數(shù)據(jù)寬度更寬,。如果你想編寫可移植的代碼,,在WINNT.H文件中有相應的宏,這些宏可以選擇合適的32位或64位結構,,并且把它們用一個不能表明大小的別名來代替(在上面的例子中,,它就是IMAGE_NT_HEADERS)。結構的選擇依賴于你想在何種模式下編譯(具體來說就是是否定義了_WIN64),。只有在你所需編譯成的PE文件的大小屬性與你正在編譯的平臺的大小屬性不同時才需要使用具體的32位或64位結構,。
MS-DOS文件頭
每一個PE文件都以一個小的MS-DOS?可執(zhí)行文件開始。早期的Windows需要這個小占位程序,,因為那時很多用戶還未使用Windows,。當可執(zhí)行文件在沒有安裝Windows的機器上運行時,這個程序至少可以輸出一條消息,,用來指明它需要運行在Windows平臺上,。
PE文件開頭是傳統(tǒng)的MS-DOS文件頭,其中前面的一部分被稱為IMAGE_DOS_HEADER,。此結構中最重要的兩個域是e_magic和e_lfanew,。e_lfanew域保存的是真正的PE文件頭的偏移。e_magic域需要被設置成0x5A4D,。它被定義為IMAGE_DOS_SIGNATURE,。如果用ASCII碼表示,0x5A4D就是“MZ”,,這是Mark Zbikowski的姓名縮寫,,他是最初的MS-DOS設計者之一。
IMAGE_NT_HEADERS文件頭
IMAGE_NT_HEADERS結構是存儲PE文件細節(jié)的主要位置,。它的偏移由文件開頭的IMAGE_DOS_HEADER結構的e_lfanew域給出,。實際有兩種版本的IMAGE_NT_HEADER結構,一種供32位可執(zhí)行文件使用,,另一種供64位使用,。它們之間的差別非常小,因此我在討論中把它們看作相同的結構,。區(qū)別這兩種結構惟一正確的,、由Microsoft官方認可的方法是通過IMAGE_OPTION_HEADER結構(很快就會講到)的Magic域。
IMAGE_NT_HEADER結構由三個域組成:
typedef struct _IMAGE_NT_HEADERS {
   DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
在一個合法的PE文件中,,Signature域被設置成0x00004550,。用ASCII碼表示為“PE\0\0”。它被定義為IMAGE_NT_SIGNATURE,。第二個域是一個類型為IMAGE_FILE_HEADER的結構,,這個結構在PE文件出現(xiàn)之前就已經(jīng)出現(xiàn)了。它包含了關于文件的一些基本信息。最重要的是,,其中有一個域指明了跟在這個結構后面的可選文件頭的大小,。在PE文件中,這個可選文件頭是必須的,,但它仍然被稱為IMAGE_OPTIONAL_HEADER,。
下表列出了IMAGE_FILE_HEADER結構的各個域及相應的描述。這個結構也可以在COFF格式的OBJ文件開頭找到,。
 
大小
描述
WORD
Machine
目標平臺CPU的類型,。常用的值有:
IMAGE_FILE_MACHINE_I386    0x014c // Intel 386
IMAGE_FILE_MACHINE_IA64    0x0200 // Intel 64
WORD
NumberOfSections
指示節(jié)表中節(jié)的數(shù)目,。節(jié)表緊跟著IMAGE_NT_HEADERS結構,。
DWORD
TimeDateStamp
指示文件創(chuàng)建時間,。這個值是從格林尼治時間(GMT)1970年1月1日00:00以來的總秒數(shù)。它比文件系統(tǒng)所指明的日期/時間更精確,。使用_ctime函數(shù)可以很容易地把這個值轉換成可讀性比較好的字符串(這個函數(shù)與時區(qū)相關),。另一個可用于這個值的函數(shù)gmtime也比較有用。
DWORD
PointerToSymbolTable
COFF符號表的文件偏移,。Microsoft的PECOFF規(guī)范5.4節(jié)描述了COFF符號表,。COFF符號表在PE文件中非常少見,因為新的調試符號格式已經(jīng)取代了它,。在Visual Studio .NET之前,,可以使用/DEBUGTYPE:COFF這個鏈接器選項來指定創(chuàng)建COFF符號表。它總是存在于OBJ文件中,。如果不存在符號表的話,,將它設置為0。
DWORD
NumberOfSymbols
符號表中的符號數(shù)(如果存在的話),。COFF符號是一個大小固定的結構,,這個域用來定位COFF符號表的結尾。緊跟著COFF符號表的是一個字符串表,,它用來保存長符號名,。                                                                                                                                                                                                                                                                                                                                                                                                                             
WORD
SizeOfOptionalHeader
IMAGE_FILE_HEADER結構后面的可選數(shù)據(jù)的大小。在PE文件中,,這個可選數(shù)據(jù)就是IMAGE_OPTIONAL_HEADER,。這個大小在32位和64位文件中是不同的。對于32位PE文件來說,,它通常是224,;對于64位PE32+文件來說,它通常是240,。但是,它們只是最小值,可能有更大的值,。
WORD
Characteristics
指示文件屬性的一組位標志,。這些標志的合法值就是WINNT.H文件中定義的IMAGE_FILE_xxx值。一些常見的值列于下表,。
下表列出了常用的IMAGE_FILE_xxx值:
標志
描述
IMAGE_FILE_RELOCS_STRIPPED
重定位信息已經(jīng)從文件中移除,。
IMAGE_FILE_EXECUTABLE_IMAGE
文件是可執(zhí)行映像。
IMAGE_FILE_AGGRESSIVE_WS_TRIM
讓操作系統(tǒng)盡量減小工作集(working set),。
IMAGE_FILE_LARGE_ADDRESS_AWARE
此應用程序可以處理大于2GB的地址,。
IMAGE_FILE_32BIT_MACHINE
需要字長為32位的機器。
IMAGE_FILE_DEBUG_STRIPPED
調試信息已經(jīng)被移到.DBG文件中,。
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP
如果可執(zhí)行映像在可移動媒體上,,把它復制到交換文件中并從交換文件中運行。
IMAGE_FILE_NET_RUN_FROM_SWAP
如果可執(zhí)行映像在網(wǎng)絡上,,把它復制到交換文件中并從交換文件中運行,。
IMAGE_FILE_DLL
文件是DLL。
IMAGE_FILE_UP_SYSTEM_ONLY
只能運行于單處理器機器上,。
   下表列出了IMAGE_OPTIONAL_HEADER結構的成員:
大小
描述
WORD
Magic
一個特征字,,用于表明文件頭的類型。兩個常用的值為:
IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b
BYTE
MajorLinkerVersion
用于創(chuàng)建這個可執(zhí)行文件的鏈接器的主版本號,。對于由Microsoft鏈接器生成的可執(zhí)行文件來說,,這個版本號對應于Visual Studio的版本號(例如Visual Studio 6.0就是版本6)。
BYTE
MinorLinkerVersion
用于創(chuàng)建這個可執(zhí)行文件的鏈接器的次版本號
DWORD
SizeOfCode
帶有IMAGE_SCN_CNT_CODE 屬性的所有節(jié)的總大小,。
DWORD
SizeOfInitializedData
所有由已初始化的數(shù)據(jù)組成的節(jié)的總大小,。
DWORD
SizeOfUninitializedData
所有由未初始化的數(shù)據(jù)組成的節(jié)的總大小。它通常是0,,因為鏈接器經(jīng)常把未初始化的數(shù)據(jù)添加到正常的數(shù)據(jù)節(jié)的末尾,。
DWORD
AddressOfEntryPoint
文件中首先被執(zhí)行的代碼的第一個字節(jié)的RVA。對于DLL來說,,入口點在進程初始化和退出期間,,以及線程創(chuàng)建和退出期間都會被調用。在大多數(shù)可執(zhí)行文件中,,這個地址并不是直接指向main,、WinMain或者DllMain,而是指向調用上述函數(shù)的運行時庫代碼,。對于DLL來說,,這個域可以設為0,這樣它就接收不到前面說的四個通知,。/NOENTRY鏈接器選項可以將這個域設置為0
DWORD
BaseOfCode
加載進內存之后代碼的第一個字節(jié)的RVA,。
DWORD
BaseOfData
理論上這是加載進內存之后數(shù)據(jù)的第一個字節(jié)的RVA。但是這個域的值在不同版本的Microsoft鏈接器間是不一致的。64位可執(zhí)行文件中并不存在這個域,。
DWORD
ImageBase
這個文件在內存中的首選加載地址,。如果有可能的話(也就是說這個內存當前并未被占用,并且它是對齊的,,同時是一個合法的地址等等),,加載器盡量把PE文件加載到這個地址。如果可執(zhí)行文件被加載到這個地址,,加載器就可以跳過基址重定位(將在本文的第二部分中描述),。對于EXE來說,默認的ImageBase為0x400000,;對于DLL來說,,它是0x10000000??梢栽阪溄訒r使用/BASE選項或者以后使用REBASE工具來設定此值,。
DWORD
SectionAlignment
加載進內存之后節(jié)的對齊值。這個對齊值必須大于或等于文件對齊值(下面將要講到),。默認的對齊值是目標平臺的頁面大小,。對于運行于Windows 9x或Windows Me上的用戶模式的可執(zhí)行文件來說,最小的對齊值是一個頁面(4KB),。這個域的值可以使用/ALIGN鏈接器選項來設定,。
DWORD
FileAlignment
節(jié)在PE文件中的對齊值。對于x86可執(zhí)行文件來說,,它或者是0x200,,或者是0x1000。不同版本的Microsoft鏈接器的默認值不一樣,。這個值必須是2的冪,,并且如果SectionAlignment域的值小于CPU的頁面大小,這個值必須與SectionAlignment域的值匹配,。鏈接器選項/OPT:WIN98將x86平臺上的可執(zhí)行文件的對齊值設為0x1000,,而/OPT:NOWIN98選項將它設為0x200。
WORD
MajorOperatingSystemVersion
所需的操作系統(tǒng)的主版本號,。隨著眾多版本W(wǎng)indows的到來,,這個域已失去了它最初的意義。
WORD
MinorOperatingSystemVersion
所需的操作系統(tǒng)的次版本號,。
WORD
MajorImageVersion
此文件的主版本號,。系統(tǒng)并未使用這個域,可以設置為0,。使用/VERSION鏈接器選項可以設定這個域的值,。
WORD
MinorImageVersion
此文件的次版本號,。
WORD
MajorSubsystemVersion
可執(zhí)行文件所需的子系統(tǒng)的主版本號。以前相對于舊版本的Windows NT界面來說,,用它來指明需要新的Windows 95或Windows NT 4.0用戶界面?,F(xiàn)在由于Windows版本繁多,,這個域已經(jīng)不使用了,,通常被設為4。使用鏈接器選項/SUBSYSTEM可以設置這個域的值,。
WORD
MinorSubsystemVersion
可執(zhí)行文件所需的子系統(tǒng)的次版本號,。
DWORD
Win32VersionValue
一個從來不用的域,通常設為0,。
DWORD
SizeOfImage
SizeOfImage包含了假設存在于最后一個節(jié)之后的那個節(jié)的RVA,。這等效于把此文件加載進內存時系統(tǒng)需要保留的內存數(shù)量。這個域的值必須是節(jié)的對齊值的倍數(shù),。
DWORD
SizeOfHeaders
MS-DOS文件頭,、PE文件頭和節(jié)表的總大小。在PE文件中,,這些內容出現(xiàn)于任何代碼或數(shù)據(jù)節(jié)之前,。這個域的值被向上舍入到文件對齊值的倍數(shù)。
DWORD
CheckSum
映像的校驗和,。IMAGEHLP.DLL中的CheckSumMappedFile API可以計算這個值,。對于內核模式的驅動程序和一些系統(tǒng)DLL來說,校驗和是必須的,。否則這個域被設置為0,。當使用/RELEASE鏈接器選項時,校驗和會被放在文件中,。
WORD
Subsystem
指示可執(zhí)行文件所需子系統(tǒng)(用戶界面類型)的一個枚舉值,。在EXE文件中這個值比較重要。一些重要的值如下:
IMAGE_SUBSYSTEM_NATIVE
// 不需要子系統(tǒng)
IMAGE_SUBSYSTEM_WINDOWS_GUI
// 使用Windows GUI
IMAGE_SUBSYSTEM_WINDOWS_CUI
// 控制臺應用程序,。當它運行時,,操作系統(tǒng)為其創(chuàng)一
// 個控制臺并提供stdin、stdout和stderr文件句柄,。
WORD
DllCharacteristics
指示DLL特征的標志,。這些值對應于WINNT.H文件中的IMAGE_DLLCHARACTERISTICS_xxx定義。當前值如下:
IMAGE_DLLCHARACTERISTICS_NO_BIND
// 不綁定映像
IMAGE_DLLCHARACTERISTICS_WDM_DRIVER
// 使用WDM模型的驅動程序
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE
// 當終端服務器加載一個并沒有準備運行于終端服務
// 器上的應用程序時,,它同時加載包含兼容代碼的DLL
DWORD
SizeOfStackReserve
在EXE文件中,,它表示進程中的線程堆棧最初可以增長到的最大值。默認是1MB,。并不是初始化時就提交這里指定的所有內存,。
DWORD
SizeOfStackCommit
在EXE文件中,,它表示初始化時提交的堆棧的大小。默認是4KB,。
DWORD
SizeOfHeapReserve
在EXE文件中,,它表示最初為默認進程堆保留的內存數(shù)量。默認是1MB,。然而對于當前版本的Windows,,在沒有用戶干預的情況下,堆可以超過這個值,。
DWORD
SizeOfHeapCommit
在EXE文件中,,它表示提交的堆的大小。默認是4KB,。
DWORD
LoaderFlags
此域已經(jīng)廢棄不用,。
DWORD
NumberOfRvaAndSizes
在IMAGE_NT_HEADERS結構末尾處是一個IMAGE_DATA_DIRECTORY結構數(shù)組。這個域包含了這個數(shù)組的元素數(shù)目,。由于以前發(fā)行的Windows NT的原因,,它被設置為16。
IMAGE_
DATA_
DIRECTORY
DataDirectory[16]
IMAGE_DATA_DIRECTORY結構數(shù)組,。每個結構包含可執(zhí)行文件中一些重要部分(例如導入表,、導出表以及資源等)的RVA和大小。
IMAGE_OPTIONAL_HEADER結構末尾的DataDirectory數(shù)組就像是可執(zhí)行文件中重要位置的地址簿,。DataDirectory的每個元素結構如下:
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;     // 數(shù)據(jù)的RVA
    DWORD   Size;               // 數(shù)據(jù)的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
節(jié)表section table
緊跟著IMAGE_NT_HEADERS結構的是節(jié)表(section table),。節(jié)表是一個IMAGE_SECTION_HEADER結構數(shù)組。此結構提供了與它相關的節(jié)的信息,,其中包括位置,、長度和屬性。下表給出了對此結構的描述,。節(jié)表中此結構的數(shù)目由IMAGE_NT_HEADERS.FileHeader.NumberOfSections給出,。
大小
描述
BYTE
Name[8]
節(jié)的名稱(ASCII碼)。節(jié)名并不保證以NULL結尾,。如果你指定的節(jié)名大于8個字節(jié),,鏈接器在生成可執(zhí)行文件時將其截斷為8個字符。在OBJ文件中存在一種機制可以讓節(jié)名更長,。節(jié)名通常以圓點開始,,但這并不是必需的。對于帶有$字符的節(jié)名鏈接器會特殊對待,。如果幾個節(jié)名中$字符以前的部分相同,,那么這些節(jié)會被合并。它們按$字符后面的部分在字母表中的順序出現(xiàn)于最終的節(jié)中,。關于節(jié)名中帶有$字符的節(jié)和它們如何被合并方面還有很多內容,,但對它的詳細討論已經(jīng)超出了本文的范圍,。
DWORD
VirtualSize
指示節(jié)實際占用的內存大小。這個域的值可能比SizeOfRawData域的值大或小,。如果大,,SizeOfRawData域表示可執(zhí)行文件中已初始化的數(shù)據(jù)的大小,VirtualSize域比它大的部分用0填充,。在OBJ文件中,,此域的值為0。
DWORD
VirtualAddress
在可執(zhí)行文件中,,它表示在內存中節(jié)的起始RVA,。在OBJ文件中它被設置為0。
DWORD
SizeOfRawData
可執(zhí)行文件或OBJ文件中的節(jié)中存儲的數(shù)據(jù)的大?。ㄒ宰止?jié)計)。對于可執(zhí)行文件來說,,它必須是PE文件頭中給出的文件對齊值的倍數(shù),。如果它被設置為0,表示這個節(jié)中是未初始化的數(shù)據(jù),。
DWORD
PointerToRawData
節(jié)中數(shù)據(jù)起始的文件偏移,。對于可執(zhí)行文件來說,它必須是PE文件頭中給出的文件對齊值的倍數(shù),。
DWORD
PointerToRelocations
節(jié)的重定位信息的文件偏移,。它只用于OBJ文件,在可執(zhí)行文件中它被設置為0,。在OBJ文件中,,如果它不為0,那么它指向一個IMAGE_RELOCATION結構,。
DWORD
PointerToLinenumbers
節(jié)中COFF行號信息的文件偏移,。如果它不為0,那么它指向一個IMAGE_LINENUMBER結構,。
WORD
NumberOfRelocations
PointerToRelocations域指向的重定位信息的數(shù)目,。在可執(zhí)行文件中應該為0。
WORD
NumberOfLinenumbers
PointerToLinenumbers域指向的行號信息的數(shù)目,。只有當生成COFF行號信息時才使用,。
DWORD
Characteristics
指示節(jié)屬性的標志(可以用“或”連接)。這些標志中的大部分可以使用鏈接器的/SECTION選項來設置,。常用的值列于下表,。
下表列出了常用的節(jié)屬性標志:
標志
描述
IMAGE_SCN_CNT_CODE
節(jié)中包含代碼。
IMAGE_SCN_MEM_EXECUTE
節(jié)是可執(zhí)行的,。
IMAGE_SCN_CNT_INITIALIZED_DATA
節(jié)中包含已初始化的數(shù)據(jù),。
IMAGE_SCN_CNT_UNINITIALIZED_DATA
節(jié)中包含未初始化的數(shù)據(jù),。
IMAGE_SCN_MEM_DISCARDABLE
這個節(jié)在最終的可執(zhí)行文件中可以被丟棄。用于保存鏈接器使用的信息,,包括.debug$節(jié),。
IMAGE_SCN_MEM_NOT_PAGED
這個節(jié)不能被交換到頁面文件中,因此它應該總是存在于物理內存中,。經(jīng)常用于內核模式驅動程序,。
IMAGE_SCN_MEM_SHARED
包含這個節(jié)的物理頁面將在加載這個可執(zhí)行文件的所有進程之間共享。因此每個進程看到的這個節(jié)中的數(shù)據(jù)的值完全一樣,。對于在進程的所有實例之間共享全局變量比較有用,。要共享某個節(jié),使用/SECTION:節(jié)名,S鏈接器選項,。
IMAGE_SCN_MEM_READ
節(jié)是可讀的,。幾乎總是設置這個值。
IMAGE_SCN_MEM_WRITE
節(jié)是可寫的,。
IMAGE_SCN_LNK_INFO
節(jié)中包含鏈接器使用的信息,。僅存在于OBJ文件中。
IMAGE_SCN_LNK_REMOVE
這個節(jié)中的內容將不成為最終的映像的一部分,。僅用于OBJ文件,。
IMAGE_SCN_LNK_COMDAT
節(jié)中的內容是公共數(shù)據(jù)(comdat)。公共數(shù)據(jù)(Communal data)是可以被定義在多個OBJ文件中的數(shù)據(jù)(或代碼),。鏈接器只將其中的一份副本包含進最終的可執(zhí)行文件中,。Comdats對于支持C++的模板函數(shù)和函數(shù)級的鏈接至關重要。它僅存在于OBJ文件中,。
IMAGE_SCN_ALIGN_xBYTES
這個節(jié)中的數(shù)據(jù)在最終的可執(zhí)行文件中的對齊值,。有各種各樣的值(_4BYTES,_8BYTES,,_16BYTES等),。如果不指定,默認為16字節(jié),。僅在OBJ文件中才設置這些標志,。
可執(zhí)行文件中的節(jié)在文件中的對齊值對文件的大小有重要影響。在Visual Studio 6.0中,,鏈接器默認的節(jié)對齊值是4KB,,除非使用/OPT:NOWIN98選項或/ALIGN選項。對于Visual Studio .NET鏈接器,,雖然仍是默認使用/OPT:NOWIN98選項,,但它要確定可執(zhí)行文件是否小于某一固定值,如果小于的話,它將使用0x200字節(jié)的對齊值,。
另一個比較有用的對齊值來自.NET文件規(guī)范,。這個規(guī)范說.NET可執(zhí)行文件在內存中的對齊值應該為8KB,而不是x86上的4KB,。這是為了確保用x86入口點代碼創(chuàng)建的.NET可執(zhí)行文件可以運行在IA-64中,。如果在內存中節(jié)的對齊值為4KB,那么IA-64將不能加載這個文件,,因為在64位Windows上頁面是按8KB對齊的
我們來看一下在可執(zhí)行文件和OBJ文件中經(jīng)常遇到的一些節(jié),。除非特別說明,否則下表中的節(jié)名都來自Microsoft,。
名稱
描述
.text
默認的代碼節(jié),。
.data
默認的可讀/可寫數(shù)據(jù)節(jié)。全局變量通常在這個節(jié)中,。
.rdata
默認的只讀數(shù)據(jù)節(jié),。字符串常量和C++/COM虛表就放在這個節(jié)中。
.idata
導入表,。實際上,,鏈接器經(jīng)常把.idata節(jié)合并到其它節(jié)中(或者是明確指定的,或者是通過鏈接器的默認行為),。默認情況下,鏈接器僅在創(chuàng)建發(fā)行版的程序時才把.idata節(jié)合并到其它節(jié)中,。
.edata
導出表,。當創(chuàng)建要導出函數(shù)或數(shù)據(jù)的可執(zhí)行文件時,鏈接器會創(chuàng)建一個.EXP文件,。這個.EXP文件包含一個.edata節(jié),,這個節(jié)被添加到最后的可執(zhí)行文件中。與.idata節(jié)一樣,,.edata節(jié)也經(jīng)常被合并到.text節(jié)或.rdata節(jié)中,。
.rsrc
資源節(jié)。這個節(jié)是只讀的,。它不應該被命名為其它名稱,,也不應該被合并到其它節(jié)中。
.bss
未初始化的數(shù)據(jù)節(jié),。在最新的鏈接器創(chuàng)建的可執(zhí)行文件中很少見到,。鏈接器擴展可執(zhí)行文件的.data節(jié)的VirtualSize域以便容納未初始化的數(shù)據(jù)。
.crt
添加到可執(zhí)行文件中的數(shù)據(jù),,用來支持C++運行時庫(CRT),。一個比較好的例子就是用于調用靜態(tài)C++對象的構造函數(shù)和析構函數(shù)的指針。要獲取更詳細的信息,,可以參考2001年1月的Under The Hood專欄,。
.tls
這個節(jié)中的數(shù)據(jù)用來支持使用__declspec(thread)語法創(chuàng)建的線程局部存儲變量,。它包括數(shù)據(jù)的初始值,以及運行時需要的附加變量,。
.reloc
可執(zhí)行文件中的基址重定位節(jié),。通常DLL需要基址重定位信息而EXE并不需要。在創(chuàng)建發(fā)行版的程序時,,鏈接器并不為EXE文件生成基址重定位信息,。可以使用/FIXED鏈接器選項移除基址重定位信息,。
.sdata
通過全局指針(Global Pointer)相對尋址的“短(Short)”可讀/可寫數(shù)據(jù),。用于IA-64和其它使用全局指針寄存器的平臺上。IA-64平臺上正常大小的全局變量在這個節(jié)中,。
.srdata
通過全局指針相對尋址的“短(Short)”只讀數(shù)據(jù),。用于IA-64和其它使用全局指針寄存器的平臺上。
.pdata
異常表,。它包含一個IMAGE_RUNTIME_FUNCTION_ENTRY結構數(shù)組,,這個結構與平臺體系結構相關。數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_EXCEPTION的項指向它,。用于使用基于表的異常處理的平臺,,例如IA-64。惟一不使用基于表的異常處理的平臺是x86(它使用的是基于堆棧的異常處理),。
.debug$S
OBJ文件中的Codeview格式的調試符號(Symbol)信息,。這是一列可變長度的CodeView格式的調試符號記錄。
.debug$T
OBJ文件中的Codeview格式的調試類型(Type)記錄,。這是一列可變長度的CodeView格式的調試類型記錄,。
.debug$P
可以在使用預編譯頭(Precompiled Headers)生成的OBJ文件中找到這個節(jié)。
.drectve
這個節(jié)包含鏈接器指令,,并且只存在于OBJ文件中,。這些指令是傳遞到鏈接器命令行的ASCII碼字符串,例如:-defaultlib:LIBC,。指令之間用空格分開,。
.didat
延遲加載導入數(shù)據(jù)??梢栽诜前l(fā)行版本的可執(zhí)行文件中找到,。在發(fā)行版本中,延遲加載數(shù)據(jù)被合并到其它節(jié)中,。

 

導出表

當一個EXE或DLL導出函數(shù)或變量時,,其它EXE或DLL就可以使用這些導出的函數(shù)或變量。為了簡單起見,我把導出的函數(shù)和導出的變量統(tǒng)稱為“符號”,。當導出一些符號時,,最起碼導出符號的地址需要能夠以一種已定義好的方式被獲取。每個導出的符號都有一個與之關聯(lián)的序數(shù),,它可以用來查找這個符號,。同時,幾乎總有一個ASCII碼格式的字符串名稱與這個導出的符號關聯(lián),。一般來說,,導出的符號名與源文件中的符號名是一樣的,盡管它們可以被修改的不一樣,。
通常,,當可執(zhí)行文件導入符號時,它使用的是符號的名稱而不是它的序號,。但是當通過名稱導入時,,系統(tǒng)僅使用這個名稱去查找所需符號對應的導出序數(shù),然后根據(jù)這個序數(shù)值去獲取相應的地址,。如果先使用的是序數(shù)值的話查找過程會快一點,。通過名稱導出和導入只是為了讓程序員使用方便罷了。
在.DEF文件中的Exports節(jié)中使用ORDINAL關鍵字可以告訴鏈接器創(chuàng)建一個導入庫,,這個導入庫強制函數(shù)只能通過序數(shù)導入而不能通過名稱導入,。
我首先介紹IMAGE_EXPORT_DIRECTORY結構,如下表所示:
大小
描述
DWORD
Characteristics
導出標志,。當前未定義任何值,。
DWORD
TimeDateStamp
導出數(shù)據(jù)的創(chuàng)建時間。這個域的定義與IMAGE_NT_HEADERS.FileHeader.TimeDateStamp相同(從GMT時間1970年1月1日00:00以來的總秒數(shù)),。
WORD
MajorVersion
導出數(shù)據(jù)的主版本號。未用,,設置為0,。
WORD
MinorVersion
導出數(shù)據(jù)的次版本號。未用,,設置為0,。
DWORD
Name
與導出符號相關的DLL的名稱ASCII字符串的RVA(例如KERNEL32.DLL)。
DWORD
Base
這個域包含了這個可執(zhí)行文件的導出符號所使用的序數(shù)值的起始值,。通常情況下這個值為1,,但并不總是這樣。當通過序數(shù)查找導出符號時,,將序數(shù)值減去這個域的值就得到了這個導出符號在導出地址表(Export Address Table ,,EAT)中的索引。
DWORD
NumberOfFunctions
EAT中的元素數(shù)。注意EAT中的某些元素可能為0,,這表明沒有
代碼/數(shù)據(jù)使用那個序數(shù)值導出,。
DWORD
NumberOfNames
導出名稱表(Export Names Table,ENT)中的元素數(shù),。這個域的值總是小于或等于NumberOfFunctions域的值,。當某些符號僅使用序數(shù)導出時,它就小于那個域的值,。如果導出序數(shù)之間有間隔,,它同樣也小于那個域的值。這個域的值也是導出序數(shù)表的大?。ㄒ娤挛模?。
DWORD
AddressOfFunctions
EAT的RVA。EAT中的每個元素都是一個RVA,。其中每個非0的RVA都對應一個導出符號,。
DWORD
AddressOfNames
ENT的RVA。ENT中的每個元素都是一個ASCII碼字符串的RVA,。其中的每個ASCII碼字符串都對應一個由名稱導出的符號,。這些字符串是按一定順序排列的。這就使得加載器在查找導出符號時可以進行二進制搜索,。名稱字符串的排序是按二進制(與C++運行時庫函數(shù)strcmp類似),,而不是與位置相關的字母表順序。
DWORD
AddressOfNameOrdinals
導出序號表的RVA,。這個表是一個WORD類型的數(shù)組,。它將ENT中的索引映射到導出地址表中相應的元素上。
導出目錄(Export Directory)指向三個數(shù)組和一個ASCII碼字符串表,。其中只有導出地址表是必需的,,它是一個由指向導出函數(shù)的指針組成的數(shù)組。導出序數(shù)是這個數(shù)組的索引(見下圖),。
讓我們通過例子來看一下導出表的工作原理,。下圖顯示了KERNEL32.DLL導出表的部分內容:
exports table:
Name:            KERNEL32.dll
Characteristics: 00000000
TimeDateStamp:   3B7DDFD8 -> Fri Aug 17 23:24:08 2001
Version:         0.00
Ordinal base:    00000001
# of functions: 000003A0
# of Names:      000003A0
Entry Pt   Ordn  Name
00012ADA     1  ActivateActCtx
000082C2     2  AddAtomA
···remainder of exports omitted
假設你調用GetProcAddress來獲取KERNEL32中的AddAtomA這個API的地址。這時系統(tǒng)開始查找KERNEL32的IMAGE_EXPORT_DIRECTORY結構,。它從那里獲取了導出名稱表的起始地址,,知道了在這個數(shù)組中有0x3A0個元素,它通過二進制搜索來查找字符串“AddAtomA”,。
假設加載器發(fā)現(xiàn)AddAtomA是這個數(shù)組中的第二個元素,。然后它從導出序數(shù)表(Export Ordinal Table)中讀取相應的第二個值。這個值就是AddAtomA的導出序數(shù),。將這個導出序數(shù)作為EAT的索引(加上Base域的值),,它最終獲取AddAtomA的相對虛擬地址(RVA)是0x82C2,。將此值與KERNEL32的加載地址相加就得到了AddAtomA的實際地址。
導出轉發(fā)
導出表一個特別聰明的地方是它能將一個導出函數(shù)轉發(fā)(Forwarding)到其它DLL,。例如在Windows NT?,、Windows? 2000和Windows XP中,KERNEL32中的HeapAlloc函數(shù)被轉發(fā)到了NTDLL導出的RtlAllocHeap函數(shù)上,。轉發(fā)是在鏈接時通過.DEF文件中的EXPORTS節(jié)中的一種特殊語法形式來實現(xiàn)的,。對于HeapAlloc這個例子,KERNEL32的.DEF文件一定包含下面的內容:
        EXPORTS
        ···
        HeapAlloc = NTDLL.RtlAllocHeap
怎樣才能區(qū)別轉發(fā)的函數(shù)與正常導出的函數(shù)呢,?這需要一些技巧,。通常EAT中包含的是導出符號的RVA。但是如果這個RVA位于導出表中(通過相應的DataDirectory中的VirtualAddress域和Size域進行判斷),,那么它就是轉發(fā)的,。
當轉發(fā)一個符號時,它的RVA很明顯不能是當前模塊中的代碼或數(shù)據(jù)的地址,。實際上,,它的RVA指向一個由DLL和轉發(fā)到的符號名稱組成的字符串。在前面的例子中,,這個字符串就是NTDLL.RtlAllocHeap,。

導入表

與導出函數(shù)或變量相反的就是導入它們。為了與前面保持一致,,我仍然使用“符號”這個術語來指代導入的函數(shù)和變量,。
導入數(shù)據(jù)被保存在IMAGE_IMPORT_DESCRIPTOR結構中。對應著導入表的數(shù)據(jù)目錄項就指向由這個結構組成的數(shù)組,。每個IMAGE_IMPORT_DESCRIPTOR結構都與一個導入的可執(zhí)行文件對應,。這個數(shù)組的最后一個元素的所有域都被設置為0。下表是這個結構的內容:
大小
描述
DWORD
OriginalFirstThunk
這個域的命名太不恰當,。它包含導入名稱表的RVA,。導入名稱表是一個IMAGE_THUNK_DATA結構數(shù)組。這個域被設置為0表示IMAGE_IMPORT_DESCRIPTOR結構數(shù)組的結尾,。
DWORD
TimeDateStamp
如果可執(zhí)行文件并未綁定導入的DLL,,這個域的值為0。當使用老的綁定類型進行綁定(參考“綁定”一節(jié))時,,這個域包含日期/時間戳。當使用新的綁定類型進行綁定時,,這個域的值為-1,。
DWORD
ForwarderChain
這是首個轉發(fā)的函數(shù)的索引。如果沒有轉發(fā)的函數(shù),,這個域被設置為-1,。它僅用于老的綁定類型,,因為那種綁定類型不能很有效地處理轉發(fā)的函數(shù)。
DWORD
Name
導入的DLL名稱字符串(ASCII碼格式)的RVA,。
DWORD
FirstThunk
導入地址表的RVA,。IAT是一個IMAGE_THUNK_DATA結構數(shù)組。
每個IMAGE_IMPORT_DESCRIPTOR結構指向兩個數(shù)組,,這兩個數(shù)組實際上是一樣的,。它們有好幾種叫法,但最常用的名稱是導入地址表(Import Address Table,,IAT)和導入名稱表(Import Name Talbe,,INT)。下圖顯示的是可執(zhí)行文件從USER32.DLL中導入一些API時的情況,。
    這兩個數(shù)組的元素均為IMAGE_THUNK_DATA類型的結構,,這個結構是一個與指針大小相同的共用體(或者稱為聯(lián)合)。每個IMAGE_THUNK_DATA結構對應著從可執(zhí)行文件中導入的一個函數(shù),。這兩個數(shù)組最后都以一個值為0的IMAGE_THUNK_DATA結構作為結尾,。這個共用體(實際是一個DWORD值)可以有如下幾種含義:
DWORD ForwarderString;// 轉發(fā)函數(shù)字符串的RVA(見上文)
DWORD Function;       // 導入函數(shù)的內存地址
DWORD Ordinal;        // 導入函數(shù)的序數(shù)
DWORD AddressOfData; // IMAGE_IMPORT_BY_NAME和導入函數(shù)名稱的RVA(見下文)
IAT中的IMAGE_THUNK_DATA結構的用途可以分為兩種。在可執(zhí)行文件中,,它們或者是導入函數(shù)的序數(shù),,或者是一個IMAGE_IMPORT_BY_NAME結構的RVA。IMAGE_IMPORT_BY_NAME結構只是一個WORD類型的值,,它后面跟著導入函數(shù)的名稱字符串,。這個WORD類型的值是一個“提示(hint)”,它提示加載器導入函數(shù)的序號可能是什么,。當加載器加載可執(zhí)行文件時,,它用導入函數(shù)的實際地址來覆蓋IAT中的每個元素。這一點是理解下文的關鍵,。我強烈建議你讀一讀本期雜志中Russell Osterlund的文章——揭開Windows加載器的神秘面紗,,這篇文章詳細講述了Windows加載器的行為。
在可執(zhí)行文件被加載之前,,是否存在一種方法能夠區(qū)分IMAGE_THUNK_DATA結構中到底包含的是導入函數(shù)的序數(shù)呢,,還是IMAGE_IMPORT_BY_NAME結構的RVA呢?答案在IMAGE_THUNK_DATA結構的最高位,。如果它為1,,那么低31位(在64位可執(zhí)行文件中是低63位)中是導入函數(shù)的序數(shù)。如果最高位為0,,那么IMAGE_THUNK_DATA結構的值就是IMAGE_IMPORT_BY_NAME結構的RVA,。
另一個數(shù)組INT,本質上與IAT是一樣的,。它也是一個IMAGE_THUNK_DATA結構數(shù)組,。關鍵的區(qū)別在于當加載器將可執(zhí)行文件加載進內存時,,它并不覆蓋INT。為什么對于從DLL中導入的每組API都需要有兩個并列的數(shù)組呢,?答案在于一個稱為綁定(binding)的概念,。當在綁定過程(后面我會講到)中覆蓋可執(zhí)行文件的IAT時,需要以某種方式保存原來的信息,。而作為這個信息的副本的INT,,正是這個用途。
INT對于可執(zhí)行文件的加載并不是必需的,。但是如果它不存在的話,,那么這個可執(zhí)行文件就不能被綁定。Microsoft鏈接器總是生成INT,,但是長期以來,,Borland鏈接器(TLINK)都不生成它。這樣,,由Borland鏈接器生成的可執(zhí)行文件就不能被綁定,。
在早期的Microsoft鏈接器中,導入節(jié)并不是專門針對于鏈接器的,。組成可執(zhí)行文件導入節(jié)的所有數(shù)據(jù)都來自導入庫,。你可以對一個導入庫文件運行DUMPBIN或PEDUMP來看一下。你會發(fā)現(xiàn)一些節(jié)名類似于.idata$3和.idata$4的節(jié),。鏈接器只是簡單地遵守它的規(guī)則來組合節(jié),,所有的結構和數(shù)組就神奇般地各就其位了。幾年前Microsoft引進了一種新的導入庫格式,,這種導入庫特別小,,以便讓鏈接器能在創(chuàng)建導入數(shù)據(jù)時更具主動性。
綁定
當可執(zhí)行文件被綁定時(例如通過Bind程序),,其IAT中的IMAGE_THUNK_DATA結構中是導入函數(shù)的實際地址,。也就是說,磁盤上的可執(zhí)行文件的IAT中存儲的就是其導入的DLL中的函數(shù)在內存中的實際地址,。當加載一個被綁定的可執(zhí)行文件時,,Windows加載器可以跳過查找每個導入函數(shù)并覆蓋IAT這一步。因為IAT中已經(jīng)是正確的地址了,。但是這只有正確對齊時才行,。我在2000年5月的Under the Hood專欄中講了一些測試標準,你可以通過它們來確定綁定可執(zhí)行文件能夠對加載性能有多大提高,。
你也許會懷疑將可執(zhí)行文件綁定是否保險,。你可能會想,如果綁定了可執(zhí)行文件,,但它導入的DLL發(fā)生了變化,,這時怎么辦呢?當這種情況發(fā)生時,,IAT中的地址已經(jīng)失效了,。加載器會檢查這種情況并隨機應變。如果IAT中的地址已經(jīng)失效,,加載器會根據(jù)INT中的信息重新解析導入函數(shù)的地址,。
在安裝程序時對其進行綁定應該是最可能發(fā)生的情況了。Windows Installer中的BindImage這個動作可以替你做這件事,。同樣,,IMAGEHLP.DLL中也提供了BindImageEx這個API。不管用哪一種方法,,綁定都是個比較好的做法,。如果加載器確定綁定信息是有效的,那么可執(zhí)行文件就會被加載的更快,。如果綁定信息失效,,它也并不會比不綁定效果差。
對加載器來說,,使綁定生效的一個關鍵步驟是確定IAT中的綁定信息是否有效,。當可執(zhí)行文件被綁定時,有關它導入的DLL的信息也被放在可執(zhí)行文件中,。加載器檢查這個信息以快速確定綁定的有效性,。在綁定的最初實現(xiàn)中并未添加這個信息,因此可執(zhí)行文件可能按老的綁定方式進行綁定,,或者按新的綁定方式進行綁定,。我在這里講的是新的綁定方式。
確定綁定信息有效性的一個關鍵數(shù)據(jù)結構是IMAGE_BOUND_IMPORT_DESCRIPTOR,。被綁定的可執(zhí)行文件中有一個此結構的列表,。每個IMAGE_BOUND_IMPORT_DESCRIPTOR結構表示一個綁定到的DLL的日期/時間戳。這個列表的RVA由數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT的元素給出,。IMAGE_BOUND_IMPORT_DESCRIPTOR結構中的成員如下:
  • TimeDateStamp,,這是包含導入的DLL的日期/時間戳的一個DWORD類型的值。
  • OffsetModuleName,,這是包含導入的DLL的名稱字符串偏移地址的一個WORD類型的值,。這個域是相對于首個IMAGE_BOUND_IMPORT_DESCRIPTOR結構的偏移(而不是RVA)。
  • NumberOfModuleForwarderRefs,,這是一個WORD類型的值,,它包含緊跟在這個結構后面的IMAGE_BOUND_FORWARDER_REF結構的數(shù)目。除了最后一個WORD類型的成員(NumberOfModuleForwarderRefs)是保留的外,,IMAGE_BOUND_FORWARDER_REF結構與IMAGE_BOUND_IMPORT_DESCRIPTOR結構一樣,。
一般情況下,,每個導入的DLL對應的IMAGE_BOUND_IMPORT_DESCRIPTOR結構簡單地組成一個數(shù)組。但是當綁定的API轉發(fā)到了另一個DLL上時,,這個轉發(fā)到的DLL的有效性也需要檢查,。在這種情況下,IMAGE_BOUND_FORWARDER_REF結構就與IMAGE_BOUND_IMPORT_DESCRIPTOR結構交叉在了一起,。下面舉一個例子來說明,。
假設你鏈接到了KERNEL32.DLL中的HeapAlloc這個API上,而它實際上被轉發(fā)到了NTDLL中的RtlAllocateHeap上,,然后你綁定這個可執(zhí)行文件,。那么在這個可執(zhí)行文件中,對應于KERNEL32.DLL這個導入的DLL就有一個相應的IMAGE_BOUND_IMPORT_DESCRIPTOR結構,,同時它后面是一個對應于NTDLL.DLL的IMAGE_BOUND_FORWARDER_REF結構,。緊跟在它們后面的可能是與你導入并綁定到的其它DLL對應的IMAGE_BOUND_IMPORT_DESCRIPTOR結構。
延遲加載數(shù)據(jù)
前面我已經(jīng)講過延遲加載(Delayload)一個DLL就是隱含導入與通過LoadLibrary和GetProcAddress顯式導入這兩種方式的混合?,F(xiàn)在讓我們來看一下延遲加載所需的數(shù)據(jù)結構以及它的工作原理,。
一定要記住延遲加載并不是操作系統(tǒng)的功能。它完全是由鏈接器和運行時庫添加的附加代碼和數(shù)據(jù)來實現(xiàn)的,。正因為如此,,WINNT.H中并沒有幾個地方涉及到延遲加載。但是你會發(fā)現(xiàn)延遲加載數(shù)據(jù)和正常導入數(shù)據(jù)二者的定義是平行的,。
DataDirectory中的IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT元素指向延遲加載數(shù)據(jù),。這個元素中實際是一個ImgDelayDescr結構數(shù)組的RVA,這個結構被定義在Visual C++的DelayImp.H文件中,。下表是這個結構的內容,。對應于每一個導入的DLL都有一個相應的ImgDelayDescr結構。
大小
描述
DWORD
grAttrs
此結構的屬性,。當前惟一定義的標志是dlattrRva(值為1),。這個標志表明此結構中的地址域是RVA,而不是虛擬地址,。設置這個標志表明延遲加載描述符是VC7.0或其后續(xù)版本,。
RVA
rvaDLLName
導入的DLL的名稱字符串的RVA。這個字符串被傳遞給LoadLibrary函數(shù),。
RVA
rvaHmod
一塊HMODULE大小的內存的RVA,。當延遲加載的DLL被加載進內存時,它的HMODULE被存儲在這個位置,。
RVA
rvaIAT
此DLL的導入地址表的RVA,。它的格式與正常的IAT相同。
RVA
rvaINT
此DLL的導入名稱表的RVA。它的格式與正常的INT相同,。
RVA
rvaBoundIAT
可選的綁定IAT的RVA,。它是此DLL的導入地址表的一個綁定副本的RVA。它的格式與正常的IAT相同,。當前這個IAT副本并未綁定,,但這個功能可能被添加到將來的BIND程序中。
RVA
rvaUnloadIAT
原始的IAT的可選副本的RVA,。它是此DLL的導入地址表的一個未綁定的副本的RVA。它的格式與正常的IAT相同,。當前總是設置為0,。
DWORD
dwTimeStamp
延遲加載導入的DLL的日期/時間戳。通常設置為0,。
我們從ImgDelayDescr結構中可以獲取的主要內容就是它包含了DLL的IAT和INT的地址,。這些表與正常情況下的表是一樣的,只不過它們是由運行時庫代碼進行讀寫而不是由操作系統(tǒng),。當你調用延遲加載的DLL中的函數(shù)時,,運行時庫代碼就調用LoadLibrary加載相應的DLL(如果需要的話),然后調用GetProcAddress來獲取函數(shù)地址,,最后將獲取的地址存儲在延遲加載IAT中,,以便將來可以直接調用這個函數(shù)。
延遲加載所使用的數(shù)據(jù)結構在設計時有一個失誤的地方需要解釋一下,。在Visual C++ 6.0中——這是它最初的形式,,ImgDelayDescr結構中的所有包含地址的域使用的都是虛擬地址,而不是RVA,。也就是說,,它們包含了延遲加載數(shù)據(jù)所在位置的實際地址。這些域都是DWORD類型的,,也就是x86上一個指針的大小,。
現(xiàn)在要全面支持IA-64了。突然,,4字節(jié)已經(jīng)不夠保存一個完整的地址了,。哎呀!在這個時候,,Microsoft做了一件正確的事,,把包含地址的域都改為包含RVA了。如前面所示,,我使用的是已經(jīng)修訂過的結構定義和名稱,。
還有一個問題就是確定ImgDelayDescr使用的是RVA還是虛擬地址。這個結構中有一個域包含了相關的標志。當grAttrs域為1時,,這個結構中的成員中包含的是RVA,。從Visual Studio? .NET和64位編譯器開始,這是惟一選項,。如果grAttrs不是1,,ImgDelayDescr結構中的域包含的都是虛擬地址。

資源節(jié)

在PE文件的所有節(jié)中,,在資源節(jié)中定位數(shù)據(jù)是最復雜的,。在這里我只講述一些獲取諸如圖標、位圖以及對話框之類的資源的原始數(shù)據(jù)所需的一些數(shù)據(jù)結構,。我不涉及它們的實際格式,,那已經(jīng)超出了本文的范圍。
資源可以在一個叫做.rsrc的節(jié)中找到,。DataDirectory中索引為IMAGE_DIRECTORY_ENTRY_RESOURCE的元素包含了資源的RVA和大小,。由于多方面的原因,資源被組織得與文件系統(tǒng)類似——有目錄和葉結點,。
DataDirectory中的資源指針指向了一個IMAGE_RESOURCE_DIRECTORY類型的結構,。這個結構中包含了目前尚未使用的Characteristics域、TimeDateStamp域以及版本號域(MajorVersion和MinorVersion),。這個結構中真正有用的域是NumberOfNamedEntries和NumberOfIdEntries,。
每個IMAGE_RESOURCE_DIRECTORY結構后面是一個IMAGE_RESOURCE_DIRECTORY_ENTRY結構數(shù)組。另外,,IMAGE_RESOURCE_DIRECTORY結構中的NumberOfNamedEntries和NumberOfIdEntries這兩個域保存的就是這個數(shù)組中IMAGE_RESOURCE_DIRECTORY_ENTRY結構的數(shù)目,。(如果你感覺這些數(shù)據(jù)結構的名稱讓你看得頭疼,說句實在話,,我將它們寫下來也挺難受的?。?/div>
每個目錄項(即IMAGE_RESOURCE_DIRECTORY_ENTRY結構)或者指向另一個資源目錄,或者指向具體的資源數(shù)據(jù),。當它指向另一個資源目錄時,,這個結構中的第二個DWORD的最高位為1,其余的31位是那個資源目錄的偏移,。這個偏移是相對于資源節(jié)開頭來說的,,而不是RVA。
當它指向實際的某種資源時,,第二個DWORD的最高位為0,,其余的31位是具體資源(例如對話框)的偏移。同上面一樣,,這個偏移同樣是相對于資源節(jié)開頭來說的,,而不是RVA。
每個目錄項可以通過名稱或者ID值來標識。它們就是你在.RC文件中為具體資源指定的名稱或ID值,。當目錄項的第一個DWORD的最高位為1時,,其余的31位是資源名稱(字符串)的偏移;如果最高位為0,,那么其低16位是資源標識(ID)的值,。
理論已經(jīng)足夠了!現(xiàn)在讓我們看一個實際的例子,。下面是PEDUMP輸出的ADVAPI32.DLL的資源節(jié)的部分內容:
Resources (RVA: 6B000)
ResDir (0) Entries:03 (Named:01, ID:02) TimeDate:00000000
    ———————————————————————————————
    ResDir (MOFDATA) Entries:01 (Named:01, ID:00) TimeDate:00000000
        ResDir (MOFRESOURCENAME) Entries:01 (Named:00, ID:01) TimeDate:00000000
            ID: 00000409 DataEntryOffs: 00000128
            DataRVA: 6B6F0 DataSize: 190F5 CodePage: 0
    ———————————————————————————————
    ResDir (STRING) Entries:01 (Named:00, ID:01) TimeDate:00000000
        ResDir (C36) Entries:01 (Named:00, ID:01) TimeDate:00000000
            ID: 00000409 DataEntryOffs: 00000138
            DataRVA: 6B1B0 DataSize: 0053C CodePage: 0
    ———————————————————————————————
    ResDir (RCDATA) Entries:01 (Named:00, ID:01) TimeDate:00000000
        ResDir (66) Entries:01 (Named:00, ID:01) TimeDate:00000000
            ID: 00000409 DataEntryOffs: 00000148
            DataRVA: 85908 DataSize: 0005C CodePage: 0
其中以“ResDir”開頭的每一行對應于一個IMAGE_RESOURCE_DIRECTORY結構,。“ResDir“后面的括號中是資源目錄的名稱,。在這個例子中,,資源目錄的名稱分別為0、MOFDATA,、MOFRESOURCENAME、STRING,、C36,、RCDATA和66。名稱后面是以名稱標識的和以ID標識的資源目錄的總個數(shù)(后面的括號中是它們分別的個數(shù)),。在這個例子中,,頂級目錄一共有3個直接的子目錄,所有其它目錄都只有一個下級子目錄,。
頂級目錄類似于文件系統(tǒng)中的根目錄,。根目錄下的每個子目錄項(也就是第二級目錄)代表資源的類型(字符串表、對話框,、菜單等等),。它們下面還有第三級子目錄。
對于某種具體的資源類型來說,,一般有三級目錄,。例如如果有五個對話框,那么第二級的DIALOG目錄下面將會有五個子目錄項,。這五個子目錄項本身也都是目錄,。在這五個目錄下面都只有一項內容,它就是具體資源的原始數(shù)據(jù)的偏移地址,。很簡單,,不是嗎?
如果你更喜歡通過讀源代碼來學習的話,,你可以仔細看一下PEDUMP中轉儲資源的那部分代碼(PEDUMP的源代碼可以從2002年2月本文的第一部分中下載),。除了顯示所有的資源目錄以及它們的元素個數(shù)外,PEDUMP還可以顯示幾種常見的資源類型,例如對話框等,。

基址重定位

在可執(zhí)行文件中的許多地方,,你都會發(fā)現(xiàn)內存地址的蹤跡。當鏈接器在生成可執(zhí)行文件時,,它假定這個可執(zhí)行文件會被加載到內存中的某一個地址處(即首選地址),。只有在可執(zhí)行文件被加載到其首選地址時,所有這些內存地址才是正確的,。這個首選地址由IMAGE_FILE_HEADER結構中的ImageBase域給出,。
如果加載器由于某種原因需要把可執(zhí)行文件加載到其它地址處時,所有這些地址都變成不正確的了,。這將會額外增加加載器的工作量,。在2000年5月的Under The Hood專欄(前面已經(jīng)提到)中我已經(jīng)講過當幾個DLL首選加載地址相同時會導致性能損失,以及如何使用REBASE工具來解決這個問題,。
基址重定位(Base Relocations)信息告訴加載器可執(zhí)行文件不能被加載到其首選地址時需要進行修改的每一個位置,。對于加載器來說,幸運的是它并不需要知道地址使用的細節(jié)問題,。它只知道有一個地址列表,,其中的每一個地址都需要以同樣的方式進行修改。
讓我們來看一個x86平臺上的可執(zhí)行文件的例子,。假設有以下指令,,它將一個全局變量(地址0x0040D434)的值加載到ECX寄存器中:
00401020: 8B 0D 34 D4 40 00 mov ecx,dword ptr [0x0040D434]
這條指令在地址0x00401020處,長為6個字節(jié),。前兩個字節(jié)(0x8B 0x0D)是指令的機器碼,。剩下的四個字節(jié)是一個DWORD值的地址(0x0040D434)。在這個例子中,,這條指令實際來自一個首選地址為0x00400000的可執(zhí)行文件,,因此這個全局變量的RVA為0xD434。
如果這個可執(zhí)行文件被加載到了0x00400000處,,這條指令當然可以正確執(zhí)行,。但是現(xiàn)在我們假設它被加載到了0x00500000處。如果真是這樣,,那么這條指令的最后的四個字節(jié)需要被改成0x0050D434,。
那么加載器是如何做的呢?它比較首選加載地址與實際加載地址,,然后計算出△(delta,,音譯為德耳塔,數(shù)學中的常用符號,,表示差值的意思),。在這個例子中,,△為0x00100000。這個△被加到變量原來的地址值(大小為DWORD)上,,形成新的地址,。在前面的例子中,關于地址0x00401022處,,即指令中的DWORD值處,,將會有一個相應的重定位信息。
簡而言之,,基址重定位信息只是可執(zhí)行文件中的一個地址列表,,當加載進內存時,這些地址中的值都要再加上△,。為了提高系統(tǒng)性能,,可執(zhí)行文件的頁面只有在需要時才會被加載進內存(可執(zhí)行文件的加載與內存映射文件類似),基址重定位信息的格式就反映了這個特性,?;分囟ㄎ恍畔⑺诘墓?jié)通常被稱為.reloc節(jié),但是查找它的正確方法是通過數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_BASERELOC的那個元素,。
基址重定位信息是一些非常簡單的IMAGE_BASE_RELOCATION結構,。此結構中的VirtualAddress域包含了需要進行重定位的內存范圍的起始RVA。SizeOfBlock域給出了重定位信息的大小,,其中包括IMAGE_BASE_RELOCATION自身的大小。
緊跟著IMAGE_BASE_RELOCATION結構后面是一組可變數(shù)目的WORD值,。這些WORD值的數(shù)目可以從IMAGE_BASE_RELOCATION結構的SizeOfBlock域推出,。其中每個WORD值由兩部分組成。高4位指明了重定位的類型,,由WINNT.H中的一系列IMAGE_REL_BASED_xxx值給出,。低12位是相對于IMAGE_BASE_RELOCATION結構的VirtualAddress域的偏移,這是應該進行重定位的地方,。
在前面那個關于基址重定位的例子中,,我把情況簡化了。實際上有多種類型的重定位方式,。對于x86平臺上的可執(zhí)行文件來說,,所有的重定位類型都是IMAGE_REL_BASED_HIGHLOW。你經(jīng)常會在一組重定位信息之后看到類型為IMAGE_REL_BASED_ABSOLUTE的重定位信息,。它們實際上并沒有什么作用,,只是為了填充空間以便下一個IMAGE_BASE_RELOCATION結構能夠按4字節(jié)的邊界對齊。
對于IA-64平臺上的可執(zhí)行文件來說,,重定位類型好像總是IMAGE_REL_BASED_DIR64,。與x86平臺一樣的是,,通常也會有作為填充的IMAGE_REL_BASED_ABSOLUTE類型的重定位信息。有趣的一點是,,盡管IA-64平臺上每個頁面是8KB,,但基址重定位信息仍舊是分成4KB的塊。
在Visual C++ 6.0中,,鏈接器在創(chuàng)建發(fā)行版的EXE文件時并不生成重定位信息,。這是由于EXE文件是最先被加載到進程的地址空間中的,因此可以絕對保證它能被加載到其首選地址上,。DLL就沒有這么幸運了,,因此DLL中總是存在基址重定位信息,除非你使用/FIXED鏈接器選項明確忽略它們,。在Visual Studio .NET中,,鏈接器在生成調試版和發(fā)行版的EXE文件時都不產生基址重定位信息。

調試目錄

當創(chuàng)建可執(zhí)行文件并生成相應的調試信息時,,通常文件中會包含這種信息格式的細節(jié)以及它的位置,。操作系統(tǒng)運行可執(zhí)行文件時并不需要調試信息,但它對于開發(fā)工具非常有用,。一個EXE文件可以包含多種格式的調試信息,,調試目錄(Debug Directory)結構指出哪種格式可用。
可以通過數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_DEBUG的元素找到調試目錄,。它是由IMAGE_DEBUG_DIRECTORY結構組成的數(shù)組,,其中每一個結構對應一種類型的調試信息,如下表所示,。調試目錄中元素的數(shù)目可以使用數(shù)據(jù)目錄中的Size域計算得出,。
大小
描述
DWORD
Characteristics
未用,設置為0,。
DWORD
TimeDateStamp
調試信息的日期/時間戳,。
WORD
MajorVersion
調試信息的主版本號,未用,。
WORD
MinorVersion
調試信息的次版本號,,未用。
DWORD
Type
調試信息的類型,。以下是經(jīng)常遇到的類型:
IMAGE_DEBUG_TYPE_COFF
IMAGE_DEBUG_TYPE_CODEVIEW      // 包含PDB文件
IMAGE_DEBUG_TYPE_FPO           // 幀指針省略
IMAGE_DEBUG_TYPE_MISC          // IMAGE_DEBUG_MISC
IMAGE_DEBUG_TYPE_OMAP_TO_SRC
IMAGE_DEBUG_TYPE_OMAP_FROM_SRC
IMAGE_DEBUG_TYPE_BORLAND       // Borland格式
DWORD
SizeOfData
文件中調試數(shù)據(jù)的大小,。不包括外部調試文件(例如.PDB文件)的大小。
DWORD
AddressOfRawData
當映射進內存時調試數(shù)據(jù)的RVA,。如果調試信息不被映射,,它被設置為0。
DWORD
PointerToRawData
調試數(shù)據(jù)的文件偏移(不是RVA),。
到目前為止,,最流行的調試信息格式是PDB文件,。PDB文件實質上是CodeView格式調試信息的發(fā)展。一個類型為IMAGE_DEBUG_TYPE_CODEVIEW的調試目錄標志著PDB信息的存在,。如果你檢查由這個元素指向的數(shù)據(jù),,會發(fā)現(xiàn)一個短的CodeView格式的頭部。這個調試數(shù)據(jù)主要是一個外部PDB文件的路徑,。在Visual Studio 6.0中,,調試頭開始處是一個NB10簽名。在Visual Studio .NET中,,這個頭開始處是RSDS,。
在Visual Studio 6.0中,可以使用/DEBUGTYPE:COFF鏈接器選項來生成COFF調試信息,。Visual Studio .NET將這項功能移除了,。對于經(jīng)過優(yōu)化的x86代碼,由于函數(shù)可能沒有正常的棧幀,,所有使用幀指針省略(Frame Pointer Omission,,F(xiàn)PO)調試信息。FPO數(shù)據(jù)允許調試器定位局部變量和參數(shù),。
有兩種OMAP調試信息僅用于Microsoft的程序,。Microsoft內部使用一種工具對可執(zhí)行文件中的代碼進行重新排列以減少分頁。(它所做的不僅僅是Working Set Tuner所能做到的,。)OMAP信息讓工具可以在調試信息中的原始地址與重排后的代碼中的新地址之間進行轉換,。
順便說一下,DBG文件也包含了一個類似于我上面講的調試目錄,。DBG文件流行于Windows NT 4.0時代,,它們主要包含COFF調試信息,但是Windows XP偏愛PDB文件而將它們淘汰了,。

.NET頭部

對于開發(fā)工具生成的用于Microsoft .NET環(huán)境下的可執(zhí)行文件來說,它們首先是PE文件,。但是在大多數(shù)情況下.NET文件中正常的代碼和數(shù)據(jù)是微不足道的,。.NET可執(zhí)行文件的主要目的是將.NET特定的信息,例如元數(shù)據(jù)和中間語言(IL),,加載進內存,。另外.NET可執(zhí)行文件鏈接到了MSCOREE.DLL文件上。這個DLL是.NET進程的起點,。當加載.NET可執(zhí)行文件時,,它的入口點通常是一個小的占位程序。這個占位程序只是跳轉到MSCOREE.DLL的一個導出函數(shù)(_CorExeMain或_CorDllMain)上,。從那里開始,,MSCOREE獲取控制權,,開始使用可執(zhí)行文件中的元數(shù)據(jù)和IL。這類似于(.NET版之前的)Visual Basic中的應用程序使用MSVBVM60.DLL所采用的方式,。.NET信息的起點是IMAGE_COR20_HEADER結構,,它當前被定義在.NET Framework SDK中的CorHDR.H文件以及最新的WINNT.H文件中。數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR的項指向IMAGE_COR20_HEADER結構,。下表列出了IMAGE_COR20_HEADER結構中的域,。關于IMAGE_COR20_HEADER指向的元數(shù)據(jù)、方法IL以及其它內容將在后續(xù)文件中詳細講述,。

類型
描述
DWORD
cb
頭部的大?。ㄒ宰止?jié)計)。
WORD
MajorRuntimeVersion
運行這個程序所需的運行時組件的最小版本號,。對于第一個發(fā)行的.NET Framework而言,,此值為2。
WORD
MinorRuntimeVersion
次版本號,,當前為0,。
IMAGE_DATA_DIRECTORY
MetaData
元數(shù)據(jù)表的RVA。
DWORD
Flags
包含這個映像屬性的標志,。當前定義了以下值:
COMIMAGE_FLAGS_ILONLY
// 映像僅包含IL代碼,,并不需要運// 行于特定CPU上
COMIMAGE_FLAGS_32BITREQUIRED // 僅運行于32位處理器上
COMIMAGE_FLAGS_IL_LIBRARY
STRONGNAMESIGNED
// 映像已經(jīng)用散列數(shù)據(jù)簽名
COMIMAGE_FLAGS_TRACKDEBUGDATA
// 讓JIT或運行時組件為方法保持// 調試信息
DWORD
EntryPointToken
映像入口點的MethodDef的記號。.NET運行時調用這個方法開始托管執(zhí)行,。
IMAGE_DATA_DIRECTORY
Resources
.NET資源的RVA和大小,。
IMAGE_DATA_DIRECTORY
StrongNameSignature
強名稱散列數(shù)據(jù)的RVA。
IMAGE_DATA_DIRECTORY
CodeManagerTable
代碼管理器表的RVA,。代碼管理器包含獲取正在運行的程序的狀態(tài)(例如堆棧跟蹤和跟蹤GC引用)所需的代碼,。
IMAGE_DATA_DIRECTORY
VTableFixups
需要被修正的函數(shù)指針組成的數(shù)組。用于支持非托管的C++虛表,。
IMAGE_DATA_DIRECTORY
ExportAddressTableJumps
由對應于導出符號的JMP形實轉換塊被寫入的位置(RVA)組成的數(shù)組的RVA,。這些形實轉換塊允許托管方法被導出,這樣非托管代碼可以調用它們,。
IMAGE_DATA_DIRECTORY
ManagedNativeHeader
在內存中供.NET運行時組件內部使用,。在可執(zhí)行文件中被設置為0。

TLS初始化

當使用__declspec(thread)定義線程局部變量時,,編譯器將它們放入一個名為.tls的節(jié)中,。當系統(tǒng)創(chuàng)建新線程時,它從進程堆中分配內存來保存用于新線程的線程局部變量,。這部分內存使用.tls節(jié)中的值進行初始化,。系統(tǒng)將分配的內存的地址保存在TLS數(shù)組中,F(xiàn)S:[2Ch]指向這個數(shù)組(在x86平臺上),。
如果數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_TLS的元素不為0,,那就表示可執(zhí)行文件中存在線程局部存儲(TLS),。而這個元素指向一個IMAGE_TLS_DIRECTORY結構,如下表所示,。
大小
描述
DWORD
StartAddressOfRawData
用于在內存中初始化新線程的TLS數(shù)據(jù)的一段內存的起始地址,。
DWORD
EndAddressOfRawData
用于在內存中初始化新線程的TLS數(shù)據(jù)的一段內存的結束地址。
DWORD
AddressOfIndex
當可執(zhí)行文件被加載進內存時,,如果它包含.tls節(jié),,加載器調用TlsAlloc給它分配一個TLS句柄,并將分配的句柄保存在這個域指定的位置處,。運行時庫使用這個句柄定位線程局部數(shù)據(jù),。
DWORD
AddressOfCallBacks
由PIMAGE_TLS_CALLBACK類型的函數(shù)指針組成的數(shù)組的地址。當創(chuàng)建或撤銷線程時,,這個列表中的每個函數(shù)都會被調用,。最后一個元素的值為0,它標志著表的結尾,。一般由Visual C++生成的可執(zhí)行文件中這個表是空的,。
DWORD
SizeOfZeroFill
已初始化數(shù)據(jù)中除了由StartAddressOfRawData和EndAddressOfRawData域組成的已初始化數(shù)據(jù)界限之外的大小(以字節(jié)計),。所有超出這個范圍的用于單個線程的數(shù)據(jù)都被初始化為0,。
DWORD
Characteristics
保留,當前被設置為0,。
注意到IMAGE_TLS_DIRECTORY中的地址都是虛擬地址而不是RVA這一點很重要,。因此如果可執(zhí)行文件不能被加載到其首選加載地址時,它們都要進行基址重定位,。同時,,IMAGE_TLS_DIRECTORY結構本身并不在.tls節(jié)中,它位于.rdata節(jié)中,。

程序異常數(shù)據(jù)

      一些平臺(包括IA-64)并不使用x86平臺上的基于幀的異常處理,,它們使用的是基于表的異常處理。在這種異常處理中有一個表,,它包含了可能會被異常展開(unwinding)影響到的每一個函數(shù)的信息,。這些信息主要包括每個函數(shù)的開始地址、結束地址以及在哪里并如何處理異常,。當發(fā)生異常時,,系統(tǒng)搜索整個表來尋找處理它的相應項并處理,。異常表是一個由IMAGE_RUNTIME_FUNCTION_ENTRY結構組成的數(shù)組,。數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_EXCEPTION的元素引向此數(shù)組。這個結構的格式因平臺而異,。對于IA-64平臺,,它的結構如下:
DWORD BeginAddress;
DWORD EndAddress;
DWORD UnwindInfoAddress;
UnwindInfoAddress數(shù)據(jù)的結構并未在WINNT.H文件中給出,。但是它的具體格式可以在Intel的"IA-64 Software Conventions and Runtime Architecture Guide"一書第11章中找到。

PEDUMP程序

      現(xiàn)在我的PEDUMP程序與1994年時的相比已經(jīng)有了很大改進,。它可以顯示本文中講的所有結構,,其中包括:
  • IMAGE_NT_HEADERS
  • 導入表/導出表
  • 資源
  • 基址重定位
  • 調試目錄
  • 延遲導入表
  • 綁定導入描述符
  • IA-64異常處理表
  • TLS初始化數(shù)據(jù)
  • .NET運行時頭
除了可以轉儲可執(zhí)行文件外,PEDUMP還可以轉儲COFF格式的OBJ文件,、COFF導入庫(新格式以及老格式),、COFF符號表和DBG文件。
PEDUMP是一個命令行程序,。對前面提到的各種文件運行PEDUMP時,,如果不加任何選項,它默認輸出的是最有用的數(shù)據(jù)結構信息,。有好幾個命令行選項可以用來添加其它的輸出信息:
名稱
描述
/A
轉儲所有內容
/B
顯示基址重定位信息
/H
包括每個節(jié)中原始數(shù)據(jù)的十六進制形式
/I
包括導入地址表形實轉換塊的地址
/L
包括行號信息
/P
包括PDATA(運行時函數(shù))
/R
包括詳細的資源信息(字符串表和對話框)
/S
顯示符號表
關于PEDUMP的源代碼有幾個地方值得注意,。首先它可以按32位或64位可執(zhí)行文件編譯和運行。如果你手邊有Itanium機器可以試一下,。另外,,無論PEDUMP以何種方式編譯,它都可以同時轉儲32位和64位文件,。換句話說,,32位版的PEDUMP可以轉儲32位和64位文件,64位版的PEDUMP也可以轉儲32位和64位文件,。
在考慮使PEDUMP可以同時處理32位和64位文件時,,我想避免為32位結構和64位結構分別寫一個函數(shù)。因此我使用了C++模板,。
在好幾個文件(特別是EXEDUMP.CPP)中,,你都會發(fā)現(xiàn)各種模板函數(shù)。大多數(shù)情況下,,模板函數(shù)的參數(shù)最終會被擴展為IMAGE_NT_HEADERS32結構或 IMAGE_NT_HEADERS64結構,。當調用這些函數(shù)時,由代碼自身確定是32位還是64位文件并用相應參數(shù)類型去調用相應的函數(shù),,引起相應的模板展開,。
伴隨PEDUMP源代碼的還有一個Visual C++ 6.0工程文件。工程配置除了傳統(tǒng)的x86 debug和 release外,,還有相應的64位配置,。要想使它正常工作,你需要把64位工具(當前在Platform SDK中)的路徑添加到Tools | Options | Directories 選項卡最上面的Executable files路徑中,,同時還要設置相應的64位Include目錄和Lib目錄的路徑,。在我的機器上這個工程文件可以正常工作,但是在你的機器上可能需要進行少量修改才行。
為了使PEDUMP可以處理的內容盡可能全面,,這就需要使用最新的Windows頭文件,。我在開發(fā)這個程序時使用的是2001年6月的Platform SDK,需要的這些文件都在.\include\prerelease和.\Include\Win64\crt目錄中,。在2001年8月的SDK中,, WINNT.H文件已經(jīng)被更新,因此也就不需要prerelease目錄中的文件了,。最終可以成功創(chuàng)建這個程序,。你需要做的可能只是盡是安裝最新的Platform SDK或在創(chuàng)建64位版的程序時對工程目錄進行一些修改。
結束語
      可移植可執(zhí)行文件格式是一種結構非常好且相對簡單的可執(zhí)行文件格式,。特別好的一點是PE文件可以被直接映射進內存,,這樣它在磁盤上的數(shù)據(jù)結構與運行時Windows使用的結構一致。

    本站是提供個人知識管理的網(wǎng)絡存儲空間,,所有內容均由用戶發(fā)布,,不代表本站觀點。請注意甄別內容中的聯(lián)系方式,、誘導購買等信息,,謹防詐騙。如發(fā)現(xiàn)有害或侵權內容,,請點擊一鍵舉報,。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多