PE文件格式
1 PE文件结构
2 文件头
PE 文件头是一个 IMAGE_NT_HEADERS 类型的结构,它在WINNT.H文件中定义。
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER OptionalHeader; } IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
Signature域是 ASCII文本 “PE\0\0”。
IMAGE_FILE_HEADER 类型的结构仅包含了文件最基本的信息。
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
WORD Machine
文件所适用于的 CPU 类型。已经定义了以下 CPU ID:
0x14d Intel i860
0x14c Intel I386 (486 和 586 也用此 ID)
0x162 MIPS R3000
0x166 MIPS R4000
0x183 DEC Alpha AXP
WORD NumberOfSections
文件中节的数目。
DWORD TimeDateStamp
链接器(对于 OBJ 文件来说是编译器)生成此文件的时间。它保存的是自 1969 年十二月 31 日下午 4:00 开始的总秒数。
DWORD PointerToSymbolTable
COFF 符号表的文件偏移。这个域只用于 OBJ 文件和带 COFF 调试信息的 PE 文件。PE 文件支持多种调试信息格式,因此调试器应该参考数据目录中
IMAGE_DIRECTORY_ENTRY_DEBUG 这一项的信息(在后面定义)。
DWORD NumberOfSymbols
COFF 符号表中的符号数,可以参考 PointerToSymbolTable 域的信息。
WORD SizeOfOptionalHeader
这个结构后面的可选文件头的大小。在 OBJ 文件中,这个域为 0。在可执行文件中,它是这个结构后面的 IMAGE_OPTIONAL_HEADER 结构的大小。
WORD Characteristics
关于文件信息的标志。下面是一些比较重要的值,其它的值在 WINNT.H 文件中定义。
0x0001 此文件中不包含重定位信息
0x0002 此文件是可执行映像(不是 OBJ 或 LIB)
0x2000 此文件是动态链接库,不是可执行程序
PE 文件头的第三个组成部分是一个IMAGE_O PTIONAL_HEADER 类型的结构。对于PE 文件来说,这一部分并不是可选的。COFF 结构允许在标准的 IMAGE_FILE_HEADER 之外定义一些附加信息。这个结构中的信息是 PE 设计者认为除 IMAGE_FILE_HEADER 中的基本信息之外非常重要的信息。 并不是 IMAGE_OPTIONAL_HEADER 结构中的所有域都很 重要。比较重要的是 ImageBase 和Subsystem 这两个域。你可以跳过其中一些域的描述。
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
DWORD SizeOfCode
所有的代码节的总大小(已经向上舍入)。通常大部分文件只有一个代码节,因此这个域就是.text 节的大小。
DWORD ImageBase
当链接器生成可执行文件时,它假定这个文件会被映射到内存的一个特定位置。这个特定位置就被保存在这个域中。事先为文件假定一个位置可以让链接器对代码进行优化。如果文件确实被加载器映射到了那个位置,那么在运行前代码并不需要做任何修正。对于用于 Windows NT 上的可执行文件,这个默认映像基址为 0x10000。对于 DLL,它为 0x400000。在 Windows 95 上,地址 0x10000 不能被用于加载 32 位 EXE,因为它位于一个被所有进程所共享的线性地址区域。由于这个原因,Microsoft 把基于 Win32的可执行文件的默认的基地址改成了 0x400000。基地址被默认为是 0x10000 的早期程序在 Windows 95 上要花费更长的时间来完成加载,因为加载器必须进行基址重定位。
WORD Subsystem
此程序的用户界面的子系统类型。WINNT.H 定义了以下值:
NATIVE 1 不需要子系统(例如设备驱动程序)
WINDOWS_GUI 2 运行于 Windows GUI 子系统
WINDOWS_CUI 3 运行于 Windows 字符模式子系统( 控制台应用程序)
OS2_CUI 5 运行于 OS/2 字符模式子系统(OS/2 1.x 版本的应用程序)
POSIX_CUI 7 运行于 Posix 字符模式子系统
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
这是一个 IMAGE_DATA_DIRECTORY 结构的数组。它前面的元素包含了可执行文件的重要部分的起始 RVA 和大小。数组最后的一些元素当前并未使用。此数组的第一个元素总是导出表(如果存在的话)的地址和大小。第二个元素是导入表的地址和大小,等等。
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
Offset (PE/PE32+) | Description |
---|---|
96/112 | Export table address and size |
104/120 | Import table address and size |
112/128 | Resource table address and size |
120/136 | Exception table address and size |
128/144 | Certificate table address and size |
136/152 | Base relocation table address and size |
144/160 | Debugging information starting address and size |
152/168 | Architecture-specific data address and size |
160/176 | Global pointer register relative virtual address |
168/184 | Thread local storage (TLS) table address and size |
176/192 | Load configuration table address and size |
184/200 | Bound import table address and size |
192/208 | Import address table address and size |
200/216 | Delay import descriptor address and size |
208/224 | The CLR header address and size |
216/232 | Reserved |
3 节表
在 PE 文件头和每个节的原始数据之间是节表。节表就像是包含映像中每个节的信息的电话簿。映像中的节是按它们的起始地址(RVA)来排列的,而不是按字母表顺序。
typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]
这是一个 8 字节的 ANSI 字符串(并不是 UNICODE),它是节的名称。大多数节名都以“.“开始(例如“.text”),但这并不是必须的。你可以在汇编语言中用段指令来命名你的节,也可以在 Microsoft C/C++编译器中用“#pragma data_seg”和 “#pragma code_seg”来命名你的节。不过要注意,如果节名长 8 字节的话,那就没有最后的那个 NULL 字节。如果你是 printf 爱好者,你可以使用%.8s 来避免将名称字符串复制到一个能以 NULL 结尾的缓冲区中。
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
这个域在 EXE 文件和 OBJ 文件中意义不同。在 EXE 文件中,它保存的是代码或数据的实际大小。这是在尚未向上舍入到离它最近的文件对齐值的倍数时的大小。这个结构后面的 SizeOfRawData 域(看起来命名好像不太恰当)保存的是已经舍入后的值。Borland 的链接器把这两个域的意义颠倒了过来,反而好像是正确的。 对 OBJ 文件来说,这个域指出了节的物理地址。第一个节的地址是 0。要找出 OBJ 文件中下一个节的地址,只需在当前节的物理地址上加上 SizeOfRawData 域的值就可以了。
DWORD VirtualAddress
在 EXE 文件中,这个域保存了这个节应该被加载器映射到的地址的 RVA。要计算一个给定的节在内存中的实际起始地址,把映像的基地址与这个域的值相加就可以了。当使用 Microsoft 的工具时,第一个节的默认 RVA 是 0x1000。对于 OBJ 文件来说,这个域是无意义的,它被设置为 0。
DWORD SizeOfRawData
在 EXE 文件中,这个域保存了节的大小(已经向上舍入到离它最近的文件对齐值的倍数)。例如假设文件对齐值是 0x200。如果前面的 VirtualSize 域指出这个节的大小是0x35A 字节时,这个域会指明这个节的大小为 0x400 字节。对于 OBJ 文件来说,这个域包含了由编译器或汇编程序生成的节的精确大小。换句话说,在 OBJ 文件中,这个域与 EXE 中的 VirtualSize 域等价。
DWORD PointerToRawData
这是基于文件的偏移,在这个偏移处可以找到由编译器或汇编程序生成的原始数据。如果你的程序自己映射 PE 或 COOF 文件(而不是让操作系统加载它)的话,这个域比VirtualAddress 域更重要。在这种情况下,你实际进行的是完全的线性映射,你会发现节的数据在这个偏移处,而不是在由 VirtualAddress 指定的 RVA 处。
DWORD PointerToRelocations
在 OBJ 文件中,这是基于文件的偏移,在这个偏移处你可以找到这个节的重定位信息。OBJ 文件的每个节的重定位信息紧跟着这个节的原始数据。在 EXE 文件中,这个域(以
及这个结构中以后的域)都是无意义的,它们都被设置为 0。在链接器创建 EXE 文件时,它已经处理了大部分的修正问题,只剩下基地址重定位和导入函数留在加载时解析。有关基址重定位和导入函数的信息被保存在它们各自的节中,因此对于 EXE 文件来说,并不需要在每个节中原始的数据之后都保存重定位信息。
DWORD PointerToLinenumbers
这是基于文件的偏移,在这个偏移处你可以找到行号表。行号表使源文件中的行号与相应行生成的代码关联了起来。在现代的调试信息格式中,例如 CodeView 格式,行号信息作为调试信息的一部分被保存。但是在 COFF 调试信息格式中,行号信息与符号名以及类型信息是分开存储的。通常情况下,只有代码节(例如.text)有行号。在 EXE
文件中,行号在节中的原始数据之后。在 OBJ 文件中,一个节的行号位于这个节的原始数据和重定位表之后。
WORD NumberOfRelocations
节中重定位表中重定位信息的数目(前面的 PointerToRelocations field 域指向重定位表)。这个域好像只与 OBJ 文件有关。
WORD NumberOfLinenumbers
节中行号表中行号信息的数目。(前面的 PointerToLinenumbers 域指向行号表)。
DWORD Characteristics
大多数程序员称为标志(Flag)的内容,在 PE/COFF 格式中称为特征(Characteristic)。这个域用一组标志用来指定节的属性(例如代码还是数据、可读还是可写等)。要获
取一个节的所有可能的属性的列表,可以参考 WINNT.H 中的 IMAGE_SC N_XXX_XXX 定义。以下是一些重要的标志:
0x00000020 这个节包含代码。通常与可执行标志(0x80000000)一起设定。
0x00000040 这个节包含已初始化的数据。除了可执行的节和.bss 节之外,几乎所有的节都设定了这个标志。
0x00000080 这个节包含未初始化的数据(例如.bss 节)。
0x00000200 这个节包含备注或其它类型的信息。典型的使用这个标志的节是由编译器生成的.drectv 节,它包含编译器传递给链接器的命令。
0x00000800 这个节的内容并不放入最后的 EXE 文件中。这些节是编译器或汇编程序用来给链接器传递信息的。
0x02000000 这个节可以被丢弃,因为一旦它被加载之后,进程就不再需要它了。最常见的可以被丢弃的节是基址重定位节(.reloc)。
0x10000000 这个节是共享的。当用于 DLL 时,这个节中的数据在所有使用这个 DLL 的进程中是共享的。数据节默认是不共享的,这意味着使用某个 DLL 的所有进程都有这个节中的数据的私有副本。说得更专业一点就是,共享节告诉内存管理器对这个节的页面映射进行一些额外设置以便使用这个 DLL 的所有进程都使用同一块物理内存。要使一个节变成共享的,可以在链接时使用 SHARED 属性。例如“LINK /SECTION:MYDATA,RWS ...”告诉链接器这个叫做 MYDATA 的节应该被设置成可读、可写和共享的。
0x20000000 这个节是可执行的。只要设置了“包含代码”标志(0x00000020),通常也会设置这个标志。
0x40000000 这个节是可读的。EXE 文件中的节总是设置这个标志。
0x80000000 这个节是可写的。如果 EXE 文件的节没有设置这个标志,加载器会把映射的页面都标记成只读和只执行的。通常.data 和.bss 节被设置这个属性。有趣的是.idata 节也设置了这个标志。
一个典型的 EXE 文件的节表
01 .text VirtSize: 00005AFA VirtAddr: 00001000
raw data offs: 00000400 raw data size: 00005C00
relocation offs: 00000000 relocations: 00000000
line # offs: 00009220 line #'s: 0000020C
characteristics: 60000020
CODE MEM_EXECUTE MEM_READ
02 .bss VirtSize: 00001438 VirtAddr: 00007000
raw data offs: 00000000 raw data size: 00001600
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000080
UNINITIALIZED_DATA MEM_READ MEM_WRITE
03 .rdata VirtSize: 0000015C VirtAddr: 00009000
raw data offs: 00006000 raw data size: 00000200
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 40000040
INITIALIZED_DATA MEM_READ
04 .data VirtSize: 0000239C VirtAddr: 0000A000
raw data offs: 00006200 raw data size: 00002400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
05 .idata VirtSize: 0000033E VirtAddr: 0000D000
raw data offs: 00008600 raw data size: 00000400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
06 .reloc VirtSize: 000006CE VirtAddr: 0000E000
raw data offs: 00008A00 raw data size: 00000800
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42000040
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
一个典型的 OBJ 文件的节表
01 .drectve PhysAddr: 00000000 VirtAddr: 00000000
raw data offs: 000000DC raw data size: 00000026
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 00100A00
LNK_INFO LNK_REMOVE
02 .debug$S PhysAddr: 00000026 VirtAddr: 00000000
raw data offs: 00000102 raw data size: 000016D0
relocation offs: 000017D2 relocations: 00000032
line # offs: 00000000 line #'s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
03 .data PhysAddr: 000016F6 VirtAddr: 00000000
raw data offs: 000019C6 raw data size: 00000D87
relocation offs: 0000274D relocations: 00000045
line # offs: 00000000 line #'s: 00000000
characteristics: C0400040
INITIALIZED_DATA MEM_READ MEM_WRITE
04 .text PhysAddr: 0000247D VirtAddr: 00000000
raw data offs: 000029FF raw data size: 000010DA
relocation offs: 00003AD9 relocations: 000000E9
line # offs: 000043F3 line #'s: 000000D9
characteristics: 60500020
CODE MEM_EXECUTE MEM_READ
05 .debug$T PhysAddr: 00003557 VirtAddr: 00000000
raw data offs: 00004909 raw data size: 00000030
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
4 节数据
.text 节是由编译器或汇编程序生成的所有通用代码组成的。
在 PE 文件中,当你调用其它模块中的函数(例如 USER32.DLL 中的 GetMessage 函数)时,由编译器生成的 CALL 指令并不是直接把控制权传到了那个 DLL 中(见图 2)。相反,CALL 指令把控制权传给了
JMP DWORD PTR [XXXXXXXX]
这种形式的指令,而这些指令也在同一.text 节中。这种 JMP 指令通过.idata 节中的一个 DWORD变量间接跳转。这个.idata 节中的 DWORD 变量包含了操作系统函数的入口点的实际地址。略一思考,我理解了为什么 DLL 的调用要以这种方式实现。通过把所有对 DLL 中的同一个函数的调用集中在一处,加载器就不需要对每条调用此函数的指令都进行修正。PE 加载器要做的就是把目标函数的正确地址放在.idata 节中相应的 DWORD 变量中。这样就不需要修正任何调用此函数的指令。这与 NE 文件形成鲜明对比。在 NE 文件中,每个段都包含了一个这个段中需要修正的位置的列表。如果在这个段中调用 DLL 中某一函数 20 次,加载器必须在这个段中写 20 次这个函数的地址。PE 文件的这种方法的不利之处是你不能用一个 DLL 函数的真实地址来初始化一个变量。
正如.text 是默认的代码节一样,.data 节是你的已初始化的数据所在的节。这个节由在编译时初始化的全局变量和静态变量组成。它也包含了字符串常量。链接器把 OBJ 文件和 LIB 文件中的所有.data 节组合成一个.data 节放入 EXE 文件中。局部变量位于线程的堆栈中,它们并不占用.data 节或.bss 节的空间。
.bss节存储的是所有未初始化的全局变量和静态变量。链接器把 OBJ 文件和 LIB 文件中的所有.bss 节组合成一个.bss 节放入 EXE 文件中。在节表中,.bss 节中的RawDataOffset 域被设置为 0,这表明这个节不占用文件的任何空间。TLINK 并不生成这个节。它通过扩展 DATA 节的虚拟大小来代替。
.CRT是另一个已初始化数据节,它由 Microsoft C/C++运行时库使用,故此得名。为什么这个节中的数据不合并到标准的.data 中我不得而知。
.rsrc 节包含了模块中所有的资源。在早期的 Windows NT 中,由 16 位的 RC.EXE 生成的 RES文件的格式 Microsoft 的 PE 链接器并不认识。由 CVTRES 程序把 RES 文件转换成 COFF 格式的 OBJ文件,把资源数据放在 OBJ 文件的.rsrc 节中。链接器只是把资源 OBJ 文件看成一个普通的 OBJ文件,这使得链接器并不需要知道关于资源方面的特别知识。最新的 Microsoft 链接器好像能直接处理 RES 文件。
.idata节包含一个模块从其它 DLL 中导入的函数(以及数据)的信息。这个节与 NE 文件的模块参考表类似。关键区别是 PE 文件中每个导入的函数都在这个节中专门列出。要在 NE 文件中找到相同的信息,你必须到每个段中的原始数据最后的重定位信息中去挖掘。
.edata节是 PE 文件为其它模块导出的函数和数据列表。它相当于 NE 文件中的入口表、常驻名称表和非常驻名称表的组合。与 16 位 Windows不同,很少需要从 EXE 文件中导出什么,因此你通常只能在 DLL 中看到.edata 节。当使用 Microsoft 的工具时,.edata 节是通过 EXP 文件才出现在 PE 文件中的。也就是说,链接器自己并不生成这种信息。相反,它依赖库管理程序(LIB32)去扫描 OBJ 文件来生成 EXP 文件。而链接器把它加入到需要链接的模块列表中。是的,就是这样!这些麻烦的 EXP 文件其实就是 OBJ 文件,不过扩展名不同罢了。
.reloc节存储的是基址重定位表。基址重定位是对指令或者已经初始化的变量的值的一种调整,它是在加载器不能把文件加载到链接器设定的位置时才需要进行的。如果加载器把映像加载到了链接器设定的位置上,那么加载器就完全忽略这个节中的重定位信息。如果你想碰碰运气,期望加载器总是把映像加载到设定的基地址上,你可以通过/FIXED 选项告诉链接器移除重定位信息。虽然这可以节省可执行文件的空间,但它可能导致可执行文件在其它基于 Win32 实现的系统上不能运行。例如假定你为 Windows NT 创建了一个 EXE 文件,并把它的基地址选在 0x10000。如果你告诉链接器移除重定位信息,这个 EXE 就不能在 Windows 95 上运行,因为地址 0x10000已经被占用了。
当你使用编译器指令__declspec(thread)时,你定义的数据并不被放入.data 节或者是.bss节,它被放入.tls节,tls 代表“线程局部存储(Thread Local Storage)”,它与 Win32 函数中的 TlsAlloc 函数家族有关。当处理.tls节时,内存管理器要设置页表,以便无论何时进程切换线程时,一组新的物理内存页面被映射到.tls 节的地址空间。这允许基于线程的全局变量。在大多数情况下,使用这种机制比以线程为基础分配内存并把其指针保存在 TlsAlloc 分配的内存槽上更容易。
对于.tls 节和__declspec(thread)变量有一个比较遗憾的地方。在 Windows NT 和 Windows 95 上,这种线程局部存储机制不适用于通过调用 LoadLibrary 而动态加载的 DLL。对 EXE 或者隐含加载的 DLL 来说,一切正常。如果你不能隐含链接到 DLL,但是需要使用基于线程的数据,你就不得不使用 TlsAlloc 和TlsGetValue 并动态分配内存。
尽管.rdata节经常位于.data 节和.bss 节之间,但你的程序通常看不到也并不使用这个节中的数据。然而.rdata 节至少用在两个地方。第一个就是在由 Microsoft 的链接器生成的 EXE中,.rdata 节用于保存调试目录,它仅存在于 EXE 文件中。(如果是 TLINK32.EXE,调试目录则是在一个名为.debug 的节中。)调试目录是一个类型为 IMAGE_DEBUG_DIRECTORY 结构的数组。
这些结构中保存了有关类型、大小以及位置等各种各样的调试信息。三种主要类型的调试信息是:
CodeView®、COFF 和 FPO。
调试目录并非必须位于.rdata 节的开始部分。要查找调试目录表,使用数据目录的第七个元素(IMAGE_DIRECTORY_ENTRY_DEBUG)中的 RVA。数据目录在 PE 文件头的末尾。要确定 Microsoft链接器生成的调试目录的数目,用调试目录的大小(在数据目录的 Size 域可以找到)除以IMAGE_DEBUG_DIRECTORY 结构的大小即可。TLINK32 生成一个简单的数,通常是 1。
.rdata 节中的另一个有用部分是描述字符串。如果在你的程序的 DEF 文件中指定了DESCRIPTION 项,则指定的描述字符 串就会出现在.rdata 节中。在NE 格式中,描述字符串总是出现在非常驻名称表的首个元素的位置。描述字符串主要是为了保存一个描述文件的有用字符串。不幸的是,我还没有发现找到它的简便方法。我曾经在一些 PE 文件中看到描述字符串在调试目录的前面,但是在其它一些文件中它却是在调试目录的后面。我找不到一致的方法去寻找描
述字符串(甚至它是否存在)。
类似.debug$S和.debug$T这些节仅存在于 OBJ 文件中。它们保存了 CodeView 格式的符号和类型信息。这些节名源自以前的 16 位编译器使用的用于调试目的的段的名称($$SYMBOLS 和$$TYPES)。.debug$T 节的惟一目的是保存 PDB 文件的路径名,这种 PDB 文件中包含工程中所有OBJ 文件的 CodeView 信息。链接器从 PDB 文件中读取信息并创建 CodeView 信息,并把创建的包含 CodeView 信息的部分放在最终的 PE 文件最后。
.drectve节仅存在于 OBJ 文件中。它包含编译器传给链接器的命令的文本表示。例如在我使用 Microsoft 编译器生成的所有 OBJ 文件中都会看到以下的字符串出现在.drectve 节:
-defaultlib:LIBC -defaultlib:OLDNAMES
当你在代码中使用__declspec(export)时,编译器简单地生成与此等价的命令行并把它放进.drectve 节中(例如“-exprot:MyFunction”)。
如果你由于某些原因要使用单独的节,可以毫不犹豫地创建你自己的节。如果用的是C/C++编译,使用#pragma c ode_seg 和#pragma data_seg 就可以了。在汇编语言中,只要在创建32 位段时(它最后成为节)使用不同于标准节的名称就可以了。如果你使用 TLINK32,你必须使用不同的类或关闭代码段包装。
PE 文件的导入表
PE 文件的.idata 节包含了加载器用以确定目标函数的地址并且在可执行映像中修正它们所需的信息。
.idata 节(或者称为导入表)以一个类型为 IMAGE_IMPORT_DESCRIPTOR 结构的数组开始。
对于 PE 文件隐含链接到的每个 DLL 都有一个相应的 IMAGE_IMPOR T_DESCRIPTOR 结构。并没有域用来指示这个数组中结构的数目。数组中的最后一个元素是通过这个结构中的所有域都是 NULL来表明的。IMAGE_IMPORT_DESCRIPTOR 结构如下所示。
DWORD Characteristics
这个域在以前可能是一个标志。现在 Microsoft 已经更改了它的意义但是并没有同时更新 WINNT.H 文件。它实际是一个指针数组的偏移地址(RVA)。其中的每个指针都指
向一个 IMAGE_IMPORT_BY_NAME 结构。
DWORD TimeDateStamp
指示文件创建日期的日期/时间戳。
DWORD ForwarderChain
这个域与函数转发(Forward)有关。转发就是把对一个 DLL 中的某个函数的调用转到另一个 DLL 的某个函数上。例如在 Windows NT 上,KERNEL32.DLL 就将它的一些导出函数转发到了 NTDLL.DLL 中。一个应用程序看起来好像调用的是 KERNEL32.DLL 中的函数,但实际上它调用的是 NTDLL.DLL 中的函数。这个域包含了 FirstThunk 数组(马上就要讲到)的索引。被这个域索引的函数会被转发到另一个 DLL 上。不幸的是,函数是如何转发的这种格式并未公开。转发函数的例子很难找到。
DWORD Name
这是一个以 NULL 结尾的 ASCII 字符串的 RVA,这个字符串包含导入的 DLL 的名称。常见的例子是“KERNEL32.DLL”和“USER32.DLL”。
PIMAGE_THUNK_DATA FirstThunk
这个域是 IMAGE_THUNK_DATA 共用体的偏移地址(RVA)。几乎在所有情况下,这个共用体都是作为指向IMAGE_IMPORT_BY_NAME结构的指针。如果这个域不是这些指针之一,那推测它应该是那个被导入的 DLL 所导出的一个序数值。从文档上看并不清楚是否可以只通过序数而不通过名称就能导入函数。
IMAGE_IMPORT _DESCRIPTOR 结构中重要的部分是导入的 DLL 的名称和两个指向IMAGE_IMPORT_BY_NAME 结构的指针数组。在 EXE 文件中,这两个数组(分别由 Characteristics域和 FirstThunk 域所指向)是并列的,并且由每个数组中的最后一个 NULL 指针标志着数组结束。这个两个数组中的指针均指向 IMAGE_IMPORT_BY_NAME 结构。
对于 PE 文件导入的每个函数都有一个相应的 IMAGE_IMPORT_BY_NAME 结构。这个结构非常简单,格式如下:
WORD Hint;
BYTE Name[?];
第一个域是要导入的函数的导出序号。与 NE 文件不同,这个值不要求绝对正确。加载器只不过是在搜索导出函数时把它作为建议的起始值。接下来的 ASCII 字符串是导入的函数的名称。
为什么会有两个并列的指向 IMAGE_IMPORT_BY_NAME 结构的指针数组呢?第一个数组(由Characteristics 域指向的那一个)总是保留原样,系统并不修改。它有时也被称为提示名称表(hint-name table)。第二个数组(由 FirstThunk 域指向的那一个)要被 PE 加载器修改。加载器首先查找这个数组中每个指针所指向的 IMAGE_IMPORT_BY_NAME 结构所代表的函数的地址。然后它用找到的这个函数地址来覆盖数组中相应的指向 IMAGE_IMPORT_BY_NAME 结构的指针。而
JMP DWORD PTR [XXXXXXXX]这条指令中的[ XXXXXXXX]部分就是这个 Fi rstThunk 数组中的某个元
素的值。由于被加载器覆盖的这个指针数组最终保存的是导入函数的地址,因此它被称为导入地址表(Import Address Table,IAT)。
一个典型的 EXE 文件的导入表
GDI32.dll
Hint/Name Table: 00013064
TimeDateStamp: 2C51B75B
ForwarderChain: FFFFFFFF
First thunk RVA: 00013214
Ordn Name
48 CreatePen
57 CreateSolidBrush
62 DeleteObject
160 GetDeviceCaps
// 表的其余部分省略……
PE 文件的导出表
在.edata 节的开始处是一个 IMAGE_EXPORT_DIRECTORY 结构。这个结构后面紧跟着的是它的域所指向的数据。
DWORD Characteristics
这个域好像并未使用,总是 0。
DWORD TimeDateStamp
指示文件创建日期的日期/时间戳。
WORD MajorVersion
WORD MinorVersion
这些域好像并未使用,总是 0。
DWORD Name
包含这个 DLL 的名称的 ASCII 字符串的 RVA。
DWORD Base
导出函数的起始序数。例如如果文件导出的函数的序数分别为 10、11、12,那么这个域的值为 10。要获得某个函数的导出序数,你需要把这个域的值AddressOfNameOrdinals 数组中的相应元素的值相加。
DWORD NumberOfFunctions
AddressOfFunctions 数组中的元素数目。这个值也是这个模块导出的函数的数目。理论上,这个值可能与 NumberOfNames 域(下一个域)不同,但实际上它们总是一样的。
DWORD NumberOfNames
AddressOfNames 数组中的元素数目。这个值看起来总是与 NumberOfFunctions 域的值一样,因此它也是导出的函数的数目。
PDWORD *AddressOfFunctions
这个域是一个 RVA,并且指向一个函数地址数组。这里的函数地址是这个模块中每个导出的函数的入口点的地址(RVA)。
PDWORD *AddressOfNames
这个域是一个 RVA,并且指向一个字符串指针数组。这里的字符串是这个模块中导出的函数的名称的字符串。
PWORD *AddressOfNameOrdinals
这个域是一个 RVA,并且指向一个 WORD 类型的数组。这里的 WORD 是这个模块中导出的函数的序号。但是,不要忘记加上 Base 域指定的起始序号。
AddressOfNameOrdinals),它们是并列的。假如你要查找导出的第N个函数的信息,你需要在每个数组中都查找其第N个元素。
Name: KERNEL32.dll
Characteristics: 00000000
TimeDateStamp: 2C4857D3
Version: 0.00
Ordinal base: 00000001
# of functions: 0000021F
# of Names: 0000021F
Entry Pt Ordn Name
00005090 1 AddAtomA
00005100 2 AddAtomW
00025540 3 AddConsoleAliasA
00025500 4 AddConsoleAliasW
00026AC0 5 AllocConsole
00001000 6 BackupRead
00001E90 7 BackupSeek
00002100 8 BackupWrite
0002520C 9 BaseAttachCompleteThunk
00024C50 10 BasepDebugDump
// 表中的其余部分省略……
PE 文件的资源
查找 PE 文件中的资源比 NE 文件稍微复杂一点。单个资源(例如菜单)的格式并没有发生什么大的变化,但你需要通过一个奇怪的层次结构才能找到它们。浏览资源目录的层次结构就像是浏览硬盘一样。有一个主目录(根目录),它下面有子目录。各个子目录还有它们自己的子目录。这些更下层的子目录可能指向了原始的资源数据(例如对话框模板)。在 PE 格式中,资源目录层次结构中的根目录和它的子目录都是IMAGE_RESOURCE_DIRECTORY 类型的结构。
DWORD Characteristics
理论上这个域可能是资源的标志,但它好像总是 0。
DWORD TimeDateStamp
指示资源创建日期的日期/时间戳。
WORD MajorVersion
WORD MinorVersion
理论上这些域应该保存资源的版本号,但它们好像总是 0。
WORD NumberOfNamedEntries
本结构后面使用名称的数组元素的个数。
WORD NumberOfIdEntries
本结构后面使用整数 ID 的数组元素的个数。
IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[]
这个域实际上并不是 IMAGE_RESOURCE_DIRECTORY 结构的一部分。它是紧跟在IMAGE_RESOURCE_DIRECTORY 结构后面的类型为IMAGE_RESOURCE_DIRECTORY_ENTRY 结构的数组。这个数组中的元素数目是 NumberOfNamedEntries 和 NumberOfIdEntries 这两个域的和。用名称作为标识的元素(而不是用整数 ID)在这个数组的前面一部分。
至少要经过三级目录。顶级目录(只有一个)总是位于资源节(.rsrc)的开头。顶级目录的子目录对应于文件中各种类型的资源。例如如果一个 PE 文件中包含对话框、字符串表和菜单,那将会有三个子目录:一个对话框目录、一个字符串表目录和一个菜单目录。这些类型的子目录中的每一个最终都会有一个 ID 子目录。对于特定的资源类型的每个实例都会有一个子目录。例如在上面的例子中,如果有三个对话框,那对话框目录将会有三个 ID 子目录。每个 ID 子目录或者有一个以字符串表示的名称(例如“MyDialog”),或者有一个整数 ID,这个 ID 就是在 RC 文件中用于标识资源的。图 5 以可视化的形式显示了资源目录的层次结构。表 13 显示的是 PEDUMP
输出的 Windows NT 的 CLOCK.EXE 文件的资源。