本文深入解析Windows PE(可移植可执行)文件格式中的关键结构——导入表(Import Table)与导入地址表(IAT, Import Address Table),阐述其动态链接机制,并提供完整的C语言遍历代码示例,适用于逆向工程与安全分析学习。
IAT表:动态链接的桥梁
在调用动态链接库(DLL)中的函数时,我们通常会观察到汇编指令并非直接 call 目标函数的绝对地址,而是采用间接寻址的方式,例如:
call dword ptr [004322d4]
这里的 004322d4 是位于.exe模块地址空间内的一个地址(一个指针),而该地址中存储的值 X 才是函数真正的入口地址。程序运行时,操作系统会将DLL加载到内存,并将函数 MessageBoxA 的实际地址(例如 77d5050b)填充到 004322d4 这个位置。这样,代码就通过 [004322d4] 间接地跳转到了 USER32.dll 的领空。
这种设计至关重要。因为DLL在每次加载时,其基地址(ImageBase)可能因地址空间冲突而改变(重定位),导致其内部函数的绝对地址无法预先确定。通过间接寻址,调用者无需关心函数最终位于何处,只需访问自身模块内固定的IAT表项,由操作系统在加载时完成地址“重写”。
在磁盘上的PE文件中,IAT表项(如 004322d4 指向的位置)初始存储的并非地址,而是函数名或序号。当所有模块(.exe及其依赖的.dll)被映射到内存并完成重定位后,系统才会将这些字符串替换为真实的函数地址。这个存储着函数引用、并将在加载时被修正的指针数组,就是IAT表。

导入表:程序的“外部依赖清单”
如果说DLL的导出表像一份提供给外部的“功能菜单”,那么.exe的导入表就是它点菜的“订单”。它详细记录了该程序需要从哪些DLL中调用哪些函数。
在PE文件头的数据目录表(Data Directory)中,第二项(索引1)即指向导入表。导入表实际上是一个由 IMAGE_IMPORT_DESCRIPTOR 结构体构成的数组,每个结构体对应一个被依赖的DLL,数组以全零结构体结束。
导入表核心结构解析
以下是定位和解析导入表的关键结构:

IMAGE_IMPORT_DESCRIPTOR 主要包含以下重要成员:
- OriginalFirstThunk:指向INT(Import Name Table,导入名称表)。这是一个
IMAGE_THUNK_DATA 数组,存储着函数名或序号信息,以0结束。
- Name:一个RVA(相对虚拟地址),指向依赖的DLL名称字符串(如“KERNEL32.dll”)。
- FirstThunk:指向IAT(Import Address Table,导入地址表)。在磁盘文件中,其内容与INT表完全相同;当文件被加载到内存后,其内容会被系统替换为函数的真实虚拟地址。
下图清晰地展示了PE文件在加载前后,这些结构之间关系的变化:

IMAGE_THUNK_DATA 与 IMAGE_IMPORT_BY_NAME
无论是INT还是IAT,它们存储的都是 IMAGE_THUNK_DATA 联合体。在32位PE文件中,它是一个4字节的DWORD。
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal; // 函数序号
PIMAGE_IMPORT_BY_NAME AddressOfData; // 指向IMAGE_IMPORT_BY_NAME结构
} u1;
} IMAGE_THUNK_DATA32;
- 当双字的最高位为1时,低31位代表函数的导出序号。
- 当最高位为0时,该值是一个RVA,指向一个
IMAGE_IMPORT_BY_NAME 结构。
IMAGE_IMPORT_BY_NAME 结构体包含了函数的详细信息:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // 提示序号,可能为空,通常为函数在导出表中的索引
BYTE Name[1]; // 函数名称字符串,以‘\0’结尾(实际长度不定)
} IMAGE_IMPORT_BY_NAME;
注意,Name 字段虽然定义为1字节数组,但实际是一个以 \0 结尾的变长字符串,需要通过指针遍历读取。
下图直观展示了如何遍历INT/IAT表,并解析出其中的函数名或序号:


实践:遍历导入表的C语言代码
理解理论后,通过代码实践能加深认识。以下C语言示例演示了如何解析PE文件,并打印出其所有导入信息(依赖的DLL及每个DLL中的函数)。
#include "windows.h"
#include "stdio.h"
VOID PrintImportTable()
{
char FilePath[] = "PETool.exe"; // 目标PE文件
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
PIMAGE_IMPORT_DESCRIPTOR pImportTable = NULL;
BYTE zero[sizeof(IMAGE_IMPORT_DESCRIPTOR)] = { 0 };
PDWORD pIATItem, pINTItem;
PIMAGE_IMPORT_BY_NAME pImportByName;
// 1. 将PE文件读入内存缓冲区 (ReadPEFile为假设的封装函数)
if (!ReadPEFile(FilePath, &pFileBuffer))
{
printf("文件读取失败\n");
return;
}
// 2. 定位DOS头、NT头、可选头
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileBuffer +
pDosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER);
// 3. 定位导入表(通过数据目录)
DWORD importTableRVA = pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer +
RVA2FOA(pFileBuffer, importTableRVA)); // RVA2FOA为RVA转文件偏移函数
// 4. 遍历所有导入描述符(每个对应一个DLL)
for (int i = 1; memcmp(zero, pImportTable, sizeof(zero)); i++)
{
printf(">>>>>>>>>> 第 %d 个DLL导入项 <<<<<<<<<<\n", i);
printf("DLL名称: %s\n", (char*)((DWORD)pFileBuffer +
RVA2FOA(pFileBuffer, pImportTable->Name)));
printf("INT表RVA: %x | IAT表RVA: %x\n",
pImportTable->OriginalFirstThunk,
pImportTable->FirstThunk);
// 4.1 遍历INT表(打印函数名/序号)
printf("[INT表内容 - 函数名/序号]:\n");
pINTItem = (PDWORD)((DWORD)pFileBuffer +
RVA2FOA(pFileBuffer, pImportTable->OriginalFirstThunk));
for (int j = 1; *pINTItem; j++)
{
if (*pINTItem & 0x80000000) // 最高位为1,表示序号导入
{
printf(" 函数[%d] 序号: %x\n", j, *pINTItem & 0x7FFFFFFF);
}
else // 按名称导入
{
pImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer +
RVA2FOA(pFileBuffer, *pINTItem));
printf(" 函数[%d] 名称: %s (Hint: %x)\n",
j, pImportByName->Name, pImportByName->Hint);
}
pINTItem++;
}
// 4.2 遍历IAT表(在文件状态下,内容应与INT表一致)
printf("[IAT表内容 (文件状态)]:\n");
pIATItem = (PDWORD)((DWORD)pFileBuffer +
RVA2FOA(pFileBuffer, pImportTable->FirstThunk));
for (int j = 1; *pIATItem; j++)
{
// 解析逻辑同上,略...
pIATItem++;
}
pImportTable++;
printf("\n");
}
// 注意:应在此处释放 pFileBuffer 内存
}
这段代码清晰地展示了两层循环结构:外层循环遍历每个导入的DLL,内层循环遍历该DLL下的每一个函数。通过解析 IMAGE_THUNK_DATA 的最高位,可以准确判断函数是以序号还是以名称导入的。理解和操作这些底层结构是逆向工程与二进制安全分析的基础。
关键点总结与拓展
- 加载时地址解析:系统加载PE文件时,会遍历INT表,对每个函数调用
GetProcAddress 获取其实际内存地址,并填入对应的IAT表项中。
- 隐式链接与显式链接:本文所述机制属于隐式链接(静态加载),依赖信息会记录在导入表中。若程序使用
LoadLibrary 和 GetProcAddress 进行显式链接(动态加载),则不会在导入表中留下痕迹,这在分析某些软件安全机制时值得注意。
- IAT Hook:由于所有外部函数调用都经由IAT表跳转,因此IAT成为安全软件和恶意代码监控或篡改程序行为的常见目标(即IAT Hook)。深入理解本文内容,是分析和防御此类技术的前提。
通过对PE文件导入表和IAT表的剖析,我们不仅掌握了Windows程序动态链接的核心机制,也为后续进行更深入的逆向分析、漏洞挖掘及安全防护打下了坚实的底层基础。