x86 反彙編/Windows 可執行檔案
COM 檔案按其顯示的完全相同的方式載入到 RAM 中;從硬碟映像到 RAM 沒有任何更改。這是由於早期 x86 系列的分段記憶體模型。兩個 16 位暫存器決定用於記憶體訪問的實際地址,一個“段”暫存器指定 1M+64K 位元組空間中的 64K 位元組視窗(以 16 位元組為增量),以及一個“偏移量”指定該視窗中的偏移量。段暫存器將由 DOS 設定,COM 檔案預計將遵守此設定並且永遠不會更改段暫存器。然而,偏移量暫存器是自由使用的,並且(對於 COM 檔案)與現代 32 位暫存器具有相同的用途。缺點是偏移量暫存器只有 16 位,因此,由於 COM 檔案無法更改段暫存器,因此 COM 檔案使用 RAM 的限制為 64K。然而,這種方法的好處是,DOS 不需要額外的工作來載入和執行 COM 檔案:只需載入檔案,設定段暫存器,然後跳轉到它。(程式可以透過只給出要跳轉到的偏移量來執行“近”跳轉。)
COM 檔案載入到 RAM 中的偏移量為 $100。在此之前空間將用於傳遞資料到和來自 DOS(例如,用於呼叫程式的命令列的內容)。
請注意,根據定義,COM 檔案不能是 32 位的。Windows 透過特殊的 CPU 模式提供對 COM 檔案的支援。
請注意,MS-DOS COM 檔案(“命令”檔案的簡稱)與元件物件模型檔案不同,後者是一種面向物件的庫技術。 |
MS-DOS 編譯器克服 64K 記憶體限制的一種方法是引入了記憶體模型。基本概念是巧妙地設定 x86 CPU 中的不同段暫存器 (CS、DS、ES、SS) 以指向相同或不同的段,從而允許不同程度地訪問記憶體。典型的記憶體模型是
- 微型
- 所有記憶體訪問都是 16 位的(段暫存器保持不變)。生成 .COM 檔案而不是 .EXE 檔案。
- 小型
- 所有記憶體訪問都是 16 位的(段暫存器保持不變)。
- 緊湊型
- 資料地址包括段和偏移量,在訪問時重新載入 DS 或 ES 暫存器,允許高達 1M 的資料。程式碼訪問不會更改 CS 暫存器,允許 64K 的程式碼。
- 中型
- 程式碼地址包括段地址,在訪問時重新載入 CS,允許高達 1M 的程式碼。資料訪問不會更改 DS 和 ES 暫存器,允許 64K 的資料。
- 大型
- 程式碼和資料地址都是 (段,偏移量) 對,始終重新載入段地址。整個 1M 位元組記憶體空間可用於程式碼和資料。
- 巨大
- 與大型模型相同,編譯器會生成額外的算術運算,以允許訪問大於 64K 的陣列。
檢視 EXE 檔案時,必須確定使用哪種記憶體模型構建該檔案。
可移植可執行檔案 (PE) 檔案是 Windows NT、Windows 95 和 Win32 下可執行檔案或 DLL 的標準二進位制檔案格式。Win32 SDK 包含一個檔案winnt.h,它宣告 PE 檔案中使用的各種結構和變數。imagehlp.dll 中還包含一些用於操作 PE 檔案的函式。PE 檔案被分解成可以檢查的不同部分。
在 Windows 環境中,可執行模組可以載入到記憶體中的任何位置,並期望在沒有任何問題的情況下執行。為了允許多個程式載入到記憶體中看似隨機的位置,PE 檔案採用了一種名為 RVA 的工具:相對虛擬地址。RVA 假設模組載入到記憶體中的“基地址”在編譯時是未知的。因此,PE 檔案將記憶體中資料的地址描述為相對於基地址的偏移量,無論該地址在記憶體中的何處。
一些處理器指令要求程式碼本身直接識別記憶體中某個資料的位置。當模組在記憶體中的位置在編譯時未知時,這不可能。解決此問題的方案在“重定位”部分中介紹。
請記住,從模組的反彙編中獲得的地址並不總是與偵錯程式在程式執行時看到的地址相匹配。
PE 可移植可執行檔案格式包含許多資訊頭,並按以下格式排列
Microsoft PE 檔案的基本格式
在十六進位制編輯器中開啟任何 Win32 二進位制可執行檔案,你會看到:前兩個字母 **始終** 是 "MZ",這是 Mark Zbikowski 的首字母縮寫,他建立了第一個 DOS 連結器。對一些人來說,檔案中確定檔案型別的頭幾個位元組被稱為 "魔數",雖然沒有規定 "魔數" 必須是單個數字。相反,我們將使用術語 "檔案 ID 標籤",或簡稱為 "檔案 ID"。有時這也稱為檔案簽名。
在檔案 ID 之後,十六進位制編輯器會顯示一些隨機符號或空白字元,然後是可讀的字串 "This program cannot be run in DOS mode"。
這是什麼呢?
MS-DOS 檔案頭的十六進位制列表
你看到的是 Win32 PE 檔案的 MS-DOS 頭。為了確保 a) 向後相容性,或 b) 新檔案型別的優雅降級,微軟在每個 PE 檔案的頭部寫入了一系列機器指令(DOS 頭結構下面列出了一個示例程式)。當一個 32 位 Windows 檔案在 16 位 DOS 環境中執行時,程式會顯示錯誤訊息:"This program cannot be run in DOS mode.",然後終止。
DOS 頭也稱為 EXE 頭。以下是 DOS 頭作為 C 資料結構的表示
struct DOS_Header
{
// short is 2 bytes, long is 4 bytes
char signature[2] = { 'M', 'Z' };
short lastsize;
short nblocks;
short nreloc;
short hdrsize;
short minalloc;
short maxalloc;
void *ss; // 2 byte value
void *sp; // 2 byte value
short checksum;
void *ip; // 2 byte value
void *cs; // 2 byte value
short relocpos;
short noverlay;
short reserved1[4];
short oem_id;
short oem_info;
short reserved2[10];
long e_lfanew; // Offset to the 'PE\0\0' signature relative to the beginning of the file
}
在 DOS 頭之後,有一個上面提到的存根程式。下面列出了一個註釋過的示例程式,它來自用 GCC 編譯的程式。
;# Using NASM with Intel syntax
push cs ;# Push CS onto the stack
pop ds ;# Set DS to CS
mov dx,message ; point to our message "This program cannot be run in DOS mode.", 0x0d, 0x0d, 0x0a, '$'
mov ah, 09
int 0x21 ;# when AH = 9, DOS interrupt to write a string
;# terminate the program
mov ax,0x4c01
int 0x21
message db "This program cannot be run in DOS mode.", 0x0d, 0x0d, 0x0a, '$'
PE 頭
[edit | edit source]從 DOS 頭開始的偏移量 60 (0x3C) 是指向可移植可執行 (PE) 檔案頭的指標 (MZ 結構中的 e_lfanew)。DOS 會列印錯誤訊息並終止,但 Windows 會遵循此指標指向下一批資訊。
PE 簽名的十六進位制列表及其指標
PE 頭只包含一個檔案 ID 簽名,其值為 "PE\0\0",其中每個 '\0' 字元都是 ASCII 空字元。此簽名表明 a) 此檔案是合法的 PE 檔案,以及 b) 檔案的位元組序。位元組序在本章中不會被考慮,所有 PE 檔案都假定為 "小端" 格式。
第一塊大資訊位於 COFF 頭中,緊接在 PE 簽名之後。
COFF 頭
[edit | edit source]COFF 頭存在於 COFF 物件檔案(在連結之前)和 PE 檔案中,在 PE 檔案中被稱為 "檔案頭"。COFF 頭包含一些對可執行檔案有用的資訊,還包含一些對物件檔案更有用的資訊。
以下是 COFF 頭作為 C 資料結構的表示
struct COFFHeader
{
short Machine;
short NumberOfSections;
long TimeDateStamp;
long PointerToSymbolTable;
long NumberOfSymbols;
short SizeOfOptionalHeader;
short Characteristics;
}
- 機器
- 此欄位確定該檔案編譯的目標機器。十六進位制值為 0x14C(十進位制為 332)是 Intel 80386 的程式碼。
以下是其可能值的列表。
| 值 | 描述 |
| 0x14c | Intel 386 |
| 0x8664 | x64 |
| 0x162 | MIPS R3000 |
| 0x168 | MIPS R10000 |
| 0x169 | MIPS 小端 WCI v2 |
| 0x183 | 舊 Alpha AXP |
| 0x184 | Alpha AXP |
| 0x1a2 | 日立 SH3 |
| 0x1a3 | 日立 SH3 DSP |
| 0x1a6 | 日立 SH4 |
| 0x1a8 | 日立 SH5 |
| 0x1c0 | ARM 小端 |
| 0x1c2 | Thumb |
| 0x1c4 | ARMv7 (Thumb-2) |
| 0x1d3 | 松下 AM33 |
| 0x1f0 | PowerPC 小端 |
| 0x1f1 | 支援浮點數的 PowerPC |
| 0x1f2 | PowerPC 64 位小端 |
| 0x200 | 英特爾 IA64 |
| 0x266 | MIPS16 |
| 0x268 | 摩托羅拉 68000 系列 |
| 0x284 | Alpha AXP 64 位 |
| 0x366 | 帶有 FPU 的 MIPS |
| 0x466 | 帶有 FPU 的 MIPS16 |
| 0xebc | EFI 位元組碼 |
| 0x8664 | AMD AMD64 |
| 0x9041 | 三菱 M32R 小端 |
| 0xaa64 | ARM64 小端 |
| 0xc0ee | clr 純 MSIL |
- NumberOfSections
- 在 PE 頭部結尾處描述的節的數量。
- TimeDateStamp
- 生成此頭的 32 位時間:用於 "繫結" 過程,見下文。
- SizeOfOptionalHeader
- 此欄位顯示 COFF 頭之後的 "PE 可選頭" 的長度。
- Characteristics
- 這是一個位標誌欄位,顯示檔案的一些特性。
| 常量名稱 | 位位置 / 掩碼 | 描述 |
| IMAGE_FILE_RELOCS_STRIPPED | 1 / 0x0001 | 重定位資訊已從檔案中刪除 |
| IMAGE_FILE_EXECUTABLE_IMAGE | 2 / 0x0002 | 該檔案是可執行檔案 |
| IMAGE_FILE_LINE_NUMS_STRIPPED | 3 / 0x0004 | COFF 行號已從檔案中刪除 |
| IMAGE_FILE_LOCAL_SYMS_STRIPPED | 4 / 0x0008 | COFF 符號表條目已從檔案中刪除 |
| IMAGE_FILE_AGGRESIVE_WS_TRIM | 5 / 0x0010 | 積極地修剪工作集(**已過時**) |
| IMAGE_FILE_LARGE_ADDRESS_AWARE | 6 / 0x0020 | 應用程式可以處理大於 2 GB 的地址 |
| IMAGE_FILE_BYTES_REVERSED_LO | 8 / 0x0080 | 字的位元組被反轉(**已過時**) |
| IMAGE_FILE_32BIT_MACHINE | 9 / 0x0100 | 計算機支援 32 位字 |
| IMAGE_FILE_DEBUG_STRIPPED | 10 / 0x0200 | 除錯資訊已被刪除並單獨儲存在另一個檔案中 |
| IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP | 11 / 0x0400 | 如果映像在可移動介質上,則將其複製到交換檔案並從交換檔案中執行 |
| IMAGE_FILE_NET_RUN_FROM_SWAP | 12 / 0x0800 | 如果映像在網路上,則將其複製到交換檔案並從交換檔案中執行 |
| IMAGE_FILE_SYSTEM | 13 / 0x1000 | 映像是一個系統檔案 |
| IMAGE_FILE_DLL | 14 / 0x2000 | 映像是一個 DLL 檔案 |
| IMAGE_FILE_UP_SYSTEM_ONLY | 15 / 0x4000 | 映像只應該在單處理器計算機上執行 |
| IMAGE_FILE_BYTES_REVERSED_HI | 16 / 0x8000 | 字的位元組被反轉(**已過時**) |
PE 可選頭
[edit | edit source]"PE 可選頭" 本身並不是 "可選的",因為它在可執行檔案中是必需的,但在 COFF 物件檔案中不是必需的。可選頭的兩個不同版本取決於檔案是 64 位還是 32 位。可選頭包含大量資訊,可用於分析檔案結構,並獲取一些關於該檔案的有用資訊。
PE 可選頭位於 COFF 頭之後,有些資料甚至將這兩個頭顯示為同一個結構的一部分。為了方便起見,本華夏公益教科書將它們分開。
以下是作為 C 資料結構表示的 64 位 PE 可選頭
struct PEOptHeader
{
/* 64 bit version of the PE Optional Header also known as IMAGE_OPTIONAL_HEADER64
char is 1 byte
short is 2 bytes
long is 4 bytes
long long is 8 bytes
*/
short signature; //decimal number 267 for 32 bit, 523 for 64 bit, and 263 for a ROM image.
char MajorLinkerVersion;
char MinorLinkerVersion;
long SizeOfCode;
long SizeOfInitializedData;
long SizeOfUninitializedData;
long AddressOfEntryPoint; //The RVA of the code entry point
long BaseOfCode;
/*The next 21 fields are an extension to the COFF optional header format*/
long long ImageBase;
long SectionAlignment;
long FileAlignment;
short MajorOSVersion;
short MinorOSVersion;
short MajorImageVersion;
short MinorImageVersion;
short MajorSubsystemVersion;
short MinorSubsystemVersion;
long Win32VersionValue;
long SizeOfImage;
long SizeOfHeaders;
long Checksum;
short Subsystem;
short DLLCharacteristics;
long long SizeOfStackReserve;
long long SizeOfStackCommit;
long long SizeOfHeapReserve;
long long SizeOfHeapCommit;
long LoaderFlags;
long NumberOfRvaAndSizes;
data_directory DataDirectory[NumberOfRvaAndSizes]; //Can have any number of elements, matching the number in NumberOfRvaAndSizes.
} //However, it is always 16 in PE files.
以下是作為 C 資料結構表示的 32 位 PE 可選頭
struct PEOptHeader
{
/* 32 bit version of the PE Optional Header also known as IMAGE_OPTIONAL_HEADER
char is 1 byte
short is 2 bytes
long is 4 bytes
*/
short signature; //decimal number 267 for 32 bit, 523 for 64 bit, and 263 for a ROM image.
char MajorLinkerVersion;
char MinorLinkerVersion;
long SizeOfCode;
long SizeOfInitializedData;
long SizeOfUninitializedData;
long AddressOfEntryPoint; //The RVA of the code entry point
long BaseOfCode;
long BaseOfData;
/*The next 21 fields are an extension to the COFF optional header format*/
long ImageBase;
long SectionAlignment;
long FileAlignment;
short MajorOSVersion;
short MinorOSVersion;
short MajorImageVersion;
short MinorImageVersion;
short MajorSubsystemVersion;
short MinorSubsystemVersion;
long Win32VersionValue;
long SizeOfImage;
long SizeOfHeaders;
long Checksum;
short Subsystem;
short DLLCharacteristics;
long SizeOfStackReserve;
long SizeOfStackCommit;
long SizeOfHeapReserve;
long SizeOfHeapCommit;
long LoaderFlags;
long NumberOfRvaAndSizes;
data_directory DataDirectory[NumberOfRvaAndSizes]; //Can have any number of elements, matching the number in NumberOfRvaAndSizes.
} //However, it is always 16 in PE files.
這是在上面兩個結構中找到的 data_directory(也稱為 IMAGE_DATA_DIRECTORY)結構
/*
long is 4 bytes
*/
struct data_directory
{
long VirtualAddress;
long Size;
}
- 簽名
- 包含識別映像的簽名。
| 常量名稱 | 值 | 描述 |
|---|---|---|
| IMAGE_NT_OPTIONAL_HDR32_MAGIC | 0x10b | 32 位可執行映像。 |
| IMAGE_NT_OPTIONAL_HDR64_MAGIC | 0x20b | 64 位可執行映像 |
| IMAGE_ROM_OPTIONAL_HDR_MAGIC | 0x107 | ROM 映像 |
- MajorLinkerVersion
- 連結器的主要版本號。
- MinorLinkerVersion
- 連結器的次要版本號。
- SizeOfCode
- 程式碼節的大小(以位元組為單位),如果有多個程式碼節,則為所有這些節的總大小。
- SizeOfInitializedData
- 已初始化資料節的大小(以位元組為單位),如果有多個已初始化資料節,則為所有這些節的總大小。
- SizeOfUninitializedData
- 未初始化資料節的大小(以位元組為單位),如果有多個未初始化資料節,則為所有這些節的總大小。
- AddressOfEntryPoint
- 指向入口點函式的指標,相對於映像基地址。對於可執行檔案,這是起始地址。對於裝置驅動程式,這是初始化函式的地址。入口點函式對於 DLL 是可選的。如果沒有入口點,此成員為零。
- BaseOfCode
- 指向程式碼節起始位置的指標,相對於映像基地址。
- BaseOfData
- 指向資料節起始位置的指標,相對於映像基地址。
- ImageBase
- 映像載入到記憶體後,第一個位元組的首選地址。此值是 64K 位元組的倍數。DLL 的預設值為 0x10000000。應用程式的預設值為 0x00400000,除了 Windows CE,其預設值為 0x00010000。
- SectionAlignment
- 載入到記憶體中的節的對齊方式(以位元組為單位)。此值必須大於或等於 FileAlignment 成員。預設值為系統的頁面大小。
- FileAlignment
- 映像檔案中節的原始資料的對齊方式(以位元組為單位)。該值應為 512 到 64K(含)之間的 2 的冪。預設值為 512。如果 SectionAlignment 成員小於系統頁面大小,則此成員必須與 SectionAlignment 相同。
- MajorOSVersion
- 所需作業系統的 major 版本號。
- MinorOSVersion
- 所需作業系統的 minor 版本號。
- MajorImageVersion
- 映像的 major 版本號。
- MinorImageVersion
- 映像的 minor 版本號。
- MajorSubsystemVersion
- 子系統的 major 版本號。
- MinorSubsystemVersion
- 子系統的次要版本號。
- Win32VersionValue
- 此成員保留,必須為 0。
- SizeOfImage
- 映像的大小(以位元組為單位),包括所有標頭。必須是 SectionAlignment 的倍數。
- SizeOfHeaders
- 以下各項的總大小,四捨五入到 FileAlignment 成員中指定的值的倍數。
- DOS_Header 的 e_lfanew 成員
- 4 位元組簽名
- COFFHeader 的大小
- 可選標頭的尺寸
- 所有節標頭的尺寸
- CheckSum
- 映像檔案的校驗和。以下檔案在載入時進行驗證:所有驅動程式、在啟動時載入的任何 DLL 以及載入到關鍵系統程序中的任何 DLL。
- Subsystem
- 將被呼叫以執行可執行檔案的子系統
| 常量名稱 | 值 | 描述 |
|---|---|---|
| IMAGE_SUBSYSTEM_UNKNOWN | 0 | 未知子系統 |
| IMAGE_SUBSYSTEM_NATIVE | 1 | 不需要子系統(裝置驅動程式和本機系統程序) |
| IMAGE_SUBSYSTEM_WINDOWS_GUI | 2 | Windows 圖形使用者介面 (GUI) 子系統 |
| IMAGE_SUBSYSTEM_WINDOWS_CUI | 3 | Windows 字元模式使用者介面 (CUI) 子系統 |
| IMAGE_SUBSYSTEM_OS2_CUI | 5 | OS/2 CUI 子系統 |
| IMAGE_SUBSYSTEM_POSIX_CUI | 7 | POSIX CUI 子系統 |
| IMAGE_SUBSYSTEM_WINDOWS_CE_GUI | 9 | Windows CE 系統 |
| IMAGE_SUBSYSTEM_EFI_APPLICATION | 10 | 可擴充套件韌體介面 (EFI) 應用程式 |
| IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER | 11 | 具有啟動服務的 EFI 驅動程式 |
| IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER | 12 | 具有執行時服務的 EFI 驅動程式 |
| IMAGE_SUBSYSTEM_EFI_ROM | 13 | EFI ROM 映像 |
| IMAGE_SUBSYSTEM_XBOX | 14 | Xbox 系統 |
| IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION | 16 | 啟動應用程式 |
- DLLCharacteristics
- 映像的 DLL 特性
| 常量名稱 | 值 | 描述 |
|---|---|---|
| 無常量名稱 | 0x0001 | 保留 |
| 無常量名稱 | 0x0002 | 保留 |
| 無常量名稱 | 0x0004 | 保留 |
| 無常量名稱 | 0x0008 | 保留 |
| IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE | 0x0040 | DLL 可以在載入時重新定位 |
| IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY | 0x0080 | 強制執行程式碼完整性檢查 |
| IMAGE_DLLCHARACTERISTICS_NX_COMPAT | 0x0100 | 映像與資料執行保護 (DEP) 相容 |
| IMAGE_DLLCHARACTERISTICS_NO_ISOLATION | 0x0200 | 映像是隔離感知的,但不應被隔離 |
| IMAGE_DLLCHARACTERISTICS_NO_SEH | 0x0400 | 映像不使用結構化異常處理 (SEH)。此映像中無法呼叫任何處理程式 |
| IMAGE_DLLCHARACTERISTICS_NO_BIND | 0x0800 | 不要繫結映像 |
| IMAGE_DLLCHARACTERISTICS_APPCONTAINER | 0x1000 | 映像必須在應用程式容器中執行 |
| IMAGE_DLLCHARACTERISTICS_WDM_DRIVER | 0x2000 | WDM 驅動程式 |
| 無常量名稱 | 0x4000 | 保留 |
| IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE | 0x8000 | 映像是終端伺服器感知的 |
- SizeOfStackReserve
- 為堆疊保留的位元組數。載入時僅提交 SizeOfStackCommit 成員指定的記憶體;其餘記憶體將每次一個頁面地提供,直到達到此保留大小。
- SizeOfStackCommit
- 為堆疊提交的位元組數。
- SizeOfHeapReserve
- 為本地堆保留的位元組數。載入時僅提交 SizeOfHeapCommit 成員指定的記憶體;其餘記憶體將每次一個頁面地提供,直到達到此保留大小。
- SizeOfHeapCommit
- 為本地堆提交的位元組數。
- LoaderFlags
- 此成員已過時。
- NumberOfRvaAndSizes
- 可選標頭其餘部分中的目錄項數。每個條目描述一個位置和大小。
- DataDirectory
- 可能是此結構中最有趣的成員。提供 RVA 和大小,用於定位各種資料結構,這些結構用於設定模組的執行環境。DataDirectory 陣列指向的資料結構可以在檔案的各個節中找到,如 節表 所示。這些結構的作用細節存在於此頁面其他部分中。DataDirectory 中最有趣的條目如下:匯出目錄、匯入目錄、資源目錄和繫結匯入目錄。.NET 描述符表 (CLI 標頭) 包含 .NET 程式集的元資料,該表是 IMAGE_COR20_HEADER 結構,在 winnt.h 中定義。請注意,以位元組為單位的偏移量相對於可選標頭的開頭。
| 常量名稱 | 值 | 描述 | PE(32 位) 偏移量 | PE32+(64 位) 偏移量 |
|---|---|---|---|---|
| IMAGE_DIRECTORY_ENTRY_EXPORT | 0 | 匯出目錄 | 96 | 112 |
| IMAGE_DIRECTORY_ENTRY_IMPORT | 1 | 匯入目錄 | 104 | 120 |
| IMAGE_DIRECTORY_ENTRY_RESOURCE | 2 | 資源目錄 | 112 | 128 |
| IMAGE_DIRECTORY_ENTRY_EXCEPTION | 3 | 異常目錄 | 120 | 136 |
| IMAGE_DIRECTORY_ENTRY_SECURITY | 4 | 安全目錄 | 128 | 144 |
| IMAGE_DIRECTORY_ENTRY_BASERELOC | 5 | 基址重定位表 | 136 | 152 |
| IMAGE_DIRECTORY_ENTRY_DEBUG | 6 | 除錯目錄 | 144 | 160 |
| IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 7 | 特定於體系結構的資料 | 152 | 168 |
| IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 8 | 全域性指標暫存器相關的虛擬地址 | 160 | 176 |
| IMAGE_DIRECTORY_ENTRY_TLS | 9 | 執行緒本地儲存目錄 | 168 | 184 |
| IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 10 | 載入配置目錄 | 176 | 192 |
| IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 11 | 繫結匯入目錄 | 184 | 200 |
| IMAGE_DIRECTORY_ENTRY_IAT | 12 | 匯入地址表 | 192 | 208 |
| IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 13 | 延遲匯入表 | 200 | 216 |
| IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | 14 | COM 或 .net 描述符表 (CLI 標頭) | 208 | 224 |
| 無常量名稱 | 15 | 保留 | 216 | 232 |
節表
[edit | edit source]在 PE 可選標頭之後,我們立即找到一個節表。節表由一系列 IMAGE_SECTION_HEADER 結構組成。我們在檔案中找到的結構數量由 COFF 標頭中的 NumberOfSections 成員確定。每個結構的長度為 40 位元組。下面是來自我正在編寫的程式的十六進位制轉儲,描述了節表
突出顯示的區域與三個 IMAGE_SECTION_HEADER 結構的 Name 成員相關聯
IMAGE_SECTION_HEADER 定義為 C 結構如下
struct IMAGE_SECTION_HEADER
{
// short is 2 bytes
// long is 4 bytes
char Name[IMAGE_SIZEOF_SHORT_NAME]; // IMAGE_SIZEOF_SHORT_NAME is 8 bytes
union {
long PhysicalAddress;
long VirtualSize;
} Misc;
long VirtualAddress;
long SizeOfRawData;
long PointerToRawData;
long PointerToRelocations;
long PointerToLinenumbers;
short NumberOfRelocations;
short NumberOfLinenumbers;
long Characteristics;
}
- Name
- 8 位元組空填充 UTF-8 字串(如果使用所有 8 個字元,則字串可能不會以空字元結尾)。對於更長的名稱,此成員將包含 '/' 後跟十進位制數字的 ASCII 表示形式,該數字是在字串表中的偏移量。可執行映像不使用字串表,也不支援大於 8 個字元的節名稱。
- Misc
- PhysicalAddress - 檔案地址。
- VirtualSize - 載入到記憶體中後,節的總大小(以位元組為單位)。如果此值大於 SizeOfRawData 成員,則該節將用零填充。此欄位僅對可執行映像有效,對於目標檔案應設定為 0。
- 除非已知連結器和使用過的連結器的行為,否則 Misc 成員應被視為不可靠。
- VirtualAddress
- 載入到記憶體中後,節的第一個位元組的地址,相對於映像基址。對於目標檔案,這是應用重定位之前第一個位元組的地址。
- SizeOfRawData
- 磁碟上初始化資料的尺寸(以位元組為單位)。此值必須是 PE 可選標頭結構的 FileAlignment 成員的倍數。如果此值小於 VirtualSize 成員,則節的其餘部分將用零填充。如果節僅包含未初始化的資料,則該值為 0。
- PointerToRawData
- 相對於檔案開頭的檔案指標,指向 COFF 檔案中的第一個頁面。此值必須是 PE 可選標頭結構的 FileAlignment 成員的倍數。如果節僅包含未初始化的資料,則該值應為 0。
- PointerToRelocations
- 指向節的重定位條目的開頭的檔案指標。如果沒有重定位,則該值為 0。
- PointerToLinenumbers
- 指向節的行號條目的開頭的檔案指標。如果沒有 COFF 行號,則該值為 0。
- NumberOfRelocations
- 節的重定位條目數。對於可執行映像,該值為 0。
- NumberOfLinenumbers
- 節的行號條目數。
- Characteristics
- 映像的特徵。
下表定義了此成員的可能 32 位掩碼值
| 常量名稱 | 值 | 描述 |
|---|---|---|
| 無常量名稱 | 0x00000000 | 保留 |
| 無常量名稱 | 0x00000001 | 保留 |
| 無常量名稱 | 0x00000002 | 保留 |
| 無常量名稱 | 0x00000004 | 保留 |
| IMAGE_SCN_TYPE_NO_PAD | 0x00000008 | 節不應填充到下一個邊界。此標誌已過時,被 IMAGE_SCN_ALIGN_1BYTES 替代 |
| 無常量名稱 | 0x00000010 | 保留 |
| IMAGE_SCN_CNT_CODE | 0x00000020 | 節包含可執行程式碼(.text 節) |
| IMAGE_SCN_CNT_INITIALIZED_DATA | 0x00000040 | 節包含初始化資料 |
| IMAGE_SCN_CNT_UNINITIALIZED_DATA | 0x00000080 | 節包含未初始化的資料 |
| IMAGE_SCN_LNK_OTHER | 0x00000100 | 保留 |
| IMAGE_SCN_LNK_INFO | 0x00000200 | 節包含註釋或其他資訊。這僅對目標檔案(.drectve 節)有效 |
| 無常量名稱 | 0x00000400 | 保留 |
| IMAGE_SCN_LNK_REMOVE | 0x00000800 | 該節將不會成為映像的一部分。這僅對目標檔案有效 |
| IMAGE_SCN_LNK_COMDAT | 0x00001000 | 節包含 COMDAT 資料。這僅對目標檔案有效 |
| 無常量名稱 | 0x00002000 | 保留 |
| IMAGE_SCN_NO_DEFER_SPEC_EXC | 0x00004000 | 重置此節的 TLB 條目中的推測性異常處理位 |
| IMAGE_SCN_GPREL | 0x00008000 | 節包含透過全域性指標引用的資料 |
| 無常量名稱 | 0x00010000 | 保留 |
| IMAGE_SCN_MEM_PURGEABLE | 0x00020000 | 保留 |
| IMAGE_SCN_MEM_LOCKED | 0x00040000 | 保留 |
| IMAGE_SCN_MEM_PRELOAD | 0x00080000 | 保留 |
| IMAGE_SCN_ALIGN_1BYTES | 0x00100000 | 將資料對齊到 1 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_2BYTES | 0x00200000 | 將資料對齊到 2 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_4BYTES | 0x00300000 | 將資料對齊到 4 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_8BYTES | 0x00400000 | 將資料對齊到 8 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_16BYTES | 0x00500000 | 將資料對齊到 16 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_32BYTES | 0x00600000 | 將資料對齊到 32 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_64BYTES | 0x00700000 | 將資料對齊到 64 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_128BYTES | 0x00800000 | 將資料對齊到 128 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_256BYTES | 0x00900000 | 將資料對齊到 256 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_512BYTES | 0x00A00000 | 將資料對齊到 512 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_1024BYTES | 0x00B00000 | 將資料對齊到 1024 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_2048BYTES | 0x00C00000 | 將資料對齊到 2048 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_4096BYTES | 0x00D00000 | 將資料對齊到 4096 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_ALIGN_8192BYTES | 0x00E00000 | 將資料對齊到 8192 位元組邊界。這僅對目標檔案有效 |
| IMAGE_SCN_LNK_NRELOC_OVFL | 0x01000000 | 本節包含擴充套件重定位。該節的重定位計數超過了節頭中為其保留的 16 位。如果節頭中的 NumberOfRelocations 欄位為 0xffff,則實際的重定位計數儲存在第一個重定位的 VirtualAddress 欄位中。如果設定了 IMAGE_SCN_LNK_NRELOC_OVFL 並且該節中的重定位少於 0xffff,則為錯誤 |
| IMAGE_SCN_MEM_DISCARDABLE | 0x02000000 | 該節可以根據需要丟棄 |
| IMAGE_SCN_MEM_NOT_CACHED | 0x04000000 | 該節不能快取 |
| IMAGE_SCN_MEM_NOT_PAGED | 0x08000000 | 該節不能分頁 |
| IMAGE_SCN_MEM_SHARED | 0x10000000 | 該節可以在記憶體中共享 |
| IMAGE_SCN_MEM_EXECUTE | 0x20000000 | 該節可以作為程式碼執行(.text 等節) |
| IMAGE_SCN_MEM_READ | 0x40000000 | 該節可以讀取 |
| IMAGE_SCN_MEM_WRITE | 0x80000000 | 該節可以寫入 |
PE 載入器會將可執行映像的節放置在這些節描述符指定的地址(相對於基地址),通常對齊方式為 0x1000,這與 x86 上的頁大小相匹配。
常見的節是
- .text/.code/CODE/TEXT - 包含可執行程式碼(機器指令)
- .textbss/TEXTBSS - 如果啟用了增量連結,則存在
- .data/.idata/DATA/IDATA - 包含已初始化資料
- .bss/BSS - 包含未初始化資料
- .rsrc - 包含資源資料
匯入和匯出 - 連結到其他模組
[edit | edit source]什麼是連結?
[edit | edit source]每當開發人員編寫程式時,都會有許多預期的子程式和函式,它們已經實現,從而使編寫者無需編寫更多程式碼或處理複雜的資料結構。相反,編碼人員只需要宣告對子程式的一次呼叫,連結器將決定接下來會發生什麼。
可以使用兩種型別的連結:靜態連結和動態連結。靜態連結使用預編譯函式庫。此預編譯程式碼可以插入到最終的可執行檔案中以實現函式,從而為程式設計師節省大量時間。相反,動態連結允許子程式程式碼駐留在另一個檔案(或*模組*)中,該檔案在執行時由作業系統載入。這也稱為“動態連結庫”,或 DLL。*庫* 是一個包含一系列函式或值的模組,這些函式或值可以*匯出*。這不同於*可執行檔案*,*可執行檔案*從庫*匯入*東西來做它想做的事情。從現在開始,“模組”表示任何 PE 格式檔案,而“庫”表示任何匯出和匯入函式和值的模組。
動態連結具有以下優點
- 如果多個可執行檔案連結到庫模組,則可以節省磁碟空間
- 允許立即更新例程,而無需為所有應用程式提供新的可執行檔案
- 透過將庫的程式碼對映到多個程序中,可以節省記憶體空間
- 增加實現的抽象。無需重新程式設計應用程式即可修改實現操作的方法。這對於與作業系統的向後相容性非常有用。
本節討論瞭如何使用 PE 檔案格式來實現這一點。在此需要指出的是,*任何*內容都可以在模組之間匯入或匯出,包括變數以及子程式。
載入
[edit | edit source]將模組動態連結在一起的缺點是,在執行時,初始化可執行檔案的軟體必須將這些模組連結在一起。由於各種原因,您無法宣告“此動態庫中的函式將始終存在於記憶體中的*這裡*”。如果該記憶體地址不可用或庫已更新,該函式將不再存在於該位置,嘗試使用它的應用程式將中斷。相反,每個模組(庫或可執行檔案)必須宣告它*匯出*給其他模組的函式或值,以及它希望從其他模組*匯入*什麼。
如上所述,模組無法宣告它期望函式或值在記憶體中的哪個位置。相反,它宣告它希望在自己的記憶體中的哪個位置找到指向它想要匯入的值的**指標**。這允許模組定址任何匯入的值,無論它出現在記憶體中的哪個位置。
匯出
[edit | edit source]*匯出* 是一個模組中已宣告與其他模組共享的函式和值。這是透過使用“匯出目錄”來完成的,該目錄用於在匯出的名稱(或“序數”,見下文)和可以在記憶體中找到程式碼或資料的地址之間進行轉換。匯出目錄的開頭由資源目錄的 IMAGE_DIRECTORY_ENTRY_EXPORT 條目標識。所有匯出資料必須存在於同一個節中。該目錄由以下結構開頭
struct IMAGE_EXPORT_DIRECTORY {
long Characteristics;
long TimeDateStamp;
short MajorVersion;
short MinorVersion;
long Name;
long Base;
long NumberOfFunctions;
long NumberOfNames;
long *AddressOfFunctions;
long *AddressOfNames;
long *AddressOfNameOrdinals;
}
“Characteristics”值通常未被使用,TimeDateStamp 描述了匯出目錄生成的時間,MajorVersion 和 MinorVersion 應該描述目錄的版本詳細資訊,但它們的性質未定義。這些值對實際匯出本身幾乎沒有影響。“Name”值是針對零終止 ASCII 字串的 RVA,該字串是此庫名稱或模組的名稱。
名稱和序數
[edit | edit source]每個匯出的值都有一個名稱和一個“序數”(一種索引)。實際的匯出本身是透過 AddressOfFunctions 描述的,AddressOfFunctions 是一個指向 RVA 陣列的 RVA,每個 RVA 指向要匯出的不同函式或值。此陣列的大小在 NumberOfFunctions 值中。每個函式都有一個序數。“Base”值用作第一個匯出的序數,陣列中的下一個 RVA 為 Base+1,依此類推。
AddressOfFunctions 陣列中的每個條目都由一個名稱標識,該名稱透過 RVA AddressOfNames 找到。AddressOfNames 指向的資料是一個 RVA 陣列,大小為 NumberOfNames。每個 RVA 指向一個零終止的 ASCII 字串,每個字串都是匯出的名稱。還有一個由 AddressOfNameOrdinals 中的 RVA 指向的第二個陣列。它的大小也是 NumberOfNames,但每個值都是一個 16 位字,每個值都是一個序數。這兩個陣列是平行的,用於從 AddressOfFunctions 獲取匯出值。要按名稱查詢匯出,請在 AddressOfNames 陣列中搜索正確的字串,然後從 AddressOfNameOrdinals 陣列中獲取相應的值。然後使用此值作為 AddressOfFunctions 的索引(是的,它實際上是 0 索引,而不是基於基數的序數,正如官方文件所建議的那樣!)。
轉發
[edit | edit source]除了能夠匯出模組中的函式和值之外,匯出目錄還可以將匯出*轉發*到另一個庫。這在重新組織庫時允許更大的靈活性:也許某些功能已分支到另一個模組中。如果是這樣,可以將匯出轉發到該庫,而不是在原始模組中進行混亂的重組。
透過在 AddressOfFunctions 陣列中使一個 RVA 指向包含匯出目錄的節來實現轉發,這是普通匯出不應該做的事情。在該位置,應該有一個格式為“LibraryName.ExportName”的零終止 ASCII 字串,用於將此匯出轉發到適當的位置。
匯入
[edit | edit source]動態連結的另一半是將函式和值匯入到可執行檔案或其他模組中。在執行時之前,編譯器和連結器不知道需要匯入的值在記憶體中的哪個位置。匯入表透過在執行時建立指標陣列來解決這個問題,每個指標都指向匯入值的記憶體位置。此指標陣列在模組內部的定義 RVA 位置存在。這樣,連結器就可以使用模組內部的地址訪問模組外部的值。
匯入目錄
[edit | edit source]匯入目錄的起始位置由資源目錄的 IMAGE_DIRECTORY_ENTRY_IAT 和 IMAGE_DIRECTORY_ENTRY_IMPORT 項都指向(原因尚不清楚)。在該位置,有一個 IMAGE_IMPORT_DESCRIPTOR 結構陣列。其中每個結構標識一個庫或模組,其中包含我們需要匯入的值。該陣列一直持續到所有值為零的條目為止。該結構如下所示
struct IMAGE_IMPORT_DESCRIPTOR {
long *OriginalFirstThunk;
long TimeDateStamp;
long ForwarderChain;
long Name;
long *FirstThunk;
}
TimeDateStamp 與“繫結”行為相關,請參見下文。Name 值是指向 ASCII 字串的 RVA,該字串命名要匯入的庫。ForwarderChain 將在後面解釋。此時,唯一感興趣的是 RVA OriginalFirstThunk 和 FirstThunk。這兩個值都指向 RVA 陣列,每個 RVA 指向一個 IMAGE_IMPORT_BY_NAMES 結構。這些陣列以一個等於零的條目結束。這兩個陣列是並行的,並按相同順序指向同一個結構。原因將在稍後解釋。
每個 IMAGE_IMPORT_BY_NAMES 結構都具有以下形式
struct IMAGE_IMPORT_BY_NAME {
short Hint;
char Name[1];
}
“Name” 是一個任意大小的 ASCII 字串,它命名要匯入的值。這在透過 AddressOfNames 陣列查詢匯出目錄中的值時使用(請參見上文)。“Hint” 值是 AddressOfNames 陣列中的索引;為了節省搜尋字串的時間,載入程式首先檢查與“Hint” 對應的 AddressOfNames 條目。
總結:匯入表由一個大的 IMAGE_IMPORT_DESCRIPTOR 陣列組成,以一個全零條目結束。這些描述符標識要從中匯入內容的庫。然後有兩個並行的 RVA 陣列,每個陣列都指向 IMAGE_IMPORT_BY_NAME 結構,這些結構標識要匯入的特定值。
使用上面的匯入目錄在執行時,載入程式查詢相應的模組,將它們載入到記憶體中,並查詢正確的匯出。但是,為了能夠使用匯出,必須在匯入模組的記憶體中的某個位置儲存指向它的指標。這就是為什麼有兩個並行的陣列 OriginalFirstThunk 和 FirstThunk 標識 IMAGE_IMPORT_BY_NAME 結構的原因。一旦解析了匯入的值,就會將指向它的指標儲存在 FirstThunk 陣列中。然後可以在執行時使用它來定址匯入的值。
PE 檔案格式還支援一項名為“繫結”的獨特功能。載入和解析匯入地址的過程可能很耗時,在某些情況下,應避免這種情況。如果開發人員相當確定庫不會被更新或更改,那麼每次載入應用程式時,匯入值的記憶體地址都不會改變。因此,可以在執行時 *之前* 預先計算匯入地址並將其儲存在 FirstThunk 陣列中,從而使載入程式能夠跳過解析匯入 - 匯入被“繫結”到特定記憶體位置。但是,如果模組之間的版本號不匹配,或者需要重新定位匯入的庫,則載入程式將假設繫結地址無效,並仍然解析匯入。
模組的匯入目錄條目的“TimeDateStamp” 成員控制繫結;如果將其設定為零,則匯入目錄不繫結。如果它是非零值,則它繫結到另一個模組。但是,匯入表中的 TimeDateStamp 必須與繫結模組的 FileHeader 中的 TimeDateStamp 相匹配,否則載入程式將丟棄繫結值。
當然,如果繫結的庫/模組將其匯出轉發到另一個模組,則繫結可能會出現問題。在這些情況下,可以繫結未轉發的匯入,但必須標識需要轉發的值,以便載入程式能夠解析它們。這透過匯入描述符的 ForwarderChain 成員完成。 “ForwarderChain” 的值是 FirstThunk 和 OriginalFirstThunk 陣列中的索引。該索引的 OriginalFirstThunk 標識需要解析的匯入的 IMAGE_IMPORT_BY_NAME 結構,而該索引的 FirstThunk 是需要解析的另一個條目的索引。這種情況會一直持續到 FirstThunk 值為 -1 為止,這表示沒有更多要匯入的轉發值。
資源是模組中的資料項,很難使用所選的程式語言進行儲存或描述。這需要一個單獨的編譯器或資源生成器,允許插入對話方塊、圖示、選單、影像和其他型別的資源,包括任意二進位制資料。雖然可以使用許多資源 API 呼叫從 PE 檔案中檢索資源,但我們將檢視沒有使用這些 API 的資源。
當我們要手動操作檔案的資源時,首先需要做的是查詢資源部分。為此,我們需要在 DataDirectory 陣列和節表中找到一些資訊。我們需要在 PE 可選頭中找到的 DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE] 結構的 VirtualAddress 成員中儲存的 RVA。一旦知道 RVA,我們就可以透過將 DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE] 結構的 VirtualAddress 成員與 IMAGE_SECTION_HEADER 的 VirtualAddress 成員進行比較,在節表中查詢該 RVA。除了極少數情況外,DataDirectory 結構的 VirtualAddress 成員將等於 IMAGE_SECTION_HEADER 的 VirtualAddress 成員的值。除了極少數情況外,該特定 IMAGE_SECTION_HEADER 的 name 成員將被命名為“.rsrc”。找到正確的 IMAGE_SECTION_HEADER 結構後,可以使用 PointerToRawData 成員來定位資源部分。PointerToRawData 成員包含從檔案開頭到資源部分的第一個位元組的偏移量。下圖顯示了左側的 DataDirectory 陣列和右側的 IMAGE_SECTION_HEADER 的示例,其中填充了資源部分的資訊。我們可以看到“2 RVA: 20480 Size: 3512” 的目錄資訊下方的突出顯示行具有 20480 的 VirtualAddress (RVA),這對應於 .rsrc (資源) 部分的 20480 的 VirtualAddress。您還可以看到 PointerToRawData 的值等於 7168。在這個特定的 PE 檔案中,我們將在從檔案開頭偏移 7168 的位置找到資源部分。
找到資源部分後,我們可以開始檢視該部分中包含的結構和資料。
IMAGE_RESOURCE_DIRECTORY 是我們遇到的第一個結構,它從資源部分的第一個位元組開始。
IMAGE_RESOURCE_DIRECTORY 結構
struct IMAGE_RESOURCE_DIRECTORY
{
long Characteristics;
long TimeDateStamp;
short MajorVersion;
short MinorVersion;
short NumberOfNamedEntries;
short NumberOfIdEntries;
}
Characteristics 未使用,TimeDateStamp 通常是建立時間,但無論它是否設定都沒有關係。MajorVersion 和 MinorVersion 與資源的版本資訊相關:這些欄位沒有定義的值。緊隨 IMAGE_RESOURCE_DIRECTORY 結構之後是一系列 IMAGE_RESOURCE_DIRECTORY_ENTRY,其數量由 NumberOfNamedEntries 和 NumberOfIdEntries 的總和定義。這些條目的第一部分用於命名資源,後一部分用於 ID 資源,具體取決於 IMAGE_RESOURCE_DIRECTORY 結構中的值。資源條目結構的實際形狀如下所示
struct IMAGE_RESOURCE_DIRECTORY_ENTRY
{
long NameId;
long *Data;
}
NameId 值具有雙重用途:如果最高有效位(或符號位)為清零,則最低 16 位是資源的 ID 號。或者,如果最高位被設定為 1,則最低 31 位構成從資源資料開頭到此特定資源的名稱字串的偏移量。Data 值也具有雙重用途:如果最高有效位被設定為 1,則剩餘的 31 位構成從資源資料開頭到另一個 IMAGE_RESOURCE_DIRECTORY 的偏移量(即此條目是資源樹的內部節點)。否則,這是一個葉節點,Data 包含從資源資料開頭到描述資源資料本身的特定結構的偏移量(可以將其視為位元組的有序流)
struct IMAGE_RESOURCE_DATA_ENTRY
{
long *Data;
long Size;
long CodePage;
long Reserved;
}
Data 值包含指向實際資源資料的 RVA,Size 不言自明,CodePage 包含用於對資源中的 Unicode 編碼字串(如果有)進行解碼的 Unicode 內碼表。Reserved 應設定為 0。
上述資源目錄和條目系統允許透過名稱或 ID 號簡單儲存資源。但是,這很快就會變得非常複雜。不同型別的資源、資源本身以及其他語言中資源的例項可能在一個資源目錄中混雜在一起。出於這個原因,資源目錄已被賦予了一個結構來使用,允許分離不同的資源。
為此,資源條目中的“資料”值指向另一個 IMAGE_RESOURCE_DIRECTORY 結構,形成類似樹狀圖的資源組織方式。第一級資源條目標識資源的 _型別_:游標、點陣圖、圖示等。它們使用 ID 方法標識資源條目,總共定義了十二個值。可以新增更多使用者定義的資源型別。每個資源條目都指向一個資源目錄,命名實際的資源本身。這些資源可以是任何名稱或值。它們又指向另一個資源目錄,該目錄使用 ID 號來區分語言,允許為使用不同語言的系統提供不同的特定資源。最後,語言目錄中的條目實際上提供了指向資源資料本身的偏移量,其格式未在 PE 規範中定義,可以被視為任意位元組流。
Windows DLL 檔案是一種 PE 檔案,但有一些關鍵區別
- .DLL 副檔名
- 一個
DllMain()入口點,而不是 WinMain() 或 main()。 - 在 PE 頭部設定的 DLL 標誌。
DLL 可以透過兩種方式之一載入:a) 載入時,或 b) 透過呼叫 LoadModule() Win32 API 函式。
使用以下語法從 DLL 檔案匯出函式
__declspec(dllexport) void MyFunction() ...
此處的 "__declspec" 關鍵字不是 C 語言標準,而是由許多編譯器實現,為函式和變數設定可擴充套件的編譯器特定選項。在 Windows 上執行的 Microsoft C 編譯器和 GCC 版本允許使用 __declspec 關鍵字和 dllexport 屬性。
也可以從常規的 .exe 檔案匯出函式,並且具有匯出函式的 .exe 檔案可以以類似於 .dll 檔案的方式動態呼叫。但是,這種情況很少見。
有幾種方法可以確定 DLL 匯出了哪些函式。一種常見的方法是使用以下方式使用 **dumpbin**
dumpbin /EXPORTS <dll file>
這將在控制檯中釋出函式匯出列表,以及它們的序號和 RVA。
與函式匯出類似,程式可以從外部 DLL 檔案匯入函式。當程式啟動時,dll 檔案將載入到程序記憶體中,函式將像本地函式一樣使用。為了使編譯器和連結器識別函式來自外部庫,需要以以下方式對 DLL 匯入進行原型化
__declspec(dllimport) void MyFunction();
在檢查程式時,確定哪些函式是從外部庫匯入的通常很有用。要將匯入檔案列出到控制檯,請使用以下方式使用 **dumpbin**
dumpbin /IMPORTS <dll file>
您也可以使用 depends.exe 列出匯入和匯出函式。Depends 是一個 GUI 工具,包含在 Microsoft Platform SDK 中。

