首页 > 代码库 > PE文件结构详解(二)可执行文件头

PE文件结构详解(二)可执行文件头

在PE文件结构详解(一)基本概念里,解释了一些PE文件的一些基本概念,从这篇开始,将详细讲解PE文件中的重要结构。

了解一个文件的格式,最应该首先了解的就是这个文件的文件头的含义,因为几乎所有的文件格式,重要的信息都包含在头部,顺着头部的信息,可以引导系统解析整个文件。所以,我们先来认识一下PE文件的头部格式。还记得上篇里的那个图吗?


DOS头和NT头就是PE文件中两个重要的文件头。

一、DOS头

DOS头的作用是兼容MS-DOS操作系统中的可执行文件,对于32位PE文件来说,DOS所起的作用就是显示一行文字,提示用户:我需要在32位windows上才可以运行。我认为这是个善意的玩笑,因为他并不像显示的那样不能运行,其实已经运行了,只是在DOS上没有干用户希望看到的工作而已,好吧,我承认这不是重点。但是,至少我们看一下这个头是如何定义的:

[cpp] view plaincopy
  1. typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header  
  2.     WORD   e_magic;                     // Magic number  
  3.     WORD   e_cblp;                      // Bytes on last page of file  
  4.     WORD   e_cp;                        // Pages in file  
  5.     WORD   e_crlc;                      // Relocations  
  6.     WORD   e_cparhdr;                   // Size of header in paragraphs  
  7.     WORD   e_minalloc;                  // Minimum extra paragraphs needed  
  8.     WORD   e_maxalloc;                  // Maximum extra paragraphs needed  
  9.     WORD   e_ss;                        // Initial (relative) SS value  
  10.     WORD   e_sp;                        // Initial SP value  
  11.     WORD   e_csum;                      // Checksum  
  12.     WORD   e_ip;                        // Initial IP value  
  13.     WORD   e_cs;                        // Initial (relative) CS value  
  14.     WORD   e_lfarlc;                    // File address of relocation table  
  15.     WORD   e_ovno;                      // Overlay number  
  16.     WORD   e_res[4];                    // Reserved words  
  17.     WORD   e_oemid;                     // OEM identifier (for e_oeminfo)  
  18.     WORD   e_oeminfo;                   // OEM information; e_oemid specific  
  19.     WORD   e_res2[10];                  // Reserved words  
  20.     LONG   e_lfanew;                    // File address of new exe header  
  21.   } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;  

 

我们只需要关注两个域:

e_magic:一个WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位‘MZ’,可执行文件必须都是‘MZ‘开头。

e_lfanew:为32位可执行文件扩展的域,用来表示DOS头之后的NT头相对文件起始地址的偏移。

二、NT头

顺着DOS头中的e_lfanew,我们很容易可以找到NT头,这个才是32位PE文件中最有用的头,定义如下:

[cpp] view plaincopy
  1. typedef struct _IMAGE_NT_HEADERS {  
  2.     DWORD Signature;  
  3.     IMAGE_FILE_HEADER FileHeader;  
  4.     IMAGE_OPTIONAL_HEADER32 OptionalHeader;  
  5. } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;  

下图是一张真实的PE文件头结构以及其各个域的取值:

Signature:类似于DOS头中的e_magic,其高16位是0,低16是0x4550,用字符表示是‘PE‘。

IMAGE_FILE_HEADER是PE文件头,c语言的定义是这样的:

[cpp] view plaincopy
  1. typedef struct _IMAGE_FILE_HEADER {  
  2.     WORD    Machine;  
  3.     WORD    NumberOfSections;  
  4.     DWORD   TimeDateStamp;  
  5.     DWORD   PointerToSymbolTable;  
  6.     DWORD   NumberOfSymbols;  
  7.     WORD    SizeOfOptionalHeader;  
  8.     WORD    Characteristics;  
  9. } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;  

每个域的具体含义如下:

Machine:该文件的运行平台,是x86、x64还是I64等等,可以是下面值里的某一个。

