(翻译)《Expert .NET 2.0 IL Assembler》 第四章 托管可执行体文件的结构 4.1 PE/COFF头(二)

返回目录

 

PE

       PE头,紧跟在COFF头的后面,提供了OS加载器的信息。虽然这个头被称为可选择的头(optional header),它只是可选择的,在某种意义上是说,对象文件通常不包括它。对于PE文件而言,这个头是强制性的。

       PE文件的大小是不固定的。它取决于定义在头中的数据目录的数量,并由COFF头中的SizeOfOptionalHeader字段详细指明。定义在Winnt.h中的PE头的结构如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
     // 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)”,识别了映像文件的状态。对于32PE文件而言,可采用的值是0x010B;对于64PE文件而言,则是0x020B;对于一个ROM映像文件而言,则是0x107。托管的PE文件必须有设置为0x010B0x020B的字段(只适于64位映像文件的2.0或更后期的版本)。

2

1

MajorLinkerVersion

连接器的主版本。VC++连接器设置这个字段为8;被其它编译器使用的纯IL文件生成器做着同样的事情。在早期的版本中,这个字段被相应的设置67

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的幂,从51264000(从0x2000x10000)。如果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头和stubCOFF头、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的依据,是它的出现是否能提供足够的信息让你理解其所指代的行为和场景。

 

数据目录表

数据目录表开始于一个32PE头的96个偏移量处和64PE头的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种数据目录表:

  1. 导出型目录表地址和大小。这个表包括了4种其它的表的信息,这些表保存了描述PE文件的非托管导出型数据。在这些托管的编译器中,只有VC++链接器和ILAsm是可以暴露托管方法的,它们被托管的PE文件导出为非托管的导出,由非托管的调用器使用。参见第18章获取更多细节。
  2. 导入型目录表地址和大小:这个表包括了由PE文件使用的非托管导入型数据。在这些托管的编译器中,只有VC++链接器才很需要这个表,导入这个非托管的外部方法,这个方法使用在非托管本地代码中,而这些代码则内嵌在当前的托管PE文件中。其他编译器——包括IL编译器,并不会内嵌这些非托管的本地代码到托管的PE文件中,因此由这些编译器生成的文件的导入表包含着一个单一的入口,而这就是CLR的入口函数。
  3. 资源表地址和大小:这个表包含内嵌在PE文件中的非托管资源,托管资源并不是这个数据的一部分。
  4. 异常表地址和大小:这个表只包含非托管的异常信息。
  5. 证书表地址和大小:这个地址入口指向一个由属性证书(attribute certificate)组成的表(用于文件验证),这个表不会作为映像文件的一部分被加载到内存。同样的,这个入口的第一个字段是一个文件指针而不是一个RVA。这个表的每个入口包括了一个4字节的文件指针,指向各自的属性证书,并具有4字节的大小。
  6. 基本重定位(Base Relocation)地址和大小:这将在本章后面一些详细讨论,参见“重定位”章节。
  7. 调试数据地址和大小:一个托管的PE文件并没有携带内嵌的调试数据;调试数据发布在一个PDB文件中。因此这种数据目录也是全都为0的,或者指向一个类型为2IMAGE_DEBUG_TYPE_CODEVIEW)的单一的30字节调试目录的入口,这将依次指向一个CodeView样式的头,包括着指向PDB文件的路径。IL编译器和C#VB.NET编译器将这些数据发布到.text区段中。
  8. 架构数据地址和大小:特定于架构的数据。这个数据目录(全都设置为0)没有用于I386IA64AMD64架构。
  9. 全局指针:存储在全局指针寄存器的RVA值。这个大小必须被设置为0。如果目标架构没有使用全局指针的概念,这个数据目录就全都被设置为0(例如I386AMD64)。
  10. TLS地址和大小:在托管编译器中,只有VC++链接器和IL编译器能够生成这些使用了TLSthread storage data)数据的代码。
  11. 加载配置表地址和大小:特定于Window NT家族操作系统的数据。(例如,GlobalFlag值)
  12. 绑定导入型表地址和大小:这个表是一个由绑定导入型描述符组成的数组,它们中的每一个都描述了一个DLL。这个映像与DLL在创建映像的时候密切相关。描述符还携带了绑定的时间戳,同时如果这种绑定是现时的,这个OS加载器将这些绑定设置为API导入的一个捷径。否则,加载器忽视这些绑定并通过这个导入表解决这些输入型的API
  13. 导入型地址表地址和大小:这个表(IAT)会在导出目录表中被引用到(数据目录1)。
  14. 延迟导入型描述符地址和大小:包括一个32ImgDelayDescr结构的数组,每个结构描述了一个延迟加载的导入。延迟加载的导入是这样一些DLL,它们被描述为隐式的导入,而被加载为显示的导入(通过对LoadLibrary这个API的调用)。动态库的延迟加载是按需执行的——在第一次调用这个DLL的时候。这就不同于隐式的导入,后者在导入的可执行体初始化的时候就立即被加载。
  15. CLR头地址和大小:CLR头结构将会在这一章的后面详细描述(参见“CLR头”)。
  16. 保留的:全都设置为0

