PE文件结构与程序装载是掌握Windows逆向、加壳、免杀等技术的基础,本文详细记录了PE文件的基本结构,用编辑器对文件结构进行分析,并介绍程序装载的相关概念和基本过程。
参考书籍:《逆向工程核心原理》《程序员的自我修养》
文章目录
一、PE文件结构
Windows PE (Protable Executable) 文件基本结构由PE头和多个节区组成,如下图所示:
下面逐一说明各个部分的基本特征和作用:
(一) PE头
1.DOS头
微软在创建PE文件格式时,DOS文件正在广泛使用,为兼容DOS文件,在PE头部添加了DOS头部分,DOS头实际上是 IMAGE_DOS_HEADER
结构体,大小为64字节,定义如下所示:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
该结构体中有2个重要成员:
WORD e_magic
:DOS签名,固定为4D5A (“MZ”),位于DOS头的第一个部分LONG e_lfanew
:标识NT头的偏移,位于DOS头的最后一个部分
2.DOS存根
可选项,大小不固定,由代码和数据组成。在32/64位Windows系统中会被识别为PE格式,此时会跨过DOS存根部分;在DOS环境中系统不能识别PE文件格式,因此按照DOS文件进行解析,此时DOS存根部分的代码会被执行。
3.NT头
实际为 IMAGE_NT_HEADERS
结构体,大小为248个字节,定义如下所示:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE Signature (“PE”00)
IMAGE_FILE_HEADER FileHeader; // PE File Header
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // PE Optional Header
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
该结构体中成员有3个,介绍如下:
(1) DWORD Signature
:PE签名,为0x50450000 (“PE”00)
(2) IMAGE_FILE_HEADER FileHeader
:该结构体大小为20个字节,定义如下所示:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
IMAGE_FILE_HEADER
结构体中又包括4种重要成员,如下所示:
① WORD Machine
:CPU标识,每种CPU对应唯一的Machine码,常见的如:Intel386 —> 0x014c,AMD64 —> 0x8664
② WORD NumberOfSections
:节区数量,PE文件按照节区的属性划分节区
③ WORD SizeOfOptionalHeader
:标识IMAGE_OPTIONAL_HEADER32
或IMAGE_OPTIONAL_HEADER64
结构体的长度
④ WORD Characteristics
:文件类型,不同类型按位向或进行组合,常见如:EXE文件 —> 0x0002,DLL文件 —> 0x2000,具体如下:
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
(3) IMAGE_OPTIONAL_HEADER32 OptionalHeader
或 IMAGE_OPTIONAL_HEADER64 OptionalHeader
:这是PE头中最大的一个结构体,标识了程序入口点、装载地址等信息,结构体定义如下所示:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
其中比较重要的成员有6个,如下所示:
① WORD Magic
:当镜像可选头为IMAGE_OPTIONAL_HEADER32
结构体时,Magic码为0x10B,镜像可选头为IMAGE_OPTIONAL_HEADER64
时,Magic码为0x20B
② DWORD AddressOfEntryPoint
:程序入口点 (EP) 的RVA值,标识程序运行代码的起始地址
③ DWORD ImageBase
:PE文件被加载到虚拟内存时的优先装载地址,一般而言使用 VB/VC++/Delphi 等开发工具编译的32/64位的EXE文件,其ImageBase值为0x400000 / 0x140000000,DLL文件的ImageBase值为0x10000000 / 0x180000000,这些值可以自己指定。程序载入内存后,EP的地址为:ImageBase+AddressOfEntryPoint
④ DWORD SectionAlignment
/ DWORD FileAlignment
:SectionAlignment标识了PE文件节区在内存中的最小单位 (页),FileAlignment标识了节区在磁盘文件中的最小单位 (扇区),PE文件在内存或磁盘中节区大小为FileAlignment或SectionAlignment值的整数倍
⑤ DWORD SizeOfImage
:PE文件在虚拟内存中所占空间的大小,一般文件在磁盘中的大小和加载到内存空间后大小是不同的
⑥ DWORD SizeOfHeaders
:PE头的大小,该值必须是FileAlignment的整数倍
⑦ IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
:由DataDirectory结构体组成的数组,每项都有定义的值,如下所示:
// Directory Entries
#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 // Copyright Directory (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
其中有2个非常重要的成员记录了导出表和导入表所在的地址,相关内容在EAT与IAT章节会详细介绍:
DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
:导出表的RVA和大小DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
:导入表的RVA和大小
4.节区头
PE文件的节区按照属性划分,节区头定义了各节区的属性和访问权限,基本可以分为三类 (根据PE文件在装载到内存时的分段方式划分):
- 可执行,可读权限:代码段
.text
, - 不可执行,可读可写权限:数据段
.data
,BSS段.bss
- 不可执行,只读权限:只读数据段
.rodata
节区头是由IMAGE_SECTION_HEADER
构成的数组,结构体定义如下所示:
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
每个结构体对应一个节区,其中重要的成员有6个,如下所示:
(1) BYTE Name[IMAGE_SIZEOF_SHORT_NAME]
:一般存放节区名字
(2) DWORD VirtualSize
:内存中节区所占大小
(3) DWORD VirtualAddress
:内存中节区的起始地址 (RVA)
(4) DWORD SizeOfRawData
:磁盘文件中节区所占大小
(5) DWORD PointerToRawData
:磁盘文件中节区的起始地址
(6) DWORD Characteristics
:标识节区属性
其中,VirtualAddress和PointerToRawData值与SectionAlignment和FileAlignment相同,仅表示对齐地址的基本单位。VirtualSize和SizeOfRawData一般不同,即磁盘中节区的大小和虚拟内存中节区的大小一般不同。节区属性标识了节区的可执行及可读写的属性,由以下两个部分OR计算组合而成:
#define IMAGE_SCN_CNT_CODE 0x00000020 // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // Section contains uninitialized data.
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // Section is executable.
#define IMAGE_SCN_MEM_READ 0x40000000 // Section is readable.
#define IMAGE_SCN_MEM_WRITE 0x80000000 // Section is writeable.
用Hex Editor打开calc.exe (64位),与PE文件头部分对应关系如下图所示:
上图基本信息已经标识出,其中注意几点:
- x86架构CPU都是小端存储,高位字节存在高地址,低位字节存在低地址,小端序看起来并不直观,涉及到多字节数据时需要逆序提取
- 内存页对齐基本单位SectionAlignment为0x1000,即4KB;磁盘扇区对齐基本单位FileAlignment为0x200,即512Byte
- 节区头名字可随意取,PE规范中未明确规定节区的名字,节区属性要看头部具体的定义
(二) PE体
PE体由多个节区组成,节区的命名通常以.
作为开始,代表系统保留命名,常见的节区及作用如下表格所示:
节区 | 作用 | 属性 |
---|---|---|
.text | 代码段,存放程序代码 | 可执行,可读 |
.data | 数据段,存放初始化了的全局静态变量和局部静态变量 | 不可执行,可读写 |
.rdata | 只读数据段,存放只读变量和字符串常量 | 不可执行,只读 |
.bss | BSS段,存放未初始化的全局变量和局部静态变量 | 不可执行,只读 |
.pdata | 异常表段,存放异常处理程序相关的信息 | 不可执行,可读写 |
.rsrc | 资源段,存放图标,菜单,位图等资源 | 不可执行,只读 |
.reloc | 存放可执行文件的基址重定位,通常仅DLL文件须要 | 不可执行,可读写 |
.debug | 调试信息段,存放调试信息 | 不可执行,可读写 |
.init / .fini | 程序初始化与结束代码段 | 可执行,可读 |
(三) EAT与IAT
导出地址表 (Export Address Table, EAT) 与导入地址表 (Import Address Table, IAT) 是PE文件中非常重要的部分,了解EAT与IAT首先要从导出表与导入表开始介绍:
1.导出地址表 EAT
当一个PE文件将一些函数和变量提供给其他PE文件使用时,这种行为即为符号导出,比如DLL文件将符号导出给EXE文件使用。需要导出的符号统一保存在 导出表 (Export Table) 结构中,这个结构实际上是 IMAGE_EXPORT_DIRECTORY
结构体,记录了全部符号名与符号地址的映射关系,结构体定义如下所示:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp; // Creation time date stamp
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // RVA of library file name
DWORD Base; // Ordinal base
DWORD NumberOfFunctions; // Number of functions
DWORD NumberOfNames; // Number of names
DWORD AddressOfFunctions; // RVA of function start address array
DWORD AddressOfNames; // RVA of function name string array
DWORD AddressOfNameOrdinals; // RVA of ordinal array
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
该结构体不在PE头而在PE体中,但其位置信息 (RVA) 记录在了PE头OptionalHeader结构的IMAGE_DATA_DIRECTORY DataDirectory[0]
部分。导出表最后的3个成员指向的是3个数组,这3个数组是导出表中最重要的结构,下面对其进行介绍:
(1) DWORD AddressOfFunctions
:指向 导出地址表 (Export Address Table, EAT) 的起始地址 (RVA),表 (数组) 中每个元素存放导出函数的RVA,元素个数即导出函数个数为NumberOfFunctions
(2) DWORD AddressOfNames
:指向 符号名表 (Name Table) 的起始地址 (RVA),表 (数组) 中每个元素存放导出函数的名字,元素个数即导出函数中有名字的个数为NumberOfNames
(3) DWORD AddressOfNameOrdinals
:指向 名字序号对应表 (Name-Ordinal Table) 的起始地址 (RVA),表 (数组) 中每个元素存放函数名对应的序号。其实序号是DOS时代的产物,受限于当时的硬件条件,将函数名全部载入内存是不现实的,因此采取将函数对应序号,利用序号将函数导出的方法,一个函数的序号值为其在EAT中的数组下标加上Base值 (IMAGE_EXPORT_DIRECTORY
中的的Base,缺省值为1)。为了保持兼容,每个导出函数必须有一个对应的序号值,但是可以没有函数名
EXE文件没有导出表,结构体DataDirectory[0]位置为0,在前边章节calc.exe二进制视图中已经标识出。这里以user32.dll为例,用Hex Editor标识出导出表位置以及EAT、符号名表和名字序号对应表的地址,首先定位导出表在PE文件中的位置,如下图所示:
因为PE头中标识的地址都是RVA,而在PE文件中找到相应的地址要将RVA转换成RAW地址,需要利用公式:RAW = RVA - (VirtualAddress - PointerToRawData) 进行转换,这里要结合PE编辑器,找出导出表地址0x99670 (RVA) 在虚拟空间中处于第几个节区,然后对应计算其在PE文件中的位置,计算过程可以用下图进行解释:
带入公式:RAW = RVA - (VirtualAddress - PointerToRawData),即0x99670-(0x88000-0x87400)=0x98A70,同理可以计算出EAT、符号名表、名字序号对应表在PE文件中的地址 (RAW),最后的映射关系图如下所示:
计算结果与用PeEdit打开user32.dll看到的信息验证一致。
2.导入地址表 IAT
当一个PE文件使用到了来自其他PE文件的函数或者变量,这种行为即为符号导入,比如EXE文件使用到来自DLL文件的函数。需要导入的符号以及所在的模块的信息统一保存在 导入表 (Import Table) 结构中。当PE文件被加载时,加载器其中一个任务就是将所有需要导入的函数地址确定并将导入表中的元素调整到正确地址,以实现动态链接。导入表实际上是 IMAGE_IMPORT_DESCRIPTOR
结构体数组,数组中每个元素对应一个导入DLL的信息,执行一个程序需要导入多少DLL库就有多少个IMAGE_IMPORT_DESCRIPTOR
结构体数组,结构体定义如下所示:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA of INT (Import Name Table)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; // RVA of library name string
DWORD FirstThunk; // RVA of IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
该结构体数组不在PE头而在PE体中,但其位置信息 (RVA) 记录在了PE头OptionalHeader结构的IMAGE_DATA_DIRECTORY DataDirectory[1]
部分。结构体中最重要的是最后1个成员 DWORD FirstThunk
,其指向1个数组,该数组即为结构体指向的DLL文件的 导入地址表 (Import Address Table, IAT),表 (数组) 中每个元素对应一个被导入符号,元素的值在不同情况下有不同的含义:
- 动态链接器在对该模块进行链接前,元素值表示导入符号的序号或者符号名
- 动态链接器在完成该模块的链接后,对符号进行了重定位,此时元素值表示该符号的RVA
仍然以user32.dll为例,标识出导入表位置以及IAT部分,计算思路和EAT相同,先定位导入表在PE文件中的RWA地址,0xA0A6C同样在虚拟空间第二个节区.rdata中,计算带入公式:RAW = RVA - (VirtualAddress - PointerToRawData),即0xA0A6C-(0x88000-0x87400)=0x9FE6C,同理可以计算出IAT在PE文件中的地址 (RAW),最后的映射关系图如下所示:
计算结果与用PeEdit打开user32.dll看到的信息验证一致。本文只是为了分析文件结构所以用HEX Editor,实际分析PE文件时用PeEdit等专用编辑器更方便。
二、程序装载
(一) 相关概念
1.虚拟内存
为了使程序运行时地址空间隔离、解决程序运行地址不确定的问题,程序使用的地址实际上是虚拟地址 (Virtual Address, VA),在程序装载到物理内存时,是通过某些映射方法,将虚拟地址转换成实际的物理地址。
一个程序能看到的地址空间取决于CPU的地址总线宽度,比如32位CPU其虚拟地址空间为4GB,就好像整个程序占有整个内存空间一样。每个进程都有自己的虚拟地址空间,且每个进程只能访问自己的虚拟地址空间。 注意程序 (PE文件) 本身是静态的概念,进程是动态的概念,进程是程序运行时的一个过程。
2.VA与RVA
虚拟内存中的地址为虚拟地址 (Virtual Address, VA),而PE头中的地址信息大多以相对虚拟地址 (Relative Virtual Address, RVA) 的形式存在,原因在于当某些PE文件 (主要是DLL,EXE文件往往是进程第一个加载的模块,所以一般加载目标地址可用) 加载到进程虚拟空间的时,其起始地址优先加载到基地址ImageBase,但该位置很可能已经加载了其他PE文件 (DLL),此时就必须通过重定位将其加载到其他位置,使用相对地址RVA便于重定位的实现,RVA是相对于基地址而言的,如果不考虑对齐地址的影响,则 VA = RVA + ImageBase,重定位只需要改变基地址ImageBase就可以实现,无需修改程序中的RVA,程序就能正常运行。
3.扇区、簇(块)、页
需要区分操作系统对磁盘和内存操作的基本单位,区分以下几种关系:
- 扇区与簇 (块):传统磁盘在读写数据时,以扇区为基本单位,而对操作系统而言,对磁盘操作的基本单位是簇 (Windows中为簇,Linux中为块),簇 (块) 是扇区大小的2n倍,磁盘扇区大小一般为512字节。扇区是磁盘物理层的概念,簇 (块) 是操作系统逻辑层的概念。
- 页:页是操作系统对内存操作时的基本单位,目前硬件规定的页的大小有4KB、8KB、2MB、4MB等,常见32位CPU一般使用4KB的页。区分物理页和虚拟页,物理页是物理内存中的页,虚拟页是虚拟内存空间的页,两者大小一般相同。
注意:现在普遍使用的固态硬盘读写数据时不再以扇区为基本单位,而是以页为基本单位,页大小一般为4KB,这要与内存页的概念区分开。
(二) 装载过程
PE文件从磁盘装载到物理内存启动运行,分为三个基本过程:
(1) 创建一个独立的虚拟地址空间
执行PE文件时,操作系统先创建进程,从操作系统角度看,一个进程最关键的特征是其拥有独立的虚拟地址空间,使得其有别于其他的进程。创建虚拟地址空间实际上是分配一个页目录,页目录用于记录虚拟地址空间到物理内存地址的映射关系。
(2) 读取PE头信息,建立虚拟空间与PE文件的映射关系
读取PE头信息,建立PE文件与虚拟空间的映射关系,每个节区都要完成到虚拟内存地址 (VA) 的映射,如果装载地址不是目标地址,则需要重定位,修改ImageBase的值。
(3) 把EIP的值设置为EP,启动运行
EP的值为ImageBase+AddressOfEntryPoint,程序启动运行,刚开始运行时操作系统只是建立了程序到虚拟空间的映射关系,指令和数据并没有装入内存,运行时采用 动态装载 的策略,将虚拟内存空间按页进行分割 (“页”是装载和操作的基本单位),程序运行时只将常用的代码页和数据页装载到内存中,其余不常用的页的留在磁盘,待需要时再进行动态装载。
可以看出,PE文件从磁盘装载到物理内存中间经过一层虚拟内存空间,装载中的映射关系如下图所示:
注意到上图中每个节区下方有一段空间内容为NULL,这是由于存储空间大小要和其基本操作单位的倍数对齐,不足的空间补0处理。图中假设磁盘操作的基本单位是1024个字节 (FileAlignment=0x400),内存操作的基本单位是4096个字节 (SectionAlignment=0x1000),这样就会导致PE文件从磁盘映射到内存中后,其大小发生改变。
考虑到对齐字节数的偏差,从RAW到RVA的映射遵循公式:RAW = RVA - (VirtualAddress - PointerToRawData),即:符号的文件偏移地址等于其相对虚拟地址减去其在虚拟内存空间中所在节区的起始地址与在PE文件中所在节区的起始地址的差值,有些拗口,但从PE头中标识的地址RVA到PE文件偏移地址RAW (用二进制编辑器静态打开PE文件时看到的地址) 映射必须要借助这个公式,很重要。
MMU将多个进程的节区装载到内存时,为了节省内存空间、提高使用效率,会按照属性 (执行及读写权限) 对内存空间进行划分,多个具有同一属性的节区会被分到一起,作为连续的页进行管理。比如.rdata和.bss属性都是不可执行、只读,这样将多个进程的这两种节区统一划分到一整块内存空间管理,避免将单个进程或单一节区划分到内存时,由于页地址对齐造成的空间浪费。