PE/ELF学习
本文最后更新于173 天前,其中的信息可能已经过时,如有错误请发送邮件到2624241828@qq.com

PE

常见后缀

  1. .exe,可执行程序,常见pe文件类型,包含程序入口点和完整执行逻辑
  2. .dll,动态链接库,提供可被多个程序共享的函数和资源,无法直接运行
  3. .sys,系统驱动程序,运行于内核态,用于硬件交互或系统级功能
  4. .ocx,activex控件,当特殊dll
  5. .scr,屏幕保护程序,改后缀exe
  6. .cpl,控制面板小程序,系统配置界面,当dll
  7. .efi,可扩展固件接口文件,用于UEFI固件启动,PE扩展

PE文件的两种状态

pe文件分为运行态和非运行态

  • 非运行态:当一个pe文件尚未被运行时,数据存储在磁盘中
  • 运行态:当一个pe文件被打开后,pe文件相关数据将被装在到内存中

文件结构

基于coff(Common Object File Format)扩展,文件头加节区

PE 文件结构概览表(32位 PE32)

结构名称对应 C 数据结构默认占用空间 (字节)备注
DOS MZ 头_IMAGE_DOS_HEADER64固定大小 (0x40)
DOS Stub(无固定结构)不固定仅用于 DOS 提示,如果不运行在 DOS 下可忽略
PE 文件头 (总)_IMAGE_NT_HEADERS248包含以下三部分 (4 + 20 + 224)
├── PE 签名Signature (DWORD)4值为 PE\0\0 (0x00004550)
├── 标准 PE 头_IMAGE_FILE_HEADER20包含机器类型、节数量等基础信息
└── 扩展 PE 头_IMAGE_OPTIONAL_HEADER224注: 64位程序此处为 240 字节
节表 (Section Table)_IMAGE_SECTION_HEADER40每个节表项的大小。总大小 = 40 × 节数量
节数据 (Sections)(无特定结构体)不固定实际的代码、数据、资源等,大小由节表决定

DOS头部

可以往里面塞东西,也不会影响后面的pe结构,DOS Header固定64字节,兼容dos系统执行,一个mz开头,4D 5A 90 00,还有个DOS Stub,大小不固定,通常会填满到128字节
关键字段:

  • e_magic:dos签名,固定为0x5A4D(字符串MZ)
  • e_lfanew:pe文件头偏移量(32位或64位),dos头过渡到pe头关键指针
    定义的结构体位为,定义于winnt.h
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE 头部结构体

    // --- 核心标识 ---
    WORD   e_magic;                     // [0x00] 魔数 (Magic number),固定为 "MZ"

    // --- DOS 程序加载信息 (现代 Windows 通常忽略) ---
    WORD   e_cblp;                      // [0x02] 文件最后一页的字节数
    WORD   e_cp;                        // [0x04] 文件总页数
    WORD   e_crlc;                      // [0x06] 重定位项的数量
    WORD   e_cparhdr;                   // [0x08] 头部大小 (以段/Paragraph为单位)
    WORD   e_minalloc;                  // [0x0A] 所需的最小附加段数
    WORD   e_maxalloc;                  // [0x0C] 所需的最大附加段数
    WORD   e_ss;                        // [0x0E] 初始 SS (堆栈段) 相对值
    WORD   e_sp;                        // [0x10] 初始 SP (堆栈指针) 值
    WORD   e_csum;                      // [0x12] 校验和
    WORD   e_ip;                        // [0x14] 初始 IP (指令指针) 值
    WORD   e_cs;                        // [0x16] 初始 CS (代码段) 相对值
    WORD   e_lfarlc;                    // [0x18] 重定位表在文件中的地址
    WORD   e_ovno;                      // [0x1A] 覆盖号 (Overlay number)

    // --- 保留及 OEM 字段 ---
    WORD   e_res[4];                    // [0x1C] 保留字段 (4个字)
    WORD   e_oemid;                     // [0x24] OEM 标识符 (用于 e_oeminfo)
    WORD   e_oeminfo;                   // [0x26] OEM 信息 (具体由 e_oemid 定义)
    WORD   e_res2[10];                  // [0x28] 保留字段 (10个字)

    // --- PE 头部指针 (核心字段) ---
    LONG   e_lfanew;                    // [0x3C] 新 EXE 头部 (PE Header) 的文件地址

} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

结构体定义

