PE文件格式详解.docx

上传人:b****7 文档编号:9011381 上传时间:2023-02-02 格式:DOCX 页数:32 大小:165.72KB
下载 相关 举报
PE文件格式详解.docx_第1页
第1页 / 共32页
PE文件格式详解.docx_第2页
第2页 / 共32页
PE文件格式详解.docx_第3页
第3页 / 共32页
PE文件格式详解.docx_第4页
第4页 / 共32页
PE文件格式详解.docx_第5页
第5页 / 共32页
点击查看更多>>
下载资源
资源描述

PE文件格式详解.docx

《PE文件格式详解.docx》由会员分享,可在线阅读,更多相关《PE文件格式详解.docx(32页珍藏版)》请在冰豆网上搜索。

PE文件格式详解.docx

PE文件格式详解

PE文件格式详解

(一)基础知识

什么是PE文件格式:

我们知道所有文件都是一些连续(当然实际存储在磁盘上的时候不一定是连续的)的数据组织起来的,不同类型的文件肯定组织形式也各不相同;PE文件格式便是一种文件组织形式,它是32位Window系统中的可执行文件EXE以及动态连接库文件DLL的组织形式。

为什么我们双击一个EXE文件之后它就会被Window运行,而我们双击一个DOC文件就会被Word打开并显示其中的内容;这说明文件中肯定除了存在那些文件的主体内容(比如EXE文件中的代码,数据等,DOC文件中的文件内容等)之外还存在其他一些重要的信息。

这些信息是给文件的使用者看的,比如说EXE文件的使用者就是Window,而DOC文件的使用者就是Word。

Window可以根据这些信息知道把文件加载到地址空间的那个位置,知道从哪个地址开始执行;加载到内存后如何修正一些指令中的地址等等。

那么PE文件中的这些重要信息都是由谁加入的呢?

是由编译器和连接器完成的,针对不同的编译器和连接器通常会提供不同的选项让我们在编译和联结生成PE文件的时候对其中的那些Window需要的信息进行设定;当然也可以按照默认的方式编译连接生成Window中默认的信息。

例如:

WindowNT默认的程序加载基址是0x40000;你可以在用VC连接生成EXE文件的时候使用选项更改这个地址值。

在不同的操作系统中可执行文件的格式是不同的,比如在Linux上就有一种流行的ELF格式;当然它是由在Linux上的编译器和连接器生成的,所以编译器、连接器是针对不同的CPU架构和不同的操作系统而涉及出来的。

在嵌入式领域中我们经常提到交叉编译器一词,它的作用就是在一种平台下编译出能在另一个平台下运行的程序;例如,我们可以使用交叉编译器在跑Linux的X86机器上编译出能在Arm上运行的程序。

程序是如何运行起来的:

一个程序从编写出来到运行一共需要那些工具,他们都对程序作了些什么呢?

里面都涉及哪些知识需要学习呢?

先说工具:

编辑器-》编译器-》连接器-》加载器;首先我们使用编辑器编辑源文件;然后使用编译器编译程目标文件OBJ,这里面涉及到编译原理的知识;连接器把OBJ文件和其他一些库文件和资源文件连接起来生成EXE文件,这里面涉及到不同的连接器的知识,连接器根据OS的需要生成EXE文件保存着磁盘上;当我们运行EXE文件的时候有Window的加载器负责把EXE文件加载到线性地址空间,加载的时候便是根据上一节中说到的PE文件格式中的哪些重要信息。

然后生成一个进程,如果进程中涉及到多个线程还要生成一个主线程;此后进程便开始运行;这里面涉及的东西很多,包括:

PE文件格式的内容;内存管理(CPU内存管理的硬件环境以及在此基础上的OS内存管理方式);模块,进程,线程的知识;只有把这些都弄清楚之后才能比较清楚的了解这整个过程。

下面就让我们先来学习PE文件格式吧。

PE文件的总体结构:

下图便是PE文件的一个总体结构:

注意,图2是在图1的基础上进一步细化了,不过图2的顺序是从下向上代表文件的从头到尾的顺序。

 

DOSMZHeader

DOSstub

PEheader

Sectiontable

Section1

Section2

Section...

Sectionn

图一

图二

我们的EXE文件在磁盘上就是按照上面的格式顺序存储的,当运行的时候它就很容易被加载器加载到线性地址空间;但是在线性空间中和在磁盘上不同,在线性空间中各个部分不一定是占据连续的线性地址空间。