[cpp] view plaincopy
  1. #define IMAGE_FILE_MACHINE_UNKNOWN           0  
  2. #define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.  
  3. #define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian  
  4. #define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian  
  5. #define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian  
  6. #define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2  
  7. #define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP  
  8. #define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian  
  9. #define IMAGE_FILE_MACHINE_SH3DSP            0x01a3  
  10. #define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian  
  11. #define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian  
  12. #define IMAGE_FILE_MACHINE_SH5               0x01a8  // SH5  
  13. #define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian  
  14. #define IMAGE_FILE_MACHINE_THUMB             0x01c2  
  15. #define IMAGE_FILE_MACHINE_AM33              0x01d3  
  16. #define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian  
  17. #define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1  
  18. #define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64  
  19. #define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS  
  20. #define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64  
  21. #define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS  
  22. #define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS  
  23. #define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64  
  24. #define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon  
  25. #define IMAGE_FILE_MACHINE_CEF               0x0CEF  
  26. #define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI Byte Code  
  27. #define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8)  
  28. #define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian  
  29. #define IMAGE_FILE_MACHINE_CEE               0xC0EE  

NumberOfSections:该PE文件中有多少个节,也就是节表中的项数。

TimeDateStamp:PE文件的创建时间,一般有连接器填写。

PointerToSymbolTable:COFF文件符号表在文件中的偏移。

NumberOfSymbols:符号表的数量。

SizeOfOptionalHeader:紧随其后的可选头的大小。

Characteristics:可执行文件的属性,可以是下面这些值按位相或。

 

[cpp] view plaincopy
  1. #define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.  
  2. #define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved externel references).  
  3. #define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.  
  4. #define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.  
  5. #define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Agressively trim working set  
  6. #define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses  
  7. #define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.  
  8. #define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.  
  9. #define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from file in .DBG file  
  10. #define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, copy and run from the swap file.  
  11. #define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, copy and run from the swap file.  
  12. #define IMAGE_FILE_SYSTEM                    0x1000  // System File.  
  13. #define IMAGE_FILE_DLL                       0x2000  // File is a DLL.  
  14. #define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine  
  15. #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位。

 

[cpp] view plaincopy
  1. typedef struct _IMAGE_OPTIONAL_HEADER {  
  2.     WORD    Magic;  
  3.     BYTE    MajorLinkerVersion;  
  4.     BYTE    MinorLinkerVersion;  
  5.     DWORD   SizeOfCode;  
  6.     DWORD   SizeOfInitializedData;  
  7.     DWORD   SizeOfUninitializedData;  
  8.     DWORD   AddressOfEntryPoint;  
  9.     DWORD   BaseOfCode;  
  10.     DWORD   BaseOfData;  
  11.     DWORD   ImageBase;  
  12.     DWORD   SectionAlignment;  
  13.     DWORD   FileAlignment;  
  14.     WORD    MajorOperatingSystemVersion;  
  15.     WORD    MinorOperatingSystemVersion;  
  16.     WORD    MajorImageVersion;  
  17.     WORD    MinorImageVersion;  
  18.     WORD    MajorSubsystemVersion;  
  19.     WORD    MinorSubsystemVersion;  
  20.     DWORD   Win32VersionValue;  
  21.     DWORD   SizeOfImage;  
  22.     DWORD   SizeOfHeaders;  
  23.     DWORD   CheckSum;  
  24.     WORD    Subsystem;  
  25.     WORD    DllCharacteristics;  
  26.     DWORD   SizeOfStackReserve;  
  27.     DWORD   SizeOfStackCommit;  
  28.     DWORD   SizeOfHeapReserve;  
  29.     DWORD   SizeOfHeapCommit;  
  30.     DWORD   LoaderFlags;  
  31.     DWORD   NumberOfRvaAndSizes;  
  32.     IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];  
  33. } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;  


Magic:表示可选头的类型。

