PE文件格式详解

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

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的顺序是从下向上代表文件的从头到尾的顺序。

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

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

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

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

几个重要的基本概念:
1)节:PE文件的真正内容划分成块,称之为sections(节)。

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

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

值得我们注意的是---- 节的划分是基于各组数据的共同属性: 而不是逻辑概念。

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

不必关
心节中类似于"data", "code"或其他的逻辑概念: 如果数据和代码拥有相同属性,它们就可以被归入同一个节中。

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

注意:上面已经说过了“节的划分是基于各组数据的共同属性: 而不是逻辑概念。

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

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

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

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

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

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

举个例子,我们说Windows加载器把一个PE 文件映像到虚拟地址空间的0x400000 处,如果此image 有一个表格开始于0x401464,那么这个表格的RVA 就是0x1464:虚拟地址0x401464 - 基地址0x400000 = RVA 0x1464只要把RVA 加上基地址,RVA 就可以被转换为一个有用的指针。

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

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

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

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

为了方便,Windows NT 和Windows 95 都以模块的基地址做为模块的ins tance handle(HINSTANCE,实例句柄)。

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

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

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

而十六进制工具里,如:Hiew、Hex Workshop等显示的地址就是文件地址,称之为偏移量(File offset) 或物理地址(RAW offset,注意这个物理地址不是内存寻址中说到的物理地址)。

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