typedef struct _IMAGE_DOS_HEADER {
    WORD   e_magic;      // 0x00: 魔数 "MZ" (4D 5A)
    WORD   e_cblp;       // 0x02
    // ......
    LONG   e_lfanew;     // 0x3C: 指向 PE 头的偏移量 (这是结构体的最后一个成员)
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

还有自写代码读取dos mz头吗,吓哭了

// PE.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <malloc.h>
#include <windows.h>

int main(int argc, char* argv[])
{
    //创建DOS对应的结构体指针
        _IMAGE_DOS_HEADER* dos;
    //读取文件,返回文件句柄
        HANDLE hFile = CreateFileA("C:\\Documents and Settings\\Administrator\\桌面\\dbghelp.dll",GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,0,0);
    //根据文件句柄创建映射
        HANDLE hMap = CreateFileMappingA(hFile,NULL,PAGE_READONLY,0,0,0);
    //映射内容
        LPVOID pFile = MapViewOfFile(hMap,FILE_MAP_READ,0,0,0);
    //类型转换,用结构体的方式来读取
        dos=(_IMAGE_DOS_HEADER*)pFile;
    //输出结构体的第一个成员,以十六进制输出
        printf("%X\n",dos->e_magic);
        return 0;
}

PE头

前四个字节 50 45 00 00pe标识,20个字节标准pe头,扩展pe头看结构体,有哪些,然后之后文件对齐之后的大小,头的大小一定是文件倍数
32位结构体

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;                            //PE文件头标识
    IMAGE_FILE_HEADER FileHeader;               //标准PE头
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;     //扩展PE头 32位
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

64位结构体

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;                            //PE文件头标识
    IMAGE_FILE_HEADER FileHeader;               //标准PE头 
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;     //扩展PE头 64位
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

PE签名

类型为dword,存储在signature变量中

标准PE头(IMAGE_FILE_HEADER)(又称coff文件头)

结构体

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;//可以运行在什么样的CPU上,如果它的值为0x0则表示可以运行在任意的CPU上,支持在Intel 386以及后续的型号CPU运行则值为0x14c,支持64位的CPU型号则值为0x8664。数据宽度word2字节,程序支持的cpu
    WORD    NumberOfSections;//表示节的数量,也就是节表中有几个结构体,word2字节,不大于96
    DWORD   TimeDateStamp;//编译器填写的时间戳 与文件属性里面(创建时间、修改时间)无关,dword4字节
    DWORD   PointerToSymbolTable;//调试相关,指向符号表,dword4字节
    DWORD   NumberOfSymbols;//调试相关,符号表中的符号个数,dword4字节
    WORD    SizeOfOptionalHeader;//可选PE头的大小(32位PE文件:0xE0  64位PE文件:0xF0)word2字节
    WORD    Characteristics;//文件属性,数据位拼接而成,word2字节
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine

IMAGE_FILE_MACHINE 常量对照表

返回的常量 (Constant)Value (Hex)说明 (Description)
IMAGE_FILE_MACHINE_UNKNOWN0x0假定此字段的内容适用于任何计算机类型
IMAGE_FILE_MACHINE_AM330x1d3松田 (Matsushita) AM33
IMAGE_FILE_MACHINE_AMD640x8664x64 (AMD64 或 Intel 64)
IMAGE_FILE_MACHINE_ARM0x1c0ARM 小端序 (Little Endian)
IMAGE_FILE_MACHINE_ARM640xaa64ARM64 小端序 (Little Endian)
IMAGE_FILE_MACHINE_ARMNT0x1c4ARM Thumb-2 小端序 (Little Endian)
IMAGE_FILE_MACHINE_EBC0xebcEFI 字节代码 (EFI Byte Code)
IMAGE_FILE_MACHINE_I3860x14cIntel 386 或更高版本的处理器 (x86 32位)
IMAGE_FILE_MACHINE_IA640x200Intel Itanium 处理器系列
IMAGE_FILE_MACHINE_LOONGARCH320x6232LoongArch 32 位处理器系列 (龙芯)
IMAGE_FILE_MACHINE_LOONGARCH640x6264LoongArch 64 位处理器系列 (龙芯)
IMAGE_FILE_MACHINE_M32R0x9041三菱 M32R 小端序
IMAGE_FILE_MACHINE_MIPS160x266MIPS16
IMAGE_FILE_MACHINE_MIPSFPU0x366使用 FPU 的 MIPS
IMAGE_FILE_MACHINE_MIPSFPU160x466具有 FPU 的 MIPS16
IMAGE_FILE_MACHINE_POWERPC0x1f0PowerPC 小端序
IMAGE_FILE_MACHINE_POWERPCFP0x1f1支持浮点运算的 PowerPC
IMAGE_FILE_MACHINE_R40000x166MIPS 小端序 (通常指 R4000)
IMAGE_FILE_MACHINE_RISCV320x5032RISC-V 32 位地址空间
IMAGE_FILE_MACHINE_RISCV640x5064RISC-V 64 位地址空间
IMAGE_FILE_MACHINE_RISCV1280x5128RISC-V 128 位地址空间
IMAGE_FILE_MACHINE_SH30x1a2Hitachi SH3
IMAGE_FILE_MACHINE_SH3DSP0x1a3Hitachi SH3 DSP
IMAGE_FILE_MACHINE_SH40x1a6Hitachi SH4
IMAGE_FILE_MACHINE_SH50x1a8Hitachi SH5
IMAGE_FILE_MACHINE_THUMB0x1c2Thumb
IMAGE_FILE_MACHINE_WCEMIPSV20x169MIPS 小端 WCE v2
常见:
  1. 0x014C (I386):代表 32位程序。
  2. 0x8664 (AMD64):代表 64位 程序。
NumberOfSections

machine后面的两个字节,说明区段数量
区段:操作系统加载程序时,根据内存属性(权限)来管理,常见区段

  • .text或.code,指令代码,可读,可执行
  • .data,已经初始化的全局变量,可读,可写
  • .rdata或.idata,存放常量(const或者字符串),导入表(调用了哪些api),只读
  • .rsrc,存放的程序的ui资源,只读
  • .reloc,重定位,如果程序不能在预期的内存地址(基址)加载,要修正时用的数据,比如开启了ASLR(随机基址的)的程序要用
SizeOfOptionalHeader

表示可选标头大小,可执行文件必需
转换为十进制就是大小,SizeOfOptionalHeader变量中的数据对于32位PE文件通常为00E0h,对于64位PE文件通常为00F0h。这里的h是用来表示这个数据为16进制。上图我们可以看到SizeOfOptionalHeader变量当中的数据为00 E0,扩展PE头大小为224,文件为32位PE文件。

Characteristics

看最后两个字节,转换为二进制,然后对应的1的含义在下表
IMAGE_FILE_HEADER – Characteristics 字段详解

数据位 (Bit)常量符号 (Constant)为 1 时的含义
0IMAGE_FILE_RELOCS_STRIPPED文件中不存在重定位信息
1IMAGE_FILE_EXECUTABLE_IMAGE文件是可执行的
2IMAGE_FILE_LINE_NUMS_STRIPPED不存在行信息
3IMAGE_FILE_LOCAL_SYMS_STRIPPED不存在符号信息
4IMAGE_FILE_AGGRESSIVE_WS_TRIM调整工作集
5IMAGE_FILE_LARGE_ADDRESS_AWARE应用程序可处理大于 2GB 的地址
6(保留)此标志保留
7IMAGE_FILE_BYTES_REVERSED_LO小尾方式 (Little Endian)
8IMAGE_FILE_32BIT_MACHINE只在 32 位平台上运行
9IMAGE_FILE_DEBUG_STRIPPED不包含调试信息
10IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP不能从可移动盘运行 (需复制到交换文件)
11IMAGE_FILE_NET_RUN_FROM_SWAP不能从网络运行 (需复制到交换文件)
12IMAGE_FILE_SYSTEM系统文件(如驱动程序),不能直接运行
13IMAGE_FILE_DLL这是一个 DLL 文件
14IMAGE_FILE_UP_SYSTEM_ONLY文件不能在多处理器计算机上运行
15IMAGE_FILE_BYTES_REVERSED_HI大尾方式 (Big Endian)

扩展PE头

标准PE头中的SizeOfOptionalHeader用于标识扩展PE头的大小,大小不固定,每个pe文件都有一个扩展pe头,用于向加载程序提供信息
有标头magic编号保证格式兼容性,可选的标头magic用于确定pe文件是pe32还是re32+可执行文件
可选标头,每个映像文件都有一个用于向加载程序提供信息的可选标头。 此标头是可选标头,因为某些文件(特别是对象文件)没有此标头。 对于映像文件,此标头是必需的。 对象文件可以具有可选标头,但通常此标头在对象文件中没有函数,只是为了增加其大小。可选标头的大小不是固定的。 COFF 标头中的 SizeOfOptionalHeader 字段必须用于验证对特定数据目录的文件的探测是否未超出 SizeOfOptionalHeader。
还应该使用可选标头的 NumberOfRvaAndSizes 字段来确保对特定数据目录条目的探测不会超出可选标头。

可选标头幻数确定映像是 PE32 还是 PE32+ 可执行文件。

映射:

  1. 坐标系变化,偏移变成地址(FOA变成VA),foa,相对于文件开头的距离,虚拟地址 (VA) = 基址 (Image Base) + RVA,因为cpu执行指令需要内存中的绝对地址
  2. 赋予权限,映射text段或者data段这些,让他们变得不可写之类的
  3. 动态修正,重定位,找到调用东西的真实地址
1. 32位与64位结构体对比

64位相比于32位区别并不大,主要是删去了一个成员(BaseOfData),以及五个成员的数据类型由 DWORD 变为 ULONGLONG

成员32位类型64位类型说明
BaseOfDataDWORD无此成员数据段基址(64位移除)
ImageBaseDWORDULONGLONG内存镜像基址
SizeOfStackReserveDWORDULONGLONG栈保留大小
SizeOfStackCommitDWORDULONGLONG栈提交大小
SizeOfHeapReserveDWORDULONGLONG堆保留大小
SizeOfHeapCommitDWORDULONGLONG堆提交大小

:因区别不明显,以下分析以 32位结构体 为例。


32位扩展 PE 头成员概览

扩展 PE 头成员较多,加粗是重点

成员数据宽度说明
MagicWORD (2字节)镜像文件的状态,可用于判断程序是32位还是64位
MajorLinkerVersionBYTE (1字节)链接器的主要版本号
MinorLinkerVersionBYTE (1字节)链接器的次要版本号
SizeOfCodeDWORD (4字节)代码段的大小
SizeOfInitializedDataDWORD (4字节)初始化数据段的大小
SizeOfUninitializedDataDWORD (4字节)未初始化数据段的大小
AddressOfEntryPointDWORD (4字节)程序入口 (OEP)
BaseOfCodeDWORD (4字节)代码开始的基址
BaseOfDataDWORD (4字节)数据开始的基址
ImageBaseDWORD (4字节)内存镜像基址
SectionAlignmentDWORD (4字节)内存对齐
FileAlignmentWORD (2字节)文件对齐
MajorOperatingSystemVersionWORD (2字节)标识操作系统版本号 (主)
MinorOperatingSystemVersionWORD (2字节)标识操作系统版本号 (次)
MajorImageVersionWORD (2字节)PE文件自身的版本号 (主)
MinorImageVersionWORD (2字节)PE文件自身的版本号 (次)
MajorSubsystemVersionWORD (2字节)运行所需子系统版本号 (主)
MinorSubsystemVersionWORD (2字节)运行所需子系统版本号 (次)
Win32VersionValueDWORD (4字节)子系统版本的值,必须为0
SizeOfImageDWORD (4字节)Image大小 (内存中)
SizeOfHeadersDWORD (4字节)所有头+节表按照文件对齐后的大小
CheckSumDWORD (4字节)校验和
SubsystemWORD (2字节)子系统类型
DllCharacteristicsWORD (2字节)文件特性 (不只是针对DLL)
SizeOfStackReserveDWORD (4字节)初始化时保留的栈大小
SizeOfStackCommitDWORD (4字节)初始化时实际提交的栈大小
SizeOfHeapReserveDWORD (4字节)初始化时保留的堆大小
SizeOfHeapCommitDWORD (4字节)初始化时实际提交的堆大小
LoaderFlagsDWORD (4字节)调试相关 (已过时)
NumberOfRvaAndSizesDWORD (4字节)目录项数目
DataDirectory[16]结构体数组数据目录表,指向导出表、导入表等
Magic

镜像文件的状态,用于区分 PE 文件的类型。

宏定义含义
IMAGE_NT_OPTIONAL_HDR32_MAGIC0x10b32位可执行映像
IMAGE_NT_OPTIONAL_HDR64_MAGIC0x20b64位可执行映像
IMAGE_ROM_OPTIONAL_HDR_MAGIC0x107ROM 镜像
版本号与段大小
  • MajorLinkerVersion / MinorLinkerVersion: 链接器版本号。
  • SizeOfCode / SizeOfInitializedData / SizeOfUninitializedData: 分别为代码段、初始化数据段、未初始化数据段的大小。
    这些值是文件对齐后的大小,由编译器填写,不一定准确
AddressOfEntryPoint (OEP)
  • 指向入口点函数的指针,是相对于 ImageBase 的偏移(RVA)。
  • 对于可执行文件:程序的起始执行地址。
  • 对于驱动程序:初始化函数的地址。
  • 对于 DLL:可选,若无入口点则为 0。
BaseOfCode / BaseOfData
  • 指向代码段/数据段开头的指针(RVA)。
    注: 编译器填写,不一定准确
ImageBase

Image (PE文件) 载入内存时第一个字节的首选地址(基址)。该值通常是 64K 的倍数。

文件类型默认值
DLL0x10000000
应用程序 (EXE)0x00400000
Windows CE0x00010000
Alignment (对齐)
  • SectionAlignment: 内存中的节对齐大小。默认是系统页面大小。
  • FileAlignment: 文件中的节对齐大小。通常为 512 (0x200) 到 64K 之间的2的幂。
    规则: SectionAlignment 必须大于或等于 FileAlignment
SizeOfImage
  • 内存中整个 PE 文件的映射尺寸。
  • 大小包括所有头和节。
  • 必须是 SectionAlignment 的整数倍。
SizeOfHeaders
  • 所有头 + 节表按照 文件对齐 后的大小。
  • 计算公式:
    text SizeOfHeaders = Align( e_lfanew (DOS头指向PE头的偏移) + 4 (PE签名 Signature) + sizeof(IMAGE_FILE_HEADER) + sizeof(IMAGE_OPTIONAL_HEADER) + sizeof(IMAGE_SECTION_HEADER) * 节数量, FileAlignment )
CheckSum

PE 文件校验和。关键系统进程、驱动程序、引导时加载的 DLL 会进行验证。

Subsystem

运行此映像所需的子系统。

宏定义含义
IMAGE_SUBSYSTEM_UNKNOWN0未知的子系统
IMAGE_SUBSYSTEM_NATIVE1不需要子系统 (驱动/本机系统进程)
IMAGE_SUBSYSTEM_WINDOWS_GUI2Windows 图形用户界面 (GUI)
IMAGE_SUBSYSTEM_WINDOWS_CUI3Windows 字符模式 (控制台)
IMAGE_SUBSYSTEM_OS2_CUI5OS/2 CUI 子系统
IMAGE_SUBSYSTEM_POSIX_CUI7POSIX CUI 子系统
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI9Windows CE 系统
IMAGE_SUBSYSTEM_EFI_APPLICATION10EFI 应用程序
IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER11EFI 引导服务驱动
IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER12EFI 运行时服务驱动
IMAGE_SUBSYSTEM_EFI_ROM13EFI ROM 镜像
IMAGE_SUBSYSTEM_XBOX14Xbox 系统
IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION16启动应用程序
DllCharacteristics

定义了 Image 的 DLL 特性(也可用于 EXE)。

宏定义含义
(Reserved)0x0001 – 0x0008保留,必须为 0
IMAGE_DLL_CHARACTERISTICS_HIGH_ENTROPY_VA0x002064位地址空间 ASLR (高熵)
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE0x0040支持 ASLR (加载时重定位)
IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY0x0080强制代码完整性检查
IMAGE_DLLCHARACTERISTICS_NX_COMPAT0x0100支持 DEP (数据执行保护)
IMAGE_DLLCHARACTERISTICS_NO_ISOLATION0x0200不应被隔离
IMAGE_DLLCHARACTERISTICS_NO_SEH0x0400不使用结构化异常处理 (SEH)
IMAGE_DLLCHARACTERISTICS_NO_BIND0x0800不要绑定映像
IMAGE_DLL_CHARACTERISTICS_APPCONTAINER0x1000在 AppContainer 中执行
IMAGE_DLLCHARACTERISTICS_WDM_DRIVER0x2000WDM 驱动
IMAGE_DLL_CHARACTERISTICS_GUARD_CF0x4000支持控制流保护 (CFG)
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE0x8000终端服务器感知
栈与堆
  • SizeOfStackReserve: 初始化保留的栈大小。
  • SizeOfStackCommit: 初始化实际提交的栈大小。
  • SizeOfHeapReserve: 初始化保留的堆大小。
  • SizeOfHeapCommit: 初始化实际提交的堆大小。
DataDirectory (数据目录)

指向数据目录中第一个 IMAGE_DATA_DIRECTORY 结构的指针。每个条目包含地址(RVA)和大小。

索引宏定义含义
0IMAGE_DIRECTORY_ENTRY_EXPORT导出表
1IMAGE_DIRECTORY_ENTRY_IMPORT导入表
2IMAGE_DIRECTORY_ENTRY_RESOURCE资源表
3IMAGE_DIRECTORY_ENTRY_EXCEPTION异常表
4IMAGE_DIRECTORY_ENTRY_SECURITY安全表
5IMAGE_DIRECTORY_ENTRY_BASERELOC基地址重定位表
6IMAGE_DIRECTORY_ENTRY_DEBUG调试表
7IMAGE_DIRECTORY_ENTRY_ARCHITECTURE架构数据 (保留0)
8IMAGE_DIRECTORY_ENTRY_GLOBALPTR全局指针 RVA
9IMAGE_DIRECTORY_ENTRY_TLSTLS 表 (线程本地存储)
10IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG加载配置表
11IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT绑定导入表
12IMAGE_DIRECTORY_ENTRY_IAT导入地址表 (IAT)
13IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT延迟导入表
14IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTORCOM 描述符表 (.NET)
15(Reserved)保留

节表

描述数据块,上文pe文件从磁盘映射到内存中,节表描述这种映射关系,节表的每一行实际上是一个节标题。 此表紧跟可选标头(如果有)。因为文件头不包含指向节表的直接指针。节表的位置是通过计算标头后第一个字节的位置来确定的。节表中的条目数由文件标头中的 NumberOfSections 字段提供。 节表中的条目从 1 (1) 开始编号。 代码和数据内存部分条目按链接器选择的顺序排列。
节的va必须由链接器分配以便于按升序和相邻,必须是可选标头中 SectionAlignment 值的倍数。

Offset大小字段说明
08名称一个 8 字节、null 填充的 UTF-8 编码字符串。如果字符串长度正好为 8 个字符,则不存在终止 null。对于较长的名称,此字段包含斜杠 (/),后跟十进制数的 ASCII 表示形式,该数字是字符串表中的偏移量。可执行映像不使用字符串表,并且不支持长度超过 8 个字符的节名称。如果对象文件中的长名称被发送到可执行文件,则会截断它们。
84VirtualSize加载到内存中的节的总大小。如果此值大于 SizeOfRawData,则节为零填充。此字段仅对可执行映像有效,应将对象文件设置为零。
124VirtualAddress对于可执行映像,是部分加载到内存中时相对于映像库的第一个字节的地址。对于对象文件,此字段是应用重定位前第一个字节的地址;为简单起见,编译器应将此设置为零。否则,它是在重定位期间从偏移量中减去的任意值。
164SizeOfRawData) 对象文件 (节的大小,或图像文件) 磁盘 (上初始化数据的大小。对于可执行映像,这必须是可选标头中的 FileAlignment 的倍数。如果小于 VirtualSize,则部分的其余部分为零填充。由于 SizeOfRawData 字段是舍入的,但 VirtualSize 字段不是,因此 SizeOfRawData 也可能大于 VirtualSize。如果节仅包含未初始化的数据,则此字段应为零。
204PointerToRawData指向 COFF 文件中节的第一页的文件指针。对于可执行映像,这必须是可选标头中的 FileAlignment 的倍数。对于对象文件,值应在 4 字节边界上对齐,以获得最佳性能。如果节仅包含未初始化的数据,则此字段应为零。
244PointerToRelocations指向节重定位条目开头的文件指针。对于可执行映像,如果没有任何重定位,则设置为零。
284PointerToLinenumbers指向节的行号条目开头的文件指针。如果没有 COFF 行号,则此值设置为零。映像的此值应为零,因为 COFF 调试信息已弃用。
322NumberOfRelocations节的重定位条目数。对于可执行映像,此值设置为零。
342NumberOfLinenumbers节的行号条目数。映像的此值应为零,因为 COFF 调试信息已弃用。
364特征描述部分特征的标志。有关详细信息,请参阅 节标志。

VA和FOA转换

  • VA:在内存中的虚拟地址
  • RVA:相对虚拟地址
  • FOA:文件偏移地址

流程

va到foa

  1. 得到PVA的值:RVA = VA – ImageBase
  2. 判断RVA是否处于PE文件头中
  3. 在文件头中则FOA=RVA,不在则判断RVA位于哪个节,差值 = RVA – 节.VirtualAddress(RVA),FOA = 节.PointerToRawData + 差值
    代码实现
// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64  0x8664

//VA转FOA 32位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER *dos,_IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
    //得到RVA的值:RVA = VA - ImageBase
    UINT rva = va - nt->OptionalHeader.ImageBase;
    //输出rva
    printf("rva:%X\n", rva);
    //找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
    UINT PeEnd = (UINT)dos->e_lfanew+sizeof(_IMAGE_NT_HEADERS);
    //输出PeEnd
    printf("PeEnd:%X\n", PeEnd);
    //判断rva是否位于PE文件头中
    if (rva < PeEnd) {
        //如果rva位于PE文件头中,则foa==rva,直接返回rva即可
        printf("foa:%X\n", rva);        
        return rva;
    }
    else {
        //如果rva在PE文件头外
        //判断rva属于哪个节
        int i;
        for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
            //计算内存对齐后节的大小
            UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize ,(UINT)sectionArr[i]->SizeOfRawData ) / (double)nt->OptionalHeader.SectionAlignment)* nt->OptionalHeader.SectionAlignment;

            if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
                //找到所属的节
                //输出内存对齐后的节的大小
                printf("SizeInMemory:%X\n", SizeInMemory);
                break;
            }
        }
        if (i >= nt->FileHeader.NumberOfSections) {
            //未找到
            printf("没有找到匹配的节\n");
            return -1;
        }
        else {
            //计算差值= RVA - 节.VirtualAddress
            int offset = rva - sectionArr[i]->VirtualAddress;
            //FOA = 节.PointerToRawData + 差值
            int foa = sectionArr[i]->PointerToRawData + offset;
            printf("foa:%X\n", foa);
            return foa;
        }

    }

}

