三年前,我曾经写了一个手工打造可执行程序的文章,可是因为时间关系,我的那篇文章还是有很多模糊的地方,我一直惦记着什么时候再写一篇完美的,没想到一等就等了三年。
因为各种原因直到三年后的今天我终于完成了它。
现在把它分享给大家,希望大家批评指正。
我们这里将不依赖任何编译器,仅仅使用一个十六进制编辑器逐个字节的手工编写一个可执行程序。
以这种方式讲解PE结构,通过这个过程读者可以学习PE结构中的PE头、节表以及导入表相关方面的知识。
为了简单而又令所有学习程序开发的人感到亲切,我们将完成一个Hello World! 程序。
功能仅仅是运行后弹出一个消息框,消息框的内容是Hello World!。
首先了解一下Win32可执行程序的大体结构,就是通常所说的PE结构。
如图1所示PE结构示意图:图1 标准PE结构图由图中可以看出PE结构分为几个部分:MS-DOS MZ 头部:所有PE文件必须以一个简单的DOS MZ 头开始。
有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ header 之后的DOS程序。
以此达到对Dos系统的兼容。
(通常情况DOS MZ header总共占用64byte)。
MS-DOS 实模式残余程序:实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,大多数情况下它是由汇编编译器自动生成。
通常,它简单调用中断21h,服务9来显示字符串"This program cannot run in DOS mode"。
(在我们写的程序中,他不是必须的,可以不予以实现,但是要保留其大小,大小为112byte,为了简洁,可以使用00来填充。
)PE文件标志:是PE文件结构的起始标志。
(长度4byte, Windows程序此值必须为0x50450000)PE文件头:是PE相关结构 IMAGE_NT_HEADERS 的简称,其中包含了许多PE装载器用到的重要域。
执行体在支持PE文件结构的操作系统中执行时,PE装载器将从DOS MZ header中找到PE header的起始偏移量,跳过了MS-DOS 实模式残余程序,直接定位到真正的文件头PE header,长度20byte。
PE文件可选头:虽然它的名字是“可选头部”,但是请确信:这个头部并非“可选”,而是“必需”的。
(长度 224byte )。
各段头部:又称节头部,一个Windows NT的应用程序典型地拥有9个预定义段(节),它们是“.text”、“.bss”、“.rdata”、“.data”、“.rsrc”、“.edata”、“.idata”、“.pdata”和“.debug”。
一些应用程序不需要所有的这些段,同样还有些应用程序为了自己特殊的需要而定义了更多的段。
(每个段头部占40byte,我们这里也不需要所有的段,仅需3个段。
)通常我们是将PE整个结构分成四个部分,把MS-DOS MZ 头部和MS-DOS 实模式残余程序作为第一部分,可以称他为DOS部分,而PE文件标志、PE文件头、PE文件可选头三个部分作为第二部分,称之为PE头部分,因为这部分才是Windows下真正需要的部分,所以从PE文件标志开始才是真正的PE部分。
各段头部是第三部分,称之为节表。
它详细描述了PE文件中各个节的详细信息。
最后就是各个节的实体部分了,称为节数据。
以上仅仅是对PE结构各部分的大体讲解。
接下来再手写这个Hello World!程序过程中,我将详细介绍每个部分的含义。
首先准备一下工具,一个十六进制编辑器足以。
我们这里使用VC++ 6.0所携带的十六进制编辑器,您也可以使用如WinHex等十六进制编辑工具。
打开VC,选择文件,新建菜单项,然后选择一个二进制文件,单击确定。
一切就绪了,下面就开始手写可执行程序,如图2所示:图2 VC6.0下的十六进制编辑器首先来完成“DOS MZ header”部分。
“DOS MZ header”的功能前面已经讲过,在这里不再重述,直接实现他。
“DOS MZ header”总共64byte,他对应的结构是IMAGE_DOS_HEADER ,在WINNT.H文件中有定义。
通过这个结构我们可以看到,这64字节被分成19个成员,每个成员都有特殊的含义,与其说我们是在逐字节的手写可执行程序,倒不如说我们是在逐个成员的写。
因为单独的一个字节并不一定具有什么意义。
我们在学习过程中,就是要按照官方的定义,将整个部分拆分成若干个成员,然后逐个成员的去学习。
(提示: 如果安装有VC开发环境,那么在其安装目录下有一个头文件WINNT.H,在这个头文件中定义了所有PE结构相关的各部分结构体。
如图3所示:)图3 VC安装目录下的WINNT.H头文件使用VC开发环境打开此文件,然后按快捷键Ctrl+F输入IMAGE_DOS_HEADER进行搜索,如图4所示:图4 VC下查找文件单击Find Next按钮即可得到如下搜索结果,如图5所示:图5可以看出IMAGE_DOS_HEADER,结构体的定义如下:typedef struct _IMAGE_DOS_HEADER { // DOS .EXE headerWORD e_magic; // Magic numberWORD e_cblp; // Bytes on last page of fileWORD e_cp; // Pages in fileWORD e_crlc; // RelocationsWORD e_cparhdr; // Size of header in paragraphsWORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS valueWORD e_sp; // Initial SP valueWORD e_csum; // ChecksumWORD e_ip; // Initial IP valueWORD e_cs; // Initial (relative) CS valueWORD e_lfarlc; // File address of relocation tableWORD e_ovno; // Overlay numberWORD e_res[4]; // Reserved wordsWORD e_oemid; // OEM identifier (for e_oeminfo)WORD e_oeminfo; // OEM information; e_oemid specificWORD e_res2[10]; // Reserved wordsLONG e_lfanew; // File address of new exe header} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;按照它的定义,我们分别完成各个成员。
第一个成员(e_magic)是个WORD类型,占2个字节,它被用于表示一个MS-DOS兼容的文件类型,他的值是固定的0x5A4D,所以在十六进制编辑器中输入“4D5A”。
(注意:因为我们是在十六进制编辑器下写数据,所以所有的数据格式都是十六进制式的。
但是我们在开发环境中通常在数据前添加“0x”用来表示十六进制数 ,即:0x5A4D。
而在十六进制编辑器中,直接写成“4D5A”即可。
后面内容都照此规定书写。
有一点需要说明,为什么十六进制值0x5A4D输入到十六进制编辑器中是4D5A呢?这是因为一个内存值,无论是占两个字节的WORD类型,还是占四个字节的DWORD类型等,如同我们学习数学中的十进制数值一样,都是有高低位之分的,从右向左位越来越高。
然而在十六进制编辑器中,十六进制位是自左向右依次增高。
因此按照高低位对齐的原则,值0x5A4D中,低位0x4D应该应该放到左边,0x5A应该放到右边。
也就得到了编辑器中的4D5A。
)第2个成员到第18个成员总共58个字节,是对DOS程序环境的初始化等操作,对于我们这个程序来说,没什么影响,我们通通用“00”来填充。
(如果您想对其进行详细了解,请查阅相关书籍。
)(提示:我们在此不可能把PE结构所有的知识点都面面俱到,因为他十分的庞大。
当然也没有必要对他作完全彻底的掌握,只需掌握关键的地方就可以了。
以后我们都将把不影响程序执行的成员填充为零,这样做,一方面使程序看起来简洁,另一方面可以使您快速定位PE结构中要重点掌握的地方。
)第19(e_lfanew)个成员非常重要,他是一个LONG类型,占4个字节,用来表示“PE文件标志”在文件中的偏移,单位是byte。
而从图5-1中可以看到“PE文件标志”紧随“MS-DOS 实模式残余程序”其后。
知道这一点,我们就可以计算一下,我们的“DOS MZ header”总共64 byte,后面的“MS-DOS 实模式残余程序”占112 byte, 64 + 112 = 176 byte。
但是要注意,我们这里的176是十进制的,转化成十六进制是0xB0。
因为是4个字节,其余三位字节应该以00补齐,所以最终的值为0x000000B0。
所以在我们的十六进制编辑器中按照高低对齐的原则应该填写“B0000000”。
接下来完成“MS-DOS 实模式残余程序”,笔者已经介绍,他是用在DOS下执行的,而我们所完成的HelloWorld程序是在win32下执行的。
所以这里的内容并不影响我们程序的执行。
因此这里直接用“00”来填充,注意总共112 byte。
这两部分完成之后代码如图6所示:图6 完成PE结构中Dos部分的编写接下来便进入真正主题,开始写真正的PE结构部分:微软将“PE文件标志”,“PE文件头”,“PE文件可选头”这三个部分用一个结构来定义,即:IMAGE_NT_HEADERS32在WINNT.H中可以搜索其定义,定义如下:typedef struct _IMAGE_NT_HEADERS {DWORD Signature;IMAGE_FILE_HEADER FileHeader;IMAGE_OPTIONAL_HEADER32 OptionalHeader;} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;可以看出这个结构含有3个成员:第一个成员(Signature)表示“PE文件标志”,是一个DWORD类型,占4个字节,它是PE开始的标记,对于Windows程序这个值必须为0x00004550,所以编辑器中填写“50450000”。