x86 反汇编/Windows 可执行文件
COM 文件按其显示的完全相同的方式加载到 RAM 中;从硬盘映像到 RAM 没有任何更改。这是由于早期 x86 系列的分段内存模型。两个 16 位寄存器决定用于内存访问的实际地址,一个“段”寄存器指定 1M+64K 字节空间中的 64K 字节窗口(以 16 字节为增量),以及一个“偏移量”指定该窗口中的偏移量。段寄存器将由 DOS 设置,COM 文件预计将遵守此设置并且永远不会更改段寄存器。然而,偏移量寄存器是自由使用的,并且(对于 COM 文件)与现代 32 位寄存器具有相同的用途。缺点是偏移量寄存器只有 16 位,因此,由于 COM 文件无法更改段寄存器,因此 COM 文件使用 RAM 的限制为 64K。然而,这种方法的好处是,DOS 不需要额外的工作来加载和运行 COM 文件:只需加载文件,设置段寄存器,然后跳转到它。(程序可以通过只给出要跳转到的偏移量来执行“近”跳转。)
COM 文件加载到 RAM 中的偏移量为 $100。在此之前空间将用于传递数据到和来自 DOS(例如,用于调用程序的命令行的内容)。
请注意,根据定义,COM 文件不能是 32 位的。Windows 通过特殊的 CPU 模式提供对 COM 文件的支持。
请注意,MS-DOS COM 文件(“命令”文件的简称)与组件对象模型文件不同,后者是一种面向对象的库技术。 |
MS-DOS 编译器克服 64K 内存限制的一种方法是引入了内存模型。基本概念是巧妙地设置 x86 CPU 中的不同段寄存器 (CS、DS、ES、SS) 以指向相同或不同的段,从而允许不同程度地访问内存。典型的内存模型是
- 微型
- 所有内存访问都是 16 位的(段寄存器保持不变)。生成 .COM 文件而不是 .EXE 文件。
- 小型
- 所有内存访问都是 16 位的(段寄存器保持不变)。
- 紧凑型
- 数据地址包括段和偏移量,在访问时重新加载 DS 或 ES 寄存器,允许高达 1M 的数据。代码访问不会更改 CS 寄存器,允许 64K 的代码。
- 中型
- 代码地址包括段地址,在访问时重新加载 CS,允许高达 1M 的代码。数据访问不会更改 DS 和 ES 寄存器,允许 64K 的数据。
- 大型
- 代码和数据地址都是 (段,偏移量) 对,始终重新加载段地址。整个 1M 字节内存空间可用于代码和数据。
- 巨大
- 与大型模型相同,编译器会生成额外的算术运算,以允许访问大于 64K 的数组。
查看 EXE 文件时,必须确定使用哪种内存模型构建该文件。
可移植可执行文件 (PE) 文件是 Windows NT、Windows 95 和 Win32 下可执行文件或 DLL 的标准二进制文件格式。Win32 SDK 包含一个文件winnt.h,它声明 PE 文件中使用的各种结构和变量。imagehlp.dll 中还包含一些用于操作 PE 文件的函数。PE 文件被分解成可以检查的不同部分。
在 Windows 环境中,可执行模块可以加载到内存中的任何位置,并期望在没有任何问题的情况下运行。为了允许多个程序加载到内存中看似随机的位置,PE 文件采用了一种名为 RVA 的工具:相对虚拟地址。RVA 假设模块加载到内存中的“基地址”在编译时是未知的。因此,PE 文件将内存中数据的地址描述为相对于基地址的偏移量,无论该地址在内存中的何处。
一些处理器指令要求代码本身直接识别内存中某个数据的位置。当模块在内存中的位置在编译时未知时,这不可能。解决此问题的方案在“重定位”部分中介绍。
请记住,从模块的反汇编中获得的地址并不总是与调试器在程序运行时看到的地址相匹配。
PE 可移植可执行文件格式包含许多信息头,并按以下格式排列
Microsoft PE 文件的基本格式
在十六进制编辑器中打开任何 Win32 二进制可执行文件,你会看到:前两个字母 **始终** 是 "MZ",这是 Mark Zbikowski 的首字母缩写,他创建了第一个 DOS 链接器。对一些人来说,文件中确定文件类型的头几个字节被称为 "魔数",虽然没有规定 "魔数" 必须是单个数字。相反,我们将使用术语 "文件 ID 标签",或简称为 "文件 ID"。有时这也称为文件签名。
在文件 ID 之后,十六进制编辑器会显示一些随机符号或空白字符,然后是可读的字符串 "This program cannot be run in DOS mode"。
这是什么呢?
MS-DOS 文件头的十六进制列表
你看到的是 Win32 PE 文件的 MS-DOS 头。为了确保 a) 向后兼容性,或 b) 新文件类型的优雅降级,微软在每个 PE 文件的头部写入了一系列机器指令(DOS 头结构下面列出了一个示例程序)。当一个 32 位 Windows 文件在 16 位 DOS 环境中运行时,程序会显示错误消息:"This program cannot be run in DOS mode.",然后终止。
DOS 头也称为 EXE 头。以下是 DOS 头作为 C 数据结构的表示
struct DOS_Header
{
// short is 2 bytes, long is 4 bytes
char signature[2] = { 'M', 'Z' };
short lastsize;
short nblocks;
short nreloc;
short hdrsize;
short minalloc;
short maxalloc;
void *ss; // 2 byte value
void *sp; // 2 byte value
short checksum;
void *ip; // 2 byte value
void *cs; // 2 byte value
short relocpos;
short noverlay;
short reserved1[4];
short oem_id;
short oem_info;
short reserved2[10];
long e_lfanew; // Offset to the 'PE\0\0' signature relative to the beginning of the file
}
在 DOS 头之后,有一个上面提到的存根程序。下面列出了一个注释过的示例程序,它来自用 GCC 编译的程序。
;# Using NASM with Intel syntax
push cs ;# Push CS onto the stack
pop ds ;# Set DS to CS
mov dx,message ; point to our message "This program cannot be run in DOS mode.", 0x0d, 0x0d, 0x0a, '$'
mov ah, 09
int 0x21 ;# when AH = 9, DOS interrupt to write a string
;# terminate the program
mov ax,0x4c01
int 0x21
message db "This program cannot be run in DOS mode.", 0x0d, 0x0d, 0x0a, '$'
PE 头
[edit | edit source]从 DOS 头开始的偏移量 60 (0x3C) 是指向可移植可执行 (PE) 文件头的指针 (MZ 结构中的 e_lfanew)。DOS 会打印错误消息并终止,但 Windows 会遵循此指针指向下一批信息。
PE 签名的十六进制列表及其指针
PE 头只包含一个文件 ID 签名,其值为 "PE\0\0",其中每个 '\0' 字符都是 ASCII 空字符。此签名表明 a) 此文件是合法的 PE 文件,以及 b) 文件的字节序。字节序在本章中不会被考虑,所有 PE 文件都假定为 "小端" 格式。
第一块大信息位于 COFF 头中,紧接在 PE 签名之后。
COFF 头
[edit | edit source]COFF 头存在于 COFF 对象文件(在链接之前)和 PE 文件中,在 PE 文件中被称为 "文件头"。COFF 头包含一些对可执行文件有用的信息,还包含一些对对象文件更有用的信息。
以下是 COFF 头作为 C 数据结构的表示
struct COFFHeader
{
short Machine;
short NumberOfSections;
long TimeDateStamp;
long PointerToSymbolTable;
long NumberOfSymbols;
short SizeOfOptionalHeader;
short Characteristics;
}
- 机器
- 此字段确定该文件编译的目标机器。十六进制值为 0x14C(十进制为 332)是 Intel 80386 的代码。
以下是其可能值的列表。
值 | 描述 |
0x14c | Intel 386 |
0x8664 | x64 |
0x162 | MIPS R3000 |
0x168 | MIPS R10000 |
0x169 | MIPS 小端 WCI v2 |
0x183 | 旧 Alpha AXP |
0x184 | Alpha AXP |
0x1a2 | 日立 SH3 |
0x1a3 | 日立 SH3 DSP |
0x1a6 | 日立 SH4 |
0x1a8 | 日立 SH5 |
0x1c0 | ARM 小端 |
0x1c2 | Thumb |
0x1c4 | ARMv7 (Thumb-2) |
0x1d3 | 松下 AM33 |
0x1f0 | PowerPC 小端 |
0x1f1 | 支持浮点数的 PowerPC |
0x1f2 | PowerPC 64 位小端 |
0x200 | 英特尔 IA64 |
0x266 | MIPS16 |
0x268 | 摩托罗拉 68000 系列 |
0x284 | Alpha AXP 64 位 |
0x366 | 带有 FPU 的 MIPS |
0x466 | 带有 FPU 的 MIPS16 |
0xebc | EFI 字节码 |
0x8664 | AMD AMD64 |
0x9041 | 三菱 M32R 小端 |
0xaa64 | ARM64 小端 |
0xc0ee | clr 纯 MSIL |
- NumberOfSections
- 在 PE 头部结尾处描述的节的数量。
- TimeDateStamp
- 生成此头的 32 位时间:用于 "绑定" 过程,见下文。
- SizeOfOptionalHeader
- 此字段显示 COFF 头之后的 "PE 可选头" 的长度。
- Characteristics
- 这是一个位标志字段,显示文件的一些特性。
常量名称 | 位位置 / 掩码 | 描述 |
IMAGE_FILE_RELOCS_STRIPPED | 1 / 0x0001 | 重定位信息已从文件中删除 |
IMAGE_FILE_EXECUTABLE_IMAGE | 2 / 0x0002 | 该文件是可执行文件 |
IMAGE_FILE_LINE_NUMS_STRIPPED | 3 / 0x0004 | COFF 行号已从文件中删除 |
IMAGE_FILE_LOCAL_SYMS_STRIPPED | 4 / 0x0008 | COFF 符号表条目已从文件中删除 |
IMAGE_FILE_AGGRESIVE_WS_TRIM | 5 / 0x0010 | 积极地修剪工作集(**已过时**) |
IMAGE_FILE_LARGE_ADDRESS_AWARE | 6 / 0x0020 | 应用程序可以处理大于 2 GB 的地址 |
IMAGE_FILE_BYTES_REVERSED_LO | 8 / 0x0080 | 字的字节被反转(**已过时**) |
IMAGE_FILE_32BIT_MACHINE | 9 / 0x0100 | 计算机支持 32 位字 |
IMAGE_FILE_DEBUG_STRIPPED | 10 / 0x0200 | 调试信息已被删除并单独存储在另一个文件中 |
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP | 11 / 0x0400 | 如果映像在可移动介质上,则将其复制到交换文件并从交换文件中运行 |
IMAGE_FILE_NET_RUN_FROM_SWAP | 12 / 0x0800 | 如果映像在网络上,则将其复制到交换文件并从交换文件中运行 |
IMAGE_FILE_SYSTEM | 13 / 0x1000 | 映像是一个系统文件 |
IMAGE_FILE_DLL | 14 / 0x2000 | 映像是一个 DLL 文件 |
IMAGE_FILE_UP_SYSTEM_ONLY | 15 / 0x4000 | 映像只应该在单处理器计算机上运行 |
IMAGE_FILE_BYTES_REVERSED_HI | 16 / 0x8000 | 字的字节被反转(**已过时**) |
PE 可选头
[edit | edit source]"PE 可选头" 本身并不是 "可选的",因为它在可执行文件中是必需的,但在 COFF 对象文件中不是必需的。可选头的两个不同版本取决于文件是 64 位还是 32 位。可选头包含大量信息,可用于分析文件结构,并获取一些关于该文件的有用信息。
PE 可选头位于 COFF 头之后,有些资料甚至将这两个头显示为同一个结构的一部分。为了方便起见,本维基教科书将它们分开。
以下是作为 C 数据结构表示的 64 位 PE 可选头
struct PEOptHeader
{
/* 64 bit version of the PE Optional Header also known as IMAGE_OPTIONAL_HEADER64
char is 1 byte
short is 2 bytes
long is 4 bytes
long long is 8 bytes
*/
short signature; //decimal number 267 for 32 bit, 523 for 64 bit, and 263 for a ROM image.
char MajorLinkerVersion;
char MinorLinkerVersion;
long SizeOfCode;
long SizeOfInitializedData;
long SizeOfUninitializedData;
long AddressOfEntryPoint; //The RVA of the code entry point
long BaseOfCode;
/*The next 21 fields are an extension to the COFF optional header format*/
long long ImageBase;
long SectionAlignment;
long FileAlignment;
short MajorOSVersion;
short MinorOSVersion;
short MajorImageVersion;
short MinorImageVersion;
short MajorSubsystemVersion;
short MinorSubsystemVersion;
long Win32VersionValue;
long SizeOfImage;
long SizeOfHeaders;
long Checksum;
short Subsystem;
short DLLCharacteristics;
long long SizeOfStackReserve;
long long SizeOfStackCommit;
long long SizeOfHeapReserve;
long long SizeOfHeapCommit;
long LoaderFlags;
long NumberOfRvaAndSizes;
data_directory DataDirectory[NumberOfRvaAndSizes]; //Can have any number of elements, matching the number in NumberOfRvaAndSizes.
} //However, it is always 16 in PE files.
以下是作为 C 数据结构表示的 32 位 PE 可选头
struct PEOptHeader
{
/* 32 bit version of the PE Optional Header also known as IMAGE_OPTIONAL_HEADER
char is 1 byte
short is 2 bytes
long is 4 bytes
*/
short signature; //decimal number 267 for 32 bit, 523 for 64 bit, and 263 for a ROM image.
char MajorLinkerVersion;
char MinorLinkerVersion;
long SizeOfCode;
long SizeOfInitializedData;
long SizeOfUninitializedData;
long AddressOfEntryPoint; //The RVA of the code entry point
long BaseOfCode;
long BaseOfData;
/*The next 21 fields are an extension to the COFF optional header format*/
long ImageBase;
long SectionAlignment;
long FileAlignment;
short MajorOSVersion;
short MinorOSVersion;
short MajorImageVersion;
short MinorImageVersion;
short MajorSubsystemVersion;
short MinorSubsystemVersion;
long Win32VersionValue;
long SizeOfImage;
long SizeOfHeaders;
long Checksum;
short Subsystem;
short DLLCharacteristics;
long SizeOfStackReserve;
long SizeOfStackCommit;
long SizeOfHeapReserve;
long SizeOfHeapCommit;
long LoaderFlags;
long NumberOfRvaAndSizes;
data_directory DataDirectory[NumberOfRvaAndSizes]; //Can have any number of elements, matching the number in NumberOfRvaAndSizes.
} //However, it is always 16 in PE files.
这是在上面两个结构中找到的 data_directory(也称为 IMAGE_DATA_DIRECTORY)结构
/*
long is 4 bytes
*/
struct data_directory
{
long VirtualAddress;
long Size;
}
- 签名
- 包含识别映像的签名。
常量名称 | 值 | 描述 |
---|---|---|
IMAGE_NT_OPTIONAL_HDR32_MAGIC | 0x10b | 32 位可执行映像。 |
IMAGE_NT_OPTIONAL_HDR64_MAGIC | 0x20b | 64 位可执行映像 |
IMAGE_ROM_OPTIONAL_HDR_MAGIC | 0x107 | ROM 映像 |
- MajorLinkerVersion
- 链接器的主要版本号。
- MinorLinkerVersion
- 链接器的次要版本号。
- SizeOfCode
- 代码节的大小(以字节为单位),如果有多个代码节,则为所有这些节的总大小。
- SizeOfInitializedData
- 已初始化数据节的大小(以字节为单位),如果有多个已初始化数据节,则为所有这些节的总大小。
- SizeOfUninitializedData
- 未初始化数据节的大小(以字节为单位),如果有多个未初始化数据节,则为所有这些节的总大小。
- AddressOfEntryPoint
- 指向入口点函数的指针,相对于映像基地址。对于可执行文件,这是起始地址。对于设备驱动程序,这是初始化函数的地址。入口点函数对于 DLL 是可选的。如果没有入口点,此成员为零。
- BaseOfCode
- 指向代码节起始位置的指针,相对于映像基地址。
- BaseOfData
- 指向数据节起始位置的指针,相对于映像基地址。
- ImageBase
- 映像加载到内存后,第一个字节的首选地址。此值是 64K 字节的倍数。DLL 的默认值为 0x10000000。应用程序的默认值为 0x00400000,除了 Windows CE,其默认值为 0x00010000。
- SectionAlignment
- 加载到内存中的节的对齐方式(以字节为单位)。此值必须大于或等于 FileAlignment 成员。默认值为系统的页面大小。
- FileAlignment
- 映像文件中节的原始数据的对齐方式(以字节为单位)。该值应为 512 到 64K(含)之间的 2 的幂。默认值为 512。如果 SectionAlignment 成员小于系统页面大小,则此成员必须与 SectionAlignment 相同。
- MajorOSVersion
- 所需操作系统的 major 版本号。
- MinorOSVersion
- 所需操作系统的 minor 版本号。
- MajorImageVersion
- 映像的 major 版本号。
- MinorImageVersion
- 映像的 minor 版本号。
- MajorSubsystemVersion
- 子系统的 major 版本号。
- MinorSubsystemVersion
- 子系统的次要版本号。
- Win32VersionValue
- 此成员保留,必须为 0。
- SizeOfImage
- 映像的大小(以字节为单位),包括所有标头。必须是 SectionAlignment 的倍数。
- SizeOfHeaders
- 以下各项的总大小,四舍五入到 FileAlignment 成员中指定的值的倍数。
- DOS_Header 的 e_lfanew 成员
- 4 字节签名
- COFFHeader 的大小
- 可选标头的尺寸
- 所有节标头的尺寸
- CheckSum
- 映像文件的校验和。以下文件在加载时进行验证:所有驱动程序、在启动时加载的任何 DLL 以及加载到关键系统进程中的任何 DLL。
- Subsystem
- 将被调用以运行可执行文件的子系统
常量名称 | 值 | 描述 |
---|---|---|
IMAGE_SUBSYSTEM_UNKNOWN | 0 | 未知子系统 |
IMAGE_SUBSYSTEM_NATIVE | 1 | 不需要子系统(设备驱动程序和本机系统进程) |
IMAGE_SUBSYSTEM_WINDOWS_GUI | 2 | Windows 图形用户界面 (GUI) 子系统 |
IMAGE_SUBSYSTEM_WINDOWS_CUI | 3 | Windows 字符模式用户界面 (CUI) 子系统 |
IMAGE_SUBSYSTEM_OS2_CUI | 5 | OS/2 CUI 子系统 |
IMAGE_SUBSYSTEM_POSIX_CUI | 7 | POSIX CUI 子系统 |
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI | 9 | Windows CE 系统 |
IMAGE_SUBSYSTEM_EFI_APPLICATION | 10 | 可扩展固件接口 (EFI) 应用程序 |
IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER | 11 | 具有启动服务的 EFI 驱动程序 |
IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER | 12 | 具有运行时服务的 EFI 驱动程序 |
IMAGE_SUBSYSTEM_EFI_ROM | 13 | EFI ROM 映像 |
IMAGE_SUBSYSTEM_XBOX | 14 | Xbox 系统 |
IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION | 16 | 启动应用程序 |
- DLLCharacteristics
- 映像的 DLL 特性
常量名称 | 值 | 描述 |
---|---|---|
无常量名称 | 0x0001 | 保留 |
无常量名称 | 0x0002 | 保留 |
无常量名称 | 0x0004 | 保留 |
无常量名称 | 0x0008 | 保留 |
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE | 0x0040 | DLL 可以在加载时重新定位 |
IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY | 0x0080 | 强制执行代码完整性检查 |
IMAGE_DLLCHARACTERISTICS_NX_COMPAT | 0x0100 | 映像与数据执行保护 (DEP) 兼容 |
IMAGE_DLLCHARACTERISTICS_NO_ISOLATION | 0x0200 | 映像是隔离感知的,但不应被隔离 |
IMAGE_DLLCHARACTERISTICS_NO_SEH | 0x0400 | 映像不使用结构化异常处理 (SEH)。此映像中无法调用任何处理程序 |
IMAGE_DLLCHARACTERISTICS_NO_BIND | 0x0800 | 不要绑定映像 |
IMAGE_DLLCHARACTERISTICS_APPCONTAINER | 0x1000 | 映像必须在应用程序容器中执行 |
IMAGE_DLLCHARACTERISTICS_WDM_DRIVER | 0x2000 | WDM 驱动程序 |
无常量名称 | 0x4000 | 保留 |
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE | 0x8000 | 映像是终端服务器感知的 |
- SizeOfStackReserve
- 为堆栈保留的字节数。加载时仅提交 SizeOfStackCommit 成员指定的内存;其余内存将每次一个页面地提供,直到达到此保留大小。
- SizeOfStackCommit
- 为堆栈提交的字节数。
- SizeOfHeapReserve
- 为本地堆保留的字节数。加载时仅提交 SizeOfHeapCommit 成员指定的内存;其余内存将每次一个页面地提供,直到达到此保留大小。
- SizeOfHeapCommit
- 为本地堆提交的字节数。
- LoaderFlags
- 此成员已过时。
- NumberOfRvaAndSizes
- 可选标头其余部分中的目录项数。每个条目描述一个位置和大小。
- DataDirectory
- 可能是此结构中最有趣的成员。提供 RVA 和大小,用于定位各种数据结构,这些结构用于设置模块的执行环境。DataDirectory 数组指向的数据结构可以在文件的各个节中找到,如 节表 所示。这些结构的作用细节存在于此页面其他部分中。DataDirectory 中最有趣的条目如下:导出目录、导入目录、资源目录和绑定导入目录。.NET 描述符表 (CLI 标头) 包含 .NET 程序集的元数据,该表是 IMAGE_COR20_HEADER 结构,在 winnt.h 中定义。请注意,以字节为单位的偏移量相对于可选标头的开头。
常量名称 | 值 | 描述 | PE(32 位) 偏移量 | PE32+(64 位) 偏移量 |
---|---|---|---|---|
IMAGE_DIRECTORY_ENTRY_EXPORT | 0 | 导出目录 | 96 | 112 |
IMAGE_DIRECTORY_ENTRY_IMPORT | 1 | 导入目录 | 104 | 120 |
IMAGE_DIRECTORY_ENTRY_RESOURCE | 2 | 资源目录 | 112 | 128 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION | 3 | 异常目录 | 120 | 136 |
IMAGE_DIRECTORY_ENTRY_SECURITY | 4 | 安全目录 | 128 | 144 |
IMAGE_DIRECTORY_ENTRY_BASERELOC | 5 | 基址重定位表 | 136 | 152 |
IMAGE_DIRECTORY_ENTRY_DEBUG | 6 | 调试目录 | 144 | 160 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 7 | 特定于体系结构的数据 | 152 | 168 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 8 | 全局指针寄存器相关的虚拟地址 | 160 | 176 |
IMAGE_DIRECTORY_ENTRY_TLS | 9 | 线程本地存储目录 | 168 | 184 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 10 | 加载配置目录 | 176 | 192 |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 11 | 绑定导入目录 | 184 | 200 |
IMAGE_DIRECTORY_ENTRY_IAT | 12 | 导入地址表 | 192 | 208 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 13 | 延迟导入表 | 200 | 216 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | 14 | COM 或 .net 描述符表 (CLI 标头) | 208 | 224 |
无常量名称 | 15 | 保留 | 216 | 232 |
节表
[edit | edit source]在 PE 可选标头之后,我们立即找到一个节表。节表由一系列 IMAGE_SECTION_HEADER 结构组成。我们在文件中找到的结构数量由 COFF 标头中的 NumberOfSections 成员确定。每个结构的长度为 40 字节。下面是来自我正在编写的程序的十六进制转储,描述了节表
突出显示的区域与三个 IMAGE_SECTION_HEADER 结构的 Name 成员相关联
IMAGE_SECTION_HEADER 定义为 C 结构如下
struct IMAGE_SECTION_HEADER
{
// short is 2 bytes
// long is 4 bytes
char Name[IMAGE_SIZEOF_SHORT_NAME]; // IMAGE_SIZEOF_SHORT_NAME is 8 bytes
union {
long PhysicalAddress;
long VirtualSize;
} Misc;
long VirtualAddress;
long SizeOfRawData;
long PointerToRawData;
long PointerToRelocations;
long PointerToLinenumbers;
short NumberOfRelocations;
short NumberOfLinenumbers;
long Characteristics;
}
- Name
- 8 字节空填充 UTF-8 字符串(如果使用所有 8 个字符,则字符串可能不会以空字符结尾)。对于更长的名称,此成员将包含 '/' 后跟十进制数字的 ASCII 表示形式,该数字是在字符串表中的偏移量。可执行映像不使用字符串表,也不支持大于 8 个字符的节名称。
- Misc
- PhysicalAddress - 文件地址。
- VirtualSize - 加载到内存中后,节的总大小(以字节为单位)。如果此值大于 SizeOfRawData 成员,则该节将用零填充。此字段仅对可执行映像有效,对于目标文件应设置为 0。
- 除非已知链接器和使用过的链接器的行为,否则 Misc 成员应被视为不可靠。
- VirtualAddress
- 加载到内存中后,节的第一个字节的地址,相对于映像基址。对于目标文件,这是应用重定位之前第一个字节的地址。
- SizeOfRawData
- 磁盘上初始化数据的尺寸(以字节为单位)。此值必须是 PE 可选标头结构的 FileAlignment 成员的倍数。如果此值小于 VirtualSize 成员,则节的其余部分将用零填充。如果节仅包含未初始化的数据,则该值为 0。
- PointerToRawData
- 相对于文件开头的文件指针,指向 COFF 文件中的第一个页面。此值必须是 PE 可选标头结构的 FileAlignment 成员的倍数。如果节仅包含未初始化的数据,则该值应为 0。
- PointerToRelocations
- 指向节的重定位条目的开头的文件指针。如果没有重定位,则该值为 0。
- PointerToLinenumbers
- 指向节的行号条目的开头的文件指针。如果没有 COFF 行号,则该值为 0。
- NumberOfRelocations
- 节的重定位条目数。对于可执行映像,该值为 0。
- NumberOfLinenumbers
- 节的行号条目数。
- Characteristics
- 映像的特征。
下表定义了此成员的可能 32 位掩码值
常量名称 | 值 | 描述 |
---|---|---|
无常量名称 | 0x00000000 | 保留 |
无常量名称 | 0x00000001 | 保留 |
无常量名称 | 0x00000002 | 保留 |
无常量名称 | 0x00000004 | 保留 |
IMAGE_SCN_TYPE_NO_PAD | 0x00000008 | 节不应填充到下一个边界。此标志已过时,被 IMAGE_SCN_ALIGN_1BYTES 替代 |
无常量名称 | 0x00000010 | 保留 |
IMAGE_SCN_CNT_CODE | 0x00000020 | 节包含可执行代码(.text 节) |
IMAGE_SCN_CNT_INITIALIZED_DATA | 0x00000040 | 节包含初始化数据 |
IMAGE_SCN_CNT_UNINITIALIZED_DATA | 0x00000080 | 节包含未初始化的数据 |
IMAGE_SCN_LNK_OTHER | 0x00000100 | 保留 |
IMAGE_SCN_LNK_INFO | 0x00000200 | 节包含注释或其他信息。这仅对目标文件(.drectve 节)有效 |
无常量名称 | 0x00000400 | 保留 |
IMAGE_SCN_LNK_REMOVE | 0x00000800 | 该节将不会成为映像的一部分。这仅对目标文件有效 |
IMAGE_SCN_LNK_COMDAT | 0x00001000 | 节包含 COMDAT 数据。这仅对目标文件有效 |
无常量名称 | 0x00002000 | 保留 |
IMAGE_SCN_NO_DEFER_SPEC_EXC | 0x00004000 | 重置此节的 TLB 条目中的推测性异常处理位 |
IMAGE_SCN_GPREL | 0x00008000 | 节包含通过全局指针引用的数据 |
无常量名称 | 0x00010000 | 保留 |
IMAGE_SCN_MEM_PURGEABLE | 0x00020000 | 保留 |
IMAGE_SCN_MEM_LOCKED | 0x00040000 | 保留 |
IMAGE_SCN_MEM_PRELOAD | 0x00080000 | 保留 |
IMAGE_SCN_ALIGN_1BYTES | 0x00100000 | 将数据对齐到 1 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_2BYTES | 0x00200000 | 将数据对齐到 2 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_4BYTES | 0x00300000 | 将数据对齐到 4 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_8BYTES | 0x00400000 | 将数据对齐到 8 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_16BYTES | 0x00500000 | 将数据对齐到 16 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_32BYTES | 0x00600000 | 将数据对齐到 32 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_64BYTES | 0x00700000 | 将数据对齐到 64 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_128BYTES | 0x00800000 | 将数据对齐到 128 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_256BYTES | 0x00900000 | 将数据对齐到 256 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_512BYTES | 0x00A00000 | 将数据对齐到 512 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_1024BYTES | 0x00B00000 | 将数据对齐到 1024 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_2048BYTES | 0x00C00000 | 将数据对齐到 2048 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_4096BYTES | 0x00D00000 | 将数据对齐到 4096 字节边界。这仅对目标文件有效 |
IMAGE_SCN_ALIGN_8192BYTES | 0x00E00000 | 将数据对齐到 8192 字节边界。这仅对目标文件有效 |
IMAGE_SCN_LNK_NRELOC_OVFL | 0x01000000 | 本节包含扩展重定位。该节的重定位计数超过了节头中为其保留的 16 位。如果节头中的 NumberOfRelocations 字段为 0xffff,则实际的重定位计数存储在第一个重定位的 VirtualAddress 字段中。如果设置了 IMAGE_SCN_LNK_NRELOC_OVFL 并且该节中的重定位少于 0xffff,则为错误 |
IMAGE_SCN_MEM_DISCARDABLE | 0x02000000 | 该节可以根据需要丢弃 |
IMAGE_SCN_MEM_NOT_CACHED | 0x04000000 | 该节不能缓存 |
IMAGE_SCN_MEM_NOT_PAGED | 0x08000000 | 该节不能分页 |
IMAGE_SCN_MEM_SHARED | 0x10000000 | 该节可以在内存中共享 |
IMAGE_SCN_MEM_EXECUTE | 0x20000000 | 该节可以作为代码执行(.text 等节) |
IMAGE_SCN_MEM_READ | 0x40000000 | 该节可以读取 |
IMAGE_SCN_MEM_WRITE | 0x80000000 | 该节可以写入 |
PE 加载器会将可执行映像的节放置在这些节描述符指定的地址(相对于基地址),通常对齐方式为 0x1000,这与 x86 上的页大小相匹配。
常见的节是
- .text/.code/CODE/TEXT - 包含可执行代码(机器指令)
- .textbss/TEXTBSS - 如果启用了增量链接,则存在
- .data/.idata/DATA/IDATA - 包含已初始化数据
- .bss/BSS - 包含未初始化数据
- .rsrc - 包含资源数据
导入和导出 - 链接到其他模块
[edit | edit source]什么是链接?
[edit | edit source]每当开发人员编写程序时,都会有许多预期的子程序和函数,它们已经实现,从而使编写者无需编写更多代码或处理复杂的数据结构。相反,编码人员只需要声明对子程序的一次调用,链接器将决定接下来会发生什么。
可以使用两种类型的链接:静态链接和动态链接。静态链接使用预编译函数库。此预编译代码可以插入到最终的可执行文件中以实现函数,从而为程序员节省大量时间。相反,动态链接允许子程序代码驻留在另一个文件(或*模块*)中,该文件在运行时由操作系统加载。这也称为“动态链接库”,或 DLL。*库* 是一个包含一系列函数或值的模块,这些函数或值可以*导出*。这不同于*可执行文件*,*可执行文件*从库*导入*东西来做它想做的事情。从现在开始,“模块”表示任何 PE 格式文件,而“库”表示任何导出和导入函数和值的模块。
动态链接具有以下优点
- 如果多个可执行文件链接到库模块,则可以节省磁盘空间
- 允许立即更新例程,而无需为所有应用程序提供新的可执行文件
- 通过将库的代码映射到多个进程中,可以节省内存空间
- 增加实现的抽象。无需重新编程应用程序即可修改实现操作的方法。这对于与操作系统的向后兼容性非常有用。
本节讨论了如何使用 PE 文件格式来实现这一点。在此需要指出的是,*任何*内容都可以在模块之间导入或导出,包括变量以及子程序。
加载
[edit | edit source]将模块动态链接在一起的缺点是,在运行时,初始化可执行文件的软件必须将这些模块链接在一起。由于各种原因,您无法声明“此动态库中的函数将始终存在于内存中的*这里*”。如果该内存地址不可用或库已更新,该函数将不再存在于该位置,尝试使用它的应用程序将中断。相反,每个模块(库或可执行文件)必须声明它*导出*给其他模块的函数或值,以及它希望从其他模块*导入*什么。
如上所述,模块无法声明它期望函数或值在内存中的哪个位置。相反,它声明它希望在自己的内存中的哪个位置找到指向它想要导入的值的**指针**。这允许模块寻址任何导入的值,无论它出现在内存中的哪个位置。
导出
[edit | edit source]*导出* 是一个模块中已声明与其他模块共享的函数和值。这是通过使用“导出目录”来完成的,该目录用于在导出的名称(或“序数”,见下文)和可以在内存中找到代码或数据的地址之间进行转换。导出目录的开头由资源目录的 IMAGE_DIRECTORY_ENTRY_EXPORT 条目标识。所有导出数据必须存在于同一个节中。该目录由以下结构开头
struct IMAGE_EXPORT_DIRECTORY {
long Characteristics;
long TimeDateStamp;
short MajorVersion;
short MinorVersion;
long Name;
long Base;
long NumberOfFunctions;
long NumberOfNames;
long *AddressOfFunctions;
long *AddressOfNames;
long *AddressOfNameOrdinals;
}
“Characteristics”值通常未被使用,TimeDateStamp 描述了导出目录生成的时间,MajorVersion 和 MinorVersion 应该描述目录的版本详细信息,但它们的性质未定义。这些值对实际导出本身几乎没有影响。“Name”值是针对零终止 ASCII 字符串的 RVA,该字符串是此库名称或模块的名称。
名称和序数
[edit | edit source]每个导出的值都有一个名称和一个“序数”(一种索引)。实际的导出本身是通过 AddressOfFunctions 描述的,AddressOfFunctions 是一个指向 RVA 数组的 RVA,每个 RVA 指向要导出的不同函数或值。此数组的大小在 NumberOfFunctions 值中。每个函数都有一个序数。“Base”值用作第一个导出的序数,数组中的下一个 RVA 为 Base+1,依此类推。
AddressOfFunctions 数组中的每个条目都由一个名称标识,该名称通过 RVA AddressOfNames 找到。AddressOfNames 指向的数据是一个 RVA 数组,大小为 NumberOfNames。每个 RVA 指向一个零终止的 ASCII 字符串,每个字符串都是导出的名称。还有一个由 AddressOfNameOrdinals 中的 RVA 指向的第二个数组。它的大小也是 NumberOfNames,但每个值都是一个 16 位字,每个值都是一个序数。这两个数组是平行的,用于从 AddressOfFunctions 获取导出值。要按名称查找导出,请在 AddressOfNames 数组中搜索正确的字符串,然后从 AddressOfNameOrdinals 数组中获取相应的值。然后使用此值作为 AddressOfFunctions 的索引(是的,它实际上是 0 索引,而不是基于基数的序数,正如官方文档所建议的那样!)。
转发
[edit | edit source]除了能够导出模块中的函数和值之外,导出目录还可以将导出*转发*到另一个库。这在重新组织库时允许更大的灵活性:也许某些功能已分支到另一个模块中。如果是这样,可以将导出转发到该库,而不是在原始模块中进行混乱的重组。
通过在 AddressOfFunctions 数组中使一个 RVA 指向包含导出目录的节来实现转发,这是普通导出不应该做的事情。在该位置,应该有一个格式为“LibraryName.ExportName”的零终止 ASCII 字符串,用于将此导出转发到适当的位置。
导入
[edit | edit source]动态链接的另一半是将函数和值导入到可执行文件或其他模块中。在运行时之前,编译器和链接器不知道需要导入的值在内存中的哪个位置。导入表通过在运行时创建指针数组来解决这个问题,每个指针都指向导入值的内存位置。此指针数组在模块内部的定义 RVA 位置存在。这样,链接器就可以使用模块内部的地址访问模块外部的值。
导入目录
[edit | edit source]导入目录的起始位置由资源目录的 IMAGE_DIRECTORY_ENTRY_IAT 和 IMAGE_DIRECTORY_ENTRY_IMPORT 项都指向(原因尚不清楚)。在该位置,有一个 IMAGE_IMPORT_DESCRIPTOR 结构数组。其中每个结构标识一个库或模块,其中包含我们需要导入的值。该数组一直持续到所有值为零的条目为止。该结构如下所示
struct IMAGE_IMPORT_DESCRIPTOR {
long *OriginalFirstThunk;
long TimeDateStamp;
long ForwarderChain;
long Name;
long *FirstThunk;
}
TimeDateStamp 与“绑定”行为相关,请参见下文。Name 值是指向 ASCII 字符串的 RVA,该字符串命名要导入的库。ForwarderChain 将在后面解释。此时,唯一感兴趣的是 RVA OriginalFirstThunk 和 FirstThunk。这两个值都指向 RVA 数组,每个 RVA 指向一个 IMAGE_IMPORT_BY_NAMES 结构。这些数组以一个等于零的条目结束。这两个数组是并行的,并按相同顺序指向同一个结构。原因将在稍后解释。
每个 IMAGE_IMPORT_BY_NAMES 结构都具有以下形式
struct IMAGE_IMPORT_BY_NAME {
short Hint;
char Name[1];
}
“Name” 是一个任意大小的 ASCII 字符串,它命名要导入的值。这在通过 AddressOfNames 数组查找导出目录中的值时使用(请参见上文)。“Hint” 值是 AddressOfNames 数组中的索引;为了节省搜索字符串的时间,加载程序首先检查与“Hint” 对应的 AddressOfNames 条目。
总结:导入表由一个大的 IMAGE_IMPORT_DESCRIPTOR 数组组成,以一个全零条目结束。这些描述符标识要从中导入内容的库。然后有两个并行的 RVA 数组,每个数组都指向 IMAGE_IMPORT_BY_NAME 结构,这些结构标识要导入的特定值。
使用上面的导入目录在运行时,加载程序查找相应的模块,将它们加载到内存中,并查找正确的导出。但是,为了能够使用导出,必须在导入模块的内存中的某个位置存储指向它的指针。这就是为什么有两个并行的数组 OriginalFirstThunk 和 FirstThunk 标识 IMAGE_IMPORT_BY_NAME 结构的原因。一旦解析了导入的值,就会将指向它的指针存储在 FirstThunk 数组中。然后可以在运行时使用它来寻址导入的值。
PE 文件格式还支持一项名为“绑定”的独特功能。加载和解析导入地址的过程可能很耗时,在某些情况下,应避免这种情况。如果开发人员相当确定库不会被更新或更改,那么每次加载应用程序时,导入值的内存地址都不会改变。因此,可以在运行时 *之前* 预先计算导入地址并将其存储在 FirstThunk 数组中,从而使加载程序能够跳过解析导入 - 导入被“绑定”到特定内存位置。但是,如果模块之间的版本号不匹配,或者需要重新定位导入的库,则加载程序将假设绑定地址无效,并仍然解析导入。
模块的导入目录条目的“TimeDateStamp” 成员控制绑定;如果将其设置为零,则导入目录不绑定。如果它是非零值,则它绑定到另一个模块。但是,导入表中的 TimeDateStamp 必须与绑定模块的 FileHeader 中的 TimeDateStamp 相匹配,否则加载程序将丢弃绑定值。
当然,如果绑定的库/模块将其导出转发到另一个模块,则绑定可能会出现问题。在这些情况下,可以绑定未转发的导入,但必须标识需要转发的值,以便加载程序能够解析它们。这通过导入描述符的 ForwarderChain 成员完成。 “ForwarderChain” 的值是 FirstThunk 和 OriginalFirstThunk 数组中的索引。该索引的 OriginalFirstThunk 标识需要解析的导入的 IMAGE_IMPORT_BY_NAME 结构,而该索引的 FirstThunk 是需要解析的另一个条目的索引。这种情况会一直持续到 FirstThunk 值为 -1 为止,这表示没有更多要导入的转发值。
资源是模块中的数据项,很难使用所选的编程语言进行存储或描述。这需要一个单独的编译器或资源生成器,允许插入对话框、图标、菜单、图像和其他类型的资源,包括任意二进制数据。虽然可以使用许多资源 API 调用从 PE 文件中检索资源,但我们将查看没有使用这些 API 的资源。
当我们要手动操作文件的资源时,首先需要做的是查找资源部分。为此,我们需要在 DataDirectory 数组和节表中找到一些信息。我们需要在 PE 可选头中找到的 DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE] 结构的 VirtualAddress 成员中存储的 RVA。一旦知道 RVA,我们就可以通过将 DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE] 结构的 VirtualAddress 成员与 IMAGE_SECTION_HEADER 的 VirtualAddress 成员进行比较,在节表中查找该 RVA。除了极少数情况外,DataDirectory 结构的 VirtualAddress 成员将等于 IMAGE_SECTION_HEADER 的 VirtualAddress 成员的值。除了极少数情况外,该特定 IMAGE_SECTION_HEADER 的 name 成员将被命名为“.rsrc”。找到正确的 IMAGE_SECTION_HEADER 结构后,可以使用 PointerToRawData 成员来定位资源部分。PointerToRawData 成员包含从文件开头到资源部分的第一个字节的偏移量。下图显示了左侧的 DataDirectory 数组和右侧的 IMAGE_SECTION_HEADER 的示例,其中填充了资源部分的信息。我们可以看到“2 RVA: 20480 Size: 3512” 的目录信息下方的突出显示行具有 20480 的 VirtualAddress (RVA),这对应于 .rsrc (资源) 部分的 20480 的 VirtualAddress。您还可以看到 PointerToRawData 的值等于 7168。在这个特定的 PE 文件中,我们将在从文件开头偏移 7168 的位置找到资源部分。
找到资源部分后,我们可以开始查看该部分中包含的结构和数据。
IMAGE_RESOURCE_DIRECTORY 是我们遇到的第一个结构,它从资源部分的第一个字节开始。
IMAGE_RESOURCE_DIRECTORY 结构
struct IMAGE_RESOURCE_DIRECTORY
{
long Characteristics;
long TimeDateStamp;
short MajorVersion;
short MinorVersion;
short NumberOfNamedEntries;
short NumberOfIdEntries;
}
Characteristics 未使用,TimeDateStamp 通常是创建时间,但无论它是否设置都没有关系。MajorVersion 和 MinorVersion 与资源的版本信息相关:这些字段没有定义的值。紧随 IMAGE_RESOURCE_DIRECTORY 结构之后是一系列 IMAGE_RESOURCE_DIRECTORY_ENTRY,其数量由 NumberOfNamedEntries 和 NumberOfIdEntries 的总和定义。这些条目的第一部分用于命名资源,后一部分用于 ID 资源,具体取决于 IMAGE_RESOURCE_DIRECTORY 结构中的值。资源条目结构的实际形状如下所示
struct IMAGE_RESOURCE_DIRECTORY_ENTRY
{
long NameId;
long *Data;
}
NameId 值具有双重用途:如果最高有效位(或符号位)为清零,则最低 16 位是资源的 ID 号。或者,如果最高位被设置为 1,则最低 31 位构成从资源数据开头到此特定资源的名称字符串的偏移量。Data 值也具有双重用途:如果最高有效位被设置为 1,则剩余的 31 位构成从资源数据开头到另一个 IMAGE_RESOURCE_DIRECTORY 的偏移量(即此条目是资源树的内部节点)。否则,这是一个叶节点,Data 包含从资源数据开头到描述资源数据本身的特定结构的偏移量(可以将其视为字节的有序流)
struct IMAGE_RESOURCE_DATA_ENTRY
{
long *Data;
long Size;
long CodePage;
long Reserved;
}
Data 值包含指向实际资源数据的 RVA,Size 不言自明,CodePage 包含用于对资源中的 Unicode 编码字符串(如果有)进行解码的 Unicode 代码页。Reserved 应设置为 0。
上述资源目录和条目系统允许通过名称或 ID 号简单存储资源。但是,这很快就会变得非常复杂。不同类型的资源、资源本身以及其他语言中资源的实例可能在一个资源目录中混杂在一起。出于这个原因,资源目录已被赋予了一个结构来使用,允许分离不同的资源。
为此,资源条目中的“数据”值指向另一个 IMAGE_RESOURCE_DIRECTORY 结构,形成类似树状图的资源组织方式。第一级资源条目标识资源的 _类型_:光标、位图、图标等。它们使用 ID 方法标识资源条目,总共定义了十二个值。可以添加更多用户定义的资源类型。每个资源条目都指向一个资源目录,命名实际的资源本身。这些资源可以是任何名称或值。它们又指向另一个资源目录,该目录使用 ID 号来区分语言,允许为使用不同语言的系统提供不同的特定资源。最后,语言目录中的条目实际上提供了指向资源数据本身的偏移量,其格式未在 PE 规范中定义,可以被视为任意字节流。
Windows DLL 文件是一种 PE 文件,但有一些关键区别
- .DLL 文件扩展名
- 一个
DllMain()
入口点,而不是 WinMain() 或 main()。 - 在 PE 头部设置的 DLL 标志。
DLL 可以通过两种方式之一加载:a) 加载时,或 b) 通过调用 LoadModule() Win32 API 函数。
使用以下语法从 DLL 文件导出函数
__declspec(dllexport) void MyFunction() ...
此处的 "__declspec" 关键字不是 C 语言标准,而是由许多编译器实现,为函数和变量设置可扩展的编译器特定选项。在 Windows 上运行的 Microsoft C 编译器和 GCC 版本允许使用 __declspec 关键字和 dllexport 属性。
也可以从常规的 .exe 文件导出函数,并且具有导出函数的 .exe 文件可以以类似于 .dll 文件的方式动态调用。但是,这种情况很少见。
有几种方法可以确定 DLL 导出了哪些函数。一种常见的方法是使用以下方式使用 **dumpbin**
dumpbin /EXPORTS <dll file>
这将在控制台中发布函数导出列表,以及它们的序号和 RVA。
与函数导出类似,程序可以从外部 DLL 文件导入函数。当程序启动时,dll 文件将加载到进程内存中,函数将像本地函数一样使用。为了使编译器和链接器识别函数来自外部库,需要以以下方式对 DLL 导入进行原型化
__declspec(dllimport) void MyFunction();
在检查程序时,确定哪些函数是从外部库导入的通常很有用。要将导入文件列出到控制台,请使用以下方式使用 **dumpbin**
dumpbin /IMPORTS <dll file>
您也可以使用 depends.exe 列出导入和导出函数。Depends 是一个 GUI 工具,包含在 Microsoft Platform SDK 中。