📄 017.txt
字号:
17.1 PE文件的结构(1)
17.1.1 概论
在一个操作系统中,可执行的代码在被最终装入内存执行之前是以文件的方式存放在磁盘中的,DOS操作系统中的COM文件是最早的也是结构最简单的可执行文件,COM文件中仅仅包括可执行代码,没有附带任何“支持性”的数据,所以COM文件在使用方便的同时也存在诸多的限制:首先是没有附加数据来指定文件入口,这样,第一句执行指令必须安排在文件头部;再就是没有重定位信息,这样代码中不能有跨段操作数据的指令,造成代码和数据,甚至包括堆栈只能限制在同一个64 KB的段中。
为了更灵活地使用可执行代码,DOS系统中又定义了另一种可执行文件,那就是我们熟悉的EXE文件,EXE文件在代码的前面加了一个文件头,文件头中包括各种说明数据,如文件入口、堆栈的位置、重定位表等等,操作系统根据文件头中的信息将代码部分装入内存,根据重定位表修正代码,最后在设置好堆栈后从文件头中指定的入口开始执行。
显然,可执行文件的格式是操作系统工作方式的写照,因为可执行文件头部的数据是供操作系统装载文件用的,不同操作系统的运行方式各不相同,所以造成可执行文件的格式各不相同。
当Windows 3.x出现的时候,可执行文件中出现了32位代码,程序运行时转到保护模式之前需要在实模式下做一些初始化,这样实模式的16位代码必须和32位代码一起放在可执行文件中,旧的DOS可执行文件格式无法满足这个要求,所以Windows 3.x执行文件使用新的LE格式的可执行文件(Linear executable/线性可执行文件),Windows 9x中的VxD驱动程序也使用LE格式,因为这些驱动程序中也同时包括16位和32位代码。
而在Windows 9x,Windows NT,Windows 2000下,纯32位的可执行文件都使用微软设计的一种新的文件格式——PE格式(Portable Executable File Format/可移植的执行体)。
PE文件的基本结构如图17.1所示,在PE文件中,代码、已初始化的数据、资源和重定位信息等数据被按照属性分类放到不同的节(Section)中,而每个节的属性和位置等信息用一个IMAGE_SECTION_HEADER结构来描述,所有的IMAGE_SECTION_HEADER结构组成一个节表(Section Table),节表数据在PE文件中被放在所有节数据的前面。我们知道,Win32中可以对每个内存页分别指定可执行、可读写等属性,PE文件将同样属性的数据分类放在一起是为了统一描述这些数据装入内存后的页面属性。
由于数据是按照属性在节中放置的,不同用途但是属性相同的数据(如导入表、导出表以及.const段指定的只读数据)可能被放在同一个节中,所以PE文件中还用一系列的数据目录结构IMAGE_DATA_DIRECTORY来分别指明这些数据的位置,数据目录表和其他描述文件属性的数据合在一起称为PE文件头,PE文件头被放置在节和节表的前面。
图17.1 PE文件的基本结构
上面介绍的这些部分是PE文件中真正用于Win32的部分,为了与DOS系统的文件格式兼容,在这部分的前面又加上了一个标准的DOS MZ格式的可执行部分,所有这些部分合起来组成了现在使用的PE文件。下面分别介绍这些组成部分。
17.1.2 DOS文件头和DOS块
PE文件中还包括一个标准的DOS可执行文件部分,如图17.1中左边的①所示,这看上去有些奇怪,但是这对于可执行文件的向下兼容性来说却是不可缺少的。
操作系统识别可执行文件的方法是按照文件格式而不是按照扩展名,所以,虽然DOS的传统EXE文件、LE格式和PE格式的可执行文件都沿用了.exe的扩展名,但是操作系统总是能够正确识别这些文件并按照正确的方法装入它们。如果文件头中的数据格式不符合任何已经定义的格式,那么系统按照COM文件的格式装入文件,也就是说将整个文件的数据全部当做代码装入执行。
这个规则说明了为什么很多非.exe扩展名的可执行文件(如LE格式的VxD文件、PE格式的.dll,.scr文件等等)也能够被装入并正确运行,也说明了为什么把可执行文件的扩展名随意修改为.exe、.com或者.bat(甚至是.pif,.scr或者.bat),系统也能正确识别并执行的原因。
但是这种方法也存在一个问题,假如一个PE格式的可执行文件在Windows中执行,那没有任何异常,因为Windows能够识别PE文件头并正确装入,但如果将PE文件放入DOS执行,那么DOS系统肯定无法识别PE文件头,假如PE文件的头部不包括一个DOS部分的话,那么按照前面介绍的规则,PE文件头的数据会被DOS系统作为代码装入并执行,这种操作几乎可以肯定会让系统立刻挂起。
为了避免这种情况,PE文件的头部包括了一个标准的DOS MZ格式的可执行部分,这样万一在DOS下执行一个PE文件,系统可以将文件解释为DOS下的.exe可执行格式,并执行DOS部分的代码。
一般来说,DOS部分的执行代码只是简单地显示一个“This program cannot be run in DOS mode.”就退出了,这段简单的代码是编译器自动生成的。
如果对编译器内定的这段简单代码不满意的话,读者可以回忆一下第2章2.2.1节中介绍link.exe参数部分的内容,如果在link时使用/stub:dos_file_name.exe选项,读者完全可以用一个全功能的DOS程序来作为PE文件的DOS部分。
笔者就见过一个CD播放程序,在DOS下执行是一个文本界面的播放器,而在Windows下执行又是标准的Windows界面。我们知道,DOS和Windows下不管是界面还是CD操作都是完全不同的概念,它们不可能在同一段代码中完成。实际上,这个程序就是用这种方法插入了一个完全独立的DOS CD播放程序。
PE文件中的DOS部分由MZ格式的文件头和可执行代码部分组成,可执行代码被称为“DOS块”(DOS stub)。MZ格式的文件头由IMAGE_DOS_HEADER结构定义:
IMAGE_DOS_HEADER STRUCT
e_magic WORD ? ;DOS可执行文件标记,为“MZ”
e_cblp WORD ?
e_cp WORD ?
e_crlc WORD ?
e_cparhdr WORD ?
e_minalloc WORD ?
e_maxalloc WORD ?
e_ss WORD ? ;DOS代码的初始化堆栈段
e_sp WORD ? ;DOS代码的初始化堆栈指针
e_csum WORD ?
e_ip WORD ? ;DOS代码的入口IP
e_cs WORD ? ;DOS代码的入口CS
e_lfarlc WORD ?
e_ovno WORD ?
e_res WORD 4 dup(?)
e_oemid WORD ?
e_oeminfo WORD ?
e_res2 WORD 10 dup(?)
e_lfanew DWORD ? ;指向PE文件头
IMAGE_DOS_HEADER ENDS
DOS文件头的前面部分并不陌生,第一个字段e_magic被定义成字符“MZ”(在Windows.inc文件中已经预定义为IMAGE_DOS_SIGNATURE)作为识别标志,后面的一些字段指明了入口地址、堆栈位置和重定位表位置等。
标准的DOS文件头的定义只到e_ovno字段位置,后面的这些字段是在Windows系统出现后为了定义LE、PE等文件格式而扩充的,DOS系统对这些字段不进行解释。对于PE文件来说,有用的是最后的e_lfanew字段,这个字段指出了真正的PE文件头(如图17.1中的②所示)在文件中的位置,这个位置总是以8字节为单位对齐的。
实际上,Windows中使用的其他几种可执行文件格式也是这样引出的,如果是LE,LX等格式的文件,那么e_lfanew字段指向的位置会是LE文件头和LX文件头。
17.1.3 PE文件头(NT文件头)
从DOS文件头的e_lfanew字段(文件头偏移003ch)得到真正的PE文件头位置后,现在来看看它的定义,PE文件头是由IMAGE_NT_HEADERS结构定义的:
IMAGE_NT_HEADERS STRUCT
Signature DWORD ? ;PE文件标识
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS
PE文件头的第一个双字是一个标志,它被定义为00004550h,也就是字符“P”,“E”加上两个0,这也是“PE”这个称呼的由来,大部分的文件属性由标志后面的IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER32结构来定义,从名称看,似乎后面的这个PE文件表头结构是可选的(Optional),但实际上这个名称是名不符实的,因为它总是存在于每个PE文件中。
1. IMAGE_FILE_HEADER结构
IMAGE_FILE_HEADER结构的定义如下所示,字段后面的注释中标出了字段相对于PE文件头的偏移量,以供读者快速参考:
IMAGE_FILE_HEADER STRUCT
Machine WORD ? ;0004h - 运行平台
NumberOfSections WORD ? ;0006h - 文件的节数目
TimeDateStamp DWORD ? ;0008h - 文件创建日期和时间
PointerToSymbolTable DWORD ? ;000ch - 指向符号表(用于调试)
NumberOfSymbols DWORD ? ;0010h - 符号表中的符号数量(用于调试)
SizeOfOptionalHeader WORD ? ;0014h - IMAGE_OPTIONAL_HEADER32结构的长度
Characteristics WORD ? ;0016h - 文件属性
IMAGE_FILE_HEADER ENDS
几个关键字段的含义解释如下。
● Machine字段
用来指定文件的运行平台,常见的定义值见表17.1所示。Windows可以运行在Intel和SUN等几种不同的硬件平台上,不同平台指令的机器码是不同的,为不同平台编译的可执行文件显然无法通用。如果Windows检测到这个字段指定的适用平台与当前的硬件平台不兼容,它将拒绝装入这个文件。
表17.1 运行平台识别码的定义(更多定义参见Windows.inc文件)
Windows.inc中的预定义值
16进制值
说 明
IMAGE_FILE_MACHINE_UNKNOWN
0
未知平台
IMAGE_FILE_MACHINE_I386
014ch
Intel 386
暂无
014dh
Intel 486
暂无
014eh
Intel 586
暂无
0160h
R3000(大尾方式)
IMAGE_FILE_MACHINE_R3000
0162h
R3000(小尾方式)
IMAGE_FILE_MACHINE_R4000
0166h
R4000(小尾方式)
IMAGE_FILE_MACHINE_R10000
0168h
R10000(小尾方式)
IMAGE_FILE_MACHINE_ALPHA
0184h
Dec Alpha AXP
IMAGE_FILE_MACHINE_POWERPC
01f0h
IBM Power PC(小尾方式)
IMAGE_FILE_MACHINE_ALPHA64
0284h
Dec Alpha AXP64
● NumberOfSections字段
指出文件中存在的节的数量(如图17.1中的④所示),同样,节表的数量(如图17.1中的③所示)也等于节的数量。
● TimeDateStamp字段
编译器创建此文件的时间,它的数值是从1969年12月31日下午4:00开始到创建时间为止的总秒数。
● PointerToSymbolTable和NumberOfSymbols字段
这两个字段并不重要,它们与调试用的符号表有关。
● SizeOfOptionalHeader字段
紧接在当前结构下面的IMAGE_OPTIONAL_HEADER32结构的长度,这个值等于00e0h。
● Characteristics字段
属性标志字段,它的不同数据位定义了不同的文件属性,具体内容如表17.2所示,这是一个很重要的字段,不同的定义将影响系统对文件的装入方式,比如,当位13为1时,表示这是一个DLL文件,那么系统将使用调用DLL入口函数的方式调用文件入口,否则的话,表示这是一个普通的可执行文件,系统直接跳到入口处执行。对于普通的可执行PE文件,这个字段的值一般是010fh,而对于DLL文件来说,这个字段的值一般是210eh。
表17.2 属性位字段的含义
数 据 位
Windows.inc中的预定义值
为1时的含义
0
IMAGE_FILE_RELOCS_STRIPPED
文件中不存在重定位信息
1
IMAGE_FILE_EXECUTABLE_IMAGE
文件是可执行的
2
IMAGE_FILE_LINE_NUMS_STRIPPED
不存在行信息
3
IMAGE_FILE_LOCAL_SYMS_STRIPPED
不存在符号信息
7
IMAGE_FILE_BYTES_REVERSED_LO
小尾方式
8
IMAGE_FILE_32BIT_MACHINE
只在32位平台上运行
数 据 位
Windows.inc中的预定义值
为1时的含义
9
IMAGE_FILE_DEBUG_STRIPPED
不包含调试信息
10
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP
不能从可移动盘(如软盘、光盘)运行
11
IMAGE_FILE_NET_RUN_FROM_SWAP
不能从网络运行
12
IMAGE_FILE_SYSTEM
系统文件(如驱动程序),不能直接运行
13
IMAGE_FILE_DLL
这是一个 DLL 文件
14
IMAGE_FILE_UP_SYSTEM_ONLY
文件不能在多处理器上计算机上运行
15
IMAGE_FILE_BYTES_REVERSED_HI
大尾方式
2. IMAGE_OPTIONAL_HEADER32结构
定义IMAGE_OPTIONAL_HEADER32结构的本意在于让不同的开发者能够在PE文件头中使用自定义的数据,这就是结构名称中“Optional”一词的由来,但实际上IMAGE_FILE_HEADER结构不足以用来定义PE文件的属性,反而在这个“可选”的部分中有着更多的定义数据,对于读者来说,可以完全不必考虑这两个结构的区别在哪里,只要把它们当成是连在一起的“PE文件头结构”就可以了。
IMAGE_OPTIONAL_HEADER32结构的定义如下,同样,字段后面的注释中标出了字段本身相对于PE文件头的偏移量:
IMAGE_OPTIONAL_HEADER32 STRUCT
Magic WORD ? ;0018h 107h=ROM Image,10Bh=exe Image
MajorLinkerVersion BYTE ? ;001ah 链接器版本号
MinorLinkerVersion BYTE ? ;001bh
SizeOfCode DWORD ? ;001ch 所有含代码的节的总大小
SizeOfInitializedData DWORD? ;0020h所有含已初始化数据的节的总大小
SizeOfUninitializedData DWORD ? ;0024h 所有含未初始化数据的节的大小
AddressOfEntryPoint DWORD ? ;0028h 程序执行入口RVA
BaseOfCode DWORD ? ;002ch 代码的节的起始RVA
BaseOfData DWORD ? ;0030h 数据的节的起始RVA
ImageBase DWORD ? ;0034h 程序的建议装载地址
SectionAlignment DWORD ? ;0038h 内存中的节的对齐粒度
FileAlignment DWORD ? ;003ch 文件中的节的对齐粒度
MajorOperatingSystemVersion WORD ? ;0040h 操作系统主版本号
MinorOperatingSystemVersion WORD ? ;0042h 操作系统副版本号
MajorImageVersion WORD ? ;0044h可运行于操作系统的最小版本号
MinorImageVersion WORD ? ;0046h
MajorSubsystemVersion WORD ?;0048h 可运行于操作系统的最小子版本号
MinorSubsystemVersion WORD ? ;004ah
Win32VersionValue DWORD ? ;004ch 未用
SizeOfImage DWORD ? ;0050h 内存中整个PE映像尺寸
SizeOfHeaders DWORD ? ;0054h 所有头+节表的大小
CheckSum DWORD ? ;0058h
Subsystem WORD ? ;005ch 文件的子系统
DllCharacteristics WORD ? ;005eh
SizeOfStackReserve DWORD ? ;0060h 初始化时的堆栈大小
SizeOfStackCommit DWORD ? ;0064h 初始化时实际提交的堆栈大小
SizeOfHeapReserve DWORD ? ;0068h 初始化时保留的堆大小
SizeOfHeapCommit DWORD ? ;006ch 初始化时实际提交的堆大小
LoaderFlags DWORD ? ;0070h 未用
NumberOfRvaAndSizes DWORD ? ;0074h 下面的数据目录结构的数量
DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>) ;0078h
IMAGE_OPTIONAL_HEADER32 ENDS
这个结构中的大部分字段都不重要,读者可以从注释中理解它们的含义,下面说明的这些字段是比较重要的。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -