零 前言
PE格式,是Windows的可执行文件的格式。Windows中的 exe文件,dll文件,都是PE格式。PE 就是Portable Executable 的缩写。Portable 是指对于不同的Windows版本和不同的CPU类型上PE文件的格式是一样的,当然CPU不一样了,CPU指令的二进制编码是不一样的。只是文件中各种东西的布局是一样的。
图 1.1
图1.1是 JIURL PEDUMP 打开 Win2K 中的 explorer.exe 的截图。JIURL PEDUMP 是我写的一个小工具,从文件开始的 Dos Header 一直到 Section Table,打开PE文件之后,点击相应结构,就会高亮显示文件中相应的部分。不过没有Sections。对了解 PE 格式有所帮助,可以很好的配合后面的介绍。可以到我的主页 http://jiurl.yeah.net 上下载。
一 PE文件格式概述
PE文件结构的总体层次分布如下所示
--------------
|DOS MZ Header |
|--------------|
|DOS Stub |
|--------------|
|PE Header |
|--------------|
|Section Table |
|--------------|
|Section 1 |
|--------------|
|Section 2 |
|--------------|
|Section ... |
|--------------|
|Section n |
--------------
1.1 DOS Header
PE文件最开始是一个简单的 DOS MZ header,它是一个 IMAGE_DOS_HEADER 结构。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随 MZ Header 之后的 DOS Stub。
1.2 DOS Stub
DOS Stub 是一个有效的 DOS 程序。当程序在DOS下运时,输出象 "This program cannot be run in DOS mode" 这样的提示。在 图1.1中就可以看到字符串 "This program cannot be run in DOS mode"。这是编译器生成的默认stub程序。你也可以通过链接选项 /STUB:filename 指定任何有效的MS-DOS可执行文件来替换它。
1.3 PE Header
紧接着 DOS Stub 的是 PE Header。它是一个 IMAGE_NT_HEADERS 结构。其中包含了很多PE文件被载入内存时需要用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从 DOS MZ header 中找到 PE header 的起始偏移量。因而?DOS stub 直接定位到真正的文件头 PE header。
1.4 Section Table
PE Header 接下来的数组结构 Section Table (节表)。如果PE文件里有5个节,那么此 Section Table 结构数组内就有5个成员,每个成员包含对应节的属性、文件偏移量、虚拟偏移量等。图1中的节表有4个成员。
1.5 Sections
PE文件的真正内容划分成块,称之为sections(节)。Sections 是以其起始位址来排列,而不是以其字母次序来排列。通过节表提供的信息,我们可以找到这些节。图1.1所示的 explorer.exe 中有4个节。程序的代码,资源等等就放在这些节中。
二 PE文件格式中的结构及其作用
这部分内容请参考下面的几篇文章,使用工具 JIURL PEDUMP 有助于快速了解。
大家不要因此,而失望不看,本文重点在后三篇,本篇只是为了有个交代,和介绍些相关内容。
注意,在WINNT.H中,有所有PE相关结构的定义。我们用到的结构定义都来自那里。
Microsoft Portable Executable and Common Object File Format Specification
MSDN
《Windows95系统程式设计大奥秘》
第8章 PE 与COFF OBJ 档案格式
Matt Pietrek 著 侯杰译
Iczelion的PE教程
PE学习笔记(一) rivershan
PE学习笔记(二) rivershan
Inside Windows
An In-Depth Look into the Win32 Portable Executable File Format
Matt Pietrek
已经被人翻译了。
Inside Windows
An In-Depth Look into the Win32 Portable Executable File Format
Matt Pietrek
三 几个要注意的问题
3.1 文件中大量的空白
在 PE Header结构 中的 OptionalHeader 结构中的成员 FileAlignment 的值是文件中节的对齐粒度,单位是字节,这个值应该是2的n次方,范围从512到64k。如果这里的值是512,那么PE文件中的节的长度都是512字节的整数倍,内容不够的部分用0填充。比如一个PE文件的 FileAlignment 为200h(十进制512),它的第一个节在400h处,长度为100h,那么从文件400h到500h中为这一节的内容,而文件对齐粒度是200h,所以为了使这一节长度为FileAlignment的整数倍,500h到600h会被用零填充。而下一个节的开始地址为600h。用16进制编辑器打开PE文件,就可以看到这种情况,PE文件头的内容结束到第一个节开始之间的地方,每一个节中内容结束到下一节开始的地方都会有大量的空白。VC6编译链接时默认的FileAlignment为1000h(4k),可以使用链接选项 /ALIGN:number 来改变这个值。比如把4k改成512时,可以明显减小生成文件的大小。
3.2 big-endian和little-endian
PE Header中的 FileHeader 的成员 Machine 中的值,根据WINNT.H中的定义,对于 Intel CPU 应该为 0x014c。但是你用16进制编辑器打开PE文件,看到这个WORD显示的却是 4c 01 。你看到的并没有错,你看到的 4c 01 就是 0x014c,只不过由于 intel cpu 是ittle-endian,所以显示出来是这样的。对于 big-endian 和 little-endian,请看下面的例子。
比如一个整形int变量。长为四个字节。
这个变量的地址比如为n。
则这个变量的4个字节地址分别为n,n+1,n+2,n+3。
当 这个整形变量 的值为 0x12345678 时,
对于 big-endian 来说
地址n+0的那个字节中的值为 0x12
地址n+1的那个字节中的值为 0x34
地址n+2的那个字节中的值为 0x56
地址n+3的那个字节中的值为 0x78
按如下方式就会显示为
n n+1 n+2 n+3
12 34 56 78
对于 ittle-endian 来说
地址n+0的那个字节中的值为 0x78
地址n+1的那个字节中的值为 0x56
地址n+2的那个字节中的值为 0x34
地址n+3的那个字节中的值为 0x12
按如下方式就会显示为
n n+1 n+2 n+3
78 56 34 12
Intel使用的是 ittle-endian 。
一个整形 int 变量 i,的地址是&i,那么这个i的四个字节是&i,&i+1,&i+2,&i+3。
可以用这样一个程序看到。
#include <stdio.h>
#include <conio.h>
void main()
{
int i;
char* p;
p=(char*)&i;
printf("i: ");
scanf("%x",&i);
printf("\n");
printf("&i+0: %x\n",*p);
printf("&i+1: %x\n",*(p+1));
printf("&i+2: %x\n",*(p+2));
printf("&i+3: %x\n",*(p+3));
printf("\n");
printf("&i-4: %x\n",*(p-4));
printf("&i-3: %x\n",*(p-3));
printf("&i-2: %x\n",*(p-2));
printf("&i-1: %x\n",*(p-1));
printf("\n");
printf("&i+4: %x\n",*(p+4));
printf("&i+5: %x\n",*(p+5));
printf("&i+6: %x\n",*(p+6));
printf("&i+7: %x\n",*(p+7));
getch();
}
当我们输入 12345678 的时候可以看到,输出
i: 12345678
&i+0: 78
&i+1: 56
&i+2: 34
&i+3: 12
&i-4: 7c
&i-3: ffffffff
&i-2: 12
&i-1: 0
&i+4: ffffffc0
&i+5: ffffffff
&i+6: 12
&i+7: 0
正是&i,&i+1,&i+2,&i+3这四个字节中储存了i的值。
对于int,WORD,DWORD等等都要注意 big-endian 和 little-endian 。
3.3 RVA (Relative Virtual Address) 相对虚拟地址
RVA是一个简单的相对于PE载入点的内存偏移。比如,PE载入点为0X400000,那么代码节中的地址0X401000的RVA为(target address) 0x401000 - (load address)0x400000 = (RVA)0x1000.换句话说 RVA是0x1000,载入点为0X400000,那么该RVA的在内存中的实际地址就是0X401000。注意一下RVA是指内存中,不是指文件中。是指相对于载入点的偏移而不是一个内存地址,只有RVA加上载入点的地址,才是一个实际的内存地址。
3.4 三种不同的地址
PE的各种结构中,涉及到很多地址,偏移。有些是指在文件中的偏移,有的是指在内存中的偏移。一定要搞清楚,这个地址或者是偏移,是指在文件中,还是指在内存中。第一种,文件中的地址。比如用16进制编辑器打开PE文件,看到的地址(偏移)就是文件中的地址,我们使用某个结构的文件地址,就可以在文件中找到该结构。第二种,文件被整个映射到内存时,比如某些PE分析软件,把整个PE文件映射到内存中,这时是内存中的地址,如果知道某一个结构在文件中的地址的话,那么这个PE文件被映射到内存之后该结构的在内存中的地址,可以用文件中的地址加上映射内存的地址,就可以得到在该结构内存中的地址。第三种,执行PE时,PE文件会被载入器载入内存,这时经常需要的是RVA。比如知道一个结构的RVA,那么载入点加上RVA就可以得到内存中该结构的实际地址。比如,某个程序,我们用16进制编辑器打开它,看到PE Header开始在16进制编辑器显示为000000C8的地方。于是我们在16进制编辑器显示为000000FC的地方找到了OptionalHeader的ImageBase,值为400000h,那么当这个程序被执行时,如果内存中400000h处没有使用,该程序就会被载入到那里。而我用CreateFileMapping将这个PE文件映射到内存中时,可以得到块内存的地址为5505024。对于映射入内存的这个PE文件,我们就可以在内存中000000FCh+05505024h=5505120处找到这个PE的OptionalHeader的ImageBase。
3.5 几个重要结构的说明
PE Header 的 FileHeader 的 NumberOfSections:这是一个很重要的字段,用来确定文件中节的数目。
PE Header 的 OptionalHeader 的 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:一个IMAGE_DATA_DIRECTORY 结构数组。到目前为止这个数组的长度是固定的,有16个元素,这16个元素分别代表
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
每个元素是一个IMAGE_DATA_DIRECTORY结构,IMAGE_DATA_DIRECTORY定义如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
第一个字段是一个RVA,第二个字段是一个大小。
Section Table 节表紧跟在OptionalHeader之后,是一个IMAGE_SECTION_HEADER结构的数组。该数组中成员的个数由 File Header (IMAGE_FILE_HEADER) 结构中 NumberOfSections 域的域值来定。节表中的成员是IMAGE_SECTION_HEADER 结构,IMAGE_SECTION_HEADER 结构的长度固定,长40个字节。整个Section Table 的长度不固定,等于 NumberOfSections*sizeof(IMAGE_SECTION_HEADER)。IMAGE_SECTION_HEADER 结构中,
VirtualAddress:本节的RVA(相对虚拟地址)。
PointerToRawData:这是本节基于文件的偏移量。
3.6 DOS MZ Header 中的 MZ
MZ是MZ格式的主要作者 Mark Zbikowski 的名字的缩写。
完