//VA转FOA 64位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa64(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS64* nt, _IMAGE_SECTION_HEADER** sectionArr) {
    //得到RVA的值:RVA = VA - ImageBase
    UINT rva = va - nt->OptionalHeader.ImageBase;
    //输出rva
    printf("rva:%X\n", rva);
    //找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
    UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS64);
    //输出PeEnd
    printf("PeEnd:%X\n", PeEnd);
    //判断rva是否位于PE文件头中
    if (rva < PeEnd) {
        //如果rva位于PE文件头中,则foa==rva,直接返回rva即可
        printf("foa:%X\n", rva);
        return rva;
    }
    else {
        //如果rva在PE文件头外
        //判断rva属于哪个节
        int i;
        for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
            //计算内存对齐后节的大小
            UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize ,(UINT)sectionArr[i]->SizeOfRawData ) / (double)nt->OptionalHeader.SectionAlignment)* nt->OptionalHeader.SectionAlignment;

            if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
                //找到所属的节
                //输出内存对齐后的节的大小
                printf("SizeInMemory:%X\n", SizeInMemory);
                break;
            }
        }
        if (i >= nt->FileHeader.NumberOfSections) {
            //未找到
            printf("没有找到匹配的节\n");
            return -1;
        }
        else {
            //计算差值= RVA - 节.VirtualAddress
            int offset = rva - sectionArr[i]->VirtualAddress;
            //FOA = 节.PointerToRawData + 差值
            int foa = sectionArr[i]->PointerToRawData + offset;
            printf("foa:%X\n", foa);
            return foa;
        }

    }

}
int main(int argc, char* argv[])
{
    //创建DOS对应的结构体指针
    _IMAGE_DOS_HEADER* dos;
    //读取文件,返回文件句柄
    HANDLE hFile = CreateFileA("C:\\Users\\lyl610abc\\Desktop\\GlobalVariety.exe", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, 0);
    //根据文件句柄创建映射
    HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, 0);
    //映射内容
    LPVOID pFile = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
    //类型转换,用结构体的方式来读取
    dos = (_IMAGE_DOS_HEADER*)pFile;
    //输出dos->e_magic,以十六进制输出
    printf("dos->e_magic:%X\n", dos->e_magic);

    //创建指向PE文件头标志的指针
    DWORD* peId;
    //让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
    peId = (DWORD*)((UINT)dos + dos->e_lfanew);
    //输出PE文件头标志,其值应为4550,否则不是PE文件
    printf("peId:%X\n", *peId);

    //创建指向可选PE头的第一个成员magic的指针
    WORD* magic;
    //让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
    magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
    //输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
    printf("magic:%X\n", *magic);
    //根据magic判断为32位程序还是64位程序
    switch (*magic) {
    case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
    {
        printf("32位程序\n");
        //确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
        //创建指向PE文件头的指针
        _IMAGE_NT_HEADERS* nt;
        //让PE文件头指针指向其对应的地址
        nt = (_IMAGE_NT_HEADERS*)peId;
        printf("Machine:%X\n", nt->FileHeader.Machine);
        printf("Magic:%X\n", nt->OptionalHeader.Magic);

        //创建一个指针数组,该指针数组用来存储所有的节表指针
        //这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
        _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**) malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

        //创建指向块表的指针
        _IMAGE_SECTION_HEADER* sectionHeader;
        //让块表的指针指向其对应的地址
        sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
        //计数,用来计算块表地址
        int cnt = 0;
        //比较 计数 和 块表的个数,即遍历所有块表
        while(cnt< nt->FileHeader.NumberOfSections){
            //创建指向块表的指针
            _IMAGE_SECTION_HEADER* section;
            //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
            section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER)*cnt);
            //将得到的块表指针存入数组
            sectionArr[cnt++] = section;
            //输出块表名称
            printf("%s\n", section->Name);
        }

        VaToFoa32(0x4198B0,dos, nt, sectionArr);

        break;
    }

    case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
    {
        printf("64位程序\n");
        //确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
        //创建指向PE文件头的指针
        _IMAGE_NT_HEADERS64* nt;
        nt = (_IMAGE_NT_HEADERS64*)peId;
        printf("Machine:%X\n", nt->FileHeader.Machine);
        printf("Magic:%X\n", nt->OptionalHeader.Magic);

        //创建一个指针数组,该指针数组用来存储所有的节表指针
        //这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
        _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

        //创建指向块表的指针
        _IMAGE_SECTION_HEADER* sectionHeader;
        //让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
        sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
        //计数,用来计算块表地址
        int cnt = 0;
        //比较 计数 和 块表的个数,即遍历所有块表
        while (cnt < nt->FileHeader.NumberOfSections) {
            //创建指向块表的指针
            _IMAGE_SECTION_HEADER* section;
            //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
            section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
            //将得到的块表指针存入数组
            sectionArr[cnt++] = section;
            //输出块表名称
            printf("%s\n", section->Name);
        }
        break;
    }

    default:
    {
        printf("error!\n");
        break;
    }

    }
    return 0;
}

