首页 > 代码库 > PE文件结构详解(二)可执行文件头
PE文件结构详解(二)可执行文件头
在PE文件结构详解(一)基本概念里,解释了一些PE文件的一些基本概念,从这篇开始,将详细讲解PE文件中的重要结构。
了解一个文件的格式,最应该首先了解的就是这个文件的文件头的含义,因为几乎所有的文件格式,重要的信息都包含在头部,顺着头部的信息,可以引导系统解析整个文件。所以,我们先来认识一下PE文件的头部格式。还记得上篇里的那个图吗?
DOS头和NT头就是PE文件中两个重要的文件头。
一、DOS头
DOS头的作用是兼容MS-DOS操作系统中的可执行文件,对于32位PE文件来说,DOS所起的作用就是显示一行文字,提示用户:我需要在32位windows上才可以运行。我认为这是个善意的玩笑,因为他并不像显示的那样不能运行,其实已经运行了,只是在DOS上没有干用户希望看到的工作而已,好吧,我承认这不是重点。但是,至少我们看一下这个头是如何定义的:
- 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;
我们只需要关注两个域:
e_magic:一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位‘MZ’,可执行文件必须都是‘MZ‘开头。
e_lfanew:为32位可执行文件扩展的域,用来表示DOS头之后的NT头相对文件起始地址的偏移。
二、NT头
顺着DOS头中的e_lfanew,我们很容易可以找到NT头,这个才是32位PE文件中最有用的头,定义如下:
- typedef struct _IMAGE_NT_HEADERS {
- DWORD Signature;
- IMAGE_FILE_HEADER FileHeader;
- IMAGE_OPTIONAL_HEADER32 OptionalHeader;
- } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
下图是一张真实的PE文件头结构以及其各个域的取值:
Signature:类似于DOS头中的e_magic,其高16位是0,低16是0x4550,用字符表示是‘PE‘。
IMAGE_FILE_HEADER是PE文件头,c语言的定义是这样的:
- 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;
每个域的具体含义如下:
Machine:该文件的运行平台,是x86、x64还是I64等等,可以是下面值里的某一个。
- #define IMAGE_FILE_MACHINE_UNKNOWN 0
- #define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
- #define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
- #define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
- #define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
- #define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
- #define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
- #define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
- #define IMAGE_FILE_MACHINE_SH3DSP 0x01a3
- #define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
- #define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
- #define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5
- #define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
- #define IMAGE_FILE_MACHINE_THUMB 0x01c2
- #define IMAGE_FILE_MACHINE_AM33 0x01d3
- #define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
- #define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1
- #define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
- #define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
- #define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
- #define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
- #define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
- #define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
- #define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon
- #define IMAGE_FILE_MACHINE_CEF 0x0CEF
- #define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code
- #define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)
- #define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian
- #define IMAGE_FILE_MACHINE_CEE 0xC0EE
NumberOfSections:该PE文件中有多少个节,也就是节表中的项数。
TimeDateStamp:PE文件的创建时间,一般有连接器填写。
PointerToSymbolTable:COFF文件符号表在文件中的偏移。
NumberOfSymbols:符号表的数量。
SizeOfOptionalHeader:紧随其后的可选头的大小。
Characteristics:可执行文件的属性,可以是下面这些值按位相或。
- #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.
可以看出,PE文件头定义了PE文件的一些基本信息和属性,这些属性会在PE加载器加载时用到,如果加载器发现PE文件头中定义的一些属性不满足当前的运行环境,将会终止加载该PE。
另一个重要的头就是PE可选头,别看他名字叫可选头,其实一点都不能少,不过,它在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。为了简单起见,我们只看32位。
- typedef struct _IMAGE_OPTIONAL_HEADER {
- WORD Magic;
- BYTE MajorLinkerVersion;
- BYTE MinorLinkerVersion;
- DWORD SizeOfCode;
- DWORD SizeOfInitializedData;
- DWORD SizeOfUninitializedData;
- DWORD AddressOfEntryPoint;
- DWORD BaseOfCode;
- DWORD BaseOfData;
- 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;
Magic:表示可选头的类型。
- #define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b // 32位PE可选头
- #define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64位PE可选头
- #define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
MajorLinkerVersion和MinorLinkerVersion:链接器的版本号。
SizeOfCode:代码段的长度,如果有多个代码段,则是代码段长度的总和。
SizeOfInitializedData:初始化的数据长度。
SizeOfUninitializedData:未初始化的数据长度。
AddressOfEntryPoint:程序入口的RVA,对于exe这个地址可以理解为WinMain的RVA。对于DLL,这个地址可以理解为DllMain的RVA,如果是驱动程序,可以理解为DriverEntry的RVA。当然,实际上入口点并非是WinMain,DllMain和DriverEntry,在这些函数之前还有一系列初始化要完成,当然,这些不是本文的重点。
BaseOfCode:代码段起始地址的RVA。
BaseOfData:数据段起始地址的RVA。
ImageBase:映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。
SectionAlignment:节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。
FileAlignment:节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。
MajorOperatingSystemVersion、MinorOperatingSystemVersion:所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。
MajorImageVersion、MinorImageVersion:映象的版本号,这个是开发者自己指定的,由连接器填写。
MajorSubsystemVersion、MinorSubsystemVersion:所需子系统版本号。
Win32VersionValue:保留,必须为0。
SizeOfImage:映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。
SizeOfHeaders:所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。
CheckSum:映象文件的校验和。
Subsystem:运行该PE文件所需的子系统,可以是下面定义中的某一个:
- #define IMAGE_SUBSYSTEM_UNKNOWN 0 // Unknown subsystem.
- #define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesn‘t require a subsystem.
- #define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Image runs in the Windows GUI subsystem.
- #define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // Image runs in the Windows character subsystem.
- #define IMAGE_SUBSYSTEM_OS2_CUI 5 // image runs in the OS/2 character subsystem.
- #define IMAGE_SUBSYSTEM_POSIX_CUI 7 // image runs in the Posix character subsystem.
- #define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // image is a native Win9x driver.
- #define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Image runs in the Windows CE subsystem.
- #define IMAGE_SUBSYSTEM_EFI_APPLICATION 10 //
- #define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11 //
- #define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER 12 //
- #define IMAGE_SUBSYSTEM_EFI_ROM 13
- #define IMAGE_SUBSYSTEM_XBOX 14
- #define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16
DllCharacteristics:DLL的文件属性,只对DLL文件有效,可以是下面定义中某些的组合:
- #define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040 // DLL can move.
- #define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 0x0080 // Code Integrity Image
- #define IMAGE_DLLCHARACTERISTICS_NX_COMPAT 0x0100 // Image is NX compatible
- #define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION 0x0200 // Image understands isolation and doesn‘t want it
- #define IMAGE_DLLCHARACTERISTICS_NO_SEH 0x0400 // Image does not use SEH. No SE handler may reside in this image
- #define IMAGE_DLLCHARACTERISTICS_NO_BIND 0x0800 // Do not bind this image.
- // 0x1000 // Reserved.
- #define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER 0x2000 // Driver uses WDM model
- // 0x4000 // Reserved.
- #define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE 0x8000
SizeOfStackReserve:运行时为每个线程栈保留内存的大小。
SizeOfStackCommit:运行时每个线程栈初始占用内存大小。
SizeOfHeapReserve:运行时为进程堆保留内存大小。
SizeOfHeapCommit:运行时进程堆初始占用内存大小。
LoaderFlags:保留,必须为0。
NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数。
DataDirectory:数据目录,这是一个数组,数组的项定义如下:
- typedef struct _IMAGE_DATA_DIRECTORY {
- DWORD VirtualAddress;
- DWORD Size;
- } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
VirtualAddress:是一个RVA。
Size:是一个大小。
这两个数有什么用呢?一个是地址,一个是大小,可以看出这个数据目录项定义的是一个区域。那他定义的是什么东西的区域呢?前面说了,DataDirectory是个数组,数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构,如下面的代码所示:
- #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
看到这么多的定义,大家估计要头疼了,好不容易要把PE文件头学习完了,又“从天而降”一大波的结构。不用紧张,有了前面的知识,后面的部分就迎刃而解了。下一篇开始将沿着这个数据目录分解其余部分,继续关注哦~
PE文件结构详解(二)可执行文件头