除了程序代码和数据是你的程序直接使用的之外,模块还内含一些支持性数据,Windows 用它来决定程序代码和数据放在内存的什么地方,在Win32,这些信息保留在PE
头部(即图1中的PE header,实际上它是一个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.DOS MZ header 和DOS Stub:
如果在DOS下执行PE格式文件就会执行后面的DOS Stub,显示字符串"This program can not run in DOS mode",如果在Window下执行PE格式文件,PE加载器就会根据DOS MZ he ader中的最后一个域e_lfnew跳过DOS Stub直接转到PE Header , DOS MZ header 和DOS Stub的贡献仅此而已。

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

3.Section table:
有了上面从PE Header获得的总体信息后,加载器并不能准确的加载文件,因为要准确的加载文件,加载器还需要一些关于每一节的更具体的信息,比如:每一节在磁盘文件上的起始位置、大小,应该被加载的线性地址空间的哪一部分,这一节是代码还是数据,读写属性如何等等。

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

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

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

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

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

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

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

思考一下:既然加载器不一定把程序加载到PE头中指定的优先加载基址,那么如果在没有加载到PE头中指定的优先加载基址的情况下,指令中的地址是不是都要依次修改呢?首先我们要明确的一点是程序指令中的地址分两大类,其中一类是在编译过程就可以确定的,这类地址采用的是相对虚拟地址(RVA),所以即使程序没有被加载到希望的基址这些地址也无需修改。

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

那么编译器和连接器对这类无法确定的地址是如何处理的呢?加载器又是根据什么如何来对它们进行修改的呢?个人感觉PE文件格式学习中这一部分内容有些繁杂,所以希望大家读后面各节的时候最好时常思考一下这两个问题。

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

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

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

1.DOS MZ header 和DOS Stub:
所有PE文件(甚至32位的DLLs) 必须以一个简单的DOS MZ header 开始。

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

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

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

通常我们也不对DOS stub 太感兴趣: 因为大多数情况下它是由汇编器/编译器自动生成。

通常,它简单调用中断21h服务9来显示字符串"This program cannot run in DOS mode"。

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

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

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

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

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

为了获得指针,你必须为RVA 加上image 的基地址:
pNTHeader = dosHeader + dosHeader->e_lfanew;
有了这个指向PE Header的指针我们就可以取得很多有用的信息了,既然我们研究的是PE 文件格式,因此PE Header才是我们研究的重点。

总之,DOS MZ header和DOS Stub之间的关系相当于PE header和EXE或者DLL之间的关系。

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

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

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

因而跳过了DOS stub 直接定位到真正的文件头PE header。

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

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

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

此结构有一个DWORD 和两个子结构:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
(1)对于PE格式的文件Signature 字段内容应该是ASCII 的PE\0\0。

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

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

(3)比较复杂也更有趣的是第三个东东即:IMAGE_OPTIONAL_HEADER,现在我们学习IMAGE_NT_HEADERS 中的最后成员optional header 结构,它包含了PE文件的逻辑分布信息。

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

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

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

(四)Section Table(节表)
到本节为止,我们已经学了许多关于DOS header 和PE header 的知识。

接下来就该轮到section table(节表)了。

节表其实就是紧挨着PE header 的一结构数组,它的作用我们在前面已经说过了。

该数组成员的数目由file header (IMAGE_FILE_HEADER) 结构中NumberOfSect ions 域的域值来决定。

节表结构又命名为IMAGE_SECTION_HEADER。

我们把它的主要成员列表如下:
好的,现在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域值并定位到该文件偏移量。

然后再读取S
izeOfRawData域值来决定映射内存的字节数。

将VirtualAddress域值加上ImageBase
(基地址)域值等于节起始的虚拟地址。

然后就准备把节映射进内存,并根据Character
istics域值设置属性。

5. 遍历整个数组,直至所有节都已处理完毕。

遍历节表的步骤:
1. PE文件有效性校验。

2. 定位到PE header 的起始地址。

3. 从file header 的NumberOfSections域获取节数。

4. 通过两种方法定位节表: ImageBase+SizeOfHeaders 或者PE header的起始地址+ P
E header结构大小。

(节表紧随PE header)。

如果不是使用文件映射的方法,可以用S
etFilePointer 直接将文件指针定位到节表。

节表的文件偏移量存放在SizeOfHeaders域
里。

(SizeOfHeaders 是IMAGE_OPTIONAL_HEADER(PE header)的结构成员) //
定位节表位置
5. 处理每个IMAGE_SECTION_HEADER 结构。

(这是结构数组)
好的,到此为止我们已经清楚了加载器加载PE文件的大部分过程,但是别忘了我们的问题,现在问题还没有解决,要解决这个问题就好弄清楚后面两节:Import Table和Export Table,这两节是最重要的当然也是最复杂的。

(五)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所示:
图1
那么,这样做有什么好处呢?试想一下,如果CALL指令后面跟的直接就是DLL中的函数地址,那么加载器就需要修补每一个调用DLL 的指令。

而现在PE 载入器需要做的,就只是把DLL 函数的真实地址放到.idata 的那个DWORD 之中,根本就没有程序代码需要修补。

嗯,现在比较清除了,加载器首先要知道所加载的程序调用了哪些DLL的哪些函数,然后找出这些函数的地址,把他们添入到.idata 的那些DWORD 之中。

那么加载器如何知道所加载的程序调用了哪些DLL的哪些函数,这就是Import Table的作用;加载器又是如何找出这些函数的地址呢,这又是Export Tabl e的作用。

现在两者的作用都很清除了,剩下的关键问题就是PE加载其如何利用这两个东东来完成上面的任务,完成了这个任务也就解决了我们开始提出的问题。

这节我们先讨论前半部分,也就是加载器如何利用Import Table找出所加载的程序调用了哪些DLL的哪些函数。

首先,最基本的是加载器如何找到Import Table呢?这就要利用前面我们提到的数据目录(D ata Directory),它是Option Header结构中的最后一个域。

Data Directory 是一个IMAGE_DAT A_DIRECTORY 结构数组,共有16个成员,每个成员包含了一个重要数据结构的信息(RVA 和大小),并且这些重要数据结构的信息在IMAGE_DATA_DIRECTORY 结构数组中的位置是固定的,这样加载器就很容易找到需要的信息了。

这就好比你有一个书架每一层都放着不同种类的书籍,并且它们始终固定,即:第一层始终放小说,第二层始终放散文…等等,当你需要散文的时候你就可以毫不犹豫的去拿第二层上的书就可以了。

下面的表就是IMAGE_DATA_DIRECTORY 结构数组的布置情况:(第0,1,12三项是和我们这两节解决问题有关系的;第5项在最后一节中用到;第9项在线程中用到)。

如果您还记得节表可以看作是PE文件各节的根目录的话,也可以认为Data Directory 是存储在这些节里的逻辑元素的根目录。

明确点,Data Directory 包含了PE文件中各重要数据结构的位置和尺寸信息。

Data Directory的每个成员都是IMAGE_DATA_DIRECTORY 结构类型的,其定义如下所示:
IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress dd ?
isize dd ?
IMAGE_DATA_DIRECTORY ENDS
VirtualAddress 实际上是数据结构的相对虚拟地址(RVA)。

比如,如果该结构是关于import s ymbols的,该域就包含指向IMAGE_IMPORT_DESCRIPTOR 数组的RVA。

isize 含有Virtual Address所指向数据结构的字节数。

现在我们知道如何找到引入表了。

Data Directory数组第二项的VirtualAddress包含引入表地址。

引入表实际上是一个IMAGE_IMPORT_DESCRIPTOR 结构数组。

每个结构包含PE文件引入函数的一个相关DLL的信息。

比如,如果该PE文件从10个不同的DLL中引入函数,那么这个数组就有10个成员。

该数组以一个全0的成员结尾。

下面详细研究结构组成: IMAGE_IMPORT_DESCRIPTOR STRUCT
union
Characteristics dd ?
OriginalFirstThunk dd ?
ends
TimeDateStamp dd ?
ForwarderChain dd ?
Name1 dd ?。

相关文档
最新文档