windows PE文件结构及其加载机制

时间:2021-09-09 10:18:25

1. 概述

PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)。它是1993年Windows NT系统引入的新可执行文件格式,到现在已经经过20多年了。虽然使用PE作为可执行文件格式的Windows操作系统已经更换了很多版本,其结构的变化、新特性的增加、文件格式的转换,以及内核的重新定位等,都发生了翻天覆地的变化,硬件架构也从16位发展到现在的64位架构,而这些变化对PE格式的影响却不大。由于PE格式有较好的数据组织方式和数据管理算法,面对如此多的变化却能保持其设计的优雅。

众所周知,Windows NT继承自VAX® VMS®和UNIX。Windows NT的许多创建者在到Microsoft之前都曾为这些平台设计和编写程序。当他们设计Windows NT时,很自然会使用以前写过的和测试过的工具以尽快开始他们的新项目。这些工具产生的和使用的可执行文件和目标模块的格式被称为COFF(Common Object File Format的首字母,通用目标文件格式)。Microsoft编译器生成的OBJ文件就使用COFF格式。

PE文件之所以被称为“可移植”是因为Windows NT在各种平台(x86、MIPS®、Alpha等等)上的所有实现都使用同样的可执行文件格式。当然,像CPU指令的二进制编码之类的内容会有所不同。重要的是操作系统加载器和编程工具不需要针对遇到的每种新的CPU再完全重写。Windows NT及其以后版本,Windows 95及其以后版本和Windows CE一直到现在的win10都使用了这个相同的格式,所以说在很大程度上,这个目的达到了。对于64位的Windows,PE格式只是进行了很少的修改。这种新的格式被叫做PE32+。没有加入新的域,只有一个域被去除。剩下的改变只是一些域从32位扩展到了64位。在这种情况下,你能写出和32位与64位PE文件都能一起工作的代码。对于C++代码,Windows头文件的能力使这些改变很不明显。EXE和DLL文件之间的不同完全是语义上的。它们都使用完全相同的PE格式。仅有的区别是用了一个单个的位来指出这个文件应该被作为EXE还是一个DLL。甚至DLL文件的扩展名也是不固定的,一些具有完全不同的扩展名的文件也是DLL,比如.OCX控件和控制面板程序(.CPL文件)。

PE文件格式主要来自于UNIX操作系统所通用的COFF规范,同时为了保证与旧版本MS-DOS及Windows操作系统的兼容,也保留了MS-DOS中那熟悉的MZ头部。PE格式被公开在WINNT.H头文件中(非常零散)。在WINNT.H文件的中间有一个“Image Format”节。这个节以MS-DOS MZ格式和PE格式开头,后面才是新的PE格式。WINNT.H提供了PE文件使用的原始数据结构的定义。

2. PE文件分析相关的概念

在详细了解PE文件结构之前,我们需要先了解几个基本的概念。理解这些概念有利于我们更好地把握和分析PE中的数据结构。

2.1. 地址

PE中涉及的地址有四类,他们分别是:

  • 虚拟内存地址(VA)
  • 相对虚拟地址(RVA)
  • 文件偏移地址(FOA)
  • 特殊地址

2.1.1. 虚拟地址(Virtual Addresses)

由于Windows NT推出时程序是运行在保护模式下,用户的PE文件被操作系统加载进内存后,PE对应的进程支配了自己独立的4GB虚拟空间。在这个空间中定位的地址称为虚拟内存地址(Virual Address,VA)。到了现在系统运行在X64架构的硬件上(长模式),内存访问可以采用线性地址的方式,同时可访问的内存也突破了4GB的限制,但是独立的进程拥有独立的虚拟地址空间的内存管理机制还在沿用。在PE中,进程本身的VA被解释为:进程的基地址 + 相对虚拟内存地址。

2.1.2. 相对虚拟地址(Relative Virtual Addresses)

PE格式大量地使用所谓的RVA(相对虚拟地址)。一个RVA,亦即一个“Relative Virtual Addresses(相对虚拟地址)”,是在你不知道基地址时,被用来描述一个内存地址的。它是需要加上基地址才能获得线性地址的数值。基地址就是PE映象文件被装入内存的地址,并且可能会随着一次又一次的调用而变化。

在一个可执行文件中,有许多在内存中的地址必须被指定的位置。例如,当引用一个全局变量时就必须指定它的地址。PE文件可以被加载到进程地址空间的任何位置。虽然它们有一个首选加载地址,但你不能依赖于可执行文件真的会被加载到那个位置。因为这个原因,指定一个地址而不依赖于可执行文件的加载位置就很重要。

为了消除PE文件中对内存地址的硬编码,于是产生了RVA。一个RVA是在内存中相对于PE文件被加载的地址的一个偏移。例如,如果一个EXE文件被加载到地址0x400000,它的代码节位于地址0x401000处。那么代码节的RVA就是:

(目标地址) 0x401000 - (加载地址)0x400000 = (RVA)0x1000.

因为PE-文件中的各部分(各节)不需要像已载入的映象文件那样对齐,事情变得复杂起来。例如,文件中的各节常按照512(十六进制的0x200)字节边界对齐,而已载入的映象文件则可能按照4096(十六进制的0x1000)字节边界对齐。参见下面的“SectionAlignment(节对齐)”和“FileAlignment(文件对齐)”。

因此,为了在PE文件中找到一个特定RVA地址的信息,你得按照文件已被载入时的那样来计算偏移量,但要按照文件的偏移量来跳过。