foa到va

  1. 判断foa是否位于pe文件头中
  2. 是则,foa=rva,不是则判断位于哪个节,差值 =  FOA – 节.PointerToRawData ,RVA = 差值 + 节.VirtualAddress(RVA)
  3. VA = ImageBase + RVA
    代码实现
// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64  0x8664

//VA转FOA 32位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER *dos,_IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
    //得到RVA的值:RVA = VA - ImageBase
    UINT rva = va - nt->OptionalHeader.ImageBase;
    //输出rva
    printf("rva:%X\n", rva);
    //找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
    UINT PeEnd = (UINT)dos->e_lfanew+sizeof(_IMAGE_NT_HEADERS);
    //输出PeEnd
    printf("PeEnd:%X\n", PeEnd);
    //判断rva是否位于PE文件头中
    if (rva < PeEnd) {
        //如果rva位于PE文件头中,则foa==rva,直接返回rva即可
        printf("foa:%X\n", rva);        
        return rva;
    }
    else {
        //如果rva在PE文件头外
        //判断rva属于哪个节
        int i;
        for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
            //计算内存对齐后节的大小
            UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize ,(UINT)sectionArr[i]->SizeOfRawData ) / (double)nt->OptionalHeader.SectionAlignment)* nt->OptionalHeader.SectionAlignment;

            if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
                //找到所属的节
                //输出内存对齐后的节的大小
                printf("SizeInMemory:%X\n", SizeInMemory);
                break;
            }
        }
        if (i >= nt->FileHeader.NumberOfSections) {
            //未找到
            printf("没有找到匹配的节\n");
            return -1;
        }
        else {
            //计算差值= RVA - 节.VirtualAddress
            UINT offset = rva - sectionArr[i]->VirtualAddress;
            //FOA = 节.PointerToRawData + 差值
            UINT foa = sectionArr[i]->PointerToRawData + offset;
            printf("foa:%X\n", foa);
            return foa;
        }

    }

}

