PE文件结构详解<一>

by evil.eagle 转载请注明出处。

http://blog.csdn.net/evileagle/article/details/11693499


<一>、基本概念


PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。那Windows是怎么区分可执行文件和非可执行文件的呢?我们调用LoadLibrary传递了一个文件名,系统是如何判断这个文件是一个合法的动态库呢?这就涉及到PE文件结构了。

PE文件的结构一般来说如下图所示:从起始位置开始依次是DOS头,NT头,节表以及具体的节。


  • DOS头是用来兼容MS-DOS操作系统的,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode.还有一个目的,就是指明NT头在文件中的位置。
  • NT头包含windows PE文件的主要信息,其中包括一个‘PE’字样的签名,PE文件头(IMAGE_FILE_HEADER)和PE可选头(IMAGE_OPTIONAL_HEADER32),头部的详细结构以及其具体意义在PE文件头文章中详细描述。
  • 节表:是PE文件后续节的描述,windows根据节表的描述加载每个节。
  • 节:每个节实际上是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义,未必是上图中的三个。

当一个PE文件被加载到内存中以后,我们称之为“映象”(image),一般来说,PE文件在硬盘上和在内存里是不完全一样的,被加载到内存以后其占用的虚拟地址空间要比在硬盘上占用的空间大一些,这是因为各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些“空洞”。

因为存在这种对齐,所以在PE结构内部,表示某个位置的地址采用了两种方式,针对在硬盘上存储文件中的地址,称为原始存储地址或物理地址表示距离文件头的偏移;另外一种是针对加载到内存以后映象中的地址,称为相对虚拟地址(RVA),表示相对内存映象头的偏移。

然而CPU的某些指令是需要使用绝对地址的,比如取全局变量的地址,传递函数的地址编译以后的汇编指令中肯定需要用到绝对地址而不是相对映象头的偏移,因此PE文件会建议操作系统将其加载到某个内存地址(这个叫基地址),编译器便根据这个地址求出代码中一些全局变量和函数的地址,并将这些地址用到对应的指令中。例如在IDA里看上去是这个样子:

这种表示方式叫做虚拟地址(VA)。

也许有人要问,既然有VA这么简单的表示方式为什么还要有前面的RVA呢?因为虽然PE文件为自己指定加载的基地址,但是windows有茫茫多的DLL,而且每个软件也有自己的DLL,如果指定的地址已经被别的DLL占了怎么办?如果PE文件无法加载到预期的地址,那么系统会帮他重新选择一个合适的基地址将他加载到此处,这时原有的VA就全部失效了,NT头保存了PE文件加载所需的信息,在不知道PE会加载到哪个基地址之前,VA是无效的,所以在PE文件头中大部分是使用RVA来表示地址的,而在代码中是用VA表示全局变量和函数地址的。那又有人要问了,既然加载基址变了以后VA都失效了,那存在于代码中的那些VA怎么办呢?答案是:重定位。系统有自己的办法修正这些值,到后续重定位表的文章中会详细描述。既然有重定位,为什么NT头不能依靠重定位采用VA表示地址呢(十万个为什么)?因为不是所有的PE都有重定位,早期的EXE就是没有重定位的。

我们都知道PE文件可以导出函数让其他的PE文件使用,也可以从其他PE文件导入函数,这些是如何做到的?PE文件通过导出表指明自己导出那些函数,通过导入表指明需要从哪些模块导入哪些函数。导入和导出表的具体结构会在单独的文章中详细解释。


<二>可执行文件头


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

一、DOS头

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

[cpp]  view plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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 plain copy
  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文件头学习完了,又“从天而降”一大波的结构。不用紧张,有了前面的知识,后面的部分就迎刃而解了。下一篇开始将沿着这个数据目录分解其余部分,继续关注哦~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值