试举一例,假若你已知道执行开始处位于RVA 0x1560地址处,并且想从那里开始的代码处反汇编。为了从文件中找到这个地址,你得先查明在RAM(内存)中各节是按照4096字节对齐的,并且“.code”节是从RVA 0x1000地址处开始,有16384字节长;然后你才知道RVA 0x1560地址位于此节的偏移量0x560处。你还要查明在文件中那节是按512字节边界对齐,且“.code”节在文件中从偏移量0x800处开始,然后你就知道在文件中代码的执行开始处就在0x800+0x560=0xd60字节处。

然后你反汇编它并发现访问一个变量的线性地址位于0x1051d0处。二进制文件的线性地址在装入时将被重定位,并常被假定使用的是优先载入地址。因为你已查明优先载入地址为0x100000,因此我们可开始处理RVA 0x51d0了。因数据节开始于RVA 0x5000处,且有2048字节长,所以它处于数据节中。又因数据节在文件中开始于偏移量0x4800处,所以该变量就可以在文件中的0x4800+0x51d0-0x5000=0x49d0处找到。

2.1.3. 文件偏移地址

文件偏移地址(File Offset Address,FOA)和内存无关,他是指某个位置距离文件头的偏移。

2.1.4. 特殊地址

在PE结构中海油一种特殊地址,其计算方法并不是从文件头算起,也不是从内存的某个位置的基地址算起,而是从特定的位置算起。这个地址在PE结构中很少见,如:在资源表里就出现过这样的地址。

2.2. 节(Section)

无论是结构化程序设计,还是面向对象程序设计,都是倡导程序和数据的独立性,因此,程序中的代码和数据通通常是分开存放的。为了保证程序执行的安全,保证内核的稳定,Windows操作系统通常对不同用途的数据设置不同的权限。比如:代码段中的字节码在程序运行的时候,一般不允许用户进行修改,数据段则允许在程序运行过程中读和写,常量只能读等。Windows操作系统在加载可执行程序时,会为这些具有不同属性的数据分别分配标记有不同属性的页面(当然,相同属性的数据可能会被放到同一个页面),以确保程序运行时的安全。正式基于这个原因,PE中才出现了所谓的节的概念。

节就是存放不同类型数据(比如代码、数据、常量、资源等)的地方,不同的节具有不同的访问权限。节是PE文件中存放代码和数据的基本单元。例如:一个目标文件中的所有代码可以组合成单个节,或者每个函数独占一格节(如果编译器允许)。增加节的数目会增加文件的开销,但是链接器在链接代码的时候就会有更大的选择余地。一个节中的所有原始数据必须被加载到连续的内存空间中。

从操作系统加载角度来看,节是相同属性数据的组合。与数据目录不同的是,尽管有些数据类型不同,分别属于不同的数据目录,但是由于其访问属性相同,便被归类到同一个节中。这个节最终可能会占用一个或多个页面;但无论有多少个,所有相关页面均会被赋予相同的页面属性。这些属性包括只读、只写、可读、可写等。

Windows操作系统在装载PE文件时会对相同和不同类型的节数据执行抛弃、合并、新增、复制等操作。这些不同的操作交叉组合导致了内存中的节和文件中的节会出现很大的不同。例如:”.data”的数据在磁盘中不存在,但是在内存中存在,而”.reloc”重定位表数据却恰恰相反。

2.3. 对齐(Alignment)

对齐这个概念并非只在PE文件中出现,许多文件格式都会有对齐的要求。有的对齐是为了美观,有的对齐则是为了效率。PE中规定了三类的对齐:数据在内存中的对齐、数据在文件中的对齐、资源文件中资源数据的对齐。

2.3.1. 内存对齐(SectionAligment)

PE 文件头里边的SectionAligment 定义了内存中区块的对齐值。由于Windows操作系统对内存属性的设置以页为单位,所以通常情况下,PE 文件被映射到内存中时,节在内存中的对齐单位必须至少是一个页的大小。对于32位的windows操作系统来说,这个值是4KB(1000h),而对64位操作系统来说,这个值就是8KB(2000h)。

2.3.2. 文件对齐(FileAligment)

PE 文件头里边的FileAligment 定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始存放。而区块的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是区块间的间隙。为了提高磁盘利用率,对齐单位通常小于内存,以一个物理扇区的大小作为对齐粒度,512字节,200h。

2.3.3. 资源数据对齐

资源字节码部分一般要求以双字(4字节)方式对齐。

3. PE文件结构

PE 文件格式被组织为一个线性的数据流,它由一个MS-DOS 头部开始,接着是一个是模式的程序残余以及一个PE 文件标志,这之后紧接着PE文件头和可选头部。这些之后是所有的段头部,段头部之后跟随着所有的段实体。文件的结束处是一些其它的区域,其中是一些混杂的信息,包括重分配信息、符号表信息、行号信息以及字串表数据。

如下图所示,PE文件结构被划分为四大部份,包括:DOS部分、PE头、节表、和节数据。

windows PE文件结构及其加载机制

PE文件至少包含两个段(节),即数据段和代码段。Windows的应用程序PE文件格式有11个预定义段,这是对Windows应用程序所通用的。例如: .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,这些段并不是都是必须的,当然,也可以根据需要定义更多的段(比如一些加壳程序)。

在应用程序中最常出现的段有以下6种:

  • 执行代码段,通常 .text (Microsoft)或 CODE(Borland)命名;
  • 数据段,通常以 .data
    、.rdata 或 .bss(Microsoft)、DATA(Borland)命名;
  • 资源段,通常以 .rsrc命名;
  • 导出表,通常以 .edata命名;
  • 导入表,通常以 .idata命名;
  • 调试信息段,通常以 .debug命名;

