当前位置:首页 > PE文件
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程序,只有两类值: Windows GUI 和 Windows CUI (控制台)。 SizeOfStackReserve 线程初始堆栈的保留大小。 SizeOfStackCommit
一开始即被提交(committed)给线程初始堆栈的内存数量。
SizeOfHeapReserve
保留给最初的process heap 的虚拟内存数量。 SizeOfHeapCommit
一开始即被提交(committed)给process heap 的内存数量。 DataDirectory
IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA,比如引入地址表等。
上面表格里最难理解的也是很重要的一个域是最后一个,即:DataDirectory;它是一个结构数组,它一共包含16个元素即共含16个结构;每一个结构对应于一个section(注意这里的section是按照第一节中按作用进行划分的section,不是最终生成的PE文件中包含的节),结构中的两个域分别描述了该section的 RVA 和 SIZE; 这样一来加载器就能够通过这个数组迅速在image 中找到特定的section,后面讲到的导入表,引出表都要用到这个数组中相应的元素,到时候还会有进一步的解释。
PE文件格式详解(四)――Section Table(节表)
到本节为止,我们已经学了许多关于 DOS header 和 PE header 的知识。接下来就该轮到 section table(节表)了。节表其实就是紧挨着 PE header 的一结构数组,它的作用我们在前面已经说过了。该数组成员的数目由 file header (IMAGE_FILE_HEADER) 结构中 NumberOfSections 域的域值来决定。节表结构又命名为 IMAGE_SECTION_HEADER。我们把它的主要成员列表如下: Field Meanings Name1
事实上本域的名称是\,只是\已被MASM用作关键字,所以我们只能用\代替。这儿的节名长不超过8字节。记住节名仅仅是个标记而已,我们选择任何名字甚至空着也行,注意这里不用null结束。命名不是一个ASCIIZ字符串,所以不用null结尾。 VirtualAddress
本节的RVA(相对虚拟地址)。PE装载器将节映射至内存时会读取本值,因此如果域值是1000h,而PE文件装在地址400000h处,那么本节就被载到401000h。 SizeOfRawData
经过文件对齐处理后节尺寸,PE装载器提取本域值了解需映射入内存的节字节数。(译者注: 假设一个文件的文件对齐尺寸是0x200,如果前面的 VirtualSize域指示本节长度是0x388字节,则本域值为0x400,表示本节是0x400字节长)。 PointerToRawData
这是节基于文件的偏移量,PE装载器通过本域值找到节数据在文件中的位置。 Characteristics
包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。
好的,现在PE文件的前半部分结构我们已经了解的差不多了,下面就让我们模拟一下加载器加载PE文件的过程吧: 加载器加载PE文件的主要步骤:
1. 当PE文件被执行,PE装载器检查 DOS MZ header 里的 PE header 偏
移量。如果找到,则跳转到 PE header。
2. PE装载器检查 PE header 的有效性。如果有效,就跳转到PE header
的尾部。 3. 紧跟 PE header 的是节表。PE装载器读取其中的节信息,并采用文件
映射方法将这些节映射到内存,同时付上节表里指定的节属性。 4. PE文件映射入内存后,PE装载器将处理PE文件中类似 import table
(引入表)逻辑部分。 加载器检查PE文件有效性步骤总结如下:
1. 首先检验文件头部第一个字的值是否等于 IMAGE_DOS_SIGNATURE,是
则 DOS MZ header 有效。
2. 一旦证明文件的 DOS header 有效后,就可用e_lfanew来定位 PE
header 了。
3. 比较 PE header 的第一个字的值是否等于 IMAGE_NT_HEADER。如果前
后两个值都匹配,那我们就认为该文件是一个有效的PE文件。 现在我们已知晓 IMAGE_SECTION_HEADER 结构,再来模拟一下 PE装载器的工作吧:
1. 读取 IMAGE_FILE_HEADER 的 NumberOfSections域,知道文件的节数
目。
2. SizeOfHeaders 域值作为节表的文件偏移量,并以此定位节表。 3. 遍历整个结构数组检查各成员值。
4. 对于每个结构,我们读取PointerToRawData域值并定位到该文件偏移
量。然后再读取SizeOfRawData域值来决定映射内存的字节数。将
VirtualAddress域值加上ImageBase域值等于节起始的虚拟地址。然后就准备把节映射进内存,并根据Characteristics域值设置属性。 5. 遍历整个数组,直至所有节都已处理完毕。 遍历节表的步骤:
1. PE文件有效性校验。
2. 定位到 PE header 的起始地址。
3. 从 file header 的 NumberOfSections域获取节数。
4. 通过两种方法定位节表: ImageBase+SizeOfHeaders 或者 PE header
的起始地址+ PE header结构大小。 (节表紧随 PE header)。如果不是使用文件映射的方法,可以用SetFilePointer 直接将文件指针定位到节表。节表的文件偏移量存放在 SizeOfHeaders域里。(SizeOfHeaders 是 IMAGE_OPTIONAL_HEADER 的结构成员) 5. 处理每个 IMAGE_SECTION_HEADER 结构。
好的,到此为止我们已经清楚了加载器加载PE文件的大部分过程,但是别忘了我们的问题,现在问题还没有解决,要解决这个问题就好弄清楚后面两节:Import Table和Export Table,这两节是最重要的当然也是最复杂的。
PE文件格式详解(五)――Improt Table(引入表)
这节即将学习的Import Table和下节的Export Table关系密切,两者联合起来就可以解决我们开始提出的问题。在说明Import Table和Export Table的作用之前先让我们明白编译器是如何处理我们调用外部库函数的。在PE 文件中,当你调用另一模块中的函数(例如USER32.DLL 中的GetMessage),编译器制造出来的CALL 指令并不会把控制权直接传给DLL 中的函数,而是传给一个JMP DWORD PTR [XXXXXXXX] 指令,后者也位于.text 中。JMP 指令跳到一个地址去,此地址储存在.idata 的一个DWORD之中。这个DWORD 内含该函数的真正地址(函数进入点),如图1 所示
那么,这样做有什么好处呢?试想一下,如果CALL指令后面跟的直接就是DLL中的函数地址,那么加载器就需要修补每一个调用DLL 的指令。而现在PE 载入器需要做的,就只是把DLL 函数的真实地址放到.idata 的那个DWORD 之中,根本就没有程序代码需要修补。嗯,现在比较清除了,加载器首先要知道所加载的程序调用了哪些DLL的哪些函数,然后找出这些函数的地址,把他们添入到.idata 的那些DWORD 之中。那么加载器如何知道所加载的程序调用了哪些DLL的哪些函数,这就是Import Table的作用;加载器又是如何找出这些函数的地址呢,这又是Export Table的作用。现在两者的作用都很清除了,剩下的关键问题就是PE加载其如何利用这两个东东来完成上面的任务,完成了这个任务也就解决了我们开始提出的问题。这节我们先讨论前半部分,也就是加载器如何利用Import Table找出所加载的程序调用了哪些DLL的哪些函数。
共分享92篇相关文档