下面对PE文件格式的介绍就按照上图中对从头到尾对每个部分进行介绍。

好的,今天刚去医院回来有些累了,就先写到这儿吧。

嗯,不行,还有几个重要而又基础的概念需要在这儿先澄清一下,否则后面就会出乱子了。

几个重要的基本概念:

1)节:

PE文件的真正内容划分成块,称之为sections(节)。

每节是一块拥有共同属性的数据,比如代码/数据、读/写等。

我们可以把PE文件想象成一逻辑磁盘,PEheader是磁盘的boot扇区,而sections就是各种文件,每种文件自然就有不同属性如只读、系统、隐藏、文档等等。

值得我们注意的是----节的划分是基于各组数据的共同属性:

而不是逻辑概念。

重要的不是数据/代码是如何使用的,如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。

不必关心节中类似于"data","code"或其他的逻辑概念:

如果数据和代码拥有相同属性,它们就可以被归入同一个节中。

(节名称仅仅是个区别不同节的符号而已,类似"data","code"的命名只为了便于识别,惟有节的属性设置决定了节的特性和功能)如果某块数据想付为只读属性,就可以将该块数据放入置为只读的节中,当PE装载器映射节内容时,它会检查相关节属性并置对应内存块为指定属性。

下面是常见的节名及作用:

节名

作用

.arch

最初的构建信息(AlphaArchitectureInformation)

.bss

未经初始化的数据

.CRT

C运行期只读数据

.data

已经初始化的数据

.debug

调试信息

.didata

延迟输入文件名表

.edata

导出文件名表

.idata

导入文件名表

.pdata

异常信息(ExceptionInformation)

.rdata

只读的初始化数据

.reloc

重定位表信息

.rsrc

资源

.text

.exe或.dll文件的可执行代码

.tls

线程的本地存储器

.xdata

异常处理表

注意:

上面已经说过了“节的划分是基于各组数据的共同属性:

而不是逻辑概念。

重要的不是数据/代码是如何使用的,如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中” 所以上面表中列出的节并不一定单独成节,也就是说即使存在上面表中的某一节,在节表(sectiontable)(后面会讲到)中也不一定就有于之对应的项,因为它可能和别的具有共同属性的节共同组成了一节。

比如.idata可以和.text合成一节而命名为.text,而在节表中只有和.text 对应的项。

这也就是后面的optionalheader中数据目录(DataDirectory)存在的作用,因为很多有用的节被合并了,因此加载器无法通过节表来定位它们,所以这就是数据目录(DataDirectory)发挥作用的时候了(具体作用后面会讲到)。

2)虚拟地址:

虚拟地址即程序中使用的地址,也就是从程序员的角度看到的地址,有时也叫逻辑地址;通常使用段地址:

偏移量的形式表示,不过在32位系统中使用的是平坦(Flat)内存模式,所以我们可以不用管段地址,只考虑32位的偏移量即可,认为32位的偏移量就是虚拟地址,这样一来程序员就可以认为他是在一个段中写程序,这个段的大小是232=4G的容量,当然这部分地址空间是程序和OS共享的,程序员可以利用的大约有2G(具体可以参考Win98和WinNT的内存布局);所以我们平时在写程序申请内存的时候实际上申请的就是这2G的线性地基空间,由于所有的4G线性地址空间都被OS作为资源来管理(这4G的线性地址空间是通过页表来表现出来的,OS分配线性地址空间給进程也就是分配相应的页表給进程),所以我们无论用什么方式使用内存最终都是转换为OS为我们分配线性地址空间,至于分配的线性地址空间又如何被映射为真正的物理内存完全是有OS负责的(更详细资料参见“Windows内存管理”),程序员不必操心。

3)相对虚拟地址:

「相对虚拟地址(RelativeVirtualAddress,RVA)」即相对于上面的基地址的偏移量。

PE文件中的许多字段内容都是以RVA表示,一个RVA是某一资料项的offset(偏移)值--从文件被映像进来的起点(即基地址)算起。

举个例子,我们说Windows加载器把一个PE文件映像到虚拟地址空间的0x400000处,如果此image有一个表格开始于0x401464,那么这个表格的RVA就是0x1464:

虚拟地址0x401464-基地址0x400000=RVA0x1464只要把RVA加上基地址,RVA就可以被转换为一个有用的指针。

在PE文件中大多数地址多是RVA而RVA只有当PE文件被PE装载器装入内存后才有意义。