PE文件一个方便的特点是磁盘上的数据结构和加载到内存中的数据结构是相同的。加载一个可执行文件到内存中 (例如,通过调用LoadLibrary)主要就是映射一个PE文件中的几个确定的区域到地址空间中。因此,一个数据结构比如IMAGE_NT_HEADERS (稍后我们会研究到)在磁盘上和在内存中是一样的。关键的一点是如果你知道怎么在一个PE文件中找到一些东西,当这个PE文件被加载到内存中后你几乎能找到相同的信息。

要注意到PE文件并不仅仅是被映射到内存中作为一个内存映射文件。代替的,Windows加载器分析这个PE文件并决定映射这个文件的哪些部分。当映射到内存中时文件中偏移位置较高的数据映射到较高的内存地址处。一个项目在磁盘文件中的偏移也许不同于它被加载到内存中时的偏移。然而,所有被表现出来的信息都允许你进行从磁盘文件偏移到内存偏移的转换 (参见下图)。

Windows装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系,只有真正执行到某个内存页中的指令或访问某一页中的数据时,这个页才会被从磁盘提交到物理内存。但因为装载可执行文件时,有些数据在装入前会被预先处理(如需要重定位的代码),装入以后,数据之间的相对位置也可能发生改变。因此,一个节的偏移和大小在装入内存前后可能是完全不同的。

windows PE文件结构及其加载机制

windows PE文件结构及其加载机制

4. PE文件结构分析

4.1. PE文件准备

为了对PE文件结构进行更好的分析,我们首先准备一个例子,这次我们通过在WIN10 X64环境下使用VS2015编译生成的一个X64架构的release程序来进行分析。这个例子是一个在WIN10 X64下通过TLS实现反调试的小程序。程序清单如下:

#include <Windows.h>
#include <tchar.h>

#pragma comment(lib,"ntdll.lib")

extern "C" NTSTATUS NTAPI NtQueryInformationProcess(HANDLE hProcess, ULONG InfoClass, PVOID Buffer, ULONG Length, PULONG ReturnLength);

#define NtCurrentProcess() (HANDLE)-1


void NTAPI __stdcall TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
if (IsDebuggerPresent())
{
MessageBoxA(NULL, "TLS_CALLBACK: Debugger Detected!", "TLS Callback", MB_OK);
// ExitProcess(1);
}
else
{
MessageBoxA(NULL, "TLS_CALLBACK: No Debugger Present!...", "TLS Callback", MB_OK);
}
}

void NTAPI __stdcall TLS_CALLBACK_2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
HANDLE DebugPort = NULL;
if (!NtQueryInformationProcess(
NtCurrentProcess(),
7, // ProcessDebugPort
&DebugPort, // If debugger is present, it will be set to -1 | Otherwise, it is set to NULL
sizeof(HANDLE),
NULL))
{
if (DebugPort)
{
MessageBoxA(NULL, "TLS_CALLBACK2: Debugger detected!", "TLS callback", MB_ICONSTOP);
}

else
{
MessageBoxA(NULL, "TLS_CALLBACK2: No debugger detected", "TLS callback", MB_ICONINFORMATION);
}
}
}

//linker spec通知链接器PE文件要创建TLS目录,注意X86和X64的区别
#ifdef _M_IX86
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
#else
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback")
#endif
//创建TLS段
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif
//end linker

//tls import定义多个回调函数
PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK, TLS_CALLBACK_2, 0 };
#pragma data_seg ()
#pragma const_seg ()
//end

int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{

MessageBoxA(NULL, "Hello Wolrd!...:)", "main()", MB_OK);
return 0;

}

程序是一个简单的windows小程序,通过使用TLS回调函数来检测调试器的存在,并弹出窗口提示检测到的状态,主程序只是一个简单的hello world消息窗。程序中涉及到PE文件中几个重要的节。程序通过在WIN10 X64环境下使用VS2015编译生成的一个X64架构的release版本tlstest.exe,程序大小为12.0 KB (12,288 字节)。

windows PE文件结构及其加载机制

4.2. MS-DOS 文件头

在 image 文件的最开始处就是 DOS 文件头,DOS 文件头包含了 DOS stub 小程序。在 WinNT.h 文件里定义了一个结构来描述 DOS 文件头。

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // 00000000 4D 5A,Magic number
WORD e_cblp; // 00000002 90 00,Bytes on last page of file
WORD e_cp; // 00000004 03 00,Pages in file
WORD e_crlc; // 00000006 00 00,Relocations
WORD e_cparhdr; // 00000008 04 00,Size of header in paragraphs
WORD e_minalloc; // 0000000A 00 00,Minimum extra paragraphs needed
WORD e_maxalloc; // 0000000C FF FF,Maximum extra paragraphs needed
WORD e_ss; // 0000000E 00 00,Initial (relative) SS value
WORD e_sp; // 00000010 B8 00,Initial SP value
WORD e_csum; // 00000012 00 00,Checksum
WORD e_ip; // 00000014 00 00,Initial IP value
WORD e_cs; // 00000016 00 00,Initial (relative) CS value
WORD e_lfarlc; // 00000018 40 00,File address of relocation table
WORD e_ovno; // 0000001A 00 00,Overlay number
WORD e_res[4]; // 0000001C 00 00 00 00,Reserved words
// 00000020 00 00 00 00
WORD e_oemid; // 00000024 00 00,OEM identifier (for e_oeminfo)
WORD e_oeminfo; // 00000026 00 00,OEM information; e_oemid specific
WORD e_res2[10]; // 00000028 00 00 00 00,Reserved words
// 0000002C 00 00 00 00
// 00000030 00 00 00 00
// 00000034 00 00 00 00
// 00000038 00 00 00 00
LONG e_lfanew; // 0000003C F8 00 00 00,File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