Section

Section头的表必须紧跟在PE头后面。由于没有文件头直接地指向这个Section表,这个表的位置被计算为这些文件头的全部大小加上1

COFF头的NumberOfSections字段,定义了section头这个表中入口的数量。Section头在这个表中的索引是从0开始的,伴随着由链接器定义的section顺序。这些Section按照Section头表中定义的顺序,一个接一个地连续地存放,(正如你所知道的那样)起始RVA对齐到PE头的SectionAlignment字段所指定的值。

Section头是定义在Winnt.h中的一个40字节的结构体,如下:

typedef struct _IMAGE_SECTION_HEADER {
     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结构中,如下:

Name8字节ASCII字符串):表示section的名称。Section名称开始于一个点(例如,.reloc)。如果Section名称包含正好8个字符,这个空的休止符就会被省略。如果Section名称少于8个字符,这个名称的数组就会以空字符来填充。映像文件不能有多于8个字符的section名称。然而,在对象文件中,Section名称可以再长一些。(想象一个冗长的文件生成器发布一个名为.myownsectionnobodyyelsecouldevergrokSection。)在这种情形中,名称被放置在字符串表中,而字段包括了字符“/”在第一个字节中,紧跟着是一个ASCII字符串——包括了在字符串表中相应偏移量的一个十进制表示值。

PhysicalAddress/ VirtualSize4字节无符号整型):在映像文件中,这个字段保存了这一section中的代码或数据的准确(未对齐的)字节大小。

VirtualAddress4字节无符号整型):不管它的名称是什么,这个字段保存了section开始部分的RVA

SizeOfRawData4字节无符号整型):在一个映像文件中,这个字段保存了磁盘上初始数据的字节大小,将大量在PE头中详细指出的FileAlignment值聚拢起来。如果SizeOfRawData小于VirtualSize,这个section的剩余部分被空字节填充——当它在内存上展开的时候。

PointerToRawData4字节无符号整型):这个字段保存了指向Section的第一页的一个文件指针。在映像文件中,这个值应该是大量在PE头文件中详细指定的FileAlignment值。

PointerToRelocations4字节无符号整型):这是一个指向Section的重定位入口起始位置的文件指针。在映像文件中,这个字段不再使用并应该被设置为0

PointerToLinenumbers4字节无符号整型):这个字段保存了指向Section的行号入口起始位置的一个文件指针。在托管PE文件中,COFF行号被剥去了,而这个字段必须被设置为0

NumberOfRelocations2字节无符号整型):在托管的映像文件中,这个字段应该被设置为0

NumberOfLinenumbers2字节无符号整型):在托管的映像文件中,这个字段应该被设置为0

Characteristics4字节无符号整型):这个字段详细指明了映像文件的特性,并保存了这些二进制标记的位或运算值,如表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

在这个sectionTLBtranslation 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_INDEXIMAGE_SCN_NO_DEFER_SPEC_EXCIMAGE_SCN_LNK_NRELOC_OVFLIMAGE_SCN_MEM_SHARED

IL编译器在PE文件中生成下面的section

.text:一个只读的section,包括了CLR头、元数据、IL代码、托管异常处理信息以及资源

.sdata:一个可读写的section,包括了数据

.reloc:一个只读的section,包括了重定位

.rsrc:一个只读的section,包括了非托管的资源

.tls:一个可读写的section,包括了TLS数据

 

posted @ 2008-08-03 23:43  包建强  Views(933)  Comments(0Edit  收藏  举报