第1章介绍了托管可执行文件,被称为托管模块,并在CLR的环境中执行。在这一章,我将会展示这样一个文件的通用结构。这个托管模块的文件格式是标准的Microsoft Windows PE/COFF(可移植的执行体和公用对象文件格式)的扩展。因此,在形式上,任何托管模块都是一个恰当的PE/COFF文件,带着额外的特性以将其识别为一个托管的可执行文件。
托管模块的文件格式符合Windows PE/COFF标准,而操作系统将托管模块当作一个可执行体。并且,一旦操作系统调用这个模块,这个扩展的特定于CLR的信息就允许运行时立刻获取对模块可执行体的控制
图4-1显示了这个托管的PE/COFF结构。
图4-1一个托管可执行文件的通用结构
既然ILAsm只生成了PE文件,这一章就将重点放在PE文件上——执行体,又被称为映像文件(Image File),因为它们可以被看作为“内存映像”——而不是纯净的COFF对象文件。(实际上,只有其中一个当前的托管编译器,Microsoft Visual C#生成了对象文件作为到PE文件的中间步骤。)
对托管PE文件结构的分析使用了下面通用的定义:
文件指针。在被加载器访问之前,文件中某一项的位置。这个位置是文件中的一个指针(偏移量),正如文件被存储在磁盘上一样。
相对虚地址(RVA):一个项的地址,一旦该项被加载进内存,映像文件的基地址就会从中减去——换句话说,映像文件中的一个项的偏移量被加载进内存。一个项的RVA几乎总是不同于它在磁盘上文件的位置(文件指针)。
虚地址(VA):与RVA相同,除了映像文件的基地址不会从中减去。这个地址被称为虚拟的,因为操作系统为每一个进程创建了一个截然不同的虚拟地址空间,独立于物理内存。出于大多数考虑,一个虚拟地址应该被简单地视作一个地址。一个虚拟地址不能被预言为一个RVA,因为如果存在一个冲突伴随着任意的映像文件被加载——基地址冲突,加载器可能不会加载映像文件到它首选的位置。
Section:PE/COFF中的代码或数据的基本单元。除了代码和数据section外,一个映像文件可以包括大量的section,如.tls(线程本地存储)或.reloc(重定位),这是有特殊意图的。在这个section中的所有原始数据必须被连续加载。
贯穿本章(并且确实的说是贯穿本书),我使用了术语“托管编译器”来表示一个编译器,它以CLR为目标并生成托管的PE文件。这个术语非必要地暗示了编译器本身是一个托管的应用程序。
PE/COFF头
图4-2解释了一个特定于操作系统的PE文件头的结构。这个头包括一个MS-DOS头和stub,PE签名,COFF头,PE头以及section头。下面的部分讨论了所有这些组件和PE头中的数据目录表。
MS-DOS头/Stub和PE签名
MS-DOS头和stub只存在于映像文件中。它们位于映像文件的起始位置,代表了一个有效的运行在DOS下的应用程序(这是否很令人兴奋呢?)。当映像文件运行在MS-DOS下的时候,默认的stub打印出消息“This message cannot be run in DOS mode”。特定于操作系统的头,可能是最没有意思的部分;唯一相关的事实是,MS-DOS头,在0x3C的偏移位置,包括了指向PE签名的文件指针,这将允许操作系统恰当地执行映像文件。
图4-2 特定于操作系统的头的内存布局
PE签名通常(不是必要的)直接位于MS-DOS stub之后,这是一个4字节的项,将文件识别为PE格式的映像文件。这个签名包含了字符P和E,紧跟在2个空字节之后
COFF头
一个标准的COFF头直接位于PE签名之后。COFF头提供了一个PE/COFF文件的大部分通用字符,适用于对于对象和可执行文件。表4-1描述了COFF头的结构和它的字段的意思。
表4-1 COFF头的格式
偏移量 |
大小 |
字段名 |
描述 |
0 |
2 |
Machine |
识别目标机器的数字。(参见表4-2。)如果托管PE文件要提供各种各样的机器类型,这个字段就应该被设置为IMAGE_FILE_MACHINE_I386(0x014C)。IL编译器具有命令行选项 /ITANIUM和/X64以分别指定IMAGE_FILE_MACHINE_IA64和IMAGE_FILE_MACHINE_AMD64这两个值 |
2 |
2 |
NumberOfSections |
Section表中实体的数量,直接跟随在头的后面。 |
4 |
4 |
TimeDateStamp |
文件创建的日期和时间。 |
8 |
4 |
PointerToSymbolTable |
COFF符号表的文件指针。因为这个表从来不在托管PE文件中使用,这个字段必须被设置为0。 |
12 |
4 |
NumberOfSymbols |
COFF符号表中的实体数量。这个字段在托管PE文件中被设置为0。 |
16 |
2 |
SizeOfOptionalHeader |
PE头的大小。这个字段特定于PE文件;在COFF文件中被设置为0 |
18 |
2 |
Characteristics |
这个标记指出了文件的特性。 |
标准COFF头的结构被定义在如下的Winnt.h 中:
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
机器类型也被定义在Winnt.h 中,正如表4-2所示。每一个类型都被命名为IMAGE_FILE_MACHINE_XXX,为避免重复,我将其简写为_XXX.
表4-2 机器字段值
常量* |
值 |
描述 |
_UNKNOWN |
0 |
只对于非托管PE文件而言,内容假定可适用于任何机器类型。 |
_I386 |
0x014c |
Intel386或随后之上。对于纯的托管PE文件,内容可使用于任何机器类型。 |
_R3000 |
0x0162 |
MIPS little endian——在最重要的字节之前的最不重要的字节。0x0160 big endian——在最不重要的字节之前的最重要的字节。 |
_R4000 |
0x0166 |
MIPS little endian。 |
_R10000 |
0x0168 |
MIPS little endian。 |
_WCEMIPSV2 |
0x0169 |
运行在Microsoft Windows CE 2上的MIPS little endian。 |
_ALPHA |
0x0184 |
Alpha AXP。 |
_SH3 |
0x01a2 |
SH3 little endian. |
_SH3DSP |
0x01a3 |
SH3DSP little endian. |
_SH3E |
0x01a4 |
SH3E little endian. |
_SH4 |
0x01a6 |
SH4 little endian. |
_ARM |
0x01c0 |
ARM little endian. |
_THUMB |
0x01c2 |
ARM processor with Thumb decompressor. |
_AM33 |
0x01d3 |
AM33处理器。 |
_POWERPC |
0x01F0 |
IBM PowerPC little endian. |
_POWERPCFP |
0x01F1 |
带有浮点指针单元(FPU)的IBM PowerPC little endian。 |
_IA 64 |
0x0200 |
Intel IA64 (Itanium)。 |
_MIPS16 |
0x0266 |
MIPS。 |
_ALPHA64 |
0x0284 |
ALPHA AXP64. |
_AXP64 |
0x0284 |
ALPHA AXP64. |
_MIPSFPU |
0x0366 |
带有FPU的MIPS。 |
_MIPSFPU16 |
0x0466 |
带有FPU的MIPS16。 |
_TRICORE |
0x0520 |
Infineon。 |
_AMD64 |
0x8664 |
AMD X64 和Intel E64T 架构。 |
_M32R |
0x9041 |
M32R little endian。 |
*第一段“常量”列,省略了前缀IMAGE_FILE_MACHINE_XXX
包包译注:little endian和big endian是表示计算机字节顺序的两种格式,所谓的字节顺序指的是长度跨越多个字节的数据的存放形式。
假设从地址0x00000000开始的一个字中保存有数据0x1234abcd,那么在两种不同的内存顺序的机器上从字节的角度去看的话分别表示为:
1) little endian:在内存中的存放顺序是0x00000000-0xcd,0x00000001-0xab,0x00000002-0x34,0x00000003-0x12
2) big endian:在内存中的存放顺序是0x00000000-0x12,0x00000001-0x34,0x00000002-0xab,0x00000003-0xcd
需要特别说明的是,以上假设机器是每个内存单元以8位即一个字节为单位的。
简单的说,ittle endian把低字节存放在内存的低位;而big endian将低字节存放在内存的高位。
现在主流的CPU,intel系列的是采用的little endian的格式存放数据,而motorola系列的CPU采用的是big endian。
“endian”这个词出自《格列佛游记》。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开,由此曾发生过六次叛乱,其中一个皇帝送了命,另一个丢了王位。
我们一般将endian翻译成“字节序”,将big endian和little endian称作“大尾”和“小尾”。但是在本书中保留英文原词。
包包译注:Alpha AXP,也称为DEC Alpha,是64位的 RISC 微处理器,最初由DEC公司制造,并被用于DEC自己的工作站和服务器中。作为VAX的后续被开发,支援VMS操作系统,如 Digital UNIX。不久之后开放源代码的操作系统也可以在其上运行,如Linux 和 BSD 。Microsoft 支持这款处理器,直到Windows NT 4.0 SP6 ,但是从Windows 2000 beta3 开始放弃了对Alpha的支援。
COFF头的特征字段包含了指出PE/COFF文件特性的标记。这些标记定义在Winnt.h中,如表4-3所示。注意到这个表提到了纯净IL(pure IL)的托管PE文件,纯净IL指出映像文件并不包含内嵌的本地代码。
这些标记的命名都以IMAGE_FILE开始,清楚起见,接下来我将省略这些前缀。
表4-3特征字段值
标记* |
值 |
描述 |
_RELOCS_STRIPPED |
0x0001 |
只是一个映像文件。这个标记指出文件不包括基本重定位并且必须在其相应的基地址加载。在基地址冲突的情形中,OS加载器会报告一个错误。这个标记不应该用于托管的PE文件的设置。 |
_EXECUTABLE_IMAGE |
0x0002 |
标记指出了这个文件是一个映像文件(EXE 或 DLL)。这个标记必须用于托管的PE文件的设置。如果没有被设置,这就通常为一个(目标代码)连接器错误。 |
_LINE_NUMS_STRIPPED |
0x0004 |
COFF行号被移除。这个标记必须用于托管的PE文件的设置,因为它们不需要使用内嵌在PE文件自身的调试信息。取代的,调试信息被保存在伴随的PDB(program database)文件中。 |
_LOCAL_SYMS_STRIPPED |
0x0008 |
COFF符号表的本地符号的入口被移除。 这个标记应该用于托管的PE文件的设置,为了在之前给出的入口的原因。 |
_AGGRESIVE_WS_TRIM |
0x0010 |
Aggressively trim the working set. This flag should not be set for pure-IL managed PE files. 侵略性的修整工作组。这个标记不应该用于纯IL的托管的PE文件的设置。 |
_LARGE_ADDRESS_AWARE |
0x0020 |
应用程序能够处理超过2GB范围的地址。这个标记不应该用于1.0和1.1版本的托管的PE文件的设置,但是可以用于2.0版本的文件。 |
_BYTES_REVERSED_LO |
0x0080 |
Little endian。这个标记应该用于托管的PE文件的设置。 |
_32BIT_MACHINE |
0x0100 |
机器是基于23位的架构。这个标记通常是由当前版本的代码生成器创建的托管PE文件设置的。可是,2.0以及更新的版本,能够创建64位特殊的映像,这将不会使这个标记被设置。 |
_DEBUG_STRIPPED |
0x0200 |
调试信息从映像文件中移除。 |
_REMOVABLE_RUN_FROM_SWAP |
0x0400 |
如果映像文件是可移除的媒体,就从交换文件复制并运行它。这个标记不应该用于纯IL的托管的PE文件的设置。 |
_NET_RUN_FROM_SWAP |
0x0800 |
If the image file is on a network, copy and run it from the swap file. This flag should not be set for pure-IL managed PE files. 如果映像文件是位于网络上的,就从交换文件复制并运行它。这个标记不应该用于纯IL的托管的PE文件的设置。 |
_SYSTEM |
0x1000 |
映像文件是一个系统文件(例如,一个设备驱动)。这个标记不应该用于纯IL托管PE文件的设置。 |
_DLL |
0x2000 |
映像文件是一个DLL而不是一个EXE。它不能直接运行。 |
_UP_SYSTEM_ONLY |
0x4000 |
托管文件应该只允许运行在一个单处理器(uniprocessor)的机器上。这个标记不应该用于纯IL托管PE文件的设置。 |
_BYTES_REVERSED_HI |
0x8000 |
Big endian。这个标记不应该用于纯IL托管PE文件的设置。 |
*第一段“常量”列,省略了前缀IMAGE_FILE_MACHINE_XXX
这些典型的特征值由现有的PE文件生成器生成——除了由VC++链接器使用,还由所有剩下的Microsoft托管编译器使用,包括ILAsm——对于EXE映像文件而言是0x010E(IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LINE_NUMS_STRIPPED | IMAGE_FILE_LOCAL_SYMS_STRIPPED | IMAGE_FILE_32BIT_MACHINE)。
对于一个DLL映像文件而言,这个值是0x210E(IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LINE_NUMS_STRIPPED | IMAGE_FILE_LOCAL_SYMS_STRIPPED | IMAGE_FILE_32BIT_MACHINE | IMAGE_FILE_DLL)。
在CLR 2.0版本中,如果这个映像是为64位目标平台而生成的,这些特征值可能没有IMAGE_FILE_32BIT_MACHINE标记。