PE文件格式分析

一、PE的基本概念

    PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。

    认识PE文件不是作为单一内存映射文件被装入内存是很重要的。Windows加载器(又称PE加载器)遍历PE文件并决定文件的哪一部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。PE文件的结构在磁盘和内存中是基本一样的,但在装入内存中时又不是完全复制。Windows加载器会决定加载哪些部分,哪些部分不需要加载。而且由于磁盘对齐与内存对齐的不一致,加载到内存的PE文件与磁盘上的PE文件各个部分的分布都会有差异。



二、PE结构分析


图1:PE文件的框架结构

PE文件至少包含两个段,即数据段和代码段。Windows NT 的应用程序有9个预定义的段,分别为 .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,这些段并不是都是必须的,当然,也可以根据需要定义更多的段(比如一些加壳程序)。
在应用程序中最常出现的段有以下6种:
.执行代码段,通常  .text (Microsoft)或 CODE(Borland)命名;
.数据段,通常以 .data 、.rdata 或 .bss(Microsoft)、DATA(Borland)命名;
.资源段,通常以 .rsrc命名;
.导出表,通常以 .edata命名;
.导入表,通常以 .idata命名;
.调试信息段,通常以 .debug命名;

1、DOS头结构

所有的PE文件都是以一个64字节的DOS头开始。这个DOS头只是为了兼容早期的DOS操作系统。DOS头的结构如下:

typedef struct IMAGE_DOS_HEADER{
      WORD e_magic;			//DOS头的标识,为4Dh和5Ah。分别为字母MZ
      WORD e_cblp;
      WORD e_cp;
      WORD e_crlc;
      WORD e_cparhdr;
      WORD e_minalloc;
      WORD e_maxalloc;
      WORD e_ss;
      WORD e_sp;
      WORD e_csum;
      WORD e_ip;
      WORD e_cs;
      WORD e_lfarlc;
      WORD e_ovno;
      WORD e_res[4];
      WORD e_oemid;
      WORD e_oeminfo;
      WORD e_res2[10];
      DWORD e_lfanew;             //指向IMAGE_NT_HEADERS的所在
}IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS头后跟一个DOS Stub数据,是链接器链接执行文件的时候加入的部分数据,一般是“This program must be run under Microsoft Windows”。这个可以通过修改链接器的设置来修改成自己定义的数据。

2、PE头文件

    紧跟着DOS stub的时PE头文件(PE Header)。PE Header是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,其中包含许多PE装载器用到的重要字段。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构中的e_lfanew字段里找到PE Header的起始偏移量,加上基址得到PE文件头的指针。

PNTHeader = ImageBase + dosHeader->e_lfanew

PE头的数据结构被定义为IMAGE_NT_HEADERS。包含三部分,其结构如下:

typedef struct IMAGE_NT_HEADERS{
      DWORD Signature;
      IMAGE_FILE_HEADER FileHeader;
      IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS; 

Signature字段:PE头的标识。双字结构。为50h, 45h, 00h, 00h. 即“PE\0\0”。

FileHeader字段:IMAGE_FILE_HEADER(映像头文件)结构包含了文件的物理层信息及文件属性。共20字节的数据,其结构如下:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;					//运行平台
    WORD    NumberOfSections;			//文件的区块数目
    DWORD   TimeDateStamp;				//文件创建日期和时间
    DWORD   PointerToSymbolTable;		//指向符号表(用于调试)
    DWORD   NumberOfSymbols;			//符号表中符号个数(用于调试)
    WORD    SizeOfOptionalHeader;		//IMAGE_OPTIONAL_HEADER32结构大小
    WORD    Characteristics;			//文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
OptionalHeader字段:IMAGE_OPTIONAL_HEADER(可选映像头)是一个可选的机构,实际上 IMAGE_FILE_HEADER结构不足以定义PE文件属性,因此可选映像头中定义了更多的数据。总共224个字节,最后128个字节为数据目录(Data Directory) ,其结构如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;							//标志字
    BYTE    MajorLinkerVersion;				//链接器主版本号
    BYTE    MinorLinkerVersion;				//链接器次版本号
    DWORD   SizeOfCode;						//所有含有代码表的总大小
    DWORD   SizeOfInitializedData;			//所有初始化数据表总大小
    DWORD   SizeOfUninitializedData;		//所有未初始化数据表总大小
    DWORD   AddressOfEntryPoint;			//程序执行入口RVA
    DWORD   BaseOfCode;						//代码表其实RVA
    DWORD   BaseOfData;						//数据表其实RVA
    DWORD   ImageBase;						//程序默认装入基地址
    DWORD   SectionAlignment;				//内存中表的对齐值
    DWORD   FileAlignment;					//文件中表的对齐值
    WORD    MajorOperatingSystemVersion;	//操作系统主版本号
    WORD    MinorOperatingSystemVersion;	//操作系统次版本号
    WORD    MajorImageVersion;				//用户自定义主版本号
    WORD    MinorImageVersion;				//用户自定义次版本号
    WORD    MajorSubsystemVersion;			//所需要子系统主版本号
    WORD    MinorSubsystemVersion;			//所需要子系统次版本号
    DWORD   Win32VersionValue;				//保留,通常设置为0
    DWORD   SizeOfImage;					//映像装入内存后的总大小
    DWORD   SizeOfHeaders;					//DOS头、PE头、区块表总大小
    DWORD   CheckSum;						//映像校验和
    WORD    Subsystem;						//文件子系统
    WORD    DllCharacteristics;				//显示DLL特性的旗标
    DWORD   SizeOfStackReserve;				//初始化堆栈大小
    DWORD   SizeOfStackCommit;				//初始化实际提交堆栈大小
    DWORD   SizeOfHeapReserve;				//初始化保留堆栈大小
    DWORD   SizeOfHeapCommit;				//初始化实际保留堆栈大小
    DWORD   LoaderFlags;					//与调试相关,默认值为0
    DWORD   NumberOfRvaAndSizes;			//数据目录表的项数
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
DataDirectory是OptionalHeader的最后128个字节,也是IMAGE_NT_HEADERS的最后一部分数据。它由16个IMAGE_DATA_DIRECTORY结构组成的数组构成,指向输出表、输入表、资源块等数据。IMAGE_DATA_DIRECTORY的结构如下:
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;			//数据块的起始RVA
    DWORD   Size;					//数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

数据表成员结构如下:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

用LordPE查看EXE文件的数据目录表:



3、区块表

3.1 区块结构

在PE文件头与原始数据之间存在一个区块表(Section Table),它是一个IMAGE_SECTION_HEADER结构数组,区块表包含每个块在映像中的信息(如位置、长度、属性),分别指向不同的区块实体。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一。另外,节表中 IMAGE_SECTION_HEADER 结构的总数总是由PE文件头 IMAGE_NT_HEADERS->FileHeader.NumberOfSections 字段来指定的。


IMAGE_SECTION_HEADER结构定义如下:

typedef struct _IMAGE_SECTION_HEADER {
    Name						//8个字节的块名
    union						
    {
        DWORD PhysicalAddress;
        DWORD VirtualSize;
    } Misc;                     //区块尺寸</span>
    DWORD VirtualAddress;		//区块的RVA地址
    DWORD SizeOfRawData;		//在文件中对齐后的尺寸
    DWORD PointerToRawData;		//在文件中偏移
    DWORD PointerToRelocations;	//在OBJ文件中使用,重定位的偏移
    DWORD PointerToLinenumbers;	//行号表的偏移(供调试使用地)
    WORD NumberOfRelocations;	//在OBJ文件中使用,重定位项数目
    WORD NumberOfLinenumbers;	//行号表中行号的数目
    DWORD Characteristics;		//区块属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

(1)Name:这是一个8位的ASCII(不是Unicode内码),用来定义块名,多数块名以,开始(如.Text),这个实际上不是必需的,注意如果块名超过了8个字节,则没有最后面的终止标志NULL字节,带有$的区块的名字会从编译器里将带有$的相同名字的区块被按字母顺序合并。
(2) VirtualSize:指出实际的,被使用的区块大小,是区块在没有对齐处理前的实际大小.如果VirtualSize > SizeOfRawData,那么SizeOfRawData是可执行文件初始化数据的大小(SizeOfRawData – VirtualSize)的字节用0来填充。这个字段在OBJ文件中被设为0。
(3)VirtualAddress:该块时装载到内存中的RVA,注意这个地址是按内存页对齐的,她总是SectionAlignment的整数倍,在工具中第一个块默认RVA为1000,在OBJ中为0。
(4)SizeofRawData:该块在磁盘中所占的大小,在可执行文件中,该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。
(5) PointerToRawData:该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。
(6) PointerToRelocations 在PE中无意义
(7) PointerToLinenumbers 行号表在文件中的偏移值,文件调试的信息
(8) NumberOfRelocations 在PE中无意义
(9) NumberOfLinenumbers 该块在行号表中的行号数目
(10) Characteristics 块属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志:


通常,区块中的数据在逻辑上是关联的。PE 文件一般至少都会有两个区块:一个是代码块,另一个是数据块。每一个区块都需要有一个截然不同的名字,这个名字主要是用来表达区块的用途。例如有一个区块叫.rdata,表明他是一个只读区块。注意:区块在映像中是按起始地址(RVA)来排列的,而不是按字母表顺序。
另外,使用区块名字只是人们为了认识和编程的方便,而对操作系统来说这些是无关紧要的。微软给这些区块取了个有特色的名字,但这不是必须的。当编程从PE 文件中读取需要的内容时,如输入表、输出表,不能以区块名字作为参考,正确的方法是按照数据目录表中的字段来进行定位。

区块名称以及意义:


每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” 或者说将包含数据的区块命名为“.Code” 都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。

在Visual C++中,用#pragma来声明,告诉编译器插入数据到一个区块内:

#pragma data_seg("MY_DATA")

链接器的一个有趣特征就是能够合并区块。如果两个区块有相似、一致性的属性,那么它们在链接的时候能被合并成一个单一的区块。这取决于是否开启编译器的 /merge 开关。下面的链接器选项将.rdata与.text区块合并为一个.text区块:

/MERGE : .rdata = .text

注意:当合并区块时,因为这没有什么硬性规定。例如,把.rdata合并到.text里不会有什么问题,但是不应该将.rsrc、.reloc或者.pdata合并到其它的区块里。

3.2 区块的对齐

    区块大小是要对齐的,有两种对齐值,一种用于磁盘文件内,另一种用于内存中。PE文件头指出了这两个值,他们可以不同。PE 文件头里边的FileAligment 定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始存放。而区块的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是区块间的间隙。例如,在PE文件中,一个典型的对齐值是200h ,这样,每个区块都将从200h 的倍数的文件偏移位置开始,假设第一个区块在400h 处,长度为90h,那么从文件400h 到490h 为这一区块的内容,而由于文件的对齐值是200h,所以为了使这一区块的长度为FileAlignment 的整数倍,490h 到 600h 这一个区间都会被00h 填充,这段空间称为区块间隙,下一个区块的开始地址为600h 。
    PE 文件头里边的SectionAligment 定义了内存中区块的对齐值。PE 文件被映射到内存中时,区块总是至少从一个页边界开始。一般在X86 系列的CPU 中,页是按4KB(1000h)来排列的;在IA-64 上,是按8KB(2000h)来排列的。所以在X86 系统中,PE文件区块的内存对齐值一般等于 1000h,每个区块按1000h 的倍数在内存中存放。

3.3 文件偏移与RVA

由于一些PE文件为减少体积,磁盘对齐值不是一个内存页 1000h,而是 200h,当这类文件被映射到内存后,同一数据相对于文件头的偏移量在内存中和磁盘文件中是不同的,这样就存在着文件偏移地址与虚拟地址的转换问题。


由上图可以看出,文件被映射到内存,DOS文件头,PE文件头,区块表的偏移位置和大小都没有发生改变。而各区块映射到内存后,起偏移位置发生了改变。
转换需要前面提到的一个公式:设:
ΔK为相对虚拟地址RVA与文件偏移地址File Offset的差值
VA = ImageBase + RVA
File Offset = RVA - ΔK
File Offset = VA - ImageBase - ΔK

3.3 输入表

3.3.1 输入表地址定位

    PE文件头可选映像头中数据目录表的第二成员指向输入表,输入表以一个 IAMGE_IMPORT_DESCRITPTOR 数组开始,每个被PE文件隐式地链接进来的DLL都有一个IID,在这个数组中没有字段指出该结构数组的项数,但他最后一个单元是NULL。

    数据目录表的第二成员 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] 就对应输入表。回忆文章上面内容可知,数据目录表 IMAGE_DATA_DIRECTORY 中结构成员 VirtualAddress 就是输入表的RVA了,而区块表 IMAGE_SECTION_HEADER 中结构成员 VirtualAddress 对应区块的RVA。通过输入表的RVA与区块的RVA比较,我们就能知道输入表在哪个区块里面。

3.3.2 输入表结构

IID的结构如下:

STRUCT IAMGE_IMPORT_DESCRIPTOR
{
    union
    {
        DWORD Characteristics;
        DWORD OriginalFirstThunk;    //指向输入名称表(INT)的RVA
    }
    DWORD TimeDateStamp;             //一个32位的时间标志
    DWORD ForwarderChain;            //这是一个被转向API的索引,一般为0
    DWORD Name;                      //DLL名字,是个以00结尾的ASCII字符的RVA地址
    DWORD FirstThunk;                //指向输入地址表(IAT)的RVA
} ;

    通过上图 OriginalFisrtThunk 和 FirstThunk 非常相似,两个数组都有一个IMAGE_THUNK_DATA结构类型的元素,他是一个指针大小的联合,每一个IAMGE_THUNK_DATA元素对应于一个从可执行文件输入的函数,两个数组的结束通过一个值为零的IAMGE_THUNK_DATA元素表示的,IMAGE_THUNK_DATA结构实际上是一个双字,该结构不同时刻有不同的含义,定义如下:

STRUCT IMAGE_THUNK_DATA
{
    union u1
      DWORD ForwarderString;    //指向一个转向者字符串的RVA
      DWORD Function;           //被输入的函数的内存地址
      DWORD Ordinal;            //被输入的API的序数值
      DWORD AddressOfData;      //指向IAMGE_IMPORT_BY_NAME
    ends
};

(1)OriginalFirstThunk:它指向输入名称表(简称INT),INT是一个 IMAGE_THUNK_DATA 结构的数组,数组中的每个 IMAGE_THUNK_DATA 结构的成员 AddressOfData都指向IMAGE_IMPORT_BY_NAME结构。
(2)TimeDateStamp:一个32位时间标志,该字段可以忽略。
(3)ForwarderChain:当程序引用一个DLL中的API,而这个API又引用别的DLL的API时使用,这种情况很少出现。
(4)Name:它表示DLL 名称的相对虚地址(译注:相对一个用null作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称,如:KERNEL32.DLL)。
(5)FirstThunk:它指向输入地址表(简称IAT),IAT是一个 IMAGE_THUNK_DATA 结构的数组。

    IAMGE_THUNK_DATA 值的最高位为1时,表示函数以序列号方式输入,这时低31位(或者一个64位可执行文件的低63位)被看做是一个函数序号,当双字的最高位为0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个RVA,指向一个IAMGE_IMPORT_BY_NAME结构,该结构定义如下:

STRUCT  IAMGE_IMPORT_BY_NAME
{
    DWORD Hint;    //本函数在其所驻留DLL的输出表中的序号 
    BYTE  name     //输入函数的函数名,函数名是一个ASCII码字符串,以NULL结尾
};

    为什么由两个并行的指针数组指向IAMGE_IMPORT_BY_NAME结构呢?
    第一个数组(由OriginalFirstThunk所指向)是但单独的一项,而且不可改变,成为INT,第二个数组(由FirstThunk所指向)是由PE装载器重写的,PE装载器首先搜索OriginalFirstThunk,如果找到了,加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真实入口地址来代替由FirstThunk指向的 IAMGE_IMPORT_BY_NAME 数组所指向数组里的元素值,JMP dword ptr[xxxxxxxx]中的[xxxxxxxx]指向FirstThunk 数组中的一个入口,所以当PE文件装载内存后准备执行时,所有函数入口地址被排列在一起,此时输入表中其他就不重要的,依靠IAT提供地址就可以正常运行。


    有些情况,一些函数仅由序号引出,也就是说不能用函数名来调用它,只能通过位置调用它,此时 IMAGE_THUNK_DATA 的值低位字指示函数序数,而高二进位(MSB)设为1,Microsoft提供了一个方便的常量测量DWORD值的MSB位,就是 IMAGE_ORDINAL_FLAG32,其值是80000000h。第二是程序的 OriginalFirstThunk 的值为0,初始化时,系统根据FirstThunk 的值指向函数名的地址串,由地址串找到函数名,再根据函数名入口地址,然后用入口地址取代 FirstThunk 指向的地址串的原值。

3.3 输出表
    输出表(Export Table)包含函数名称,输出序数等,序数是指定DLL中某个函数的16位数字,在所指向的DLL里是独一无二的。数据目录表中的第一个成员 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] 指向输出表。输出表 IAMGE_EXPORT_DIRECTORY 结构如下:

STRUCT  IAMGE_EXPORT_DIRECTORY
{
    DWORD Characteristics;          //未使用,总为0
    DWORD TimeDateStamp;            //创建输出表创建时间(GMT时间)
    WORD MajorVersion;              //主版本号,一般为0
    WORD MinorVersion;              //次版本号,一般为0
    DWORD Name;                     //模块的真实名称
    DWORD Base;                     //基数,加上序数就是函数数组的索引值
    DWORD NumberOfFunctions;        //AddressOfFunctions阵列中的元素个数
    DWORD NumberOfNames;            //AddressOfNameS阵列中的元素个数
    DWORD AddressOfFunctions;       //指向函数地址数组
    DWORD AddressOfNames;           //函数名字的指针地址
    DWORD AddressOfNameOrdinals;    //指向输出序号数组
};
Name:指向一个ASCII字符串的RVA,既模块的名字。
Base:序号的基数,按序号导出函数的序号值从Base开始递增。当通过序数来查询一个输出函数时,这个值从序数里被减去,结果被用作进入输出地址表(EAT)的索引。
NumberOfFunctions:输出地址表(EAT)中的条目数量,即所有导出函数的数量。
NumberOfNames: 输出名称表(ENT)中的条目数量,即 按名称导出函数的数量,这个值总是小于或者等于NumberOfFunctions的值。
AddressOfFunctions:EAT的RVA,指向一个DWORD数组,数组中的每一项是一个导出函数的RVA,顺序与导出序号相同。
AddressOfNames:ENT的RVA,指向一个DWORD数组,数组中的每一项仍然是一个RVA,指向一个表示函数名字。