//VA转FOA 64位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa64(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS64* nt, _IMAGE_SECTION_HEADER** sectionArr) {
    //得到RVA的值:RVA = VA - ImageBase
    UINT rva = va - nt->OptionalHeader.ImageBase;
    //输出rva
    printf("rva:%X\n", rva);
    //找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
    UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS64);
    //输出PeEnd
    printf("PeEnd:%X\n", PeEnd);
    //判断rva是否位于PE文件头中
    if (rva < PeEnd) {
        //如果rva位于PE文件头中,则foa==rva,直接返回rva即可
        printf("foa:%X\n", rva);
        return rva;
    }
    else {
        //如果rva在PE文件头外
        //判断rva属于哪个节
        int i;
        for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
            //计算内存对齐后节的大小
            UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize ,(UINT)sectionArr[i]->SizeOfRawData ) / (double)nt->OptionalHeader.SectionAlignment)* nt->OptionalHeader.SectionAlignment;           
            if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
                //找到所属的节
                //输出内存对齐后的节的大小
                printf("SizeInMemory:%X\n", SizeInMemory);
                break;
            }
        }
        if (i >= nt->FileHeader.NumberOfSections) {
            //未找到
            printf("没有找到匹配的节\n");
            return -1;
        }
        else {
            //计算差值= RVA - 节.VirtualAddress
            UINT offset = rva - sectionArr[i]->VirtualAddress;
            //FOA = 节.PointerToRawData + 差值
            UINT foa = sectionArr[i]->PointerToRawData + offset;
            printf("foa:%X\n", foa);
            return foa;
        }

    }

}