如果我们直接将文件映射到内存而不是通过PE装载器载入,那么我们就不能直接使用那些RVA。

必须先将那些RVA转换成文件偏移量,RVAToOffset函数就起到这个作用。

4)基地址:

「基地址(baseaddress)」是一个重要概念,用来描述被映像到内存中的EXE或DLL的起始地址。

为了方便,WindowsNT和Windows95都以模块的基地址做为模块的instancehandle(HINSTANCE,实例句柄)。

Windows95加载器把一个PE文件映像到虚拟地址空间的0x400000处;而WindowNT加载器把一个PE文件映像到虚拟地址空间的0x10000处。

5)文件偏移量:

文件中的地址与内存中表示不同,它是用偏移量(Fileoffset)来表示的,文件中的第一个字节的偏移量是0,后面的字节依次递增。

在SoftICE和W32Dasm下显示的地址值是内存线性地址,或称之为虚拟地址(VirualAddress,VA)。

而十六进制工具里,如:

Hiew、HexWorkshop等显示的地址就是文件地址,称之为偏移量(Fileoffset)或物理地址(RAWoffset,注意这个物理地址不是内存寻址中说到的物理地址)。

6)模块:

「模块(module)」一词表示一个EXE或DLL被加载内存后的程序代码、数据和资源(就是被加载到内存后的EXE或DLL整体,包括代码、数据和资源,而不是说代码、数据、资源分别都是模块)。

除了程序代码和数据是你的程序直接使用的之外,模块还内含一些支持性数据,Windows用它来决定程序代码和数据放在内存的什么地方,在Win32,这些信息保留在PE头部(即图1中的PEheader,实际上它是一个IMAGE_NT_HEADERS结构)中。

7)逻辑地址:

见“虚拟地址”

8)线性地址:

线性地址是由虚拟地址(逻辑地址)转换来的,转换需要CPU和OS共同合作来完成;里面涉及到全局描述符表GDT和局部描述符表LDT;不过由于32位的Window系统采用flat内存模式,所以我们可以认为虚拟地址就是线性地址,即我们可以认为逻辑地址中的32位偏移量就是线性地址。

9)物理地址:

即最终发往地址总线上的地址,它对应着实际的物理内存,在32位的Window存储管理中它是通过页表由线性地址转换出来的。

10)实际地址:

即“物理地址”。

其中前面的6个概念是学习PE文件格式需要知道的,后面的几个主要在内存管理里面提到,在这里为了便于区别一起列了出来。

(二)PE格式总览 

上一节我们已经了解了PE文件格式的作用和其总体结构,从这节开始我们就开始按照上一节中的总体结构从上到下来解析PE文件各个部分的具体结构和作用,当然我不会对每个部分的每一个字段都详细描述它的作用,因为讲解PE文件格式的资料很多,讲解的都很详细,所以我在这里只是按照程序执行的线索和基本原理把那些最重要的字段讲解一下,为了让我们对PE文件格式有个比较清楚的宏观认识,在具体讲解每一部分之前先让我们大概了解一下各部分的作用。

1.DOSMZheader和DOSStub:

如果在DOS下执行PE格式文件就会执行后面的DOSStub,显示字符串"ThisprogramcannotruninDOSmode",如果在Window下执行PE格式文件,PE加载器就会根据DOSMZheader中的最后一个域e_lfnew跳过DOSStub直接转到PEHeader,DOSMZheader和DOSStub的贡献仅此而已。

2. PEHeader:

当加载器跳到PEHeader后,根据里面的各个域首先检查这是不是有效的PE文件格式,能否在当前的CPU架构下运行,优先加载基址是多少,一共有几个节(section),这是一个EXE文件还是DLL文件等总体信息,有了这些总体信息之后加载器就会跳到下面的Sectiontable。

3.Sectiontable:

有了上面从PEHeader获得的总体信息后,加载器并不能准确的加载文件,因为要准确的加载文件,加载器还需要一些关于每一节的更具体的信息,比如:

每一节在磁盘文件上的起始位置、大小,应该被加载的线性地址空间的哪一部分,这一节是代码还是数据,读写属性如何等等。

所有这些信息都保存在Sectiontable里面,Sectiontable是一个结构数组,数组里面的每一个结构对应PE文件中的一个节。

PE加载器就会遍历这个结构数组把PE文件的每一节准确的加载到线性地址空间。