[cpp] view plaincopy
  1. #define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b  // 32位PE可选头  
  2. #define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b  // 64位PE可选头  
  3. #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文件所需的子系统,可以是下面定义中的某一个:

[cpp] view plaincopy
  1. #define IMAGE_SUBSYSTEM_UNKNOWN              0   // Unknown subsystem.  
  2. #define IMAGE_SUBSYSTEM_NATIVE               1   // Image doesn‘t require a subsystem.  
  3. #define IMAGE_SUBSYSTEM_WINDOWS_GUI          2   // Image runs in the Windows GUI subsystem.  
  4. #define IMAGE_SUBSYSTEM_WINDOWS_CUI          3   // Image runs in the Windows character subsystem.  
  5. #define IMAGE_SUBSYSTEM_OS2_CUI              5   // image runs in the OS/2 character subsystem.  
  6. #define IMAGE_SUBSYSTEM_POSIX_CUI            7   // image runs in the Posix character subsystem.  
  7. #define IMAGE_SUBSYSTEM_NATIVE_WINDOWS       8   // image is a native Win9x driver.  
  8. #define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI       9   // Image runs in the Windows CE subsystem.  
  9. #define IMAGE_SUBSYSTEM_EFI_APPLICATION      10  //  
  10. #define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER  11   //  
  11. #define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER   12  //  
  12. #define IMAGE_SUBSYSTEM_EFI_ROM              13  
  13. #define IMAGE_SUBSYSTEM_XBOX                 14  
  14. #define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16  

DllCharacteristics:DLL的文件属性,只对DLL文件有效,可以是下面定义中某些的组合:

[cpp] view plaincopy
  1. #define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040     // DLL can move.  
  2. #define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY    0x0080     // Code Integrity Image  
  3. #define IMAGE_DLLCHARACTERISTICS_NX_COMPAT    0x0100     // Image is NX compatible  
  4. #define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION 0x0200     // Image understands isolation and doesn‘t want it  
  5. #define IMAGE_DLLCHARACTERISTICS_NO_SEH       0x0400     // Image does not use SEH.  No SE handler may reside in this image  
  6. #define IMAGE_DLLCHARACTERISTICS_NO_BIND      0x0800     // Do not bind this image.  
  7. //                                            0x1000     // Reserved.  
  8. #define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER   0x2000     // Driver uses WDM model  
  9. //                                            0x4000     // Reserved.  
  10. #define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE     0x8000  


SizeOfStackReserve:运行时为每个线程栈保留内存的大小。

SizeOfStackCommit:运行时每个线程栈初始占用内存大小。

SizeOfHeapReserve:运行时为进程堆保留内存大小。

SizeOfHeapCommit:运行时进程堆初始占用内存大小。

LoaderFlags:保留,必须为0。

NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数。

DataDirectory:数据目录,这是一个数组,数组的项定义如下:

 

[cpp] view plaincopy
  1. typedef struct _IMAGE_DATA_DIRECTORY {  
  2.     DWORD   VirtualAddress;  
  3.     DWORD   Size;  
  4. } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;  

VirtualAddress:是一个RVA。

Size:是一个大小。

这两个数有什么用呢?一个是地址,一个是大小,可以看出这个数据目录项定义的是一个区域。那他定义的是什么东西的区域呢?前面说了,DataDirectory是个数组,数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构,如下面的代码所示:

[cpp] view plaincopy
  1. #define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory  
  2. #define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory  
  3. #define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory  
  4. #define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory  
  5. #define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory  
  6. #define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table  
  7. #define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory  
  8. //      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)  
  9. #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data  
  10. #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP  
  11. #define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory  
  12. #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory  
  13. #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers  
  14. #define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table  
  15. #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors  
  16. #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor  

看到这么多的定义,大家估计要头疼了,好不容易要把PE文件头学习完了,又“从天而降”一大波的结构。不用紧张,有了前面的知识,后面的部分就迎刃而解了。下一篇开始将沿着这个数据目录分解其余部分,继续关注哦~

PE文件结构详解(二)可执行文件头