//FOA转VA 32位
//第一个参数为要转换的在文件中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT FoaToVa32(UINT foa, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
    //找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
    UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS);
    //判断FOA是否位于PE文件头中
    if (foa < PeEnd) {
        //如果foa位于PE文件头中,则foa==rva,直接返回foa+ImageBase即可
        printf("va:%X\n", foa+nt->OptionalHeader.ImageBase);
        return foa + nt->OptionalHeader.ImageBase;
    }
    else {
        //如果foa在PE文件头外
        //判断foa属于哪个节
        int i;
        for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {

            if (foa >= sectionArr[i]->PointerToRawData && foa < (sectionArr[i]->PointerToRawData + sectionArr[i]->SizeOfRawData)) {
                //找到所属的节                
                break;
            }
        }
        if (i >= nt->FileHeader.NumberOfSections) {
            //未找到
            printf("没有找到匹配的节\n");
            return -1;
        }
        else {
            //计算差值= FOA - 节.PointerToRawData 
            UINT offset = foa - sectionArr[i]->PointerToRawData;
            //RVA = 差值 + 节.VirtualAddress(RVA)
            UINT rva =  offset+ sectionArr[i]->VirtualAddress;
            printf("va:%X\n", rva + nt->OptionalHeader.ImageBase);
            return rva + nt->OptionalHeader.ImageBase;
        }
    }
    return 0;
}

//FOA转VA 64位
//第一个参数为要转换的在文件中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT FoaToVa64(UINT foa, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS64* nt, _IMAGE_SECTION_HEADER** sectionArr) {
    //找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
    UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS64);
    //判断FOA是否位于PE文件头中
    if (foa < PeEnd) {
        //如果foa位于PE文件头中,则foa==rva,直接返回foa+ImageBase即可
        printf("va:%X\n", foa + nt->OptionalHeader.ImageBase);
        return foa + nt->OptionalHeader.ImageBase;
    }
    else {
        //如果foa在PE文件头外
        //判断foa属于哪个节
        int i;
        for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {

            if (foa >= sectionArr[i]->PointerToRawData && foa < (sectionArr[i]->PointerToRawData + sectionArr[i]->SizeOfRawData)) {
                //找到所属的节                
                break;
            }
        }
        if (i >= nt->FileHeader.NumberOfSections) {
            //未找到
            printf("没有找到匹配的节\n");
            return -1;
        }
        else {
            //计算差值= FOA - 节.PointerToRawData 
            UINT offset = foa - sectionArr[i]->PointerToRawData;
            //RVA = 差值 + 节.VirtualAddress(RVA)
            UINT rva = offset + sectionArr[i]->VirtualAddress;
            printf("va:%X\n", rva + nt->OptionalHeader.ImageBase);
            return rva + nt->OptionalHeader.ImageBase;
        }
    }
    return 0;
}

int main(int argc, char* argv[])
{
    //创建DOS对应的结构体指针
    _IMAGE_DOS_HEADER* dos;
    //读取文件,返回文件句柄
    HANDLE hFile = CreateFileA("C:\\Users\\sixonezero\\Desktop\\GlobalVariety.exe", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, 0);
    //根据文件句柄创建映射
    HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, 0);
    //映射内容
    LPVOID pFile = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
    //类型转换,用结构体的方式来读取
    dos = (_IMAGE_DOS_HEADER*)pFile;
    //输出dos->e_magic,以十六进制输出
    printf("dos->e_magic:%X\n", dos->e_magic);

    //创建指向PE文件头标志的指针
    DWORD* peId;
    //让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
    peId = (DWORD*)((UINT)dos + dos->e_lfanew);
    //输出PE文件头标志,其值应为4550,否则不是PE文件
    printf("peId:%X\n", *peId);

    //创建指向可选PE头的第一个成员magic的指针
    WORD* magic;
    //让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
    magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
    //输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
    printf("magic:%X\n", *magic);
    //根据magic判断为32位程序还是64位程序
    switch (*magic) {
    case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
    {
        printf("32位程序\n");
        //确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
        //创建指向PE文件头的指针
        _IMAGE_NT_HEADERS* nt;
        //让PE文件头指针指向其对应的地址
        nt = (_IMAGE_NT_HEADERS*)peId;
        printf("Machine:%X\n", nt->FileHeader.Machine);
        printf("Magic:%X\n", nt->OptionalHeader.Magic);

        //创建一个指针数组,该指针数组用来存储所有的节表指针
        //这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
        _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**) malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

        //创建指向块表的指针
        _IMAGE_SECTION_HEADER* sectionHeader;
        //让块表的指针指向其对应的地址
        sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
        //计数,用来计算块表地址
        int cnt = 0;
        //比较 计数 和 块表的个数,即遍历所有块表
        while(cnt< nt->FileHeader.NumberOfSections){
            //创建指向块表的指针
            _IMAGE_SECTION_HEADER* section;
            //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
            section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER)*cnt);
            //将得到的块表指针存入数组
            sectionArr[cnt++] = section;
            //输出块表名称
            printf("%s\n", section->Name);
        }

        VaToFoa32(0x4198B0,dos, nt, sectionArr);
        FoaToVa32(0x176B0, dos, nt, sectionArr);

        break;
    }

    case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
    {
        printf("64位程序\n");
        //确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
        //创建指向PE文件头的指针
        _IMAGE_NT_HEADERS64* nt;
        nt = (_IMAGE_NT_HEADERS64*)peId;
        printf("Machine:%X\n", nt->FileHeader.Machine);
        printf("Magic:%X\n", nt->OptionalHeader.Magic);

        //创建一个指针数组,该指针数组用来存储所有的节表指针
        //这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
        _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

        //创建指向块表的指针
        _IMAGE_SECTION_HEADER* sectionHeader;
        //让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
        sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
        //计数,用来计算块表地址
        int cnt = 0;
        //比较 计数 和 块表的个数,即遍历所有块表
        while (cnt < nt->FileHeader.NumberOfSections) {
            //创建指向块表的指针
            _IMAGE_SECTION_HEADER* section;
            //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
            section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
            //将得到的块表指针存入数组
            sectionArr[cnt++] = section;
            //输出块表名称
            printf("%s\n", section->Name);
        }
        VaToFoa32(0x4198B0,dos, nt, sectionArr);
        FoaToVa32(0x176B0, dos, nt, sectionArr);
        break;
    }

    default:
    {
        printf("error!\n");
        break;
    }

    }
    return 0;
}