这个结构名叫 IMAGE_DOS_HEADER 共 64 bytes,以 IMAGE_DOS_HEADER 结构描述的 DOS 文件头结构从 image 的 0x00000000 - 0x0000003F(64 bytes)

结构的 e_magic 域是 DOS 头文件签名,它的值是:0x5A4D 代表字符 MZ,它在 WinNT.h 里定义为:

#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ

e_lfanew 域是一个 offset 值,它指出 NT 文件头的位置。

下面看看tlstest.exe 的 DOS 文件头内容:

windows PE文件结构及其加载机制

绿色部分是 DOS 签名,蓝色部分是 PE header offset(NT 文件头)值,也就是 IMAGE_DOS_HEADER 里的 e_lfanew 值,表明 NT 文件头在 image 文件的 0x000000F8 处。

4.2.1. DOS stub 程序

在 DOS 文件头下面紧跟着一小段 stub 程序,其内容随着链接时使用的链接器不同而不同,在PE中并没有与之对应的相关结构。在本例中从 0x00000040 - 0x0000004D 共 14 bytes是代码,其后是显示数据等内容,这段 dos stub 程序是这样的:

00000040  0E                push cs
00000041 1F pop ds
00000042 BA0E00 mov dx,0xe
00000045 B409 mov ah,0x9
00000047 CD21 int 0x21
00000049 B8014C mov ax,0x4c01
0000004C CD21 int 0x21

当 windows 的 PE 文件放在 DOS 上执行时,将会执行这一段 DOS stub 程序,作用是打印信息:This program cannot be run in DOS mode…. 然后调用 int 21 来终止执行返回到 DOS,看看它是怎样运行的:

00000014  00 00      // ip
00000016 00 00 // cs
00000018 40 00 // e_lfarlc

这个 DOS 执行环境中,CS 和 IP 被初始化为 0(对应值在IMAGE_DOS_HEADER结构中的e_ip、e_cs字段),e_lfarlc 是 DOS 环境的 relocate 表,它的值是 0x40 ,那么信息字符串的位置是:0x0040 + 0x000e = 0x4e,在 image 文件 0x0000004e 正好这字符串的位置。

4.3. NT 文件头

NT 文件头是 PE 文件头的核心部分,由 IMAGE_DOS_HEADER 结构的 e_lfanew 域指出它的位置。

同样 NT 文件头部分由一个结构 IMAGE_NT_HEADER 来描述,在 WinNT.h 里定义如下:

typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#endif

可见这个结构分为 32 和 64 位版本,IMAGE_NT_HEADER 结构分为三大部分:

  • PE 文件签名:Signature
  • IMAGE_FILE_HEADER 文件头:FileHeader
  • IMAGE_OPTINAL_HEADER(32/64) 可选头:OptionalHeader

IMAGE_NT_HEADERS32 和 IMAGE_NT_HEADERS64 的匹别在于 IMAGE_OPTIONAL_HEADER 结构,分别为:IMAGE_OPTIONAL_HEADERS32 和 IMAGE_OPTIONAL_HEADERS64

在 Win32 下 IMAGE_NT_HEADERS32 是 248 bytes,在 Win64 下 IMAGE_NT_HEADERS64 是 264 bytes,因此 tlstest.exe的 NT 文件头从 0x000000F8 - 0x000001FF 共 264 bytes

4.3.1. PE 签名

在 WinNT.h 文件里定义了 PE 文件的签名,它是:

#define IMAGE_NT_SIGNATURE 0x00004550 // PE00

这个签名值是 32 位,值为:0x00004550 即:PE 的 ASCII 码,下面看看 tlstest.exe 中的 PE 签名:

windows PE文件结构及其加载机制

4.3.2. IMAGE_FILE_HEADER 文件头结构

PE 签名接着是 IMAGE_FILE_HEADER 结构,它在 WinNT.h 中的定义为:

