1. 概述
PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)。它是1993年Windows NT系统引入的新可执行文件格式,到现在已经经过20多年了。虽然使用PE作为可执行文件格式的Windows操作系统已经更换了很多版本,其结构的变化、新特性的增加、文件格式的转换,以及内核的重新定位等,都发生了翻天覆地的变化,硬件架构也从16位发展到现在的64位架构,而这些变化对PE格式的影响却不大。由于PE格式有较好的数据组织方式和数据管理算法,面对如此多的变化却能保持其设计的优雅。
众所周知,Windows NT继承自VAX® VMS®和UNIX。Windows NT的许多创建者在到Microsoft之前都曾为这些平台设计和编写程序。当他们设计Windows NT时,很自然会使用以前写过的和测试过的工具以尽快开始他们的新项目。这些工具产生的和使用的可执行文件和目标模块的格式被称为COFF(Common Object File Format的首字母,通用目标文件格式)。Microsoft编译器生成的OBJ文件就使用COFF格式。
PE文件之所以被称为“可移植”是因为Windows NT在各种平台(x86、MIPS®、Alpha等等)上的所有实现都使用同样的可执行文件格式。当然,像CPU指令的二进制编码之类的内容会有所不同。重要的是操作系统加载器和编程工具不需要针对遇到的每种新的CPU再完全重写。Windows NT及其以后版本,Windows 95及其以后版本和Windows CE一直到现在的win10都使用了这个相同的格式,所以说在很大程度上,这个目的达到了。对于64位的Windows,PE格式只是进行了很少的修改。这种新的格式被叫做PE32+。没有加入新的域,只有一个域被去除。剩下的改变只是一些域从32位扩展到了64位。在这种情况下,你能写出和32位与64位PE文件都能一起工作的代码。对于C++代码,Windows头文件的能力使这些改变很不明显。EXE和DLL文件之间的不同完全是语义上的。它们都使用完全相同的PE格式。仅有的区别是用了一个单个的位来指出这个文件应该被作为EXE还是一个DLL。甚至DLL文件的扩展名也是不固定的,一些具有完全不同的扩展名的文件也是DLL,比如.OCX控件和控制面板程序(.CPL文件)。
PE文件格式主要来自于UNIX操作系统所通用的COFF规范,同时为了保证与旧版本MS-DOS及Windows操作系统的兼容,也保留了MS-DOS中那熟悉的MZ头部。PE格式被公开在WINNT.H头文件中(非常零散)。在WINNT.H文件的中间有一个“Image Format”节。这个节以MS-DOS MZ格式和PE格式开头,后面才是新的PE格式。WINNT.H提供了PE文件使用的原始数据结构的定义。
2. PE文件分析相关的概念
在详细了解PE文件结构之前,我们需要先了解几个基本的概念。理解这些概念有利于我们更好地把握和分析PE中的数据结构。
2.1. 地址
PE中涉及的地址有四类,他们分别是:
- 虚拟内存地址(VA)
- 相对虚拟地址(RVA)
- 文件偏移地址(FOA)
- 特殊地址
2.1.1. 虚拟地址(Virtual Addresses)
由于Windows NT推出时程序是运行在保护模式下,用户的PE文件被操作系统加载进内存后,PE对应的进程支配了自己独立的4GB虚拟空间。在这个空间中定位的地址称为虚拟内存地址(Virual Address,VA)。到了现在系统运行在X64架构的硬件上(长模式),内存访问可以采用线性地址的方式,同时可访问的内存也突破了4GB的限制,但是独立的进程拥有独立的虚拟地址空间的内存管理机制还在沿用。在PE中,进程本身的VA被解释为:进程的基地址 + 相对虚拟内存地址。
2.1.2. 相对虚拟地址(Relative Virtual Addresses)
PE格式大量地使用所谓的RVA(相对虚拟地址)。一个RVA,亦即一个“Relative Virtual Addresses(相对虚拟地址)”,是在你不知道基地址时,被用来描述一个内存地址的。它是需要加上基地址才能获得线性地址的数值。基地址就是PE映象文件被装入内存的地址,并且可能会随着一次又一次的调用而变化。
在一个可执行文件中,有许多在内存中的地址必须被指定的位置。例如,当引用一个全局变量时就必须指定它的地址。PE文件可以被加载到进程地址空间的任何位置。虽然它们有一个首选加载地址,但你不能依赖于可执行文件真的会被加载到那个位置。因为这个原因,指定一个地址而不依赖于可执行文件的加载位置就很重要。
为了消除PE文件中对内存地址的硬编码,于是产生了RVA。一个RVA是在内存中相对于PE文件被加载的地址的一个偏移。例如,如果一个EXE文件被加载到地址0x400000,它的代码节位于地址0x401000处。那么代码节的RVA就是:
(目标地址) 0x401000 - (加载地址)0x400000 = (RVA)0x1000.
因为PE-文件中的各部分(各节)不需要像已载入的映象文件那样对齐,事情变得复杂起来。例如,文件中的各节常按照512(十六进制的0x200)字节边界对齐,而已载入的映象文件则可能按照4096(十六进制的0x1000)字节边界对齐。参见下面的“SectionAlignment(节对齐)”和“FileAlignment(文件对齐)”。
因此,为了在PE文件中找到一个特定RVA地址的信息,你得按照文件已被载入时的那样来计算偏移量,但要按照文件的偏移量来跳过。
试举一例,假若你已知道执行开始处位于RVA 0x1560地址处,并且想从那里开始的代码处反汇编。为了从文件中找到这个地址,你得先查明在RAM(内存)中各节是按照4096字节对齐的,并且“.code”节是从RVA 0x1000地址处开始,有16384字节长;然后你才知道RVA 0x1560地址位于此节的偏移量0x560处。你还要查明在文件中那节是按512字节边界对齐,且“.code”节在文件中从偏移量0x800处开始,然后你就知道在文件中代码的执行开始处就在0x800+0x560=0xd60字节处。
然后你反汇编它并发现访问一个变量的线性地址位于0x1051d0处。二进制文件的线性地址在装入时将被重定位,并常被假定使用的是优先载入地址。因为你已查明优先载入地址为0x100000,因此我们可开始处理RVA 0x51d0了。因数据节开始于RVA 0x5000处,且有2048字节长,所以它处于数据节中。又因数据节在文件中开始于偏移量0x4800处,所以该变量就可以在文件中的0x4800+0x51d0-0x5000=0x49d0处找到。
2.1.3. 文件偏移地址
文件偏移地址(File Offset Address,FOA)和内存无关,他是指某个位置距离文件头的偏移。
2.1.4. 特殊地址
在PE结构中海油一种特殊地址,其计算方法并不是从文件头算起,也不是从内存的某个位置的基地址算起,而是从特定的位置算起。这个地址在PE结构中很少见,如:在资源表里就出现过这样的地址。
2.2. 节(Section)
无论是结构化程序设计,还是面向对象程序设计,都是倡导程序和数据的独立性,因此,程序中的代码和数据通通常是分开存放的。为了保证程序执行的安全,保证内核的稳定,Windows操作系统通常对不同用途的数据设置不同的权限。比如:代码段中的字节码在程序运行的时候,一般不允许用户进行修改,数据段则允许在程序运行过程中读和写,常量只能读等。Windows操作系统在加载可执行程序时,会为这些具有不同属性的数据分别分配标记有不同属性的页面(当然,相同属性的数据可能会被放到同一个页面),以确保程序运行时的安全。正式基于这个原因,PE中才出现了所谓的节的概念。
节就是存放不同类型数据(比如代码、数据、常量、资源等)的地方,不同的节具有不同的访问权限。节是PE文件中存放代码和数据的基本单元。例如:一个目标文件中的所有代码可以组合成单个节,或者每个函数独占一格节(如果编译器允许)。增加节的数目会增加文件的开销,但是链接器在链接代码的时候就会有更大的选择余地。一个节中的所有原始数据必须被加载到连续的内存空间中。
从操作系统加载角度来看,节是相同属性数据的组合。与数据目录不同的是,尽管有些数据类型不同,分别属于不同的数据目录,但是由于其访问属性相同,便被归类到同一个节中。这个节最终可能会占用一个或多个页面;但无论有多少个,所有相关页面均会被赋予相同的页面属性。这些属性包括只读、只写、可读、可写等。
Windows操作系统在装载PE文件时会对相同和不同类型的节数据执行抛弃、合并、新增、复制等操作。这些不同的操作交叉组合导致了内存中的节和文件中的节会出现很大的不同。例如:”.data”的数据在磁盘中不存在,但是在内存中存在,而”.reloc”重定位表数据却恰恰相反。
2.3. 对齐(Alignment)
对齐这个概念并非只在PE文件中出现,许多文件格式都会有对齐的要求。有的对齐是为了美观,有的对齐则是为了效率。PE中规定了三类的对齐:数据在内存中的对齐、数据在文件中的对齐、资源文件中资源数据的对齐。
2.3.1. 内存对齐(SectionAligment)
PE 文件头里边的SectionAligment 定义了内存中区块的对齐值。由于Windows操作系统对内存属性的设置以页为单位,所以通常情况下,PE 文件被映射到内存中时,节在内存中的对齐单位必须至少是一个页的大小。对于32位的windows操作系统来说,这个值是4KB(1000h),而对64位操作系统来说,这个值就是8KB(2000h)。
2.3.2. 文件对齐(FileAligment)
PE 文件头里边的FileAligment 定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始存放。而区块的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是区块间的间隙。为了提高磁盘利用率,对齐单位通常小于内存,以一个物理扇区的大小作为对齐粒度,512字节,200h。
2.3.3. 资源数据对齐
资源字节码部分一般要求以双字(4字节)方式对齐。
3. PE文件结构
PE 文件格式被组织为一个线性的数据流,它由一个MS-DOS 头部开始,接着是一个是模式的程序残余以及一个PE 文件标志,这之后紧接着PE文件头和可选头部。这些之后是所有的段头部,段头部之后跟随着所有的段实体。文件的结束处是一些其它的区域,其中是一些混杂的信息,包括重分配信息、符号表信息、行号信息以及字串表数据。
如下图所示,PE文件结构被划分为四大部份,包括:DOS部分、PE头、节表、和节数据。
PE文件至少包含两个段(节),即数据段和代码段。Windows的应用程序PE文件格式有11个预定义段,这是对Windows应用程序所通用的。例如: .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,这些段并不是都是必须的,当然,也可以根据需要定义更多的段(比如一些加壳程序)。
在应用程序中最常出现的段有以下6种:
- 执行代码段,通常 .text (Microsoft)或 CODE(Borland)命名;
- 数据段,通常以 .data
、.rdata 或 .bss(Microsoft)、DATA(Borland)命名; - 资源段,通常以 .rsrc命名;
- 导出表,通常以 .edata命名;
- 导入表,通常以 .idata命名;
- 调试信息段,通常以 .debug命名;
PE文件一个方便的特点是磁盘上的数据结构和加载到内存中的数据结构是相同的。加载一个可执行文件到内存中 (例如,通过调用LoadLibrary)主要就是映射一个PE文件中的几个确定的区域到地址空间中。因此,一个数据结构比如IMAGE_NT_HEADERS (稍后我们会研究到)在磁盘上和在内存中是一样的。关键的一点是如果你知道怎么在一个PE文件中找到一些东西,当这个PE文件被加载到内存中后你几乎能找到相同的信息。
要注意到PE文件并不仅仅是被映射到内存中作为一个内存映射文件。代替的,Windows加载器分析这个PE文件并决定映射这个文件的哪些部分。当映射到内存中时文件中偏移位置较高的数据映射到较高的内存地址处。一个项目在磁盘文件中的偏移也许不同于它被加载到内存中时的偏移。然而,所有被表现出来的信息都允许你进行从磁盘文件偏移到内存偏移的转换 (参见下图)。
Windows装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系,只有真正执行到某个内存页中的指令或访问某一页中的数据时,这个页才会被从磁盘提交到物理内存。但因为装载可执行文件时,有些数据在装入前会被预先处理(如需要重定位的代码),装入以后,数据之间的相对位置也可能发生改变。因此,一个节的偏移和大小在装入内存前后可能是完全不同的。
4. PE文件结构分析
4.1. PE文件准备
为了对PE文件结构进行更好的分析,我们首先准备一个例子,这次我们通过在WIN10 X64环境下使用VS2015编译生成的一个X64架构的release程序来进行分析。这个例子是一个在WIN10 X64下通过TLS实现反调试的小程序。程序清单如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
程序是一个简单的windows小程序,通过使用TLS回调函数来检测调试器的存在,并弹出窗口提示检测到的状态,主程序只是一个简单的hello world消息窗。程序中涉及到PE文件中几个重要的节。程序通过在WIN10 X64环境下使用VS2015编译生成的一个X64架构的release版本tlstest.exe,程序大小为12.0 KB (12,288 字节)。
4.2. MS-DOS 文件头
在 image 文件的最开始处就是 DOS 文件头,DOS 文件头包含了 DOS stub 小程序。在 WinNT.h 文件里定义了一个结构来描述 DOS 文件头。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
这个结构名叫 IMAGE_DOS_HEADER 共 64 bytes,以 IMAGE_DOS_HEADER 结构描述的 DOS 文件头结构从 image 的 0x00000000 - 0x0000003F(64 bytes)
结构的 e_magic 域是 DOS 头文件签名,它的值是:0x5A4D 代表字符 MZ,它在 WinNT.h 里定义为:
- 1
e_lfanew 域是一个 offset 值,它指出 NT 文件头的位置。
下面看看tlstest.exe 的 DOS 文件头内容:
绿色部分是 DOS 签名,蓝色部分是 PE header offset(NT 文件头)值,也就是 IMAGE_DOS_HEADER 里的 e_lfanew 值,表明 NT 文件头在 image 文件的 0x000000F8 处。
4.2.1. DOS stub 程序
在 DOS 文件头下面紧跟着一小段 stub 程序,其内容随着链接时使用的链接器不同而不同,在PE中并没有与之对应的相关结构。在本例中从 0x00000040 - 0x0000004D 共 14 bytes是代码,其后是显示数据等内容,这段 dos stub 程序是这样的:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
当 windows 的 PE 文件放在 DOS 上执行时,将会执行这一段 DOS stub 程序,作用是打印信息:This program cannot be run in DOS mode…. 然后调用 int 21 来终止执行返回到 DOS,看看它是怎样运行的:
- 1
- 2
- 3
这个 DOS 执行环境中,CS 和 IP 被初始化为 0(对应值在IMAGE_DOS_HEADER结构中的e_ip、e_cs字段),e_lfarlc 是 DOS 环境的 relocate 表,它的值是 0x40 ,那么信息字符串的位置是:0x0040 + 0x000e = 0x4e,在 image 文件 0x0000004e 正好这字符串的位置。
4.3. NT 文件头
NT 文件头是 PE 文件头的核心部分,由 IMAGE_DOS_HEADER 结构的 e_lfanew 域指出它的位置。
同样 NT 文件头部分由一个结构 IMAGE_NT_HEADER 来描述,在 WinNT.h 里定义如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
可见这个结构分为 32 和 64 位版本,IMAGE_NT_HEADER 结构分为三大部分:
- PE 文件签名:Signature
- IMAGE_FILE_HEADER 文件头:FileHeader
- IMAGE_OPTINAL_HEADER(32/64) 可选头:OptionalHeader
IMAGE_NT_HEADERS32 和 IMAGE_NT_HEADERS64 的匹别在于 IMAGE_OPTIONAL_HEADER 结构,分别为:IMAGE_OPTIONAL_HEADERS32 和 IMAGE_OPTIONAL_HEADERS64
在 Win32 下 IMAGE_NT_HEADERS32 是 248 bytes,在 Win64 下 IMAGE_NT_HEADERS64 是 264 bytes,因此 tlstest.exe的 NT 文件头从 0x000000F8 - 0x000001FF 共 264 bytes
4.3.1. PE 签名
在 WinNT.h 文件里定义了 PE 文件的签名,它是:
- 1
这个签名值是 32 位,值为:0x00004550 即:PE 的 ASCII 码,下面看看 tlstest.exe 中的 PE 签名:
4.3.2. IMAGE_FILE_HEADER 文件头结构
PE 签名接着是 IMAGE_FILE_HEADER 结构,它在 WinNT.h 中的定义为:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
这个 IMAGE_FILE_HEADER 对 PE 文件大致的描述,这个结构共 20 bytes,它的域描述如下:
域 | size | 值 | 描述 |
---|---|---|---|
Machine | WORD | IMAGE_FILE_MACHINE_xxx | 表示目标平台 processor 类型,例:IMAGE_FILE_MACHINE_I386 |
NumberOfSections | WORD | 节数量 | 表示映象中有多少个 section |
TimeDataStamp | DWORD | 从1970年1月1日0:00 以来的总秒数 | 表示文件创建的时间 |
PointerToSymbolTable | DWORD | COFF 符号表偏移量 | 在 image 文件中很少见,总是为 0 |
NumberOfSymbols | DWORD | COFF 符号表的个数 | 如果存在的话,表示符号表的个数 |
SizeOfOptionalHeader | WORD | IMAGE_OPTIONAL_HEADER 结构大小 | 该域表示 IMAGE_NT_HEADER 中的 IMAGE_OPTIONAL_HEADER 结构的大小 |
Characteristics | WORD | IMAGE_FILE_xxx | 表示文件属性,例如:IMAGE_FILE_DLL 属性 |
IMAGE_FILE_HEADER 结构中比较重要的域是:Machine 和 SizeOfOptionalHeader, Machine 可以用来判断目标平台,比如:值为 0x8664 是代表 AMD64(即:x64 平台)它也适合 Intel64 平台。SizeOfOptionalHeader 指出 IMAGE_OPTIONAL_HEADER 结构的大小。
WinNT.h 中定义了一些常量值用来描述 Machine,以 IMAGE_FILE_MACHINE_XXX 开头,下面是一些典型的常量值:
- 1
- 2
- 3
- 4
- 5
WinNT.h 中还针对 Characteristics 域定义了一些常量值,以 IMAGE_FILE_XXX 开头,代表目标 image 文件的类型,下面是一些常见的值:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
可以看出Characteristics是以每一位表示文件属性,它的每一个bit代表的含义如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
NumberOfSections 表示 image 有多个 section,另一个重要的域是:SizeOfOptionalHeader 它指出接下来 IMAGE_OPTIONAL_HEADER 的大小,它有两个 size:Win32 的 0xDC 和 Win64 的 0xF0.
下面是 tlstest.exe的 IMAGE_FILE_HEADER 结构,从 0x000000F8 - 0x0000010F 共 20 bytes:
将这些值分解为:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- Machine 是 0x0x8664,它的值是 IMAGE_FILE_MACHINE_AMD64,说明这个 image
文件的目标平台是 AMD64,即:X64 平台 - NumberOfSections 是 0x08,说明 image 文件内含有 8 个 sections
- SizeOfOptionalHeader 是 0xF0,说明接下来的 IMAGE_OPTIONAL_HEADERS64 将是
0xF0(224 bytes)
它的 Characteristics 是 0x0022 = IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LARGE_ADDRESS_AWARE,说明这个 image 是 64 位可执行的映像。可以在 >2G 地址上,并且指明了接下来的 IMAGE_OPTIONAL_HEADER 结构是 0xf0 bytes(240 个字节)。
4.3.3. IMAGE_OPTIONAL_HEADER64 结构
在 IMAGE_FILE_HEADER 结构里已经指明了 image 是 64 位,并且 IMAGE_OPTIONAL_HEADER 的大小是 240 bytes,那么这个结构就是 IMAGE_OPTIONAL_HEADER64 结构。
可以根据 IMAGE_FILE_HEAER 结构的 Machine 来判断 image 是 Win32 还是 Win64 平台的。但是 Microsoft 官方推荐及认可的方法是从 IMAGE_OPTIONAL_HEADER 里的 magic 的值来判断目标平台 。
在 WinNT.h 里 IMAGE_OPTIONAL_HEADER64 的定义如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
64 位的 IMAGE_OPTIONAL_HEADER64 里没有 BaseOfData 域,其它的与 IMAGE_OPTIONAL_HEADER32 结构的域是一样的,只是一些域扩展为 64 位值,它们包括:
- ImageBase
- SizeOfStackReserve
- SizeOfStackCommit
- SizeOfHeapRerserve
- SizeOfHeapCommit
这些域在 64 位结构里被定义为 ULONGLONG 类型。
IMAGE_OPTIONAL_HEADER64 的定义的各个域含义如下:
偏移 | 大小 | 字段 | 描述 |
---|---|---|---|
0 | 2 | Magic | The unsigned integer that identifies the state of the image file. The most common number is 0x10B, which identifies it as a normal executable file. 0x107 identifies it as a ROM image, and 0x20B identifies it as a PE32+ executable. 确定这是什么类型的头。两个最常用的值是0x10b和0x20b. |
2 | 1 | MajorLinkerVersion | The linker major version number. 创建可执行文件的链接器的主版本号。对于Microsoft的链接器生成的PE文件,这个版本号的Visual Studio的版本号相一致 |
3 | 1 | MinorLinkerVersion | The linker minor version number. 创建可执行文件的链接器的次版本号。 |
4 | 4 | SizeOfCode | The size of the code (text) section, or the sum of all code sections if there are multiple sections. 所有具有IMAGE_SCN_CNT_CODE属性的节的总的大小。 |
8 | 4 | SizeOfInitializedData | The size of the initialized data section, or the sum of all such sections if there are multiple data sections. 所有包含已初始数据的节的总的大小。 |
12 | 4 | SizeOfUninitializedData | The size of the uninitialized data section (BSS), or the sum of all such sections if there are multiple BSS sections. 所有包含未初始化数据的节的总的大小。这个域总是0,因为链接器可以把未初始化数据附加到常规数据节的末尾。 |
16 | 4 | AddressOfEntryPoint | The address of the entry point relative to the image base when the executable file is loaded into memory. For program images, this is the starting address. For device drivers, this is the address of the initialization function. An entry point is optional for DLLs. When no entry point is present, this field must be zero. 文件中将被执行的第一个代码字节的RVA。对于DLL,这个进入点将在进程初始化和关闭时以及线程被创建和销毁时调用。在大多数可执行文件中,这个地址并不直接指向main,WinMain或DllMain函数,而是指向运行时库代码,由运行时库调用前述函数。在DLL中,这个域可以被设为0。链接器选项/NOENTRY可以设置这个域为0。 |
20 | 4 | BaseOfCode | The address that is relative to the image base of the beginning-of-code section when it is loaded into memory. 加载到内存后代码的第一个字节的RVA |
24 | 4 | BaseOfData | The address that is relative to the image base of the beginning-of-data section when it is loaded into memory. 理论上,它表示加载到内存后数据的第一个字节的RVA。然而,这个域的值对于不同版本的Microsoft链接器是不一致的。在64位的可执行文件中这个域不出现。 |
28/24 | 4/8 | ImageBase | The preferred address of the first byte of image when loaded into memory; must be a multiple of 64 K. The default for DLLs is 0x10000000. The default for Windows CE EXEs is 0x00010000. The default for Windows NT, Windows 2000, Windows XP, Windows 95, Windows 98, and Windows Me is 0x00400000. 文件在内存中的首选加载地址。加载器尽可能地把PE文件加载到这个地址(就是说,如果当前这块内存没有被占用,它是对齐的并且是一个合法的地址,等等)。如果可执行文件被加载到这个地址,加载器就可以跳过进行基址重定位这一步。在Win32下,对于EXE,缺省的ImageBase是0x400000。对于DLL,缺省是0x10000000。在链接时可以通过/BASE 选项来指定ImageBase,或者以后用REBASE工具重新设置。 |
32/32 | 4 | SectionAlignment | The alignment (in bytes) of sections when they are loaded into memory. It must be greater than or equal to FileAlignment. The default is the page size for the architecture. 加载到内存后节的对齐大小。这个值必须大于等于FileAlignment(下一个域)。缺省的对齐值是目标CPU的页大上。对于运行在Windows 9x或Windows Me下的用户模式可执行文件,最小对齐大小是一页(4KB)。这个域可以通过链接器选项/ALIGN来设置。 |
36/36 | 4 | FileAlignment | The alignment factor (in bytes) that is used to align the raw data of sections in the image file. The value should be a power of 2 between 512 and 64 K, inclusive. The default is 512. If the SectionAlignment is less than the architecture’s page size, then FileAlignment must match SectionAlignment. 在PE文件中节的对齐大小。对于x86下的可执行文件,这个值通常是0x200或0x1000。不同版本的Microsoft链接器缺省值不同。这个值必须是2的幂,并且如果SectionAlignment小于CPU的页大小,这个域必须和SectionAlignment相匹配。链接器选项/OPT:WIN98可设置x86可执行文件的文件对齐为0x1000,/OPT:NOWIN98设置文件对齐为0x200。 |
40/40 | 2 | MajorOperatingSystemVersion | The major version number of the required operating system. 所要求的操作系统的主版本号。随着那么多版本Windows的出现,这个域的值就变得很不确切。 |
42/42 | 2 | MinorOperatingSystemVersion | The minor version number of the required operating system. 所要求的操作系统的次版本号。 |
44/44 | 2 | MajorImageVersion | The major version number of the image. 这个文件的主版本号。不被系统使用并可设为0。可以通过链接器选项/VERSION来设置。 |
46/46 | 2 | MinorImageVersion | The minor version number of the image. 这个文件的次版本号。 |
48/48 | 2 | MajorSubsystemVersion | The major version number of the subsystem. 可执行文件所要求的操作子系统的主版本号。它曾经被用来表示需要较新的Windows 95或Windows NT用户界面,而不是老版本的Windows NT界面。今天随着各种不同版本Windows的出现,这个域已不被系统使用,并且通常被设为4。可通过链接器选项/SUBSYSTEM设置这个域的值。 |
50/50 | 2 | MinorSubsystemVersion | The minor version number of the subsystem. 可执行文件所要求的操作子系统的次版本号。 |
52/52 | 4 | Win32VersionValue | Reserved, must be zero. 另一个不被使用的域,通常设为0。 |
56/56 | 4 | SizeOfImage | The size (in bytes) of the image, including all headers, as the image is loaded in memory. It must be a multiple of SectionAlignment. 映像的大小。它表示了加载文件到内存中时系统必须保留的内存的数量。这个域的值必须是SectionAlignmnet的倍数。 |
60/60 | 4 | SizeOfHeaders | The combined size of an MS-DOS stub, PE header, and section headers rounded up to a multiple of FileAlignment. MS-DOS头,PE头和节表的总的大小。PE文件中所有这些项目出现在任何代码或数据节之前。这个域的值被调整为文件对齐大小的整数倍。 |
64/64 | 4 | CheckSum | The image file checksum. The algorithm for computing the checksum is incorporated into IMAGHELP.DLL. The following are checked for validation at load time: all drivers, any DLL loaded at boot time, and any DLL that is loaded into a critical Windows process. 映像的校验和。IMAGEHLP.DLL中的CheckSumMappedFile函数可以计算出这个值。校验和用于内核模式的驱动和一些系统DLL。对于其它的,这个域可以为0。当使用链接器选项/RELEASE时校验和被放入文件中。 |
68/68 | 2 | Subsystem | The subsystem that is required to run this image. For more information, see “Windows Subsystem” later in this specification. 指示可执行文件期望的子系统(用户界面类型)的枚举值。这个域只用于EXE。一些重要的值包括: IMAGE_SUBSYSTEM_NATIVE // 映像不需要子系统 IMAGE_SUBSYSTEM_WINDOWS_GUI // 使用Windows GUI IMAGE_SUBSYSTEM_WINDOWS_CUI // 作为控制台程序运行。 // 运行时,操作系统创建一个控制台 // 窗口并提供stdin,stdout和stderr // 文件句柄。 |
70/70 | 2 | DllCharacteristics | For more information, see “DLL Characteristics” later in this specification. 标记DLL的特性。对应于IMAGE_DLLCHARACTERISTICS_xxx定义。当前的值是: IMAGE_DLLCHARACTERISTICS_NO_BIND // 不要绑定这个映像 IMAGE_DLLCHARACTERISTICS_WDM_DRIVER // WDM模式的驱动程序 IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE // 当终端服务加载一个不是 // Terminal- Services-aware 的应用程 // 序时,它也加载一个包含兼容代码 // 的DLL。 |
72/72 | 4/8 | SizeOfStackReserve | The size of the stack to reserve. Only SizeOfStackCommit is committed; the rest is made available one page at a time until the reserve size is reached. 在EXE文件中,为线程保留的堆栈大小。缺省是1MB,但并不是所有的内存一开始都被提交。 |
76/80 | 4/8 | SizeOfStackCommit | The size of the stack to commit. 在EXE文件中,为堆栈初始提交的内存数量。缺省情况下,这个域是4KB。 |
80/88 | 4/8 | SizeOfHeapReserve | The size of the local heap space to reserve. Only SizeOfHeapCommit is committed; the rest is made available one page at a time until the reserve size is reached. 在EXE文件中,为默认进程堆初始保留的内存大小。缺省是1MB。然而在当前版本的Windows中,堆不经过用户干涉就能超出这里指定的大小。 |
84/96 | 4/8 | SizeOfHeapCommit | The size of the local heap space to commit. 在EXE文件中,提交到堆的内存大小。缺省情况下,这里的值是4KB。 |
88/104 | 4 | LoaderFlags | Reserved, must be zero. 不使用。 |
92/108 | 4 | NumberOfRvaAndSizes | The number of data-directory entries in the remainder of the optional header. Each describes a location and size. 在IMAGE_NT_HEADERS结构的末尾是一个IMAGE_DATA_DIRECTORY结构数组。此域包含了这个数组的元素个数。自从最早的Windows NT发布以来这个域的值一直是16。 |
上面表格中的 offset 值两个,前面的是 IMAGE_OPTIONAL_HEADER32 的 offset 值,后面的是 IMAGE_OPTIONAL_HEADER64,这是因为在 64 位版本中一些域被扩展为 64 位值,而 BaseOfData 域在 64 位版中是不存在的。
Magic 域是一个幻数值,在 WinNT.h 里定义了一些常量值:
- 1
- 2
- 3
- 值 0x10b 说明这个 image 是 32 位的,PE 文件格式是 PE32
- 值 0x20b 说明这个 image 是 64 位的,PE 文件格式是 PE32+
PE32+ 代表的扩展的 PE 文件格式,扩展为 64 位。在 PE 文件规范中并没有 PE64 这种文件格式,Microsoft 官方的判断 image 文件是 32 位还是 64 位的方法就是通过 Magic 的值来确定。
在这些基本的域里可以获得 linker 的版本,text 节,data 节以及 bss 节的大小,下面看一看tlstest.exe 的 IMAGE_OPTIONAL_HEADER64 结构,从 0x00000110 - 0x000001FF
Magic 是 0x020B 表明这个 image 文件是 64 位的 PE32+格式,这里看出 linker 的版本是 14.00
.text 节的 size 是 0x00001000 bytes,.data 节的 size 是 0x00022000 bytes,还有一个重要的信息,代码的 RVA 入口在 0x00001338,它是基于 ImageBase 的 RVA 值。tlstest.exe 的 ImageBase 是0x0000000140000000,那么 tlstest.exe 映象的入口在:ImageBase + AddressOfEntryPoint = 0x0000000140000000 + 0x00001338 = 0x0000000140001338,这个地址是 __scrt_common_main() 的入口。
上面的 SectionAlinment 域值为 0x1000 是表示映象被加载到 virtual address 以是 0x1000(4K byte)为单位的倍数,也就是加载在 virtual address 的 4K 边界上。例如:tlstest.exe映象的 .text 节被加载到以 ImageBase(virtual address 为 0x00000001_40000000)为基址的第 1 个 4K 边界上(即:0x00000001_40001000 处),.rdata 节加载到第 2 个 4K 边界上(即:0x00000001_40002000 处)。FileAlinment 域的值为 0x200 表示执行映象从 0x200 为边界开始加载到 virtual address 上。例如,t.exe 映象中 code 位于文件映象的 0x400 处(0x200 边界上),因此,t.exe 文件映象 code 从 0x400 处开始加载到 virtual address。
4.3.3.1. IMAGE_DATA_DIRECTORY表格
在 IMAGE_OPTIONAL_HEADER64 未端是一组 IMAGE_DATA_DIRECTORY 结构的数组,上图所示:从 0x00000180 到 0x000001FF。在 WinNT.h 里定义为:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
这个结构十分重要,它用来描述 windows 执行映象中所使用的各种表格的位置和大小。VirtualAddress 域是一个 RVA(Relative Virtual Address)值,更明白一点就是:它是一个偏移量(基于 PE 文件头),Size 域表示这个表格有多大。IMAGE_NUMBEROF_DIRECTORY_ENTRIES 的值为 16,因此有 16 个 Directory,也就是表示,在执行映象中最多可以使用 16 个表格。
这 16 个 Driectory 指引出 16 不同格式的 table,实际上这 16 个表格是固定的,对于这些表格 Microsoft 都作了统一的规定,在 WinNT.h 里都作了定义:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
从定义上得出,第 0 项是 export table(导出表),第 1 项是 import table(导入表)等等,在 Microsoft 的 MSDN 网站里有一个知识点介绍:http://msdn.microsoft.com/en-us/library/ms680305(VS.85).aspx
下面,我将这些表格归纳如下:
表项 | 表格 |
---|---|
0 | export table |
1 | import table |
2 | resource table |
3 | exception table |
4 | certificate table |
5 | base relocation table |
6 | debug |
7 | architecute |
8 | global pointer |
9 | TLS table |
10 | load configuration table |
11 | bound import |
12 | import address table |
13 | delay import descriptor |
14 | CLR runtime header |
15 | reserved, must bo zero |
在 WinNT.h 有对这些 table 的结构的全部定义。
那么,下面我们来看一看tlstest.exe 映象中使用了哪些表?
上面表格显示我们的示例程序 t.exe 仅仅使用了 8 个 driectory 表格:import table、resource table 、exception table 、relocation table 、debug table、TLS table、load configuration table以及 import address table。
4.4. section 表
IMAGE_NT_HEADER 结构后面紧接着就是 section table(节表)结构。现在来看一看tlstest.exe 的 section 表,从 0x00000200 - 0x0000033F,共 320 bytes
这个节表结构在 WinNT.h 中定义为
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
每个 section 表的大小为 40 bytes,section 表的作用是指出 image 映象 section 所在tlstest.exe 映象的 IMAGE_FILE_HEADER 结构的 NumberOfSections 域里已经指出tlstest.exe 映象中包含有 8 个 sections,因此整个 section 表的大小为:320 bytes。
tlstest.exe的 section 表如下:
这 8 个 section 分别是:
- .text 节
- .rdata 节
- .data 节
- .pdata 节
- .tls 节
- .gfids 节
- .rsrc 节
- .reloc 节
在 IMAGE_SECTION_HEADER 结构的第 1 个域 Name,用来标识 section table 的名字。它的长度固定为 8 bytes(前面定义的宏),这将意味着,不存在超过 8 bytes 的节表名。接下来使用 VirtualSize 来用表示 section talbe 大小。VirtualAddress 表示 section table 的 RVA,它是基于 ImageBase 的 RVA 值,它指出 section 的所在,SizeOfRawData 是在 image 文件里占有的空间,它是 FileAlignment 的倍数,即:0x200 的倍数,也就是说 0x200 的边界。PointerToRawData是 section 在 image 文件的位置,同样也是 FileAligment 即:0x200 边界上。
域 | size | 描述 |
---|---|---|
Name | 8 bytes | Section 表名字 |
VirtualSize | DWORD | Section 表的大小 |
VirtualAddress | DWORD | Section 表的 RVA,即:section 表的位置 |
SizeOfRawData | DWORD | section 表占用映像的大小,这个 size 是以 0x200 为单位的 |
PointerToRawData | DWORD | section 表在映像中的物理位置,即是:file 位置,而非 virtual 位置 |
PointerToRelocation | DOWRD | |
PointerToLinenumber | DWORD | |
NumberOfRelocation | WORD | |
NumberOfLineumbers | WORD | |
Characteristics | DWORD | section 的属性 flags,可用于 ‘ |
所有的 Characteristics 都在 WinNT.h 中有定义,下面是一些常用的 flags:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
下面以 .text 节为例,看看tlstest.exe的 .text 是什么。
.text 节的 Name 是 “.text”,VirtualSize 是 0x00000E08 bytes,.text 节在 RVA 为 0x00001000 的位置上,section 的分配单位为 0x1000(4K bytes),第 1 个 section 一般都分配在 0x1000 位置上,SizeOfRawData 为 0x1000,这是映像文件分配单位。PointerToRawData 为 0x400,说明 .text 节在映像文件的 0x400 处。Characteristics 是 0x60000020,说明 .text 是 executable/readable/code 属性。
本文转自:http://blog.csdn.net/liuyez123/article/details/51281905