PE的基本概念
地址:
PE中设计的地址有四类:
1.虚拟内存地址(VA):
用户的PE文件被操作系统加载进内存后,PE对应的进程支配了自己独立的4GB虚拟空间。在这个空间中定位的地址称为虚拟和内存地址(Virtual Address VA),
所以虚拟内存地址的范围是0000 0000h ~ 0fffffffh 在PE中,进程本身的VA被解释为:进程的基地址+相对虚拟内存地址
2,相对虚拟内存(RVA):
一个进程被操作系统加载到虚拟内存空间后,其相关的动态库也会被加载,这些同时加载到进程地址空间的文件称为模块。
相对虚拟内存地址(Reverse Virtual Address,RVA)是相对于基地址的偏移,即RVA使虚拟内存中用来定位某个特定位置的地址,该地址的值是找个特定位置距离某个模块基地址的偏移量,所以说RVA是针对某个模块而存在的。
3.文件偏移地址(FOA):
文件偏移地址(File Offest Address,FOA)和内存无关,它是指某个位置距离文件头的偏移。
4.特殊地址:
在PE结构中还有一种特殊地址,其计算方法并不是从文件头算起,也不是从内存的某个模块的基地址算起,而是从某个特定的位置算起。这种地址在PE结构中很少见,如在资源表里就出现过这样的地址。
注:RVA是相对于模块而言的,VA是相对于整个内存空间而言的。
指针:
PE数据结构中的指针的定义:如果数据结构中某个字段存储的值为一个地址,那么这个字段就是一个指针。
数据目录:
PE中有一个数据结构称为数据目录,其中记录了所有可能的数据类型,这些类型中,目前定义的又15种,包括导出表,导入表,资源表,等等(在后文有介绍)。
节:
节就是存放不同数据的地方,不同的节具有不同的访问权限。
对齐:
PE中规定了三类对齐:数据在内存中的对齐、数据在文件中的对齐、资源文件中资源数据的对齐
- 内存对齐:由于Windows操作系统对内存属性的设置以页为单位,所以通常情况下,节在内存中的对齐单位必须至少是一个页的大小,4kb(1000h)
- 文件对齐:定义的节在文件中的对齐单位要远小于对齐的单位:通常会以一个物理扇区的大小作为对齐粒度的值,即512字节,十六进制表示为200h
- 资源数据对齐:资源文件中,资源字节码部分一般要求以双字(4个字节)方式对齐。
PE文件结构
PE结构大致可以分为两部分:DOS头和冗余数据
DOS文件头,有60个字节,会使用到IMAGE_DOS_HEADER结构体
PE文件头部解析
DOS MZ头 IMAGE_DOS_HEADER`
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE 头结构定义开始 文件中的位置(偏移量)
WORD e_magic; // 魔术数字 0x00000000-0x00000001
WORD e_cblp; // 文件最后页的字节数 0x00000002-0x00000003
WORD e_cp; // 文件页数 0x00000004-0x00000005
WORD e_crlc; // 重定位元素个数 0x00000006-0x00000007
WORD e_cparhdr; // 以段落为单位的头部大小 0x00000008-0x00000009
WORD e_minalloc; // 所需的最小附加段 0x0000000A-0x0000000B
WORD e_maxalloc; // 所需的最大附加段 0x0000000C-0x0000000D
WORD e_ss; // 初始的堆栈段(SS)相对偏移量值 0x0000000E-0x0000000F
WORD e_sp; // 初始的堆栈指针(SP)值 0x00000010-0x00000011
WORD e_csum; // 校验和 0x00000012-0x00000013
WORD e_ip; // 初始的指令指针(IP)值 0x00000014-0x00000015
WORD e_cs; // 初始的代码段(CS)相对偏移量值 0x00000016-0x00000017
WORD e_lfarlc; // 重定位表在文件中的偏移地址 0x00000018-0x00000019
WORD e_ovno; // 覆盖号 0x0000001A-0x0000001B
WORD e_res[4]; // 保留字(一般都是为确保对齐而预留) 0x0000001C-0x00000023 WORD e_oemid; // OEM 标识符(相对于 e_oeminfo) 0x00000024-0x00000025
WORD e_oeminfo; // OEM 信息,即 e_oemid 的细节 0x00000026-0x00000027 WORD e_res2[10]; // 保留字(一般都是为确保对齐而预留) 0x00000028-0x0000003B LONG e_lfanew; // 新 exe 头在文件中的偏移地址 0x0000003C-0x0000003F } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; //DOS .EXE 头结构定义结束;引用结构名:IMAGE_DOS_HEADER,结构指针 名:PIMAGE_DOS_HEADER
DOS头的第一个表示位,5A4D,是一个固定值,DOS头的字节数为60字节,最后4个字节存放的是相对偏移量。
NT头由一个简单的标记,如果是PE文件,标志恒为0x00004550
如下图所示,ASCII为(PE00)
在32位系统下的PE结构:
在32位系统中,刚好相反,即DOS头称为冗余,所谓冗余,针对DOS头不参与32位系统运行过程而言的。
1 定位标准头
- 定位标准PE头:
e_lfanew 即起这个作用,该字段的值是一个相对偏移量,绝对定位是需要加上DOSMZ头的基地址:
PE_start=DOSMZ基地址+
IMAGE_DOS_HANDER.e_lfanew
=000h+E0
=00E0h
PE文件结构
节表是PE中所有节的目录,每一个目录都是一个“BookStore”,其字节码就是节内容。它按照目录里的指针指向的地址,分别将节的字节码在文件空间中排列起来,从而组成了一个完整的PE文件,PE文件头部等于DOS头+PE头,PE头的大小是456个字节。
一个标准的PE文件一般由四大部分组成:
DOS头
PE头
节表(多个IMAGE_SECTION_HEADER结构)
节内容
其中PE头的数据最为复杂,PE头包含:
4个字节的标识号(Signature)(45 50)
20个字节的基本头信息(IMAGE_FILE_HANDER)(如图1-2 加粗部分)
216个字节的扩展头信息(IMAGE_OPTIONAL_HEADER32)
说明
如果按照“头部+身体”的信息组织方式来看:
PE文件头部 = DOS头+PE头+节表
PE文件身体 = 节内容
标准PE头 IMAGE_FILE_HEADER
标准PE头IMAGE_FILE_HEADER紧跟在PE头表示后,即位于IMAGE_DOS_HEADER的e_flanew值的+4位置
IMAGE_FILE_HEADER fileheader:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; 1 (文件的运行平台)
WORD NumberOfSections; 2 (区段的数量)
DWORD TimeDateStamp; 3 (文件创建时间)
DWORD PointerToSymbolTable; 4 (符号表偏移,用于调试)
DWORD NumberOfSymbols; 5 (符号个数,用于调试)
WORD SizeOfOptionalHeader; 6 (扩展头的大小)
WORD Characteristics; 7 (PE文件的一些属性)
} IMAGE_FILE_HEADER, * PIMAGE_FILE_HEADER;
该结构体常用于判断PE文件是EXE文件,还是DLL文件类型,不但可以通过结构体得到PE文件中节的总量,还可以当成对节去信息进行遍历操作时的循环次数(NumberOfSections)。
一共有2 + 2 + 4 + 4 + 4 + 2 + 2 = 20个字节。
NumberOfSections(区段的数量):0004
TimeDateStamp(文件创建时间):4A 5B CD F0
PointerToSymolTable(符号表偏移):00 00 00 00
NumberOfSymbols(符号表示数):00 00 00 00
SizeOfOptionalHeader(扩展头的大小):00 E0
Characteristics:01 02
扩展PE头IMAGE_OPTIONAL_HEADER32
IMAGE_OPTIONAL_HEADER32 OptionalHeader
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic; 1 文件类型标识,32位一般是0x010B,64位的PE文件一般是0x020B,还有0x0170,代表ROM镜像。
BYTE MajorLinkerVersion; 2 连接器主版本
BYTE MinorLinkerVersion; 3 连接器次版本
DWORD SizeOfCode; 4 (重要)指所有代码区段(节)的总大小
DWORD SizeOfInitializedData; 5 已初始化数据的总大小
DWORD SizeOfUninitializedData; 6 未初始化数据的总大小,在磁盘中不占用空间,在加载进内存之后,会预留这么大的空间。一般存储在.bss区段中。
DWORD AddressOfEntryPoint; 7 (重要)程序开始执行的相对虚拟地址(RVA),也叫OEP,Orginal Entry Point ,源入口点。
DWORD BaseOfCode; 8 (重要)起始代码的相对虚拟地址(RVA),一般这个值为0x00001000.
DWORD BaseOfData; 9 起始数据的相对虚拟地址(RVA)
// NT additional fields.
DWORD ImageBase; 10 (重要)默认加载基址(如果没有加载到这个地址,会发生重定位.)
DWORD SectionAlignment; 11 (重要)块对齐数,就是在映射到内存中的区段(节)对齐,这个数必须大于文件对齐数,一般是0x1000
DWORD FileAlignment; 12 (重要)文件对齐数,就是在硬盘中的文件的区段(节)对齐,一般是0x200
WORD MajorOperatingSystemVersion; 13 主操作系统版本号
WORD MinorOperatingSystemVersion; 14 次操作系统版本号
WORD MajorImageVersion; 15 主映像版本
WORD MinorImageVersion; 16 次映像版本
WORD MajorSubsystemVersion; 17 主子系统版本
WORD MinorSubsystemVersion; 18 次子系统版本
DWORD Win32VersionValue; 19 保留值,一般是0
DWORD SizeOfImage; 20 (重要)要把文件加载进内存,所需要的内存大小,注意是进行了块对齐之后
DWORD SizeOfHeaders; 21 所有头部大小,Dos头、PE头、区段表的尺寸之和
DWORD CheckSum; 22 校验和(一般无用)对于驱动和一些系统dll来说需要校验(使用IMAGEHLP.DLL中的CheckSumMappedFile API)
WORD Subsystem; 23 (重要)子系统值
WORD DllCharacteristics ; 24 (重要)指示Dll特征的标志,DllMain()函数何时被调用,默认为0.
DWORD SizeOfStackReserve; 25 初始化时栈的大小
DWORD SizeOfStackCommit; 26 初始化时实际提交的栈的大小
DWORD SizeOfHeapReserve; 27 初始化时保留的堆的大小
DWORD SizeOfHeapCommit; 28 初始化时实际提交的堆的大小
DWORD LoaderFlags; 29 与调试相关
DWORD NumberOfRvaAndSizes; 0 数据目录的个数,也就是下面那个数组中元素的个数。
IMAGE_DATA_DIRECTORY DataDirectory[ IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; 31 (非常重要)数据目录表
} IMAGE_OPTIONAL_HEADER32, * PIMAGE_OPTIONAL_HEADER32;
一共有96个字节再加上IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]的大小,32位系统中扩展头大小一般是224(0X00E0),在64位系统中一般是240(oxoofo)
Magic:01 0B
AddressOfEntryPoint(入口点):63 E5 (在Magic的正下方)
ImageBase(镜像基址):01 00 00 00
SectionAlignment(内存对其):00 00 10 00
FileAlignment:(文件对其):00 00 02 00
SizeOflmage:0B 00
SizeOfHeaders:400
PE头IMAGE_NT_HEADERS
这个结构是广义上的PE头,在标准的PE文件中其大小为456个字节,他是前面提到的3个结构体的组合,即IMAGE_NT_HEADERS=4个字节的PE表示+IMAGE_FILE_HEADERS + IMAGE_OPTIONAL_HEADER32
IMAGE_NT_HEADERS STRUCT
Signature DWORD ?;
FileHeader IMAGE_FILE_HEADER <> ;
OptionalHeader IMAGE_OPTIIONAL_HEADER32 <> ;
IMAGE_DATA_DIRECTORY
这是一个宏,值为0x10 表示一般情况下有16个数据目录,字节数为40,或者0x28
IMAGE_OPTIONAL_HEADER32结构的最后一个字段为DataDirectory。该字段定义了PE文件中出现的所有不同的类型的数据目录信息。
typedef struct _IMAGE_DATA_DIRECTORY
{
DWORD VirtualAddress; // 数据的相对虚拟地址(RVA)
DWORD Size; // 数据的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
结构体两个字段一次为VirtualAddress 和 isize,总的数据目录一共有16个相同的IMAGE_DATA_DIRECTORY结构连续排列在一起组成
节表项IMAGE_SECTION_HEADER
PE头IMAGE_NT_HEADERS后紧跟着节表。它是由许多个节表项(IMAGE_SECTION_HEADER)组成,每个字节表项纪录了PE中与摸个特定的节有关的信息,
IMAGE_SECTION_HEADER
typedef struct _IMAGE_SECTION_HEADER
{
+0 BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text”
//IMAGE_SIZEOF_SHORT_NAME=8
+8 union
{
DWORD PhysicalAddress; // 物理地址
DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一
// 般是取后一个
} Misc; //这个区段的大小
+ch DWORD VirtualAddress; // 节区的 RVA 地址
+10h DWORD SizeOfRawData; // 在文件中对齐后的尺寸
+14h DWORD PointerToRawData; // 在文件中的偏移量
+18h DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移
+1ch DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)
+1eh WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目
+20h WORD NumberOfLinenumbers; // 行号表中行号的数目
+24h DWORD Characteristics; // 节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
这个表在IMAGE_OPTIONAL_HEADER32后面,每个表占8个字节,以此类推,
前4个字节是表的其实RVA,后4个字节是表的长度,
如图1-3 前8个字节是0,说明没有导出表,
PE内存映像
可以用OD去查看,打开文件后,选择查看->内存选项,查看其内存分配。
大致分为:PE文件头+代码+输入表+数据
每个部分都按照1000h对齐,因为在Windos操作系统中,每个运行的程序都有自己独立的地址空间
文件以200h对齐,内存映像1000h 对齐
RVA与FOA的转换
RVA是相对虚拟地址,FOA是文件偏移,PE文件头和PE内存映射的文件头大小都是一样的,它们受对齐粒度不同的映像;节的数据在内存和磁盘文件的大小是不一样的,节表项纪录了内存映像中的其实RVA,也通用记录了本节在文件中的起始偏移,节表项IMAGE_SECTION_HEADER是我们可以找到这两个字段之间存在关联的唯一地方。
步骤如下:
- 判断指定的RVA落在哪个节内
- 求出该节的起始RVA0=IMAGE_SECTION_HEADER.VirtualAddress.
- 求出偏移量 offset=RVA0-RVA
- 求出该RVA相对于磁盘文件头的偏移
FOA= IMAGE_SECTION_HEADER.PointerToRawData+off
步骤1 很明显,通过对比RVA20<RVA<RAV30 ,指定的 RVA落在节2内
步骤2 该节起始为 RVA0 = RVA20
步骤3 距离本节的偏移为off = RVA – RVA20
步骤4 该RVA相对于磁盘文件的偏移为:
FOA= IMAGE_SECTION_HEADER.PointerToRadData +off
= FOA20+OFF
=FOA20 +RVA – RVA 20
代码如下:`
char* g_lpBase;
//RVA to FOA
DWORD RVAtoFOA(DWORD dwRVA)
{
//落在哪个区段内
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)g_lpBase;
PIMAGE_NT_HEADERS pNt =
(PIMAGE_NT_HEADERS)
(pDos->e_lfanew + g_lpBase);
//找到区段表头地址
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
//获取区段个数
DWORD dwCount = pNt->FileHeader.NumberOfSections;
for (DWORD i = 0; i < dwCount; i++)
{
if (dwRVA >= pSection->VirtualAddress &&
dwRVA < pSection->VirtualAddress + pSection->SizeOfRawData)
{
return dwRVA -
pSection->VirtualAddress +
pSection->PointerToRawData;
}
pSection++;
}
return 0;
}
//判定是否是PE文件
BOOL IsPE(char* lpBase)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBase;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE/*0x4D5A*/)
{
return FALSE;
}
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpBase);
if (pNt->Signature != IMAGE_NT_SIGNATURE/*0x4550*/)
{
return FALSE;
}
return TRUE;
}
//把文件读到内存
char* ReadFileToMemory(char* lpFilePath)
{
//打开文件获取句柄
HANDLE hFile = CreateFileA(lpFilePath,
GENERIC_READ,
FALSE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("文件打开失败\n");
return 0;
}
//获取文件大小
DWORD dwSize = GetFileSize(hFile, NULL);
/*char* pBuf*/g_lpBase = new char[dwSize]{};
//读文件
DWORD dwCount = 1;
BOOL bRet =
ReadFile(hFile, g_lpBase/*pBuf*/, dwSize, &dwCount, NULL);
if (bRet)
{
return g_lpBase/*pBuf*/;
}
//释放资源
CloseHandle(hFile);
delete g_lpBase/*pBuf*/;
return 0;
}
//解析PE头部
void ShowHeaderInfo(char* lpBase)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBase;
PIMAGE_NT_HEADERS pNt =
(PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpBase);
//OEP
printf("OEP: %08X\n", pNt->OptionalHeader.AddressOfEntryPoint);
//ImageBase
printf("ImageBase: %08X\n", pNt->OptionalHeader.ImageBase);
//区段个数
printf("区段个数: %08X\n", pNt->FileHeader.NumberOfSections);
}
int _tmain(int argc, _TCHAR* argv[])
{
g_lpBase = ReadFileToMemory("RVAtoFOA.exe");
if (!g_lpBase)
{
//printf("")
return 0;
}
if (!IsPE(g_lpBase))
{
return 0;
}
ShowHeaderInfo(g_lpBase);
return 0;
}
导出表
代码重用机制提供了重用代码的动态链接库,它会向调用者说明库里的哪些函数是可以被别人使用的,这些用来说明的信息便组成了导出表。
对一个动态链接库里导出的函数调用,既可以通过函数名称来进行,也可以通过函数在导出表的索引来进行。每一个导出的函数,都有一个唯一的序号与之对应,有的情况下,会没有函数名,但是会有函数地址和序号,导出的东西包括:1函数地址,2 序号 3函数名
导出表,是根据数据目录(DataDirectory)的第一个元素相对虚拟地址,再通过相对虚拟地址文件偏移的方式,可以找到它。
导出表数据结构
- 在PE文件里定位导出表
注:PE文件可能没有导出表 即全为0
在96字节的扩展表后就是导出表。
导出表所在地址RVA = 016CC0
导出表数据大小 = 17C
2. 导出表的数据结构组织
导出目录IMAGE_EXPORT_DIRECTORY
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;//未使用,总是定义为0
DWORD TimeDateStamp;//文件生成时间
WORD MajorVersion;//未使用,总是定义为0
WORD MinorVersion;//未使用,总是定义为0
DWORD Name; //模块的真实名称的RVA
DWORD Base; //基数,加上序数就是函数地址数组的索引值
DWORD NumberOfFunctions;//导出函数的总数
DWORD NumberOfNames; //以名称方式导出的函数的总数
DWORD AddressOfFunctions; // RVA from base of image指向输出函数地址的RVA
DWORD AddressOfNames; // RVA from base of image指向输出函数名字的RVA
DWORD AddressOfNameOrdinals; // RVA from base of image向输出函数序号的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Name ,双字,该字段指示的地址指向了一个以“\0”结尾的字符串,字符串记录了导出表所在的文件的最初文件名。
NumberOfFunctions 双字,该字段定义了文件中导出函数的总个数
NumberOfName 双字,再导出表中,有些函数是定义米工资的,有些是没有定义名字的,该字段记录了所有定义名字函数个数。如果这个值是0,则表示所有函数都没有定义名字。
AddressOfFunctions 双字,该指针指向了全部导出函数的入口地址的起始,从入口地址开始为双字数组,数组的个数由段IMAGE_EXPORT_DIRECTORY.NumberOfFunction决定。导出函数的每一个地址按函数的标号顺序依次往后排开,再内存中,我们可以通过函数标号来定位摸个函数的地址。
Base 双字,导出函数标号的起始值,DLL中的第一个导出函并不是从0开始的,某导出函数的标号等于从AddressOfFunctions开始的顺序号加上这个值。
AddressOfName 双字,该值也是一个指针,该指针指向的位置是一连串的双字值,这些双字值均为指向了对应的定义了函数名的函数的字符串地址。
AddressOFNameOrdinals 双字,该值也是一个指针,与AddressOfName是一一对应,所不同的是,AddressOfName 指向的是字符串的指针数组,二AddressOfNameOrdinals则指向了该函数在AddressOfFunctions的索引值。
导出表实例
//遍历导出表
void ShowExportTableInfo(char* lpBase)
{
//找到导出表
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBase;
PIMAGE_NT_HEADERS pNt =
(PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpBase);
PIMAGE_DATA_DIRECTORY pDir =
&pNt->OptionalHeader.DataDirectory[0];
DWORD dwExportFOA = RVAtoFOA(pDir->VirtualAddress);
//导出表在文件中的位置
PIMAGE_EXPORT_DIRECTORY pExportTable =
(PIMAGE_EXPORT_DIRECTORY)
(dwExportFOA + lpBase);
printf("模块名称%s\n", (RVAtoFOA(pExportTable->Name) + lpBase));
//遍历
DWORD dwFunCount = pExportTable->NumberOfFunctions;
DWORD dwOrdinalCount = pExportTable->NumberOfNames;
//地址表
DWORD* pFunAddr =
(DWORD*)(RVAtoFOA(pExportTable->AddressOfFunctions) + lpBase);
//名称表
DWORD* pNameAddr =
(DWORD*)(RVAtoFOA(pExportTable->AddressOfNames) + lpBase);
//序号表
WORD* pOrdinalAddr =
(WORD*)(RVAtoFOA(pExportTable->AddressOfNameOrdinals) + lpBase);
for (DWORD i = 0; i < dwFunCount; i++)
{
//如果为0说明是无效地址,直接跳过
if (pFunAddr[i] == 0)
{
continue;
}
//遍历序号表中是否有此序号,如果有说明此函数有名字
BOOL bFlag = FALSE;
for (DWORD j = 0; j < dwOrdinalCount; j++)
{
if (i == pOrdinalAddr[j])
{
bFlag = TRUE;
DWORD dwNameRVA = pNameAddr[j];
printf("函数名:%s,函数序号:%04X,函数序号:%04X\n",
RVAtoFOA(dwNameRVA) + lpBase,
i + pExportTable->Base);
}
}
//如果序号表中没有,说明此函数只有序号没有名字
if (!bFlag)
{
printf("函数名【NULL】,函数序号:%04X\n", i + pExportTable->Base);
}
}
}
导入表
1导入是这个PE文件在运行时,使用到了其他PE文件中的函数,变量,类等这样的行为。
2 导入表时根据数据目录表(DataDirectory)的第二个元素找到的。
3 导入表存储的是从其他PE文件导入过来的函数名,序号。在加载到内存之后,还存储这些函数的地址。
4 由于一个PE文件可能会需要多个PE文件的支持,所以导入表结构一般由多个,就是说导入表其实是一个结构体数组,以一个全零元素为结尾,每一个数组的元素,代表一个PE文件的导入信息。
导入函数是从动态链接库引入的函数,所以,导入函数的地址位于被加载的进程地址控件中的相应的动态连接库模块内,系统在执行用户程序对导入函数的调用语句时,会跳转到该地址处执行函数代码
重定位表
如图 加粗部分即为重定位表数据目录项信息,
重定位表所在地址RVA = 0230 9000
重定位表数据大小 = 0017 9EF4