︱ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄︱
︱ MS-DOS ︱
︱ MZ 头部 ︱--------------> 64 byte
︱ ︱
︱  ̄  ̄  ̄  ̄  ̄  ̄︱
︱ MS-DOS ︱
︱ 实模式残余程序 ︱--------------> 112 byte
︱ ︱
︱ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄︱
︱ PE文件标志 ︱--------------> 4 byte
︱ ︱
︱ ̄  ̄  ̄  ̄  ̄  ̄ ︱
︱ PE文件头 ︱--------------> 20 byte
︱ ︱
︱ ̄  ̄  ̄  ̄  ̄  ̄ ︱
︱ PE文件可选头 ︱--------------> 224 byte
︱ ︱
︱ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄︱
︱ 各段头部 ︱--------------> n * 40 byte
︱ ︱
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄





struct _IMAGE_NT_HEADERS {
这个结构含有3个成员:
第一个成员表示“PE文件标识”,可以看到他是一个DWORD类型,因此占4个字节,它是PE开始的标记,是一个#define IMAGE_NT_SIGNATURE定义了这个值,对Windows程序这个值必须为“50450000”。DOS头部的e_lfanew字段正是指向“PE\0\0”:

成员TimeDateStamp,占4个字节,表示文件创建日期和时间,从1970.1.1 00:00:00以来的秒数,我们这里填“0000”即可。
成员PointerToSymbolTable,占4个字节,表示符号表的指针,主要用于调试,在这里填“0000”。
成员SizeOfOptionalHeader,占2个字节,表示后面的“PE文件可选头 ”部分所占空间大小,我们已经知道“PE文件可选头 ”的大小是224 byte,转换成十六进制就是E0,所以这里的值为“E000”
成员Characteristics,占2个字节,表示关于文件信息的标记,比如文件是exe还是dll。这个值实际上是二进制位进行或运算得到的值。
Bit 0 :置1表示文件中没有重定向信息。每个段都有它们自己的重定向信息。这个标志在可执行文件中没有使用,在可执行文件中是用一个叫做基址重定向目录表来表示重定向信息的,这将在下面介绍。
Bit 1 :置1表示该文件是可执行文件(也就是说不是一个目标文件或库文件)。
Bit 2 :置1表示没有行数信息;在可执行文件中没有使用。
Bit 3 :置1表示没有局部符号信息;在可执行文件中没有使用。
Bit 4 :
Bit 7
Bit 8 :表示希望机器为32位机。这个值永远为1。
Bit 9 :表示没有调试信息,在可执行文件中没有使用。
Bit 10:置1表示该程序不能运行于可移动介质中(如软驱或CD-ROM)。在这种情况下,OS必须把文件拷贝到交换文件中执行。
Bit 11:置1表示程序不能在网上运行。在这种情况下,OS必须把文件拷贝到交换文件中执行。
Bit 13:置1表示文件是一个动态链接库(DLL)。
Bit 14:表示文件被设计成不能运行于多处理器系统中。
Bit 15:表示文件的字节顺序如果不是机器所期望的,那么在读出之前要进行交换。在可执行文件中它们是不可信的(操作系统期望按正确的字节顺序执行程序)。
注意,因为我们写的是可执行程序,所以Bit 1必须置为1,其他的按照需要置位即可,这里我们仅将第二位置位,由此得到成员7的值为“0200”。
成员1,占2个字节,表示文件的格式,值为0x010B表示.EXE文件,为0x0107表示ROM映像,因为我们写的是一个可执行程序,所以此值应该为“0B01”。
成员2,占1个字节,表示链接器的主版本号,此值不会影响程序的执行,我们这里填充零,此值为“00”。
成员3,占1个字节,表示链接器的幅版本号,此值不会影响程序的执行,我们这里填充零,此值为“00”。
成员4,占4个字节,表示可执行代码的长度,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员5,占4个字节,表示初始化数据的长度(数据段)。此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员6,占4个字节,表示未初始化数据的长度(bss段)。此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
(在介绍成员7之前,有必要了解一个很重要的知识------文件映射到内存。在可执行程序运行之前,PE加载器将把PE文件加载到进程空间的内存中去,并且初始化每个段实体。那么加载到内存中的哪个地址去呢?这将由IMAGE_OPTIONAL_HEADER32结构的成员10的值指出加载的起始地址(又叫基地址)。这个值通常是“00400000”, 那么PE文件的首地址“00000”就被映射到内存地址“00400000”处,那么相对于文件偏移10个字节的地址为“00010”,被映射到内存后的偏移也应该是10个字节,映射后的地址应该为“00400010”。)
成员7,4个字节,表示代码的入口RVA(文件映射到内存的偏移地址)地址,程序从这儿开始执行。PE装载器准备运行的PE文件的第一个指令的RVA。若您要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。那么这个值我们怎么得到呢?我们知道在文件中有个.text段,他包含了所有的代码,我们可以从中找到我们的入口地址,在这里就是.text段里的第一行代码,也就是.text段的首地址,而在.text段头部就给出了他映射到内存后的首地址的偏移,我们找到他取出添到此处,这里为“00100000”。(此处不理解没关系,我们讲完段结构后自能迎刃而解。)
成员8,4个字节,表示可执行代码起始位置。当然就是.text段的首地址,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员9,4个字节,表示初始化数据的起始位置,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员10,4个字节,就是上面所讲的文件映射到内存是的基地址。PE文件的优先装载地址。通常设为“00400000”,PE装载器将尝试把文件装到虚拟地址空间的00400000h处。字眼"优先"表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。我们这里的值设为“00400000”。
成员11,4个字节,表示段加载后在内存中的对齐方式。内存中节对齐的粒度。例如,如果该值是4096 (1000h),那么每节的起始地址必须是4096的倍数。若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始,即使401000h和402000h之间还有很多空间没被使用。因为Windows管理内存采用分页管理的方式,而每页的大小为4k,也就是1000h,所以我们这个值为“00100000”。
成员12,4个字节,表示段在文件中的对齐方式。文件中节对齐的粒度。例如,如果该值是(200h),,那么每节的起始地址必须是512的倍数。若第一节从文件偏移量200h开始且大小是10个字节,则下一节必定位于偏移量400h: 即使偏移量512和1024之间还有很多空间没被使用。此值最好设为200h,所以该成员的值为“00020000”。
成员13,2个字节,表示操作系统主版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
成员14,2个字节,表示操作系统副版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
成员15,2个字节,表示程序主版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
成员16,2个字节,表示程序副版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
成员17,2个字节,表示子系统主版本号。win32子系统版本。PE文件是专门为Win32设计的,该子系统版本必定是4.0那么此处值为“04”。
成员18,2个字节,表示子系统副版本号,根据上面所说,此值应为“00”。
成员19,2个字节,此值一般为“00”。
成员20,4个字节,表示程序调入后占用内存大小(字节),等于所有段的长度之和。所有头和节经过节对齐处理后的大小。我们知道,我们文件PE结构总长小于1000h,但是内存中的对齐粒度是1000h,所以PE结构被映射后要占1000h,尽管很多空间没有使用,另外我们有3个段,每个段的长度小于1000h,但是被映射后同样要占1000h,所以总共占用内存的大小为1000h + 3 * 1000h = 4000h,因此此值为“00400000”。
成员21,4个字节,表示所有文件头的长度之和(从文件开始到第一个段之间的大小)。所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。可以以此值作为PE文件第一节的文件偏移量。那么我们怎么得到这个值呢?我们的PE文件结构总大小为:64 + 112 + 4 + 20 + 224 + 3 * 40 = 544 byte 转化成十六进制为220h,那么此值就是220h吗?
不是的,因为我们文件中的对齐粒度是200h,那么220h实际上要占用400h的空间,所以此值为“00040000”。
成员22,4个字节,表示校验和。它仅用在驱动程序中,在可执行文件中可能为0。它的计算方法Microsoft不公开,在imagehelp.dll中的CheckSumMappedFile()函数可以计算它,此处我们设为填充零,此值为“00000000”。
成员23,2个字节,表示NT子系统,可能是以下的值:
IMAGE_SUBSYSTEM_NATIVE (1) 不需要子系统。用在驱动程序中。
IMAGE_SUBSYSTEM_WINDOWS_GUI(2) WIN32 graphical程序(它可用AllocConsole()来打开一个控制台,但是不能在一开始自动得到)。
IMAGE_SUBSYSTEM_WINDOWS_CUI(3) WIN32 console程序(它可以一开始自动建立)。
IMAGE_SUBSYSTEM_OS2_CUI(5) OS/2 console程序(因为程序是OS/2格式,所以它很少用在PE)。
IMAGE_SUBSYSTEM_POSIX_CUI(7) POSIX console程序。
Windows程序总是用WIN32子系统,所以只有2和3是合法的值。也就是说此值必须为2或3,如果是3,那么程序运行后会自动打开一个控制台,我们为了看一下效果,这里设为3,此值为“0300“。
成员24,2个字节,表示Dll状态,我们这里填充零,此值为“0000”。
成员25,4个字节,保留堆栈大小,我们这里填充零,此值为“00000000”。
成员26,4个字节,启动后实际申请的堆栈数,可随实际情况变大,我们这里填充零,此值为“00000000”。
成员27,4个字节,保留堆大小,我们这里填充零,此值为“00000000”。
成员28,4个字节,实际堆大小,我们这里填充零,此值为“00000000”。
成员29,4个字节,装载标志,我们这里填充零,此值为“00000000”。
成员30,4个字节,在讲这个成员之前,我们应该先了解成员31,成员31实际上是一个IMAGE_DATA_DIRECTORY结构的数组,成员30的值就是表示该数组的大小。通常有16个元素,所以此值为:“10000000”。
IMAGE_DATA_DIRECTORY结构有两个成员,各占4个字节,那么也就得到成员31的总大小:2 * 4 * 16 = 128byte。
中每个元素代表一个目录表,每个目录表表示的目录如下:
IMAGE_DIRECTORY_ENTRY_EXPORT (0)导出目录用于DLL
IMAGE_DIRECTORY_ENTRY_IMPORT (1导入目录
IMAGE_DIRECTORY_ENTRY_RESOURCE (2)资源目录
IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)异常目录
IMAGE_DIRECTORY_ENTRY_SECURITY (4)安全目录
IMAGE_DIRECTORY_ENTRY_BASERELOC (5)重定位表
IMAGE_DIRECTORY_ENTRY_DEBUG (6)调试目录
IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)描述版权串
IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)机器值
IMAGE_DIRECTORY_ENTRY_TLS (9)Thread local storage目录
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)Load configuration 目录
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)Bound import directory目录
IMAGE_DIRECTORY_ENTRY_IAT (12)Import Address Table输入地址表目录
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
是不是所有的目录表都要关心呢?其实要把这些目录表都研究清楚是个很大的课题,对于我们这个程序,只需关心第2个元素,导入目录,它标识了我们的程序从其他模块导入的函数信息。因为我们要显示一个消息框,所以要导入user32.dll库中的MessageBoxA函数,程序退出,又要导入kernel32.dll库中的ExitProcess函数,这个目录表需要使用。然而上面已说明每个目录是一个IMAGE_DATA_DIRECTORY结构,该结构具有两个成员,第一个成员表示目录表的起始RVA地址,第二个成员表示目录表的长度。这两个值要根据.rdata段实体来确定,暂时先不填写。为了记录该位置,我们先都填写为x,即:
“xxxxxxxx","xxxxxxxx"。其余的统统添零即可。
接下来是各段头部,我们这里有3个段,.text(代码段), .rdata(只读数据段),data(全局变量数据段)。每段是一个IMAGE_SECTION_HEADER 结构,具有10个成员。首先我们来看.text段。

