您好,欢迎访问三七文档
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的顺序是从下向上代表文件的从头到尾的顺序。DOSMZHeaderDOSstubPEheaderSectiontableSection1Section2Section...Sectionn图一图二我们的EXE文件在磁盘上就是按照上面的格式顺序存储的,当运行的时候它就很容易被加载器加载到线性地址空间;但是在线性空间中和在磁盘上不同,在线性空间中各个部分不一定是占据连续的线性地址空间。下面对PE文件格式的介绍就按照上图中对从头到尾对每个部分进行介绍。好的,今天刚去医院回来有些累了,就先写到这儿吧。嗯,不行,还有几个重要而又基础的概念需要在这儿先澄清一下,否则后面就会出乱子了。几个重要的基本概念:1)节:PE文件的真正内容划分成块,称之为sections(节)。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。我们可以把PE文件想象成一逻辑磁盘,PEheader是磁盘的boot扇区,而sections就是各种文件,每种文件自然就有不同属性如只读、系统、隐藏、文档等等。值得我们注意的是----节的划分是基于各组数据的共同属性:而不是逻辑概念。重要的不是数据/代码是如何使用的,如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。不必关心节中类似于data,code或其他的逻辑概念:如果数据和代码拥有相同属性,它们就可以被归入同一个节中。(节名称仅仅是个区别不同节的符号而已,类似data,code的命名只为了便于识别,惟有节的属性设置决定了节的特性和功能)如果某块数据想付为只读属性,就可以将该块数据放入置为只读的节中,当PE装载器映射节内容时,它会检查相关节属性并置对应内存块为指定属性。下面是常见的节名及作用:节名作用.arch最初的构建信息(AlphaArchitectureInformation).bss未经初始化的数据.CRTC运行期只读数据.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获得的总体信息后,加载器并不能准确的加载文件,因为要准确的加载文件,加载器还需要一些关于每一节的更
本文标题:PE文件格式详解
链接地址:https://www.777doc.com/doc-3435869 .html