AddressOfNameOrdinals:输出序数表的RVA,指向一个WORD数组。数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在AddressOfFunctions中的序号。

下图是一个经典的输出表结构:


3.4 基址重定位

    重定位就是你本来这个程序理论上要占据这个地址,但是由于某种原因,这个地址现在不能让你霸占,你必须转移到别的地址,这就需要基址重定位。如果可执行文件不在首选的地址装入,那么文件中每一个定位都需要被修正。对加载器来说,它不需要知道关于地址如何使用的任何细节,它只需要知道有一系列的数据需要以某种一致的方式来修正就可以了。

    对于EXE文件来说,每个文件总是使用独立的虚拟地址空间,所以EXE总能够按照这个地址装入,不需要重定位信息。而对于DLL来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证装入地址没有被其它的DLL使用,所以DLL文件中必须包含重定位信息。

    下面以实例 DllDemo.DLL为例讲述其定位过程。

......
:0040100E  6800204000               push  00402000
......
    在这个例子中汇编语句将一个指针压栈,402000h是某一字符串的指针。指令是来自一个基置为 00400000h 的DLL文件,因此这个字符串的RVA是2000h。如果DLL确实在 00400000h 处装入,那么指令能够按照现在的样子正确执行。假设当DLL执行时,Windows加载器决定将其映射到 870000h 处,此时就需要进行基址重定位,计算方式如下:

