更新时间:2024-04-24 10:49
PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)
一个操作系统的可执行文件格式在很多方面是这个系统的一面镜子。虽然学习一个可执行文件格式通常不是一个程序员的首要任务,但是你可以从这其中学到大量的知识。在这篇文章中,我会给出 Microsoft 的所有基于win32系统(如winnt,win9x)的可移植可执行(PE)文件格式的详细介绍。在可预知的未来,包括Windows2000, PE文件格式在 MicroSoft 的操作系统中扮演一个重要的角色。如果你在使用 Win32 或 Winnt ,那么你已经在使用 PE 文件了。甚至你只是在 Windows3.1 下使用 Visual C++编程,你使用的仍然是 PE 文件(Visual C++ 的 32 位MS-DOS扩展组件用这个格式)。简而言之,PE 格式已经普遍应用,并且在不短的将来仍是不可避免的。
线程局部变量
我最后不会让你盯住无穷无尽的十六进制Dump,也不会详细讨论页面的每一个单独的位的重要性。代替的,我会向你介绍包含在 PE 文件中的概念,并且将他们和你每天都遇到的东西联系起来。比如,线程局部变量的概念,如下所述:
declspec(thread) int i;
除了一个不同的可执行文件格式, MicroSoft 还引入了一个用它的编译器和汇编器生成的新的目标模块格式。这个新的 OBJ 文件格式有许多和PE 文件共同的东东。我做了许多无用功去查找这个新的OBJ 文件格式的文档。所以我以自己的理解对它进行解析,并且,在这里,除了 PE 文件,我会描述它的一部分。
微软系统工具
大家都知道,Windows NT继承了 VAX? VMS? 和 UNIX? 的传统。许多 Windows NT 的创始人在进入微软前都在这些平台上进行设计和编码。当他们开始设计 Windows NT 时,很自然的,为了最小化项目启动时间,他们会使用以前写好的并且已经测试过的工具。用这些工具生成的并且工作的可执行和 OBJ 文件格式叫做 COFF (Common Object File Format 的首字母缩写)。COFF 的相对年龄可以用八进制的域来指定。COFF 本身是一个好的起点,但是需要扩展到一个现代操作系统如 Windows 95 和 Windows NT 的需要。这个更新的结果就是(PE格式)可移植可执行文件x86,Alpha,MIPS等等)上实现的WindowsNT 都使用相同的可执行文件格式。当然了,也有许多不同的东西如二进制代码的CPU指令。重要的是操作系统的装入器和程序设计工具不需要为任何一种CPU完全重写就能达到目的。
MicroSoft 抛弃现存的32位工具和可执行文件格式的事实证实了他们想让 WindowsNT 升级并且运行的更快的决心。为16位Windows编写的虚拟设备驱动程序用一种不同的32位文件布局--LE文件格式--WindowsNT出现很早以前就存在了。比这更重要的是对 OBJ 文件的替换。在 WindowsNT 的 C编译器以前,所有的微软编译器都用 Intel 的 OMF ( Object Module Format ) 规范。就像前面提到的,MicroSoft 的 Win32编译器生成 COFF 格式的 OBJ 文件。一些微软的竞争者,如 Borland 和 Symentec ,选择放弃了 COFF 格式并坚持 Intel 的 OMF文件格式。这样的结果是制作 OBJ 和 LIB 的公司为了使用多个不同的编译器,不得不为每个不同的编译器分发这些库的不同版本(如果他们不这么做)。
PE 文件格式在 winnt.h 头文件文件头和 NE 文件头移入新的PE文件头之前,这个块就开始于一个小栏。WINNT.H提供PE文件用到的生鲜数据结构的定义,但只有很少有助于理解这些数据结构和标志变量的注释。不管谁为PE文件格式写出这样的头文件都肯定是一个信徒无疑(突然持续地冒出Michael J. O'Leary的名字来)。描述名字,连同深嵌的结构体和宏。当你配套winnt.h进行编码时,类似下面这样的表达式并不鲜见:
pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG]
.VirtualAddress;
为了有助于逻辑的理解这些winnt.h中的信息,阅读可移植可执行和公共对象文件格式的规格说明,这些在MSDN既看光盘中是可用的,一直包括到2001年8月。
现在让我们转换到COFF格式的OBJ文件的主体上来,WINNT.H包括COFF OBJ和LIB的结构化定义和类型定义。不幸的是,我还没有找到上面提到的可执行文件格式的类似文档。既然PE文件和COFF OBJ文件是如此的相似,我决定是时间把这些文件带到重点上来,并且把它们也文档化。仅仅读过了关于PE文件的组成,你自己也想Dump一些PE文件来看这些概念。如果你用微软基于32位WINDOWS的开发工具,DUMPBIN 程序可以将PE文件和COFF OBJ/LIB文件转化为可读的形式。在所有的PEDump器中,DUMPBIN是最容易理解的。它恰好有一些很好的选项来反汇编它正解析的文件的代码块,Borland用户可以使用tdump来浏览PE文件,但tdump不能解析 COFF OBJ/LIB 文件。这不是一个重要的东西因为Borland的编译器首先就不生成 COFF 格式的OBJ文件。
我写了一个PE和COFF OBJ 文件的Dump程序--PEDUMP,我想提供一些比DUMPBIN更加可理解的输出。虽然它没有反汇编器以及和LIB库文件一起工作,它在其他方面和DUMPBIN是一样的,并且加入了一些新的特性来使它值得被认同。它的源代码在任何一个MSJ电子公报版上都可以找到,所有我不打算在这里把他全部列出。作为代替,我展示一些从PEDUMP得到的示例输出来阐明我为它们描述的概念。
程序代码
表 1 PEDUMP.C
file://--------------------/
MODULE标识模块
让我们复习一下几个透过PE可执行文件或一个DLL载入内存的代码(CODE)、数据(DATA)、资源(RESOURCES),除了代码和数据是你的程序直接使用的,一个模块还可以由WINDOWS用来确定数据和代码载入的位置的支撑数据结构组成。在16位WINDOWS中,这些支撑数据结构在模块数据库(用一个HMODULE来指示的段)中。在WIN32里面,这些数据结构在PE文件头中,这些我将会简要地解释一下。
PE文件的两因素
关于PE文件最重要的是,磁盘上的可执行文件和它被WINDOWS调入内存之后是非常相像的。WINDOWS载入器不必为从磁盘上载入一个文件而辛辛苦苦创建一个进程。载入器使用内存映射文件机制来把文件中相似的块映射到虚拟空间中。用一个构造式的分析模型,一个PE文件类似一个预制的屋子。它本质上开始于这样一个空间,这个空间后面有几个把它连到其余空间的机件(就是说,把它联系到它的DLL上,等等)。这对PE格式的DLL是一样容易应用的。一旦这个模块被载入,Windows 就可以有效的把它和其它内存映射文件同等对待。
和16位Windows不同的是。16位NE文件的载入器读取文件的一部分并且创建完全不同的数据结构在内存中表示模块。当数据段或者代码段需要载入时,载入器必须从全局堆中新申请一个段,从可执行文件中找出生鲜数据,转到这个位置,读入这些生鲜数据,并且要进行适当的修正。除此而外,每个16位模块都有责任记住当前它使用的所有段选择器,而不管这个段是否被丢弃了,如此等等。
对Win32来讲,模块所使用的所有代码,数据,资源,导入表,和其它需要的模块数据结构都在一个连续的内存块中。在这种形势下,你只需要知道载入器把可执行文件映射到了什么地方。通过作为映像的一部分的指针,你可以很容易的找到这个模块所有不同的块。
另一个你需要知道的概念是相对虚拟地址(RVA)。PE文件中的许多域都用术语RVA来指定。一个RVA只是一些项目相对于文件映射到内存的偏移。比如说,载入器把一个文件映射到虚拟地址0x10000开始的内存块。如果一个映像中的实际的表的首址是0x10464,那么它的RVA就是0x464。
(虚拟地址 0x10464)-(基地址 0x10000)=RVA 0x00464
为了把一个RVA转化成一个有用的指针,只需要把RVA值加到模块的基地址上即可。基地址是内存映射EXE和DLL文件的首址,在Win32中这是一个很重要的概念。为了方便起见,WindowsNT 和 Windows9x用模块的基地址作为这个模块的实例句柄(HINSTANCE数据段地址空间。术语INSTANCE仍然保持16位windows和32位Windows之间的连续性。在Win32中重要的是你可以对任何DLL调用(GetModuleHandle)得到一个指针去访问它的组件(译注)。
译注
如果 dllname 为 NULL,则得到执行体自己的模块句柄。这是非常有用的,如通常编译器产生的启动代码将取得这个句柄并将它作为一个参数hInstance传给WinMain !
和其它可执行文件格式一样,PE文件在众所周知的地方有一些定义文件其余部分面貌的域。首部就包含这样像代码和数据的位置和尺寸的地方,操作系统要对它进行干预,比如初始堆栈大小,和其它重要的块的信息,我将要简短的介绍一下。和微软其它可执行格式相比,主要的首部不是在文件的最开始。典型的PE文件最开始的数百个字节被DOS错误信息。当载入器把一个Win32程序映射到内存,这个映射文件的第一个字节对应于DOS残留部分的第一个字节。那是无疑的。和你启动的任一个基于Win32 的程序一起,都有一个基于DOS的程序连带被载入。
和微软的其它可执行格式一样,你可以通过查找它的起始偏移来得到真实首部,这个偏移放在DOS残留首部中。WINNT.H头文件包含了DOS残留程序的数据结构定义,使得很容易找到PE首部的起始位置。e_lfanew 域是PE真实首部的偏移。为了得到PE首部在内存中的指针,只需要把这个值加到映像的基址上即可。
file://忽/略类型转化和指针转化 ...
pNTHeader = dosHeader + dosHeader->e_lfanew;
一旦你有了PE主首部的指针,游戏就可以开始了!PE主首部是一个IMAGE_NT_HEADERS的结构,在WINNT.H中定义。这个结构由一个双字(DWORD)和两个子结构组成,布局如下:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
标志域用ASCII驱动程序(VxD)。LX表示这个文件是OS/2 2.0文件。
PE DWORD标志后的是结构 IMAGE_FILE_HEADER 。这个域只包含这个文件最基本的信息。这个结构表现为并未从它的原始COFF实现更改过。除了是PE首部的一部分,它还表现在微软Win32编译器生成的COFF OBJ 文件的最开始部分。IMAGE_FILE_HEADER的这个域显示在下面:
表2 IMAGE_FILE_HEADER Fields
WORD Machine
表示CPU的类型,下面定义了一些CPU的ID
0x14d Intel i860
0x14c Intel I386 (same ID used for 486 and 586)
0x162 MIPS R3000
0x166 MIPS R4000
WORD NumberOfSections
这个文件中的块数目。
DWORD TimeDateStamp
连接器产生这个文件的日期(对OBJ文件是编译器),这个域保存的数是从1969年12月下午4:00开始到现在经过的秒数。
DWORD PointerToSymbolTable
COFF符号表的文件偏移量。这个域只用于有COFF调试信息的OBJ文件和PE文件,PE文件支持多种调试信息格式,所以调试器应该指向数据目录的IMAGE_DIRECTORY_ENTRY_DEBUG条目。
DWORD NumberOfSymbols
COFF符号表的符号数目。见上面。
WORD SizeOfOptionalHeader
这个结构后面的可选首部的尺寸。在OBJ文件中,这个域是0。在可执行文件中,这是跟在这个结构后的IMAGE_OPTIONAL_HEADER结构的尺寸。
WORD Characteristics
关于这个文件信息的标志。一些重要的域如下:
0x0001 这个文件中没有重定位信息
0x0002 可执行文件映像(不是OBJ或LIB文件)
0x2000 文件是动态连接库,而非程序
其它域定义在WINNT.H中。
并非 IMAGE_OPTIONAL_HEADER 的所有域都是重要的(见图4)。比较重要,需要知道的是ImageBase 和 SubSystem 域。你可以忽略其它域的描述。
表3 IMAGE_FILE_HEADER 的域:
WORD Magic
表现为一些类别的标志字,通常是0X010B 。
BYTE MajorLinkerVersion
BYTE MinorLinkerVersion
生成这个文件的连接器的版本。这个数字以十进制显示比用十六进制好。一个典型的连接器版本是2.23。
DWORD SizeOfCode
所有代码块的进位尺寸。通常大多数文件只有一个代码块,所以这个域和 .TEXT 块匹配。
DWORD SizeOfInitializedData
已初始化的数据组成的块的大小(不包括代码段)。然而,和它在文件中的表现形式并不一致。
DWORD SizeOfUninitializedData
DWORD AddressOfEntryPoint
载入器开始执行这个程序的地址,即这个PE文件的入口地址。这是一个RVA,通常在 .text 块中。
DWORD BaseOfCode
代码块起始地址的RVA 。在内存中,代码块通常在PE首部之后,数据块之前。在微软的连接器产生的EXE文件中,这个值通常是0x1000 。Borland 的连接器 TLINK32 也一样,把映像第一个代码块的RVA和映像基址相加,填入这个域。
译注:这个域好像一直没有什么用
DWORD BaseOfData
数据块起始地址的RVA 。在内存中,数据块经常在最后,在PE首部和代码块之后。
译注:这个域好像也一直没有什么用
DWORD ImageBase
连接器创建一个可执行文件时,它假定这个文件被映射到内存中的一个指定的地方,这个地址就存在这个域中,假定一个载入地址可以使连接器优化以便节省空间。如果载入器真的把这个文件映射到了这个地方,在运行之前代码不需要任何改变。在为WindowsNT 创建的可执行文件中,默认的ImageBase 是0x10000。对DLL,默认是0x40000。在Window95中,地址0x10000不能用来载入32位EXE文件,因为这个区域在一个被所有进程共享的线性地址空间中。因此,微软把Win32可执行文件的默认基址改为0x40000,假定基址为0x10000 的老程序坐在Windows95 中需要更长的载入时间,这是因为载入器需要重定位基址。
DWORD SectionAlignment
映射到内存中时,每个块都必须保证开始于这个值的整数倍。为了分页的目的,默认的SectionAlignment 是 0x1000。
DWORD FileAlignment
在PE文件中,组成每个块的生鲜数据必须保证开始于这个值的整数倍。默认值是0x200 字节,也许是为了保证块都开始于一个磁盘扇区(一个扇区通常是 512 字节)。这个域和NE文件中的段/资源对齐(segment/resource alignment)尺寸是等价的。和NE文件不同的是,PE文件通常没有数百个的块,所以,为了对齐而浪费的通常空间很少。
WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
这个程序运行需要的操作系统的最小版本号。这个域有点含糊,因为Subsystem 域(后面将会说到)可以提供类似的功能。这个域在到目前为止的Win32中默认是1.0。
WORD MajorImageVersion
WORD MinorImageVersion
一个可由用户定义的域。这允许你有不同的EXE和DLL版本。你可以通过链接器
WORD MajorSubsystemVersion
WORD MinorSubsystemVersion
这个程序运行需要的最小子系统版本号。这个域的一个典型值是3.10 (表示WindowsNT 3.1)。
DWORD Reserved1
通常是 0 。
DWORD SizeOfImage
载入器必须关心的这个映像所有部分的大小总和。是从映像的开始到最后一个块结尾这段区域的大小。最后一个块结尾按SectionAlignment进位。
译注:这个很重要,可以大,但不可以小!
DWORD SizeOfHeaders
PE首部和块表的大小。块的实际数据紧跟在所有首部组件之后。
DWORD CheckSum
这个文件的CRC校验和。在微软可执行格式中,这个域被忽略并且置为0 。这个规则的一个例外情况是信任服务,这类EXE文件必须有一个合法的校验和。
WORD Subsystem
可执行文件的用户界面使用的子系统类型。WINNT.H 定义了下面这些值:
NATIVE 1 不需要子系统(比如设备驱动)
WINDOWS_GUI 2 在Windows图形用户界面子系统下运行
WINDOWS_CUI 3 在Windows字符子系统下运行(控制台程序)
OS2_CUI 5 在OS/2字符子系统下运行(仅对OS/2 1.x)
POSIX_CUI 7 在 Posix 字符子系统下运行
WORD DllCharacteristics
指定在何种环境下一个DLL的初始化函数(比如DllMain)将被调用的标志变量。这个值经常被置为0 。但是操作系统在下面四种情况下仍然调用DLL的初始化函数。
下面的值定义为:
1 DLL第一次载入到进程中的地址空间中时调用
2 一个线程结束时调用
4 一个线程开始时调用
8 退出DLL时调用
DWORD SizeOfStackReserve
为初始线程保留的虚拟内存总数。然而并不是所有这些内存都被提交(见下一个域)。这个域的默认值是0x100000(1Mbytes)。如果你在CreateThread 中把堆栈尺寸指定为 0 ,结果将是用这个相同的值(0x10000)。
DWORD SizeOfStackCommit
开始提交的初始线程堆栈总数。对微软的连接器,这个域默认是0x1000字节(一页),TLINK32 是两页。
DWORD SizeOfHeapReserve
为初始进程的堆保留的虚拟内存总数。这个堆的句柄可以用GetPocessHeap 得到。并不是所有这些内存都被提交(见下一个域)。
DWORD SizeOfHeapCommit
开始为进程堆提交的内存总数。默认是一页。
文件的含义
PE文件的意思是Portable Executable(可移植,可执行),它是win32可执行文件的标准格式.它的一些特性继承unix的COFF文件格式,同时保留了与旧版MS-DOS和WINDOWS的兼容.其可移植可执行意味着是跨win32平台的.
文件的层次结构
PE文件最前面紧随DOS MZ文件头的是一个DOS可执行文件(Stub).这使得PE文件成为一个合法的MS-DOS可执行文件.DOS MZ文件头后面是一个32位的PE文件标志0x50450000(IMAGE_NT_SIGNATURE),即PE00.接下来的是PE的映像文件头,包含的信息有该程序的运行平台,有多少个节,文件链接的时间,文件的命名格式.后面还紧跟一个可选映像头,包含PE文件的逻辑分布信息,程序加载信息,开始地址,保留的堆栈数量,数据段大小等.可选头还有一个重要的域,称为:“数据目录表”的数组,表的每一项都是指向某一节的指针.可选映像头后面紧跟的是节表和节.节通过节表来实现索引.实际上,节的内容才是真正执行的数据和程序.每一个节都有相关的标志.每一个节会被一个或多个目录表指向,目录表可通过可选头的“数据目录表”的入口找到.就像输出函数表或基址重定位表.也存在没有目录表指向的节.
文件层次解释
A.DOS STUB和DOS头
DOS插桩程序在大多数情况下由汇编器/编译器自动产生.通常它调用INT 21H服务9来显示上述字符串
文件头
在DOS STUB后是PE文件头(PE header).PE文件头是PE相关结构IMGAE_NT_HEADERS的简称,即NT映像头,存放PE整个文件信息发布的重要字段,包含了PE装载器用到的重要域.执行体在操作系统中执行时,PE装载器将从DOS MZ头中找到PE头文件的起始偏移量e_lfanew,从而跳过DOS STUB直接定位真正的PE文件.它由3部分组成:
(1)PE文件标志(4H字节)
PE文件标志0x50450000即PE00,标志着NT映像头的开始,也是PE文件中与windows有关内容的开始.
(2)映像文件(14H字节)
是NT映像文件的主要部分,包含PE文件的基本信息
(3)可选映像头
包含PE文件的逻辑分布信息.
C.节表
节表其实是紧跟NT映像文件的一个结构数组.其成员数目由映像文件头结构NumberOFSectios域的值来决定.
D.节
PE文件的真正内容划分为块,称之为节.节的划分基于各组数据的共同属性.惟有节的属性设置决定了节的特性和功能.典型的windows NT应用程序可以具有9个节:.texr,.bss.rdata,.data,.rsrc,edata,idata,pdata,.debug
判断一个文件是否为PE文件
var //检测指定文件是否有效PE文件
PEDosHead: TImageDosHeader;
PENTHead: TImageNtHeaders;
m_file: integer;
begin
Result := False;
m_file := FileOpen(filename, fmOpenRead or fmShareDenyNone); //只读和其它任意
if m_File > 0 then
try
FileSeek(m_file, 0, soFromBeginning); //将指针挪至文件头
FileRead(m_file, PEDosHead, SizeOf(PEDosHead)); //读PEDosHead结构
FileSeek(m_file, PEDosHead._lfanew, soFromBeginning); //将指针挪至_lfanew
FileRead(m_file, PENTHead, SizeOf(PENTHead)); //读PENTHead结构
finally
FileClose(m_file);
end;
if (PENTHead.Signature = IMAGE_NT_SIGNATURE) then //检验文件头部第一个字的值是否等于 IMAGE_DOS_SIGNATURE
Result := True;
end;
pe文件结构图