(翻译)《Expert .NET 2.0 IL Assembler》 第四章 托管可执行体文件的结构 4.1 PE/COFF头(二)
PE头
PE头,紧跟在COFF头的后面,提供了OS加载器的信息。虽然这个头被称为可选择的头(optional header),它只是可选择的,在某种意义上是说,对象文件通常不包括它。对于PE文件而言,这个头是强制性的。
PE文件的大小是不固定的。它取决于定义在头中的数据目录的数量,并由COFF头中的SizeOfOptionalHeader字段详细指明。定义在Winnt.h中的PE头的结构如下:
// Standard fields
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
// NT additional fields
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_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
表4-4 描述了PE头的字段
表4-4 PE头字段
偏移量 32/64 |
大小 32/64 |
字段名 |
描述 |
0 |
2 |
Magic |
“魔数(Magic number)”,识别了映像文件的状态。对于32位PE文件而言,可采用的值是0x010B;对于64位PE文件而言,则是0x020B;对于一个ROM映像文件而言,则是0x107。托管的PE文件必须有设置为0x010B或0x020B的字段(只适于64位映像文件的2.0或更后期的版本)。 |
2 |
1 |
MajorLinkerVersion |
连接器的主版本。VC++连接器设置这个字段为8;被其它编译器使用的纯IL文件生成器做着同样的事情。在早期的版本中,这个字段被相应的设置6和7。 |
3 |
1 |
MinorLinkerVersion |
连接器的次版本 |
4 |
4 |
SizeOfCode |
代码区段(.text)的大小,或者如果这些多样的代码区段存在的话,就是所有这些代码区段的总合。IL编译器总是发布一个简单的代码区段。 |
8 |
4 |
SizeOfInitializedData |
初始化数据区段的大小(保存在相应的section头的SizeOfRawData字段中)或者所有这些section的总合。这个初始化的数据定义为一个特定的数值,存储在磁盘的映像文件中。 |
12 |
4 |
SizeOfUninitializedData |
未初始化的数据区段(.bss)的大小或所有这样的区段的总合。这个数据并不是磁盘文件的一部分并且不具备明确的值,但是在这个文件被加载时,OS加载器会为这个数据提交内存空间。 |
16 |
4 |
AddressOfEntryPoint |
RVA的入口点方法。对于非托管的PE文件,这个值可以是0。对于托管的PE文件,这个值总是指向CLR调用的stub。 |
20 |
4 |
BaseOfCode |
文件代码区段的开始部分的RVA。 |
24/- |
4/- |
BaseOfData |
文件数据区段的开始部分的RVA。这个入口不存在于64位可选择的头中。 |
28/24 |
4/8 |
ImageBase |
映像的首选起始虚拟地址;必须排列在64KB的边界上(0x10000)。在ILAsm中,这个字段可以通过指令.imagebase<integer value>和/或命令行选项/BASE=<integer value>显示的指定。这个命令行选项优先于指令。 |
32 |
4 |
SectionAlignment |
区段加载到内存后的对齐值。。这个设置必须大于等于FileAlignment字段的值。默认为内存页的大小。 |
36 |
4 |
FileAlignment |
在磁盘映像 区段的对齐值。这个值应该是2的幂,从512到64000(从0x200到0x10000)。如果SectionAlignment被设置为小于内存页的大小,FileAlignment必须与SectionAlignment相匹配。在ILAsm中,这个字段由指令.file alignment <integer value>和/或命令行选项/ALIGNMENT=<integer value>显示指明。这个命令行选项优先于指令。 |
40 |
2 |
MajorOperatingSystemVersion |
所需操作系统的主版本号。 |
42 |
2 |
MinorOperatingSystemVersion |
所需操作系统的次版本号。 |
44 |
2 |
MajorImageVersion |
应用程序的主版本号。 |
46 |
2 |
MinorImageVersion |
应用程序的次版本号。 |
48 |
2 |
MajorSubsystemVersion |
子系统的主版本号。 |
50 |
2 |
MinorSubsystemVersion |
子系统的次版本号。 |
52 |
4 |
Win32VersionValue |
保留的。 |
56 |
4 |
SizeOfImage |
映像文件的大小(按字节),包括了所有的头。这个字段必须被设置为SectionAlignment值的若干倍。 |
60 |
4 |
SizeOfHeaders |
MS-DOS头和stub、COFF头、PE头和section头的大小总合,成上舍入为FileAlignment值的若干倍。 |
64 |
4 |
CheckSum |
磁盘映像文件的Checksum |
68 |
2 |
Subsystem |
需要运行这个映像文件的用户接口子系统。这个值定义在Winnt.h中,如下: NATIVE(1):不需要子系统(例如,一个设备驱动)。 WINDOWS_GUI(2) :运行在Windows GUI子系统上。 WINDOWS_CUI(3):运行在Windows控制台模式。 OS2_CUI (5):运行在OS/2 1.x控制台模式。 POSIX_CUI (7):运行在POSIX控制台模式。 NATIVE_WINDOWS (8):映像文件是一个本地的Win9x驱动。 WINDOWS_CE_GUI (9):运行在Windows CE GUI子系统上。 在ILAsm中,这个字段由指令.subsystem<integer value>和/或命令行选项/SUBSYSTEM=<integer value>显示指明。这个命令行选项优先于指令。 |
70 |
2 |
DllCharacteristics |
在映像文件的1.0版本中,总是被设置为0。在托管文件的1.1或更新版本中,总是被设置为0x400:没有非托管的Windows结构化异常处理。 |
72 |
4/8 |
SizeOfStackReserve |
用于初始线程栈的虚拟内存大小。只有SizeOfStackCommit字段(所指定大小的内存)被提交了,余下部分会随着使用逐页增加。对于32位映像的默认值为1MB,对于64位映像的则为4MB。在ILAsm中,这个字段由指令.stackreserve <integer value>和/或命令行选项/STACK=<integer value>显示指明。这个命令行选项优先于指令。 |
76/80 |
4/8 |
SizeOfStackCommit
|
初始提交于初始线程栈的虚拟内存大小。对于32位映像默认为一页(4KB),对于64位映像默认为16KB。 |
80/88 |
4/8 |
SizeOfHeapReserve |
用于初始进程堆的虚拟内存大小。只有SizeOfHeapCommit字段被提交;剩下的在一页的增加中是有效的。对于32位和64位映像默认值都是1MB。 |
84/96 |
4/8 |
SizeOfHeapCommit |
初始提交给进程堆的虚拟内存大小。对于32位映像默认为4KB(一个操作系统的内存页),对于64位映像默认为2KB。 |
88/104 |
4 |
LoaderFlags |
已经被废弃,设置为0。 |
92/108 |
4 |
NumberOfRvaAndSizes |
DataDirectory数组中的入口数量,至少为16。虽然理论上发布大于16个数据目录是可能的,但是所有现有的托管编译器严格地发布16个数据目录,伴随着第16个(最后一个)数据目录从来不使用(保留的)。 |
包包译注:魔数(Magic number),一个不能提供任何额外信息的数字,当你看到它时,会感到“莫名其妙”。因此,判定一个数字是否是magic number的依据,是它的出现是否能提供足够的信息让你理解其所指代的行为和场景。
数据目录表
数据目录表开始于一个32位PE头的96个偏移量处和64位PE头的112个偏移量处。数据目录表中的每一个入口都包括了RVA,以及这个独特的数据目录表所描述的表或字符串的大小;这些信息由操作系统使用。数据目录表入口是一个定义在Winnt.h中的8字节结构,如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
然而,第一个字段,命名为VirtualAddress的那个,不是一个虚拟地址,而是一个RVA。在这个表给出的RVA并没有必要指向一个section的起始部分,而这个包含特殊表的section也没有必要有特殊的名称。第二个字段是字节大小。
在数据目录表中定义了16种数据目录表:
- 导出型目录表地址和大小。这个表包括了4种其它的表的信息,这些表保存了描述PE文件的非托管导出型数据。在这些托管的编译器中,只有VC++链接器和ILAsm是可以暴露托管方法的,它们被托管的PE文件导出为非托管的导出,由非托管的调用器使用。参见第18章获取更多细节。
- 导入型目录表地址和大小:这个表包括了由PE文件使用的非托管导入型数据。在这些托管的编译器中,只有VC++链接器才很需要这个表,导入这个非托管的外部方法,这个方法使用在非托管本地代码中,而这些代码则内嵌在当前的托管PE文件中。其他编译器——包括IL编译器,并不会内嵌这些非托管的本地代码到托管的PE文件中,因此由这些编译器生成的文件的导入表包含着一个单一的入口,而这就是CLR的入口函数。
- 资源表地址和大小:这个表包含内嵌在PE文件中的非托管资源,托管资源并不是这个数据的一部分。
- 异常表地址和大小:这个表只包含非托管的异常信息。
- 证书表地址和大小:这个地址入口指向一个由属性证书(attribute certificate)组成的表(用于文件验证),这个表不会作为映像文件的一部分被加载到内存。同样的,这个入口的第一个字段是一个文件指针而不是一个RVA。这个表的每个入口包括了一个4字节的文件指针,指向各自的属性证书,并具有4字节的大小。
- 基本重定位(Base Relocation)地址和大小:这将在本章后面一些详细讨论,参见“重定位”章节。
- 调试数据地址和大小:一个托管的PE文件并没有携带内嵌的调试数据;调试数据发布在一个PDB文件中。因此这种数据目录也是全都为0的,或者指向一个类型为2(IMAGE_DEBUG_TYPE_CODEVIEW)的单一的30字节调试目录的入口,这将依次指向一个CodeView样式的头,包括着指向PDB文件的路径。IL编译器和C#和VB.NET编译器将这些数据发布到.text区段中。
- 架构数据地址和大小:特定于架构的数据。这个数据目录(全都设置为0)没有用于I386、IA64或AMD64架构。
- 全局指针:存储在全局指针寄存器的RVA值。这个大小必须被设置为0。如果目标架构没有使用全局指针的概念,这个数据目录就全都被设置为0(例如I386或AMD64)。
- TLS地址和大小:在托管编译器中,只有VC++链接器和IL编译器能够生成这些使用了TLS(thread storage data)数据的代码。
- 加载配置表地址和大小:特定于Window NT家族操作系统的数据。(例如,GlobalFlag值)
- 绑定导入型表地址和大小:这个表是一个由绑定导入型描述符组成的数组,它们中的每一个都描述了一个DLL。这个映像与DLL在创建映像的时候密切相关。描述符还携带了绑定的时间戳,同时如果这种绑定是现时的,这个OS加载器将这些绑定设置为API导入的一个捷径。否则,加载器忽视这些绑定并通过这个导入表解决这些输入型的API。
- 导入型地址表地址和大小:这个表(IAT)会在导出目录表中被引用到(数据目录1)。
- 延迟导入型描述符地址和大小:包括一个32位ImgDelayDescr结构的数组,每个结构描述了一个延迟加载的导入。延迟加载的导入是这样一些DLL,它们被描述为隐式的导入,而被加载为显示的导入(通过对LoadLibrary这个API的调用)。动态库的延迟加载是按需执行的——在第一次调用这个DLL的时候。这就不同于隐式的导入,后者在导入的可执行体初始化的时候就立即被加载。
- CLR头地址和大小:CLR头结构将会在这一章的后面详细描述(参见“CLR头”)。
- 保留的:全都设置为0。
Section头
Section头的表必须紧跟在PE头后面。由于没有文件头直接地指向这个Section表,这个表的位置被计算为这些文件头的全部大小加上1。
COFF头的NumberOfSections字段,定义了section头这个表中入口的数量。Section头在这个表中的索引是从0开始的,伴随着由链接器定义的section顺序。这些Section按照Section头表中定义的顺序,一个接一个地连续地存放,(正如你所知道的那样)起始RVA对齐到PE头的SectionAlignment字段所指定的值。
Section头是定义在Winnt.h中的一个40字节的结构体,如下:
BYTE Name[8];
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;
这些字段包含在IMAGE_SECTION_HEADER结构中,如下:
Name(8字节ASCII字符串):表示section的名称。Section名称开始于一个点(例如,.reloc)。如果Section名称包含正好8个字符,这个空的休止符就会被省略。如果Section名称少于8个字符,这个名称的数组就会以空字符来填充。映像文件不能有多于8个字符的section名称。然而,在对象文件中,Section名称可以再长一些。(想象一个冗长的文件生成器发布一个名为.myownsectionnobodyyelsecouldevergrok的Section。)在这种情形中,名称被放置在字符串表中,而字段包括了字符“/”在第一个字节中,紧跟着是一个ASCII字符串——包括了在字符串表中相应偏移量的一个十进制表示值。
PhysicalAddress/ VirtualSize(4字节无符号整型):在映像文件中,这个字段保存了这一section中的代码或数据的准确(未对齐的)字节大小。
VirtualAddress(4字节无符号整型):不管它的名称是什么,这个字段保存了section开始部分的RVA。
SizeOfRawData(4字节无符号整型):在一个映像文件中,这个字段保存了磁盘上初始数据的字节大小,将大量在PE头中详细指出的FileAlignment值聚拢起来。如果SizeOfRawData小于VirtualSize,这个section的剩余部分被空字节填充——当它在内存上展开的时候。
PointerToRawData(4字节无符号整型):这个字段保存了指向Section的第一页的一个文件指针。在映像文件中,这个值应该是大量在PE头文件中详细指定的FileAlignment值。
PointerToRelocations(4字节无符号整型):这是一个指向Section的重定位入口起始位置的文件指针。在映像文件中,这个字段不再使用并应该被设置为0。
PointerToLinenumbers(4字节无符号整型):这个字段保存了指向Section的行号入口起始位置的一个文件指针。在托管PE文件中,COFF行号被剥去了,而这个字段必须被设置为0。
NumberOfRelocations(2字节无符号整型):在托管的映像文件中,这个字段应该被设置为0。
NumberOfLinenumbers(2字节无符号整型):在托管的映像文件中,这个字段应该被设置为0。
Characteristics(4字节无符号整型):这个字段详细指明了映像文件的特性,并保存了这些二进制标记的位或运算值,如表4-5所描述。
这些section的特性标记定义在Winnt.h中。其中一些标记是保留的,而一些是只相对于对象文件的。表4-5列出了这些对PE文件有效的标记。所有标记的名称以IMAGE_SCN开始,我将会和通常一样将其忽略;换句话说,IMAGE_SCN_SCALE_INDEX将会变成_SCALE_INDEX。
表4-5 PE文件中Section的特性标记
标记 |
值 |
描述 |
_SCALE_INDEX |
0x00000001 |
TLS描述符的表索引是依比例决定的。 |
_CNT_CODE |
0x00000020 |
Section包括了可执行的代码。在IL编译器生成的PE文件中,只有.text这个section能够携带这种标记。 |
_CNT_INITIALIZED_DATA |
0x00000040 |
Section包括了初始化的数据。 |
_CNT_UNINITIALIZED_DATA |
0x00000080 |
Section包括了未初始化的数据。 |
_LNK_INFO |
0x00000200 |
Section包括了评论或一些其它类型的辅助信息。 |
_NO_DEFER_SPEC_EXC |
0x00004000 |
在这个section的TLB(translation look aside buffer)入口中,重新设置暂定的异常处理位。 |
_LNK_NRELOC_OVFL |
0x01000000 |
Section包括扩展的重定向。 |
_MEM_DISCARDABLE |
0x02000000 |
Section可以按需被废弃 |
_MEM_NOT_CACHED |
0x04000000 |
Section能够被缓存。 |
_MEM_NOT_PAGED |
0x08000000 |
Section不能被分页。 |
_MEM_SHARED |
0x10000000 |
Section可以在内存中共享。 |
_MEM_EXECUTE |
0x20000000 |
Section可以作为代码被执行。在IL编译器生成的PE文件中,只有.text这个section能够携带这种标记。 |
_MEM_READ |
0x40000000 |
Section可以被读取。 |
_MEM_WRITE |
0x80000000 |
Section可以被写入。在由IL编译器生成的PE文件中,只有.sdata和.tls 这些section能够携带这种标记。 |
下面的标记是不允许出现在section的托管文件中的:IMAGE_SCN_SCALE_INDEX,IMAGE_SCN_NO_DEFER_SPEC_EXC,IMAGE_SCN_LNK_NRELOC_OVFL和IMAGE_SCN_MEM_SHARED。
IL编译器在PE文件中生成下面的section:
.text:一个只读的section,包括了CLR头、元数据、IL代码、托管异常处理信息以及资源
.sdata:一个可读写的section,包括了数据
.reloc:一个只读的section,包括了重定位
.rsrc:一个只读的section,包括了非托管的资源
.tls:一个可读写的section,包括了TLS数据