RVA和FOA之间的差异归根结底就是在于文件对齐和内存对齐的差异上

前面补充

关于正着读和倒着读

计算机使用小端序可以加快运行速度,所以计算机用于计算,寻址,判断大小的数据都要倒着看,包括地址、偏移量、大小、计数、标志位、特征值。从右往左,高地址往低地址
本身的字节数组形式存在的数据,设计给人看的名字(比如text那些),要正着读
但是mz和pe的标志,本质是数据,但被设计成了字符串形式
在hex编辑器里正着显示,代码里和分析数据时得倒着写

关于对象和映像

对象文件,常见后缀obj,就是编译c或者cpp文件时生成的o,不需要被执行,只是半成品,直接以coff文件头开始,没有ms dos头
映像文件,常见后缀,exe,dll,sys,链接器linker产物,是成品,将多个obj文件和库文件lib拼装在一起形成最终文件(映像解释,结构被设计为可以直接映射到内存上运行,在磁盘样子和加载到内存中很想,为了兼容,必须以msdos头和存根开始,然后是pe签名,然后是coff文件头)
两者布局区别
对象文件

[ COFF 文件头 ] <--- 文件直接从这里开始 (偏移量 0)
[ 节表 ]
[ 原始数据... ]

映像文件

[ MS-DOS 头 ]
[ MS-DOS 存根 ]
[ PE 签名 ]
[ COFF 文件头 ] <--- 这里才是 COFF 头,结构与上面一样,但位置靠后
[ 可选头 (Optional Header) ] <--- 映像文件特有
[ 节表 ]
[ 原始数据... ]

关于dos

头和存根,头指元数据,描述文件的作用,结构化数据,告诉window系统(加载器)处理文件的方式,描述文件数据结构,存根(stub)是一段可以执行的机器码,兼容性处理,ms ods stub是一个完整的16位dos程序,存储在pe文件的头部
dos,磁盘操作系统
stub作用,防报错,就是比如16位系统识别不了,就直接通过stub终止程序,但是window,dos头那里有偏移量,就会跳过这个stub

一些零碎概念

名称描述
属性证书用于将可验证声明与映像关联的证书。许多不同的可验证声明可以与文件相关联;其中最有用的一个声明是软件制造商的声明,此声明指示映像的消息摘要应该是什么。消息摘要类似于检验和,只是很难伪造。因此,很难修改文件以使其具有与原始文件相同的消息摘要。可以使用公钥或私钥加密方案来验证声明是否是由制造商发出的。本文档描述了有关属性证书的详细信息,但不允许将其插入到图像文件中。
日期/时间戳在 PE 或 COFF 文件中的多个位置用于不同目的的戳记。在大多数情况下,每个戳记的格式与 C 运行时库中的时间函数使用的格式相同。有关异常,请参见调试类型中 IMAGE_DEBUG_TYPE_REPRO 的说明。如果戳记值为 0 或 0xFFFFFFFF,则它不表示实际或有意义的日期/时间戳。
文件指针链接器(对于目标文件)或加载器(对于映像文件)处理之前文件本身中项的位置。换句话说,这是存储在磁盘上的文件内的位置。
链接器随 Microsoft Visual Studio 一起提供的链接器引用。
对象文件作为链接器输入提供的文件。链接器生成一个映像文件,而此映像文件又用作加载器的输入。术语“目标文件”并不一定意味着与面向对象的编程有任何联系。
已保留,必须为 0字段的描述,指示该字段的值对于生成器来说必须为零,而使用者必须忽略该字段。
相对虚拟地址 (RVA)在映像文件中,这是项目加载到内存并从中减去映像文件基地址后的地址。项目的 RVA 几乎总是与其在磁盘上文件中的位置(文件指针)不同。
在目标文件中,RVA 的意义不大,因为未分配内存位置。在这种情况下,RVA 将是一个段内的地址(此表后面将进行描述),稍后在链接期间会对此地址应用重定位。为简单起见,编译器应只将每个部分中的第一个 RVA 设置为零。
部分PE 或 COFF 文件中代码或数据的基本单位。例如,目标文件中的所有代码都可以组合在单个部分中,或者(取决于编译器行为)每个函数都可以占用自己的部分。部分越多,文件开销就越大,但链接器能够更有选择性地链接代码。一个部分类似于 Intel 8086 体系结构中的段。一个部分中的所有原始数据都必须连续加载。此外,映像文件可以包含多个具有特殊用途的部分,例如 .tls 或 .reloc 。
虚拟地址 (VA)与 RVA 相同,只不过不减去映像文件的基址。该地址称为 VA,因为 Windows 会为每个进程创建一个独立于物理内存的不同 VA 空间。对于几乎所有目的,VA 应只被视为一个地址。VA 不如 RVA 那么可预测,因为加载器可能不会在其首选位置加载映像。
英文术语中文术语描述
Virtual Address (VA)虚拟地址进程虚拟内存空间中的绝对地址。
Relative Virtual Address (RVA)相对虚拟地址内存地址相对于模块基址的偏移。
File Pointer文件指针文件在磁盘上的物理偏移位置。
Section节 / 段代码或数据的逻辑分组,具有相同的内存属性(如只读、可执行)。
Data Directory数据目录位于可选头末尾的数组,指向各种表(导入、导出、资源等)。
Thunk转换层 / 桩代码这里通常指导入地址表 (IAT) 中的项。

ELF

比起PE,ELF分了section给编译器和segment给加载器,处理外部函数的时候调用了GOT/PLT(延迟绑定),PE则用的IAT
elf格式由system v标准定义,设计更灵活,支持可执行文件,共享库,目标文件等多种类型
常见后缀

  1. 无后缀或.bin,可执行文件如/bin/ls、/usr/bin/python3,通常无后缀
  2. .so,共享对象,类似windows的dll,提供动态链接功能
  3. .a,静态链接库,包含多个目标文件归档,编译时会被完整嵌入可执行文件
  4. .o,目标文件,编译器输出的中间文件,要链接器处理后生成可执行文件
  5. .ko,内核模块,linux内核的驱动程序,运行于内核空间,相当于window里面的.sys(驱动程序),运行在ring 0(最高权限),比起普通elf,ko驱动没有main,只有module_init()(插入模块时执行)和对应的exit,会看到大量重定位表,因为要被“链接”进内核
  6. .mod,部分系统的模块文件,如早期unix功能类似ko,包含了模块的版本信息(Version Magic)和符号依赖关系。