typedef struct _IMAGE_FILE_HEADER {   
WORD Machine; //运行平台
WORD NumberOfSections; //块(section)数目
DWORD TimeDateStamp; //时间日期标记
DWORD PointerToSymbolTable; //COFF符号指针,这是程序调试信息
DWORD NumberOfSymbols; //符号数
WORD SizeOfOptionalHeader; //可选部首长度,是IMAGE_OPTIONAL_HEADER的长度
WORD Characteristics; //文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

这个 IMAGE_FILE_HEADER 对 PE 文件大致的描述,这个结构共 20 bytes,它的域描述如下:

size 描述
Machine WORD IMAGE_FILE_MACHINE_xxx 表示目标平台 processor 类型,例:IMAGE_FILE_MACHINE_I386
NumberOfSections WORD 节数量 表示映象中有多少个 section
TimeDataStamp DWORD 从1970年1月1日0:00 以来的总秒数 表示文件创建的时间
PointerToSymbolTable DWORD COFF 符号表偏移量 在 image 文件中很少见,总是为 0
NumberOfSymbols DWORD COFF 符号表的个数 如果存在的话,表示符号表的个数
SizeOfOptionalHeader WORD IMAGE_OPTIONAL_HEADER 结构大小 该域表示 IMAGE_NT_HEADER 中的 IMAGE_OPTIONAL_HEADER 结构的大小
Characteristics WORD IMAGE_FILE_xxx 表示文件属性,例如:IMAGE_FILE_DLL 属性

IMAGE_FILE_HEADER 结构中比较重要的域是:Machine 和 SizeOfOptionalHeader, Machine 可以用来判断目标平台,比如:值为 0x8664 是代表 AMD64(即:x64 平台)它也适合 Intel64 平台。SizeOfOptionalHeader 指出 IMAGE_OPTIONAL_HEADER 结构的大小。

WinNT.h 中定义了一些常量值用来描述 Machine,以 IMAGE_FILE_MACHINE_XXX 开头,下面是一些典型的常量值:

#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)

WinNT.h 中还针对 Characteristics 域定义了一些常量值,以 IMAGE_FILE_XXX 开头,代表目标 image 文件的类型,下面是一些常见的值:

#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.

可以看出Characteristics是以每一位表示文件属性,它的每一个bit代表的含义如下:

         Bit 0 :置1表示文件中没有重定向信息。每个段都有它们自己的重定向信息。
这个标志在可执行文件中没有使用,在可执行文件中是用一个叫做基址重定向目录表来表示重定向信息的,这将在下面介绍。
Bit 1 :置1表示该文件是可执行文件(也就是说不是一个目标文件或库文件)。
Bit 2 :置1表示没有行数信息;在可执行文件中没有使用。
Bit 3 :置1表示没有局部符号信息;在可执行文件中没有使用。
Bit 4
Bit 7
Bit 8 :表示希望机器为32位机。
Bit 9 :表示没有调试信息,在可执行文件中没有使用。
Bit 10:置1表示该程序不能运行于可移动介质中(如软驱或CD-ROM)。在这 种情况下,OS必须把文件拷贝到交换文件中执行。
Bit 11:置1表示程序不能在网上运行。在这种情况下,OS必须把文件拷贝到交换文件中执行。
Bit 12:置1表示文件是一个系统文件例如驱动程序。在可执行文件中没有使用。
Bit 13:置1表示文件是一个动态链接库(DLL)。
Bit 14:表示文件被设计成不能运行于多处理器系统中。
Bit 15:表示文件的字节顺序如果不是机器所期望的,那么在读出之前要进行
交换。在可执行文件中它们是不可信的(操作系统期望按正确的字节顺序执行程序)。

NumberOfSections 表示 image 有多个 section,另一个重要的域是:SizeOfOptionalHeader 它指出接下来 IMAGE_OPTIONAL_HEADER 的大小,它有两个 size:Win32 的 0xDC 和 Win64 的 0xF0.

下面是 tlstest.exe的 IMAGE_FILE_HEADER 结构,从 0x000000F8 - 0x0000010F 共 20 bytes:

windows PE文件结构及其加载机制

将这些值分解为:

000000FC  64 86                   // Machine
000000FE 07 00 // NumberOfSections
00000100 F1 71 1E 57 // TimeDateStamp
00000104 00 00 00 00 // PointerToSymbolTable
00000108 00 00 00 00 // NumberOfSymbols
0000010C F0 00 // SizeOfOptionalHeader
0000010E 22 00 // Characteristics
  • Machine 是 0x0x8664,它的值是 IMAGE_FILE_MACHINE_AMD64,说明这个 image
    文件的目标平台是 AMD64,即:X64 平台
  • NumberOfSections 是 0x08,说明 image 文件内含有 8 个 sections
  • SizeOfOptionalHeader 是 0xF0,说明接下来的 IMAGE_OPTIONAL_HEADERS64 将是
    0xF0(224 bytes)

它的 Characteristics 是 0x0022 = IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LARGE_ADDRESS_AWARE,说明这个 image 是 64 位可执行的映像。可以在 >2G 地址上,并且指明了接下来的 IMAGE_OPTIONAL_HEADER 结构是 0xf0 bytes(240 个字节)。

4.3.3. IMAGE_OPTIONAL_HEADER64 结构

在 IMAGE_FILE_HEADER 结构里已经指明了 image 是 64 位,并且 IMAGE_OPTIONAL_HEADER 的大小是 240 bytes,那么这个结构就是 IMAGE_OPTIONAL_HEADER64 结构。
可以根据 IMAGE_FILE_HEAER 结构的 Machine 来判断 image 是 Win32 还是 Win64 平台的。但是 Microsoft 官方推荐及认可的方法是从 IMAGE_OPTIONAL_HEADER 里的 magic 的值来判断目标平台 。

在 WinNT.h 里 IMAGE_OPTIONAL_HEADER64 的定义如下:

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;

64 位的 IMAGE_OPTIONAL_HEADER64 里没有 BaseOfData 域,其它的与 IMAGE_OPTIONAL_HEADER32 结构的域是一样的,只是一些域扩展为 64 位值,它们包括:

  • ImageBase
  • SizeOfStackReserve
  • SizeOfStackCommit
  • SizeOfHeapRerserve
  • SizeOfHeapCommit

这些域在 64 位结构里被定义为 ULONGLONG 类型。

IMAGE_OPTIONAL_HEADER64 的定义的各个域含义如下:

偏移 大小 字段 描述
0 2 Magic The unsigned integer that identifies the state of the image file. The most common number is 0x10B, which identifies it as a normal executable file. 0x107 identifies it as a ROM image, and 0x20B identifies it as a PE32+ executable. 确定这是什么类型的头。两个最常用的值是0x10b和0x20b.
2 1 MajorLinkerVersion The linker major version number. 创建可执行文件的链接器的主版本号。对于Microsoft的链接器生成的PE文件,这个版本号的Visual Studio的版本号相一致
3 1 MinorLinkerVersion The linker minor version number. 创建可执行文件的链接器的次版本号。
4 4 SizeOfCode The size of the code (text) section, or the sum of all code sections if there are multiple sections. 所有具有IMAGE_SCN_CNT_CODE属性的节的总的大小。
8 4 SizeOfInitializedData The size of the initialized data section, or the sum of all such sections if there are multiple data sections. 所有包含已初始数据的节的总的大小。
12 4 SizeOfUninitializedData The size of the uninitialized data section (BSS), or the sum of all such sections if there are multiple BSS sections. 所有包含未初始化数据的节的总的大小。这个域总是0,因为链接器可以把未初始化数据附加到常规数据节的末尾。
16 4 AddressOfEntryPoint The address of the entry point relative to the image base when the executable file is loaded into memory. For program images, this is the starting address. For device drivers, this is the address of the initialization function. An entry point is optional for DLLs. When no entry point is present, this field must be zero. 文件中将被执行的第一个代码字节的RVA。对于DLL,这个进入点将在进程初始化和关闭时以及线程被创建和销毁时调用。在大多数可执行文件中,这个地址并不直接指向main,WinMain或DllMain函数,而是指向运行时库代码,由运行时库调用前述函数。在DLL中,这个域可以被设为0。链接器选项/NOENTRY可以设置这个域为0。
20 4 BaseOfCode The address that is relative to the image base of the beginning-of-code section when it is loaded into memory. 加载到内存后代码的第一个字节的RVA
24 4 BaseOfData The address that is relative to the image base of the beginning-of-data section when it is loaded into memory. 理论上,它表示加载到内存后数据的第一个字节的RVA。然而,这个域的值对于不同版本的Microsoft链接器是不一致的。在64位的可执行文件中这个域不出现。
28/24 4/8 ImageBase The preferred address of the first byte of image when loaded into memory; must be a multiple of 64 K. The default for DLLs is 0x10000000. The default for Windows CE EXEs is 0x00010000. The default for Windows NT, Windows 2000, Windows XP, Windows 95, Windows 98, and Windows Me is 0x00400000. 文件在内存中的首选加载地址。加载器尽可能地把PE文件加载到这个地址(就是说,如果当前这块内存没有被占用,它是对齐的并且是一个合法的地址,等等)。如果可执行文件被加载到这个地址,加载器就可以跳过进行基址重定位这一步。在Win32下,对于EXE,缺省的ImageBase是0x400000。对于DLL,缺省是0x10000000。在链接时可以通过/BASE 选项来指定ImageBase,或者以后用REBASE工具重新设置。
32/32 4 SectionAlignment The alignment (in bytes) of sections when they are loaded into memory. It must be greater than or equal to FileAlignment. The default is the page size for the architecture. 加载到内存后节的对齐大小。这个值必须大于等于FileAlignment(下一个域)。缺省的对齐值是目标CPU的页大上。对于运行在Windows 9x或Windows Me下的用户模式可执行文件,最小对齐大小是一页(4KB)。这个域可以通过链接器选项/ALIGN来设置。
36/36 4 FileAlignment The alignment factor (in bytes) that is used to align the raw data of sections in the image file. The value should be a power of 2 between 512 and 64 K, inclusive. The default is 512. If the SectionAlignment is less than the architecture’s page size, then FileAlignment must match SectionAlignment. 在PE文件中节的对齐大小。对于x86下的可执行文件,这个值通常是0x200或0x1000。不同版本的Microsoft链接器缺省值不同。这个值必须是2的幂,并且如果SectionAlignment小于CPU的页大小,这个域必须和SectionAlignment相匹配。链接器选项/OPT:WIN98可设置x86可执行文件的文件对齐为0x1000,/OPT:NOWIN98设置文件对齐为0x200。
40/40 2 MajorOperatingSystemVersion The major version number of the required operating system. 所要求的操作系统的主版本号。随着那么多版本Windows的出现,这个域的值就变得很不确切。
42/42 2 MinorOperatingSystemVersion The minor version number of the required operating system. 所要求的操作系统的次版本号。
44/44 2 MajorImageVersion The major version number of the image. 这个文件的主版本号。不被系统使用并可设为0。可以通过链接器选项/VERSION来设置。
46/46 2 MinorImageVersion The minor version number of the image. 这个文件的次版本号。
48/48 2 MajorSubsystemVersion The major version number of the subsystem. 可执行文件所要求的操作子系统的主版本号。它曾经被用来表示需要较新的Windows 95或Windows NT用户界面,而不是老版本的Windows NT界面。今天随着各种不同版本Windows的出现,这个域已不被系统使用,并且通常被设为4。可通过链接器选项/SUBSYSTEM设置这个域的值。
50/50 2 MinorSubsystemVersion The minor version number of the subsystem. 可执行文件所要求的操作子系统的次版本号。
52/52 4 Win32VersionValue Reserved, must be zero. 另一个不被使用的域,通常设为0。
56/56 4 SizeOfImage The size (in bytes) of the image, including all headers, as the image is loaded in memory. It must be a multiple of SectionAlignment. 映像的大小。它表示了加载文件到内存中时系统必须保留的内存的数量。这个域的值必须是SectionAlignmnet的倍数。
60/60 4 SizeOfHeaders The combined size of an MS-DOS stub, PE header, and section headers rounded up to a multiple of FileAlignment. MS-DOS头,PE头和节表的总的大小。PE文件中所有这些项目出现在任何代码或数据节之前。这个域的值被调整为文件对齐大小的整数倍。
64/64 4 CheckSum The image file checksum. The algorithm for computing the checksum is incorporated into IMAGHELP.DLL. The following are checked for validation at load time: all drivers, any DLL loaded at boot time, and any DLL that is loaded into a critical Windows process. 映像的校验和。IMAGEHLP.DLL中的CheckSumMappedFile函数可以计算出这个值。校验和用于内核模式的驱动和一些系统DLL。对于其它的,这个域可以为0。当使用链接器选项/RELEASE时校验和被放入文件中。
68/68 2 Subsystem The subsystem that is required to run this image. For more information, see “Windows Subsystem” later in this specification. 指示可执行文件期望的子系统(用户界面类型)的枚举值。这个域只用于EXE。一些重要的值包括:
IMAGE_SUBSYSTEM_NATIVE // 映像不需要子系统
IMAGE_SUBSYSTEM_WINDOWS_GUI // 使用Windows GUI IMAGE_SUBSYSTEM_WINDOWS_CUI // 作为控制台程序运行。
// 运行时,操作系统创建一个控制台
// 窗口并提供stdin,stdout和stderr
// 文件句柄。
70/70 2 DllCharacteristics For more information, see “DLL Characteristics” later in this specification. 标记DLL的特性。对应于IMAGE_DLLCHARACTERISTICS_xxx定义。当前的值是:
IMAGE_DLLCHARACTERISTICS_NO_BIND // 不要绑定这个映像
IMAGE_DLLCHARACTERISTICS_WDM_DRIVER // WDM模式的驱动程序
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE // 当终端服务加载一个不是
// Terminal- Services-aware 的应用程
// 序时,它也加载一个包含兼容代码
// 的DLL。
72/72 4/8 SizeOfStackReserve The size of the stack to reserve. Only SizeOfStackCommit is committed; the rest is made available one page at a time until the reserve size is reached. 在EXE文件中,为线程保留的堆栈大小。缺省是1MB,但并不是所有的内存一开始都被提交。
76/80 4/8 SizeOfStackCommit The size of the stack to commit. 在EXE文件中,为堆栈初始提交的内存数量。缺省情况下,这个域是4KB。
80/88 4/8 SizeOfHeapReserve The size of the local heap space to reserve. Only SizeOfHeapCommit is committed; the rest is made available one page at a time until the reserve size is reached. 在EXE文件中,为默认进程堆初始保留的内存大小。缺省是1MB。然而在当前版本的Windows中,堆不经过用户干涉就能超出这里指定的大小。
84/96 4/8 SizeOfHeapCommit The size of the local heap space to commit. 在EXE文件中,提交到堆的内存大小。缺省情况下,这里的值是4KB。
88/104 4 LoaderFlags Reserved, must be zero. 不使用。
92/108 4 NumberOfRvaAndSizes The number of data-directory entries in the remainder of the optional header. Each describes a location and size. 在IMAGE_NT_HEADERS结构的末尾是一个IMAGE_DATA_DIRECTORY结构数组。此域包含了这个数组的元素个数。自从最早的Windows NT发布以来这个域的值一直是16。

上面表格中的 offset 值两个,前面的是 IMAGE_OPTIONAL_HEADER32 的 offset 值,后面的是 IMAGE_OPTIONAL_HEADER64,这是因为在 64 位版本中一些域被扩展为 64 位值,而 BaseOfData 域在 64 位版中是不存在的。
Magic 域是一个幻数值,在 WinNT.h 里定义了一些常量值:

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
  • 值 0x10b 说明这个 image 是 32 位的,PE 文件格式是 PE32
  • 值 0x20b 说明这个 image 是 64 位的,PE 文件格式是 PE32+

PE32+ 代表的扩展的 PE 文件格式,扩展为 64 位。在 PE 文件规范中并没有 PE64 这种文件格式,Microsoft 官方的判断 image 文件是 32 位还是 64 位的方法就是通过 Magic 的值来确定。

在这些基本的域里可以获得 linker 的版本,text 节,data 节以及 bss 节的大小,下面看一看tlstest.exe 的 IMAGE_OPTIONAL_HEADER64 结构,从 0x00000110 - 0x000001FF

windows PE文件结构及其加载机制

windows PE文件结构及其加载机制

Magic 是 0x020B 表明这个 image 文件是 64 位的 PE32+格式,这里看出 linker 的版本是 14.00
.text 节的 size 是 0x00001000 bytes,.data 节的 size 是 0x00022000 bytes,还有一个重要的信息,代码的 RVA 入口在 0x00001338,它是基于 ImageBase 的 RVA 值。tlstest.exe 的 ImageBase 是0x0000000140000000,那么 tlstest.exe 映象的入口在:ImageBase + AddressOfEntryPoint = 0x0000000140000000 + 0x00001338 = 0x0000000140001338,这个地址是 __scrt_common_main() 的入口。

上面的 SectionAlinment 域值为 0x1000 是表示映象被加载到 virtual address 以是 0x1000(4K byte)为单位的倍数,也就是加载在 virtual address 的 4K 边界上。例如:tlstest.exe映象的 .text 节被加载到以 ImageBase(virtual address 为 0x00000001_40000000)为基址的第 1 个 4K 边界上(即:0x00000001_40001000 处),.rdata 节加载到第 2 个 4K 边界上(即:0x00000001_40002000 处)。FileAlinment 域的值为 0x200 表示执行映象从 0x200 为边界开始加载到 virtual address 上。例如,t.exe 映象中 code 位于文件映象的 0x400 处(0x200 边界上),因此,t.exe 文件映象 code 从 0x400 处开始加载到 virtual address。

4.3.3.1. IMAGE_DATA_DIRECTORY表格

windows PE文件结构及其加载机制

在 IMAGE_OPTIONAL_HEADER64 未端是一组 IMAGE_DATA_DIRECTORY 结构的数组,上图所示:从 0x00000180 到 0x000001FF。在 WinNT.h 里定义为:

//
// Directory format.
//
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16

这个结构十分重要,它用来描述 windows 执行映象中所使用的各种表格的位置和大小。VirtualAddress 域是一个 RVA(Relative Virtual Address)值,更明白一点就是:它是一个偏移量(基于 PE 文件头),Size 域表示这个表格有多大。IMAGE_NUMBEROF_DIRECTORY_ENTRIES 的值为 16,因此有 16 个 Directory,也就是表示,在执行映象中最多可以使用 16 个表格。

这 16 个 Driectory 指引出 16 不同格式的 table,实际上这 16 个表格是固定的,对于这些表格 Microsoft 都作了统一的规定,在 WinNT.h 里都作了定义:

// 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
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (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

从定义上得出,第 0 项是 export table(导出表),第 1 项是 import table(导入表)等等,在 Microsoft 的 MSDN 网站里有一个知识点介绍:http://msdn.microsoft.com/en-us/library/ms680305(VS.85).aspx
下面,我将这些表格归纳如下:

表项 表格
0 export table
1 import table
2 resource table
3 exception table
4 certificate table
5 base relocation table
6 debug
7 architecute
8 global pointer
9 TLS table
10 load configuration table
11 bound import
12 import address table
13 delay import descriptor
14 CLR runtime header
15 reserved, must bo zero

在 WinNT.h 有对这些 table 的结构的全部定义。

那么,下面我们来看一看tlstest.exe 映象中使用了哪些表?

windows PE文件结构及其加载机制

上面表格显示我们的示例程序 t.exe 仅仅使用了 8 个 driectory 表格:import table、resource table 、exception table 、relocation table 、debug table、TLS table、load configuration table以及 import address table。

4.4. section 表

IMAGE_NT_HEADER 结构后面紧接着就是 section table(节表)结构。现在来看一看tlstest.exe 的 section 表,从 0x00000200 - 0x0000033F,共 320 bytes

这个节表结构在 WinNT.h 中定义为

//
// Section header format.
//
#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;
#define IMAGE_SIZEOF_SECTION_HEADER 40

每个 section 表的大小为 40 bytes,section 表的作用是指出 image 映象 section 所在tlstest.exe 映象的 IMAGE_FILE_HEADER 结构的 NumberOfSections 域里已经指出tlstest.exe 映象中包含有 8 个 sections,因此整个 section 表的大小为:320 bytes。

tlstest.exe的 section 表如下:

windows PE文件结构及其加载机制

这 8 个 section 分别是:

  • .text 节
  • .rdata 节
  • .data 节
  • .pdata 节
  • .tls 节
  • .gfids 节
  • .rsrc 节
  • .reloc 节
    windows PE文件结构及其加载机制

在 IMAGE_SECTION_HEADER 结构的第 1 个域 Name,用来标识 section table 的名字。它的长度固定为 8 bytes(前面定义的宏),这将意味着,不存在超过 8 bytes 的节表名。接下来使用 VirtualSize 来用表示 section talbe 大小。VirtualAddress 表示 section table 的 RVA,它是基于 ImageBase 的 RVA 值,它指出 section 的所在,SizeOfRawData 是在 image 文件里占有的空间,它是 FileAlignment 的倍数,即:0x200 的倍数,也就是说 0x200 的边界。PointerToRawData是 section 在 image 文件的位置,同样也是 FileAligment 即:0x200 边界上。

size 描述
Name 8 bytes Section 表名字
VirtualSize DWORD Section 表的大小
VirtualAddress DWORD Section 表的 RVA,即:section 表的位置
SizeOfRawData DWORD section 表占用映像的大小,这个 size 是以 0x200 为单位的
PointerToRawData DWORD section 表在映像中的物理位置,即是:file 位置,而非 virtual 位置
PointerToRelocation DOWRD
PointerToLinenumber DWORD
NumberOfRelocation WORD
NumberOfLineumbers WORD
Characteristics DWORD section 的属性 flags,可用于 ‘

所有的 Characteristics 都在 WinNT.h 中有定义,下面是一些常用的 flags:

#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_LNK_NRELOC_OVFL 0x01000000 // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // Section can be discarded.
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED 0x10000000 // Section is shareable.
#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.

下面以 .text 节为例,看看tlstest.exe的 .text 是什么。

windows PE文件结构及其加载机制

.text 节的 Name 是 “.text”,VirtualSize 是 0x00000E08 bytes,.text 节在 RVA 为 0x00001000 的位置上,section 的分配单位为 0x1000(4K bytes),第 1 个 section 一般都分配在 0x1000 位置上,SizeOfRawData 为 0x1000,这是映像文件分配单位。PointerToRawData 为 0x400,说明 .text 节在映像文件的 0x400 处。Characteristics 是 0x60000020,说明 .text 是 executable/readable/code 属性。