402000h + (870000h - 400000h)= 872000h

    基址重定位表(Base Relocation Table)位于一个叫.reloc 的区块内,但是找到它们正确方式是通过数据目录表的 IMAGE_DIRECTORY_ENTRY_BASERELOC 条目。基址重定位数据组织方法采用类似按页分割的方法,其许多重定位块串接在成的,每个块存放4KB(1000h)大小的重定位信息,每个重定位数据块的大小必须以DWORD(4字节)对齐,他们以IMAGE_BASE_RELOCATION 结构开始,格式如下:

STRUCT IMAGE_BASE_RELOCATION
{
    DWORD VirtualAddress;     //重定位数据开始RVA地址
    DWORD SizeOfSlock;       //重定位块的长度
    WORD TypeOffset;          //重定项位数组
};

VirtualAddress:这是一组重定位数据的开始RVA地址,各重定位项的地址加上这个值才是重定位项完整的RVA地址。
SizeOfBlock:是当前重定位结构的大小,因为VirtualAddress 和 SizeOfBlock 大小都是固定的4个字节,因此这个项减去8,则是TypeOffset 大小。
TypeOffset:是一个数组。数组每项大小为两个字节,共16位,它又分为高4位与低12位,高四位代表重定位类型,低12位是重定位的地址,与VirtualAddress 相加即是指向PE映像中需要修改地址数据的指针。
    对于X86 可执行文件,所有的基址重定位类型都是IMAGE_REL_BASED_HIGHLOW,在一组重定位结束的地方会出现一个类型是IAMGE_REL_BASED_ABSOLUTE的重定位,这些重定位什么都不做,在哪里只是填充,以便下一个IAMGE_BASE_RELOCATION 是以4个字节分界线来对齐,所有重定位最终以一个 VirtualAddress 字段为0的 IAMGE_BASE_RELOCATION 结构对齐。


    重定位表的结构如下图,由数个 IMAGE_BASE_RELOCATION 结构组成,每个结构VirtualAddress,SizeOfSlock 和 TypeOffset 三部分组成。







  • 9
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值