结构

区域位置组件名称 (中文)英文对应说明
顶部ELF 文件头ELF Header文件的总索引,包含魔数、版本等。
上部程序头部表Program Header Table描述段(Segment)信息,用于执行视图。
中部.textCode Section代码节(指令)。
中部.dataData Section数据节(已初始化全局变量)。
中部.bssBSS Section未初始化数据节。
中部.symtabSymbol Table符号表(函数名、变量名)。
中部.debugDebug Info调试信息。
中部.lineLine Number Table行号表(源码行号对应)。
中部Others其他各类节。
底部节区头部表Section Header Table描述节(Section)信息,链接视图的核心,记录了上面所有节的位置。

ELF头部

文件起始位置,固定大小(32位为52字节,64位位64字节)
结构体

#define EI_NIDENT 16
typedef struct {
    unsigned char e_ident[EI_NIDENT];  // ELF标识符
    Elf32_Half    e_type;             // 文件类型
    Elf32_Half    e_machine;          // 目标架构
    Elf32_Word    e_version;          // ELF版本
    Elf32_Addr    e_entry;            // 程序入口点
    Elf32_Off     e_phoff;            // 程序头表偏移
    Elf32_Off     e_shoff;            // 节头表偏移
    Elf32_Word    e_flags;            // 处理器特定标志
    Elf32_Half    e_ehsize;           // ELF头部大小
    Elf32_Half    e_phentsize;        // 程序头表项大小
    Elf32_Half    e_phnum;            // 程序头表项数量
    Elf32_Half    e_shentsize;        // 节头表项大小
    Elf32_Half    e_shnum;            // 节头表项数量
    Elf32_Half    e_shstrndx;         // 节名称字符串表索引
} Elf32_Ehdr;

其中,e_ident字段的前4个字节固定为0x7f、’E’、’L’、’F’,这是ELF文件的魔数标识。

程序头表(Program Header Table)

描述段(segment)信息,用于程序加载和执行
结构体

typedef struct {
    Elf32_Word p_type;    // 段类型
    Elf32_Off  p_offset;  // 段在文件中的偏移
    Elf32_Addr p_vaddr;   // 段在内存中的虚拟地址
    Elf32_Addr p_paddr;   // 物理地址(通常与虚拟地址相同)
    Elf32_Word p_filesz;  // 段在文件中的大小
    Elf32_Word p_memsz;   // 段在内存中的大小
    Elf32_Word p_flags;   // 段标志(读/写/执行)
    Elf32_Word p_align;   // 段对齐方式
} Elf32_Phdr;

节头表(Section Header Table)

描述节的信息(section),主要用于链接和调试

typedef struct {
    Elf32_Word sh_name;      // 节名称(字符串表索引)
    Elf32_Word sh_type;      // 节类型
    Elf32_Word sh_flags;     // 节标志
    Elf32_Addr sh_addr;      // 节在内存中的地址
    Elf32_Off  sh_offset;    // 节在文件中的偏移
    Elf32_Word sh_size;      // 节大小
    Elf32_Word sh_link;      // 链接到其他节的索引
    Elf32_Word sh_info;      // 附加信息
    Elf32_Word sh_addralign; // 节对齐方式
    Elf32_Word sh_entsize;   // 条目大小(如果有)
} Elf32_Shdr;

节存储实际内容,常见节有:

  • .text:可执行代码
  • .data:已初始化的全局变量
  • .bss:未初始化的全局变量(不占用文件空间)
  • .rodata:只读数据
  • .symtab:符号表
  • .strtab:字符串表
  • .rel.*:重定位信息
  • .dynamic:动态链接信息
  • .interp:程序解释器路径(如/lib/ld-linux.so.2)

ELF文件的两种视图

链接视图

对应节头表,将文件按功能划分为多个节,主要用于静态链接过程,特点是粒度细(就是分类细),便于编译器生成和链接器处理,实际执行中,链接器会将多个节合并成段提高空间利用率

执行视图

对应程序头表,描述了怎么将文件内容映射到进程的虚拟地址空间,关注段
按照权限合并,比如text和rodata都只读,data和bss都可读可写
常见段类型

  • PT_LOAD,通常一个程序至少两个ptload段,一个只读,一个读写
  • PT_DYNAMIC,动态链接段,对应节.dynamic,描述需要哪个so库(DT_NEEDED),符号表,重定位表,初始化函数,调用外部库函数
  • PT_INTERP,解释器路径,对应节.interp,通常是字符串,路径指向动态链接器,运行前准备
  • PT_NOTE,对应节.note.ABU-tag等,辅助说明
  • PT_TLS,线程局部存储,对应节,.tdata,.tbss,告诉系统如何为每个线程初始化变量
  • PT_GNU_STACK,栈控制,虚拟段,告诉栈是否可执行
    PT,给操作系统内核看,决定段属性,DT,给动态链接器看,决定动态链接细节

静态链接

静态链接过程

在编译时将库代码直接复制到最终可执行文件的链接方式,过程如

  1. 编译器生成目标文件.o
  2. 链接器扫描所有目标文件和静态库.a
  3. 解析所有符号引用
  4. 将使用的库代码复制到可执行文件
  5. 生成完全独立的可执行文件
    特点:地址预确定,链接阶段为所有符号分配最终虚拟地址

静态加载过程

  1. 解析elf文件头部,验证文件格式
  2. 根据程序头表将各段映射到链接时确定的固定虚拟地址
  3. 建立内存管理结构(mm_struct)管理虚拟内存区域(VMA)
  4. 初始化页表映射
  5. cpu直接从elf头部的e_entry获取入口地址开始执行
    所有内存访问都使用绝对地址,通过MMU硬件将链接时确定的虚拟地址转换为物理地址。整个过程完全复用链接阶段确定的地址布局,无需运行时重定位。
    特点:程序自包含,但可执行文件体积大

动态链接

动态链接过程

程序运行时才加载链接和链接所需库

  1. 编译器生成.o
  2. 链接器记录动态库.so以来信息
  3. 生成包含为解析符号的可执行文件
  4. 程序运行时,动态链接器(ld.so)
    1. 查找并加载所需共享库
    2. 解析符号引用
    3. 执行重定位操作
      特点:地址延迟确定,实际地址在运行时才解析

动态加载过程

  1. 内核映射主程序的段到内存
  2. 加载动态链接器(ld.so)
  3. 动态链接器按需加载依赖的共享库
  4. 通过GOT和PLT机制完成符号解析和地址重定位
  5. 所有库代码采用位置无关代码(PLC)技术,支持多进程共享
  6. 支持ASLR(地址空间布局随机化)
    特点:节省内存,依赖管理复杂

文章,笔记
文章,详细讲解,太强了
文章
文章

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