成员2,4个字节,表示有效代码所占的字节数。我们这里所有代码数一下总共26h个,固此值为“26000000”。
成员3,4个字节,表示在.text段映射到内存中的起始地址,那么这个值如何得来呢?我们知道.text是紧跟PE结构后的,然后整个PE结构映射到内存后占的大小为1000h(因为PE结构小于1000h个字节,而对齐力度粒度是1000h),那么此值便得到了,为“00100000”。
成员4,4个字节,表示.text段在文件中所占的大小。因为我们的实际代码只有26h个字节,那么这个值是不是26h呢?并不是,一定要注意段在文件中的对齐粒度是200h,所以此值为“00020000”。 成员5,4个字节,表示.text段在文件中的起始地址,上面已经计算过PE文件的总长度为400h,他实际上也就是.text的起始偏移地址,此值为“00040000”。
成员6,7,8,9,均占2个字节,都仅用于目标文件,我们这里统统填为零。
成员10,4个字节。包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。这个值实际上是二进制位进行或运算得到的值。各二进制位表示的意义如下:
bit 5 (IMAGE_SCN_CNT_CODE),置1,节内包含可执行代码。
bit 6 (IMAGE_SCN_CNT_INITIALIZED_DATA)置1,节内包含的数据在执行前是确定的。
bit 7 (IMAGE_SCN_CNT_UNINITIALIZED_DATA) 置1,本节包含未初始化的数据,执行前即将被初始化为0。一般是BSS.
bit 9 (IMAGE_SCN_LNK_INFO) 置1,节内不包含映象数据除了注释,描述或者其他文档外,是一个目标文件的一部分,可能是针对链接器的信息。比如哪个库被需要。
bit 11 (IMAGE_SCN_LNK_REMOVE) 置1,在可执行文件链接后,作为文件一部分的数据被清除。
bit 12 (IMAGE_SCN_LNK_COMDAT) 置1,节包含公共块数据,是某个顺序的打包的函数。
bit 15 (IMAGE_SCN_MEM_FARDATA) 置1,不确定。
bit 17 (IMAGE_SCN_MEM_PURGEABLE) 置1,节的数据是可清除的。
bit 18 (IMAGE_SCN_MEM_LOCKED) 置1,节不可以在内存内移动。
bit 19 (IMAGE_SCN_MEM_PRELOAD)置1, 节必须在执行开始前调入。
Bits 20 to 23指定对齐。一般是库文件的对象对齐。
bit 24 (IMAGE_SCN_LNK_NRELOC_OVFL) 置1, 节包含扩展的重定位。
bit 25 (IMAGE_SCN_MEM_DISCARDABLE) 置1,进程开始后节的数据不再需要。
bit 26 (IMAGE_SCN_MEM_NOT_CACHED) 置1,节的 数据不得缓存。
bit 27 (IMAGE_SCN_MEM_NOT_PAGED) 置1,节的 数据不得交换出去。
bit 28 (IMAGE_SCN_MEM_SHARED) 置1,节的数据在所有映象例程内共享,如DLL的初始化数据。
bit 29 (IMAGE_SCN_MEM_EXECUTE) 置1,进程得到“执行”访问节内存。
bit 30 (IMAGE_SCN_MEM_READ) 置1,进程得到“读出”访问节内存。
bit 31 (IMAGE_SCN_MEM_WRITE)置1,进程得到“写入”访问节内存。
在我们这里,因为这是代码段,所以bit 5 (IMAGE_SCN_CNT_CODE)位置1,一般代码段都含有初始化数据,那么bit 6 (IMAGE_SCN_CNT_INITIALIZED_DATA)位置1,有因为代码段的代码可以执行的,所以
bit 29 (IMAGE_SCN_MEM_EXECUTE) 置1,那么这3个二进制位进行或运算最终得到此成员值“20000060”。
最后的编写结果如下:


首先编写.text段,他紧接着PE结构后面,但是我们如何编写这些内容呢?前面已经说过,.text段中存放所有的可执行代码(机器码),我们可以通过先编写汇编指令(调用MessageBoxA和ExitProcess两个函数),然后反汇编出机器代码抄到这里就可以了。这里有一点要注意,我们在为MessageBoxA函数传递参数时,如何将“Hello Kinney!This is the first PE program!”字符串这就要用到我们的.data(全局变量数据段)了,我们可以把这两个字符串放到这个段中,然后把字符串的偏移首地址作为参数传给MessageBoxA即可。因为要以200h对齐,所以剩余部分用零补齐,最终得到的代码如下:






(注意对齐问题,补足200h字节。)