(这里还要注意两点:

一是PE加载器把PE文件的每一节加载到线性地址空间并不是说把磁盘上的文件调入物理内存;而只是为它分配线性地址空间,分配线性地址空间意味着申请本进程需要的页表,并把相应的信息添入页表中。

线性地址空间也可以看作是一种资源,它是通过页表来体现的,当一个页表被添入相应的信息被占用之后那么这个页表对应的那块线性地址空间也就被分配出去了。

需要注意的另一点是PE加载器对每一节采用文件映射的方式把相应的磁盘文件映射到内存,而不是把整个PE文件采用文件映射的方法把磁盘文件映射到内存。

更具体的解释我会在“Windows内存管理”中提到。

4.Sections:

PE文件最后的部分就是各个节了,比如.text,.data,.idata等等,各种节的作用后面会有一个简要介绍。

思考一下:

既然加载器不一定把程序加载到PE头中指定的优先加载基址,那么如果在没有加载到PE头中指定的优先加载基址的情况下,指令中的地址是不是都要依次修改呢?

首先我们要明确的一点是程序指令中的地址分两大类,其中一类是在编译过程就可以确定的,这类地址采用的是相对虚拟地址(RVA),所以即使程序没有被加载到希望的基址这些地址也无需修改。

另一类地址是编译过程和连接过程都无法确定的,比如那些引用外部库的函数地址,因为外部库之后在被加载器加载后里面的函数地址才能确定下来,所以程序中的这类地址要在程序被加载后进行修改。

那么编译器和连接器对这类无法确定的地址是如何处理的呢?

加载器又是根据什么如何来对它们进行修改的呢?

个人感觉PE文件格式学习中这一部分内容有些繁杂,所以希望大家读后面各节的时候最好时常思考一下这两个问题。

从下一节开始我们将对PE文件的各个部分作更为详尽的讲解。

重点部分会放在对上面两个问题的解决上。

(三)DOSHeader&PEHeader

上一节中我们对PE文件的各个部分的作用有了一个总体的认识,从这节起我们会对PE文件的每个部分作更进一步的解释,当然别忘记了上一节中我提出的两个问题。

1.DOSMZheader和DOSStub:

所有PE文件(甚至32位的DLLs)必须以一个简单的DOSMZheader开始。

我们通常对此结构没有太大兴趣。

有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZheader之后的DOSstub。

DOSstub实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串"ThisprogramrequiresWindows"或者程序员可根据自己的意图实现完整的DOS代码。

通常我们也不对DOSstub太感兴趣:

因为大多数情况下它是由汇编器/编译器自动生成。

通常,它简单调用中断21h服务9来显示字符串"ThisprogramcannotruninDOSmode"。

在Window95下运行32位程序的时候这个部分并不会被加载器映射的线性地址空间,当Win32加载器把一个PE文件映像到内存,内存映像文件(memorymappedfile)的第一个字节对应到DOSStub的第一个字节。

WINNT.H为DOSstub表头DOSMZheader定义了一个结构,第一个域e_magic,被称为魔术数字,它被用于表示一个MS-DOS兼容的文件类型,其作用类似于PEheader中的Signature域,所有MS-DOS兼容的可执行文件都将这个值设为0x5A4D,表示ASCII字符MZ。

MS-DOS头部之所以有的时候被称为MZ头部,就是这个缘故。

还有许多其它的域对于MS-DOS操作系统来说都有用,但是对于WindowsNT来说,这个结构中只有一个有用的域——最后一个域e_lfnew,PE头部就是由它定位的。

循此我们将非常容易找到PE头部,它是一个相对偏移值(或说是RVA),指向真正的PE头部(PEheader)。

为了获得指针,你必须为RVA加上image的基地址:

  pNTHeader=dosHeader+dosHeader->e_lfanew;

有了这个指向PEHeader的指针我们就可以取得很多有用的信息了,既然我们研究的是PE文件格式,因此PEHeader才是我们研究的重点。

总之,DOSMZheader和DOSStub之间的关系相当于PEheader和EXE或者DLL之间的关系。

   2.PEHeader:

PEheader是PE相关结构IMAGE_NT_HEADERS的简称,其中包含了许多PE装载器用到的重要域。

当我们更加深入研究PE文件格式后,将对这些重要域耳目能详。

执行体在支持PE文件结构的操作系统中执行时,PE装载器将从DOSMZheader中找到PEheader的起始偏移量。

因而跳过了DOSstub直接定位到真正的文件头PEheader。

PE头部整个是个IMAGE_NT_HEADERS结构,定义于WINNT.H。

这个结构正是Windows95的moduledatabase(“模块”的概念在第一节中说过了,操作系统就是利用这个结构感知“模块”的存在、获得“模块”的信息等;这个结构我会在以后的“模块”学习当中提及)。

每一个被载入的EXE或DLL都以一个IMAGE_NT_HEADERS结构表现出来。

此结构有一个DWORD和两个子结构:

DWORDSignature;

IMAGE_FILE_HEADERFileHeader;

IMAGE_OPTIONAL_HEADEROptionalHeader;

(1) 对于PE格式的文件Signature字段内容应该是ASCII的PE\0\0。

(2) IMAGE_FILE_HEADERFileHeader:

Fieldname

Meanings

Machine

该文件运行所要求的CPU。

对于Intel平台,该值是IMAGE_FILE_MACHINE_I386(14Ch)。

我们尝试了LUEVELSMEYER的pe.txt声明的14Dh和14Eh,但Windows不能正确执行。

看起来,除了禁止程序执行之外,本域对我们来说用处不大。

NumberOfSections

文件的节数目。

如果我们要在文件中增加或删除一个节,就需要修改这个值。

TimeDateStamp

文件创建日期和时间。

我们不感兴趣。

PointerToSymbolTable

用于调试。

NumberOfSymbols

用于调试。

SizeOfOptionalHeader

指示紧随本结构之后的OptionalHeader结构大小,必须为有效值。

Characteristics

关于文件信息的标记,比如文件是exe还是dll。

IMAGE_FILE_HEADER结构比较简单,也比较容易理解,在此不做过多的解释;简言之,只有三个域对我们有一些用:

Machine,NumberOfSections和Characteristics。

通常不会改变Machine和Characteristics的值,但如果要遍历节表就得使用NumberOfSections。

(3)比较复杂也更有趣的是第三个东东即:

IMAGE_OPTIONAL_HEADER,现在我们学习IMAGE_NT_HEADERS中的最后成员optionalheader结构,它包含了PE文件的逻辑分布信息。

该结构共有31个域,一些是很关键,另一些不太常用。

这里只介绍那些真正有用的域。

Field

Meanings

AddressOfEntryPoint

PE装载器准备运行的PE文件的第一个指令的RVA。

若您要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。

ImageBase

PE文件的优先装载地址。

比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。

字眼"优先"表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。

SectionAlignment

内存中节对齐的粒度。

例如,如果该值是4096(1000h),那么每节的起始地址必须是4096的倍数。

若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始,即使401000h和402000h之间还有很多空间没被使用。

FileAlignment

文件中节对齐的粒度。

例如,如果该值是(200h),,那么每节的起始地址必须是512的倍数。

若第一节从文件偏移量200h开始且大小是10个字节,则下一节必定位于偏移量400h:

即使偏移量512和1024之间还有很多空间没被使用/定义。

MajorSubsystemVersion

MinorSubsystemVersion

win32子系统版本。

若PE文件是专门为Win32设计的,该子系统版本必定是4.0否则对话框不会有3维立体感。

SizeOfImage

内存中整个PE映像体的尺寸。

它是所有头和节经过节对齐处理后的大小。

SizeOfHeaders

所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。

可以以此值作为PE文件第一节的文件偏移量。

Subsystem

NT用来识别PE文件属于哪个子系统。

对于大多数Win32程序,只有两类值:

WindowsGUI和WindowsCUI(控制台)。

SizeOfStackReserve

线程初始堆栈的保留大小。

SizeOfStackCommit

一开始即被提交(committed)给线程初始堆栈的内存数量。

SizeOfHeapReserve

保留给最初的processheap的虚拟内存数量。

SizeOfHeapCommit

一开始即被提交(committed)给processheap的内存数量。

 

DataDirectory

IMAGE_DATA_DIRECTORY结构数组。

每个结构给出一个重要数据结构的RVA,比如引入地址表等。

上面表格里最难理解的也是很重要的一个域是最后一个,即:

DataDirectory;它是一个结构数组,它一共包含16个元素即共含16个结构;每一个结构对应于一个section(注意这里的section是按照第一节中按作用进行划分的section,不是最终生成的PE文件中包含的节),结构中的两个域分别描述了该section的RVA和SIZE;这样一来加载器就能够通过这个数组迅速在image中找到特定的section,后面讲到的导入表,引出表都要用到这个数组中相应的元素,到时候还会有进一步的解释。

(四)S

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > PPT模板 > 简洁抽象

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1