A!die Software Studio Welcome to A!Die Software Studio

PE文件详解

by adie
2011-05-29 10:04:08
PE文件格式最近好像炒得沸沸扬扬,由于我正在做一个这样的程序,索性将自己的心得写出来与大家同享。 
PE文件头分两大部分: 
1:DOS ‘MZ’ HEADER 
2:IMAGE_NT_HEADERS 
其中IMAGE_NT_HEADERS中包含 
PE signature 
IMAGE_FILE_HEADER 
IMAGE_OPTIONAL_HEADER(其中包含Data Direcotry) 
文件头后紧跟着为 
Section Table (array of IMAGE_SECTION_HEADERs) 
在DELPHI的windows.pad中已经有定义的有: 
TImageDosHeader; 
TImageNtHeaders; 
TImageSectionHeader; { size of Tim..der is $28 } 
定义变量后按住Ctrl可以察看具体的项目,这里我就不多说了,这方面的东西也很多。 
而其他的如TImageResourceDirectory等,在DELPHI中却没有定义,察看其他资料,我在这里给出他们的结构和简单说明: 
以下是我写的PEDump.exe的类型说明: 

type 
PIMAGE_RESOURCE_DIRECTORY = ^TImageResourceDirectory; 
_IMAGE_RESOURCE_DIRECTORY = packed record 
Characteristics:DWORD; 
TimeDateStamp:DWORD; 
MajorVersion:WORD; 
MinorVersion:WORD; 
NumberOfNamedEntries:WORD; 
NumberOfIdEntries:WORD; 
end; 
TImageResourceDirectory = _IMAGE_RESOURCE_DIRECTORY; 
{ 资源目录的格式说明 } 

PIMAGE_RESOURCE_DIRECTORY_ENTRY = ^TImageResourceDirectoryEntry; 
_IMAGE_RESOURCE_DIRECTORY_ENTRY = packed record 
Name:DWORD; { NameOffset:31,NameIsString:1 } 
// Id:WORD; 
OffsetToData:DWORD; { OffsetToDirectory:31,DataIsDirectory:1 } 
end; 
TImageResourceDirectoryEntry = _IMAGE_RESOURCE_DIRECTORY_ENTRY; 
{ 资源目录进入点的格式说明 } 

PIMAGE_RESOURCE_DIRECTORY_STRING = ^TImageResourceDirectoryString; 
_IMAGE_RESOURCE_DIRECTORY_STRING = packed record 
Length:WORD; 
NameString:CHAR; 
end; 
TImageResourceDirectoryString = _IMAGE_RESOURCE_DIRECTORY_STRING; 
{ 资源目录名的格式说明 } 

PIMAGE_RESOURCE_DIR_STRING_U = ^TImageResourceDirStringU; 
_IMAGE_RESOURCE_DIR_STRING_U = packed record 
Length:WORD; 
NameString:WCHAR; 
end; 
TImageResourceDirStringU = _IMAGE_RESOURCE_DIR_STRING_U; 
{ unicode形式的资源目录名的格式说明 } 

PIMAGE_RESOURCE_DATA_ENTRY = ^TImageResourceDataEntry; 
_IMAGE_RESOURCE_DATA_ENTRY = packed record 
OffsetToData:DWORD; 
Size:DWORD; 
CodePage:DWORD; 
Reserved:DWORD; 
end; 
TImageResourceDataEntry = _IMAGE_RESOURCE_DATA_ENTRY; 
{ 资源目录数据进入点的格式说明 } 

const 
IMAGE_RESOURCE_NAME_IS_STRING = $80000000; 
{ 检测TImageResourceDirectoryEntry.Name的最高为是否设立, 
是则说明剩下的31位指向IMAGE_RESOURCE_DIR_STRING_U的偏移, 
否则说明剩下的31位为一个整数ID。 } 
IMAGE_RESOURCE_DATA_IS_DIRECTORY = $80000000; 
{ 检测TImageResourceDirectoryEntry.OffsetToData的最高为是否设立, 
是则说明剩下的31位指向另一个IMAGE_RESOURCE_DIRECTORY的偏移, 
否则说明剩下的31位指向IMAGE_RESOURCE_DATA_ENTRY的偏移。 } 

{ 以下是文件属性具体值常量说明 } 
{ File Characteristics } 
IMAGE_FILE_RELOCS_STRIPPED = $0001; // Relocation info stripped from file. 
IMAGE_FILE_EXECUTABLE_IMAGE = $0002; // File is executable. 
IMAGE_FILE_LINE_NUMS_STRIPPED = $0004; // Line nunbers stripped from file. 
IMAGE_FILE_LOCAL_SYMS_STRIPPED = $0008; // Local symbols stripped from file. 
IMAGE_FILE_AGGRESIVE_WS_TRIM = $0010; // Agressively trim working set 
IMAGE_FILE_LARGE_ADDRESS_AWARE = $0020; // App can handle >2gb addresses 
IMAGE_FILE_BYTES_REVERSED_LO = $0080; // Bytes of machine word are reversed. 
IMAGE_FILE_32BIT_MACHINE = $0100; // 32 bit word machine. 
IMAGE_FILE_DEBUG_STRIPPED = $0200; 
// Debugging info stripped from file in .DBG file 
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP = $0400; 
// If Image is on removable media, copy and run from the swap file. 
IMAGE_FILE_NET_RUN_FROM_SWAP = $0800; 
// If Image is on Net, copy and run from the swap file. 
IMAGE_FILE_SYSTEM = $1000; // System File. 
IMAGE_FILE_DLL = $2000; // File is a DLL. 
IMAGE_FILE_UP_SYSTEM_ONLY = $4000; // File should only be run on a UP machine 
IMAGE_FILE_BYTES_REVERSED_HI = $8000; // Bytes of machine word are reversed. 

{ 以下是文件头机器属性值的具体说明 } 
{ Machine } 
IMAGE_FILE_MACHINE_UNKNOWN = $0; 
IMAGE_FILE_MACHINE_I386 = $014c; // Intel 386. 
IMAGE_FILE_MACHINE_R3000 = $0162; // MIPS little-endian, $160 big-endian 
IMAGE_FILE_MACHINE_R4000 = $0166; // MIPS little-endian 
IMAGE_FILE_MACHINE_R10000 = $0168; // MIPS little-endian 
IMAGE_FILE_MACHINE_WCEMIPSV2 = $0169; // MIPS little-endian WCE v2 
IMAGE_FILE_MACHINE_ALPHA = $0184; // Alpha_AXP 
IMAGE_FILE_MACHINE_SH3 = $01a2; // SH3 little-endian 
IMAGE_FILE_MACHINE_SH3E = $01a4; // SH3E little-endian 
IMAGE_FILE_MACHINE_SH4 = $01a6; // SH4 little-endian 
IMAGE_FILE_MACHINE_SH5 = $01a8; // SH5 
IMAGE_FILE_MACHINE_ARM = $01c0; // ARM Little-Endian 
IMAGE_FILE_MACHINE_THUMB = $01c2; 
IMAGE_FILE_MACHINE_ARM33 = $01d3; 
IMAGE_FILE_MACHINE_POWERPC = $01F0; // IBM PowerPC Little-Endian 
IMAGE_FILE_MACHINE_IA64 = $0200; // Intel 64 
IMAGE_FILE_MACHINE_MIPS16 = $0266; // MIPS 
IMAGE_FILE_MACHINE_ALPHA64 = $0284; // ALPHA64 
IMAGE_FILE_MACHINE_MIPSFPU = $0366; // MIPS 
IMAGE_FILE_MACHINE_MIPSFPU16 = $0466; // MIPS 
// IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64 
IMAGE_FILE_MACHINE_AMD64 = $0500; // AMD K8 
IMAGE_FILE_MACHINE_TRICORE = $0520; // Infineon 
IMAGE_FILE_MACHINE_CEF = $0CEF; 

{ 以下是SECTION的属性值具体说明 } 
{ Section characteristics } 
// IMAGE_SCN_TYPE_REG = $00000000; // Reserved. 
// IMAGE_SCN_TYPE_DSECT = $00000001; // Reserved. 
// IMAGE_SCN_TYPE_NOLOAD = $00000002; // Reserved. 
// IMAGE_SCN_TYPE_GROUP = $00000004; // Reserved. 
IMAGE_SCN_TYPE_NO_PAD = $00000008; // Reserved. 
// IMAGE_SCN_TYPE_COPY = $00000010; // Reserved. 

IMAGE_SCN_CNT_CODE = $00000020; // Section contains code. 
IMAGE_SCN_CNT_INITIALIZED_DATA = $00000040; // Section contains initialized data. 
IMAGE_SCN_CNT_UNINITIALIZED_DATA = $00000080; // Section contains uninitialized data. 

IMAGE_SCN_LNK_OTHER = $00000100; // Reserved. 
IMAGE_SCN_LNK_INFO = $00000200; 
// Section contains comments or some other type of information. 
// IMAGE_SCN_TYPE_OVER = $00000400; // Reserved. 
IMAGE_SCN_LNK_REMOVE = $00000800; 
// Section contents will not become part of image. 
IMAGE_SCN_LNK_COMDAT = $00001000; // Section contents comdat. 
// = $00002000; // Reserved. 
// IMAGE_SCN_MEM_PROTECTED - Obsolete = $00004000; 
IMAGE_SCN_NO_DEFER_SPEC_EXC = $00004000; 
// Reset speculative exceptions handling bits in the TLB entries for this section. 
IMAGE_SCN_GPREL = $00008000; 
// Section content can be accessed relative to GP 
IMAGE_SCN_MEM_FARDATA = $00008000; 
// IMAGE_SCN_MEM_SYSHEAP - Obsolete = $00010000; 
IMAGE_SCN_MEM_PURGEABLE = $00020000; 
IMAGE_SCN_MEM_16BIT = $00020000; 
IMAGE_SCN_MEM_LOCKED = $00040000; 
IMAGE_SCN_MEM_PRELOAD = $00080000; 

IMAGE_SCN_ALIGN_1BYTES = $00100000; // 
IMAGE_SCN_ALIGN_2BYTES = $00200000; // 
IMAGE_SCN_ALIGN_4BYTES = $00300000; // 
IMAGE_SCN_ALIGN_8BYTES = $00400000; // 
IMAGE_SCN_ALIGN_16BYTES = $00500000; 
// Default alignment if no others are specified. 
IMAGE_SCN_ALIGN_32BYTES = $00600000; // 
IMAGE_SCN_ALIGN_64BYTES = $00700000; // 
IMAGE_SCN_ALIGN_128BYTES = $00800000; // 
IMAGE_SCN_ALIGN_256BYTES = $00900000; // 
IMAGE_SCN_ALIGN_512BYTES = $00A00000; // 
IMAGE_SCN_ALIGN_1024BYTES = $00B00000; // 
IMAGE_SCN_ALIGN_2048BYTES = $00C00000; // 
IMAGE_SCN_ALIGN_4096BYTES = $00D00000; // 
IMAGE_SCN_ALIGN_8192BYTES = $00E00000; // 
// Unused = $00F00000; 
IMAGE_SCN_ALIGN_MASK = $00F00000; 

IMAGE_SCN_LNK_NRELOC_OVFL = $01000000; // Section contains extended relocations. 
IMAGE_SCN_MEM_DISCARDABLE = $02000000; // Section can be discarded. 
IMAGE_SCN_MEM_NOT_CACHED = $04000000; // Section is not cachable. 
IMAGE_SCN_MEM_NOT_PAGED = $08000000; // Section is not pageable. 
IMAGE_SCN_MEM_SHARED = $10000000; // Section is shareable. 
IMAGE_SCN_MEM_EXECUTE = $20000000; // Section is executable. 
IMAGE_SCN_MEM_READ = $40000000; // Section is readable. 
IMAGE_SCN_MEM_WRITE = $80000000; // Section is writeable. 

我写了检测是否包含此属性的函数 
function BeTrue(fg:Cardinal,Value):Boolean; 
begin 
Result:=fg and not Value=0; 
end; 
如果fg的属性值在Value中,则为True,否则为False; 
例如 BeTrue(IMAGE_FILE_RELOCS_STRIPPED,PENTHead.FileHeader.Characteristics); 

至于资源目录的读取,至少需要两重循环来定位,具体实现就要靠你的算法功力了:)


[文档]

NT头---可选头---IMAGE_DATA_DIRECTORY---IMAGE_DIRECTORY_ENTRY_RESOURCE--->
IMAGE_SECTION_HEADER[](节头/表)
…… 

节n---->IMAGE_RESOURCE_DIRECTORY_ENTRY[]---IMAGE_RESOURCE_DIRECTORY[]



-----------------0:DOS头

-----------------1:NT头
typedef struct _IMAGE_NT_HEADERS { 
DWORD Signature;//PE文件头标志 :"PE\0\0"。在开始DOS header的偏移3CH处所指向的地址开始 
IMAGE_FILE_HEADER FileHeader; //PE文件物理分布的信息 
IMAGE_OPTIONAL_HEADER32 OptionalHeader;//PE文件逻辑分布的信息 
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; 

-----------------1.1:文件头
typedef struct _IMAGE_FILE_HEADER { 
WORD Machine; //该文件运行所需要的CPU,对于Intel平台是14Ch 
WORD NumberOfSections; //文件的节数目 
DWORD TimeDateStamp; //文件创建日期和时间 
DWORD PointerToSymbolTable; //用于调试 
DWORD NumberOfSymbols; //符号表中符号个数 
WORD SizeOfOptionalHeader; //OptionalHeader 结构大小 
WORD Characteristics; //文件信息标记,区分文件是exe还是dll 
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; 

-----------------1.2:可选头
typedef struct _IMAGE_OPTIONAL_HEADER { 
WORD Magic; //标志字(总是010bh) 
BYTE MajorLinkerVersion; //连接器版本号 
BYTE MinorLinkerVersion; // 
DWORD SizeOfCode; //代码段大小 
DWORD SizeOfInitializedData; //已初始化数据块大小 
DWORD SizeOfUninitializedData;//未初始化数据块大小 
DWORD AddressOfEntryPoint; //PE装载器准备运行的PE文件的第一个指令的RVA,若要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。(许多文章都有介绍RVA,请去了解) 
DWORD BaseOfCode; //代码段起始RVA 
DWORD BaseOfData; //数据段起始RVA 
DWORD ImageBase; //PE文件的装载地址 
DWORD SectionAlignment; //块对齐 
DWORD FileAlignment; //文件块对齐 
WORD MajorOperatingSystemVersion;//所需操作系统版本号 
WORD MinorOperatingSystemVersion;// 
WORD MajorImageVersion; //用户自定义版本号 
WORD MinorImageVersion; // 
WORD MajorSubsystemVersion; //win32子系统版本。若PE文件是专门为Win32设计的 
WORD MinorSubsystemVersion; //该子系统版本必定是4.0否则对话框不会有3维立体感 
DWORD Win32VersionValue; //保留 
DWORD SizeOfImage; //内存中整个PE映像体的尺寸 
DWORD SizeOfHeaders; //所有头+节表的大小 
DWORD CheckSum; //校验和 
WORD Subsystem; //NT用来识别PE文件属于哪个子系统 
WORD DllCharacteristics; // 
DWORD SizeOfStackReserve; // 
DWORD SizeOfStackCommit; // 
DWORD SizeOfHeapReserve; // 
DWORD SizeOfHeapCommit; // 
DWORD LoaderFlags; // 
DWORD NumberOfRvaAndSizes; // 
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//=16
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

-----------------1.2.1:数据目录?
typedef struct _IMAGE_DATA_DIRECTORY { 
DWORD VirtualAddress; //表的RVA地址 
DWORD Size; //大小 
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; 

-----------------1.2.2数据入口
// 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

-----------------1.2.2.0导出函数表?
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

-----------------1.2.2.1引入函数表
-----------------1.2.2.2资源表
-----------------1.2.2.3异常表?
-----------------1.2.2.4安全表?
-----------------1.2.2.5重定向表
-----------------1.2.2.6调试信息表
……
-----------------2:节表(段表)
typedef struct _IMAGE_SECTION_HEADER { 
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节表名称,如“.text” 
union { 
DWORD PhysicalAddress; //物理地址 
DWORD VirtualSize; //真实长度 
} Misc; 
DWORD VirtualAddress; //RVA 
DWORD SizeOfRawData; //物理长度 
DWORD PointerToRawData; //节基于文件的偏移量 
DWORD PointerToRelocations; //重定位的偏移 
DWORD PointerToLinenumbers; //行号表的偏移 
WORD NumberOfRelocations; //重定位项数目 
WORD NumberOfLinenumbers; //行号表的数目 
DWORD Characteristics; //节属性 
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; 

-----------------3:节……

-----------------3.1资源目录(_IMAGE_RESOURCE_DIRECTORY) 
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

----------------3.2资源目录入口(_IMAGE_RESOURCE_DIRECTORY_ENTRY) 
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

-----------------3.211资源目录名
typedef struct _IMAGE_RESOURCE_DIRECTORY_STRING {
WORD Length;
CHAR NameString[ 1 ];
} IMAGE_RESOURCE_DIRECTORY_STRING, *PIMAGE_RESOURCE_DIRECTORY_STRING;

-----------------3.212资源目录Unicode名
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length;
WCHAR NameString[ 1 ];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;

-----------------3.22资源数据入口 
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData;//偏移地址。并非在文件中的偏移!
DWORD Size; //大小
DWORD CodePage;
DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

-----------------9:其他
如果是在资源根目录,id为:
1: cursor
2: bitmap
3: icon
4: menu
5: dialog
6: string table
7: font directory
8: font
9: accelerators
10: unformatted resource data
11: message table
12: group cursor
14: group icon
16: version information 


题目:深度探索Win32可执行文件格式
来自:不详
作者:Matt Pietrek
翻译:姜庆东


摘要

对可执行文件的深入认识将带你深入到系统深处。如果你知道你的exe/dll里是些什么东东,你就是一个更有知识的程序员。作为系列文章的第一章,将关注这几年来PE格式的变化,同时也简单介绍一下PE格式。经过这次更新,作者加入了PE格式是如何与.NET协作的及PE文件表格(PE FILE SECTIONS),RVA,The DataDirectory,函数的输入等内容。

  ====================

很久以前,我给Microsoft Systems Journal(现在的MSDN)写了一篇名为“Peering Inside the PE: A Tour of the Win32 Portable Executable File Format”的文章。后来比我期望的还流行,到现在我还听说有人在用它(它还在MSDN里)。不幸的是,那篇文章的问题依旧存在,WIN32的世界静悄悄地变了好多,那篇文章已显得过期了。从这个月开始我将用这两篇文章来弥补。

你可能会问为什么我应当了解PE格式,答案依旧:操作系统的可执行文件格式和数据结构暴露出系统的底层细节。通过了解这些,你的程序将编的更出色。

当然,你可以阅读微软的文档来了解我将要告诉你的。但是,像很多文档一样,‘宁可晦涩,但为瓦全’。

我把焦点放在提供一些不适合放在正式文档里的内容。另外,这篇文章里的一些知识不见得能在官方文档里找到。

1. 裂缝的撕开

让我给你一些从1994年我写那篇文章来PE格式变化的例子。WIN16已经成为历史,也就没有必要作什么比较和说明了。另外一个可憎的东西就是用在WINDOWS 3.1 中的WIN32S,在它上面运行程序是那么的不稳定。

那时候,WINDOWS 95(也叫Chicago)还没有发行。NT还是3.5版。微软的连接器还没开始大规模的优化,尽管如此,there were MIPS and DEC Alpha implementations of Windows NT that added to the story.

那么究竟,这么些年来,有些什么新的东西出来呢?64位的WINDOWS有了它自己的PE变种,WINDOWS CE 支持各种CPU了,各种优化如DLL的延迟载入,节表的合并,动态捆绑等也已出台。

有很多类似的东西发生了。

让我们最好忘了.NET。它是如何与系统切入的呢?对于操作系统,.NET的可执行文件格式是与旧的PE格式兼容的。虽然这么说,在运行时期,.NET还是按元数据和中间语言来组织数据的,这毕竟是它的核心。这篇文章当中,我将打开.NET元数据这扇门,但不做深入讨论。

如果WIN32的这些变化都不足以让我重写这篇文章,就是原来的那些错误也让我汗颜。比如我对TLS的描述只是一带而过,我对时间戳的描述只有你生活在美国西部才行等等。还有,一些东西已是今是作非了, 我曾说过.RDATA几乎没排上用场,今天也是,我还说过.IDATA节是可读可写的,但是一些搞API拦截的人发现好像是错的。

在更新这篇文章的过程当中,我也检查了PEDUMP这个用来倾印PE文件的程序.这个程序能够在0X86和IA-64平台下编译和运行。

2. PE格式概览

微软的可执行文件格式,也就是大家熟悉的PE 格式,是官方文档的一部分。但是,它是从VAX/VMS上的COFF派生出来的,就WINDOWS NT小组的大部分是从DEC转过来的看来,这是可以理解的。很自然,这些人在NT的开发上会用他们以往的代码。

采用术语“PORTABLE EXECUTABLE”是因为微软希望有一个通用在所有WINDOWS平台上和所有CPU上的文件格式。从大的方面讲,这个目标已经实现。它适用于NT及其后代,95及其后代,和CE.

微软产生的OBJ文件是用COFF格式的。当你看到它的很多域都是用八进制的编码的,你会发现她是多么古老了。COFF OBJ文件用到了很多和PE一样的数据结构和枚举,我马上会提到一些。

64位的WINDOWS只对PE格式作了一点点改变。这个新的格式叫做PE32+。没有增加一个字段,且只删了一个字段。其他的改变就是把以前的32位字段扩展成64位。对于C++代码,通过宏定义WINDOWS的头文件已经屏蔽了这些差别。

EXE与DLL的差别完全是语义上的。它们用的都是同样一种文件格式-PE。唯一的区别就是其中有一个字段标识出是EXE还是DLL .还有很多DLL的扩展比如OCX,CPL等都是DLL.它们有一样的实体。

你首先要知道的关于PE的知识就是磁盘中的数据结构布局和内存中的数据结构布局是一样的。 载入可执行文件(比如LOADLIBARY)的首要任务就是把磁盘中的文件映射到进程的地址空间.因此像IMAGE_NT_HEADER(下面解释)在磁盘和内存中是一样的。关键的是你要懂得你怎样在磁盘中获得PE文件某些信息的, 当它载入内存时你可以一样获得,基本上是没什么不同的(即内存映射文件)。但是知道与映射普通的内存映射文件不同是很重要的。WINDOWS载入器察看PE文件才决定映射到哪里,然后从文件的开始处 往更高的地址映射,但是有的东西在文件中的偏移和在内存中的偏移会不一样。尽管如此,你也有了足够的信息把文件偏移转化成内存偏移。

当Windows载入器把PE载入内存,在内存中它称作模块(MODULE), 文件从HMODULE这个地址开始映射。记住这点:给你个HMODULE,从那你可以知道一个数据结构(IMAGE_DOS_HEADER),然后你还可以知道所有得数据结构。这个强大的功能对于API拦截特别有意义。(准确地说:对于WINDOWS CE,这是不成立的,不过这是后话)。

内存中的模块代表着进程从这个可执行文件中所需要的所有代码,数据,资源。其他部分可以被读入,但是可能不映射(如,重定位节)。还有一些部分根本就不映射,比如当调试信息放到文件的尾部的时候。有一个字段告诉系统把文件映射到内存需要多少内存。不需要的数据放在文件的尾部,而在过去,所有部分都映射。 在WINNT.H描述了PE格式。在这个文件中,几乎有所有的关于PE的数据结构,枚举,#DEFINE。当然,其它地方也有相关文档,但是还是WINNT.H说了算。

有很多检测PE文件的工具,有VISUAL STUDIO的DUMPBIN,SDK中的DEPENDS,我比较喜欢DEPENDS,因为它以一种简洁的方式检测出文件的引入引出。一个免费的PE察看器,PEBrowse,来自smidgenosoft。我的pedump也是很有用的,它和dumpbin有一样的功能。

从api的立场看,imagehlp.dll提供了读写pe文件的机制。

在开始讨论pe文件前,回顾一下pe文件的一些基本概念是有意义的。在下面几节,我将讨论:pe 节,相对虚拟地址(rva),数据目录,函数的引入。

3. PE节

PE节以某钟顺序表示代码或数据。代码就是代码了,但是却有多种类型的数据,可读写的程序数据(如全局变量),其它的节包含API的引入引出表,资源,重定位。 每个节有自己的属性,包括是否是代码节,是否只读还是可读可写,节的数据是否全局共享。

通常,节中的数据逻辑上是关联的。PE文件一般至少要有两个节,一个是代码,另一个为数据。一般还有一个其它类型的数据的节。后面我将描述各种类型的节。

每个节都有一个独特的名字。这个名字是用来传达这个节的用途的。比如,.RDATA表示一个只读节, 节的名字对于操作系统毫无意义,只是为了人们便于理解。把一个节命名为FOOBAR和.TEXT是一样有用的。微软给他们的节命名了个有特色的名字,但是这不是必需的。Borland的连接器用的是code和data。

一般编译器将产生一系列标准的节,但这没有什么不可思议的。你可以建立和命名自己的节,连接器会自动在程序文件中包含它们。在visual c++中,你能用#pragma指令让编译器插入数据到一个节中。像下面这样:

 #pragma data_seg("MY_DATA")
 ...有必要初始化
 #pragma data_seg()

你也可以对.data做同样的事。大部分的程序都只用编译器产生的节,但是有时候你却需要这样。比如建立一个全局共享节。

节并不是全部由连接器确定的,他们可以在编译阶段由编译器放入obj文件。连接器的工作就是合并所有obj和库中需要的节成一个最终的合适的节。比如,你的工程中的所有obj可能都有一个包含代码的.text节,连接器把这些节合并成一个.text节。同样对于.data等。这些主题超出了这篇文章的范围了。还有更多的规则关于连接器的。在obj文件中是专门给linker用的,并不放入到pe文件中,这种节是用来给连接器传递信息的。

节有两个关于对齐的字段,一个对应磁盘文件,另一个对应内存中的文件。Pe文件头指出了这两个值,他们可以不一样。每个节的偏移从对齐值的倍数开始。比如,典型的对齐值是0x200,那么每个节的的偏移必须是0x200的倍数。一旦载入内存,节的起始地址总是以页对齐。X86cpu的页大小为4k,al-64为8k。

下面是pedump倾印出的Windows XP KERNEL32.DLL.的.text .data节的信息:

 Section Table
 01 .text VirtSize: 00074658 VirtAddr: 00001000
 raw data offs: 00000400 raw data size: 00074800
 ...
 02 .data VirtSize: 000028CA VirtAddr: 00076000
 raw data offs: 00074C00 raw data size: 00002400

建立一个节在文件中的偏移和它相对于载入地址的偏移相同的pe文件是可能的。在98/me中,这会加速大文件的载入。Visual studio 6.0 的默认选项 /opt:win98j就是这样产生文件的。在Visual studio.net中是否用/opt:nowin98取决于文件是否够小。

一个有趣的连接器特征是合并节的能力。如果两个节有相似兼容的属性,连接的时候就可以合并为一个节。这取决于是否用/merger开关。像下面就把.rdata和.text合并为一个节.text

 /MERGE:.rdata=.text

合并节的优点就是对于磁盘和内存节省空间。每个节至少占用一页内存,如果你可以把可执行文件的节数从4减到3,很可能就可以少用一页内存。当然,这取决于两个节的空余空间加起来是否达到一页。

当你合并节事情会变得有意思,因为这没有什么硬性和容易的规则。比如你可以合并.rdata到.text,

但是你不可以把.rsrc.reloc.pdata合并到别的节。先前Visual Studio .NET允许把.idata合并,后来又不允许了。但是当发行的时候,连接器还是可以把.idata合并到别的节。

因为引入节的一部分在载入器载入时将被写入,你可能惊奇它是如何被放入一个只读节的。是这样的,在载入的时候系统会临时改变那些包含引入节的页为可读可写,初始化完成后,又恢复原来属性。

4. 相对虚拟地址

在可执行文件中,有很多地方需要指定内存地址,比如,引用全局变量时,需要指定它的地址。Pe文件尽管有一个首选的载入地址,但是他们可以载入到进程空间的任何地方,所以你不能依赖于pe的载入点。由于这点,必须有一个方法来指定地址而不依赖于pe载入点的地址。为了避免把内存地址硬编码进pe文件,提出了RVA。RVA是一个简单的相对于PE载入点的内存偏移。比如,PE载入点为0X400000,那么代码节中的地址0X401000的RVA为(target address) 0x401000 - (load address)0x400000 = (RVA)0x1000。把RVA加上PE的载入点的实际地址就可以把RVA转化实际地址。顺便说一下,按PE的说法,内存中的实际地址称为VA(VIRTUAL ADDRESS).不要忘了早点我说的PE的载入点就是HMODULE。

想对探索内存中的任意DLL吗?用GetModuleHanle(LPCTSTR)取得载入点,用你的PE知识来干活吧

5. 数据目录

PE文件中有很多数据结构需要快速定位。显然的例子有引入函数,引出函数,资源,重定位。这些东西是以一致的方式来定位的,这就是数据目录。

数据目录是一个结构数组,包含16个结构。每个元素有一个定义好的标识,如下:

 // Export Directory

 #define IMAGE_DIRECTORY_ENTRY_EXPORT 0

 // Import Directory

 #define IMAGE_DIRECTORY_ENTRY_IMPORT 1

 // Resource Directory

 #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2

 // Exception Directory

 #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3

 // Security Directory

 #define IMAGE_DIRECTORY_ENTRY_SECURITY 4

 // Base Relocation Table

 #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5

 // Debug Directory

 #define IMAGE_DIRECTORY_ENTRY_DEBUG 6

 // Description String

 #define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7

 // Machine Value (MIPS GP)

 #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8

 // TLS Directory

 #define IMAGE_DIRECTORY_ENTRY_TLS 9

 // Load Configuration Directory

 #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10

 typedef struct _IMAGE_DATA_DIRECTORY {

   ULONG VirtualAddress;

   ULONG Size;

 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

6. 引入函数

当你使用别的DLL中的代码或数据,称为引入。当PE载入时,载入器的工作之一就是定位所有引入函数及数据,使那些地址对于载入的PE可见。具体细节在后面讨论,在这里只是大概讲一下。

当你用到了一个DLL中的代码或数据,你就暗中连接到这个DLL。但是你不必为“把这些地址变得对你的代码有效”做任何事情,载入器为你做这些。方法之一就是显式连接,这样你就要确定DLL已被载入,及函数的地址。调用LOADLIBARY和GETPROCADDRESS就可以了。

当你暗式连接DLL,LOADLIBARY和GETPROCADDRESS同样还是执行了的。只不过载入器为你做了这些。载入器还保证PE文件所需得任何附加的DLL都已被载入。比如,当你连接了KERNEL32.DLL,而它又引入了NTDLL.DLL的函数,又比如当你连接了GDI32.DLL,而它又依赖于USER32, ADVAPI32,NTDLL, 和 KERNEL32 DLLs的函数,载入器会保证这些DLL被载入及函数的决议。

暗式连接时,决议过程在PE文件在载入时就发生了。如果这时有什么问题(比如这个DLL文件找不到),进程终止。

VISUAL C++ 6.0 加入了DLL的延迟载入的特征。它是暗式连接和显式连接的混合。当你延迟载入DLL,连接器做出一些和引入标准规则DLL类似的东西,但是操作系统却不管这些东西,而是在第一次调用这个DLL中的函数的时候载入(如果还没载入),然后调用GetProcAddress取得函数的地址。

对于pe文件要引入的dll都有一个对应的结构数组,每个结构指出这个dll的名字及指向一个函数指针数组的指针,这个函数指针数组就是所谓的 IAT(IMORT ADDRESS TABLE)。每个输入函数,在IAT中都有一个保留槽,载入器将在那里写入真正的函数地址。最后特别重要一点的是:模块一旦载入,IAT中包含所要调用的引入函数的地址。

把所有输入函数放在IAT一个地方是很有意义的,这样无论代码中多少次调用一个引入函数,都是通过IAT中的一个函数指针。

让我们看看是怎样调用一个引入函数的。有两种情况需要考虑:有效率的和效率差的。最好的情况像下面这样:

 CALL DWORD PTR [0x00405030]

直接调用[0x405030]中的函数,0x405030位于IAT部分。效率差的方式如下:

 CALL 0x0040100C

 ...

 0x0040100C:

 JMP DWORD PTR [0x00405030]

这种情况,CALL把控制权转到一个子程序,子程序中的JMP指令跳转到位于IAT中的0x00405030,简单说,它多用了5字节和JMP多花的时间。

你可能惊讶引入函数就采用了这种方式,有个很好的解释,编译器无法区别引入函数的调用和普通函数调用,对于每个函数调用,编译器只产生如下指令:

 CALL XXXXXXXX

XXXXXXXX是一个由连接器填入的RVA。注意,这条指令不是通过函数指针来的,而是代码中的实际地址。

为了因果的平衡,连接器必须产生一块代码来代替取代XXXXXXXX,简单的方法就是象上面所示调用一个JMP STUB.

那么JMP STUB 从那里来呢?令人惊异的是,它取自输入函数的引入库。如果你去察看一个引入库,在输入函数名字的关联处,你会发现与上面JMP STUB相似的指令。

接着,另一个问题就是如何优化这种形式,答案是你给编译器的修饰符,__declspec(import) 修饰符告诉编译器,这个函数来自另一个dll,这样编译器就会产生第一种指令。另外,编译器将给函数加上__imp_前缀然后送给连接器决议,这样可以直接把__imp_xxx送到iat,就不需要jmp stub了。

对于我们这有什么意义呢,如果你在写一个引出函数的东西并提供一个头文件的话,别忘了在函数前加上修饰符__declspec(import)

 __declspec(dllimport) void Foo(void);

在winnt.h等系统头文件中就是这样做的。

7. PE 文件结构

现在让我们开始研究PE文件格式,我将从文件的头部开始,描述每个PE文件中都有的各种数据结构,然后,我将讨论更多的专门的数据结构比如引入表和资源,除非特殊说明,这些结构都定义在WINNT.H中。

一般地,这些结构都有32和64位之分,如IMAGE_NT_HEADERS32 ,IMAGE_NT_HEADER64等,他们基本上是一样的,除了64位的扩展了某些字段。通过#DEFINE WINNT.H都屏蔽了这些区别,选择那个数据结构取决于你要如何编译了(如,是否定义_WIN64)

The MS-DOS Header

每个PE文件是以一个DOS程序开始的,这让人想起WINDOWS在没有如此可观的使用者的早期年代。当可执行文件在非WINDOWS平台上运行的时候至少可以显示出一条信息表示它需要WINDOWS。

PE文件的开头是一个IMAGE_DOS_HEADER结构,结构中只有两个重要的字段e_magic and e_lfanew。e_lfanew指出pe file header的偏移,e_magic需要设定位0x5a4d,被#define 成IMAGE_DOS_SIGNATURE 它的ascii为’MZ’,Mark Zbikowski的首字母,DOS 的原始构建者之一。

The IMAGE_NT_HEADERS Header

这个结构是PE文件的主要定位信息的所在。它的偏移由IMAGE_DOS_HEADER的e_lfanew给出

确实有64和32位之分,但我在讨论中将不作考虑,他们几乎没有区别。

 typedef struct _IMAGE_NT_HEADERS {

  DWORD Signature;

  IMAGE_FILE_HEADER FileHeader;

  IMAGE_OPTIONAL_HEADER32 OptionalHeader;

 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

在一个有效的pe文件里,Signture被设为0x00004500,ascii 为’PE00’,#define IMAGE_NT_SIGNTURE 0X00004500;第二个字段是一个IMAGE_FILE_HEADER结构,它包含文件的基本信息,特别重要的是它指出了IMAGE_OPTIONAL_HEADER的大小(重要吗?);在PE文件中,IMAGE_OPTIONAL_HEADER是非常重要的,但是仍称作IMAGE_OPTIONAL_HEADER。

IMAGE_OPTIONAL_HEADER结构的末尾就是用来定位pe文件中重要信息的地址簿-数据目录,它的定义如下:

 typedef struct _IMAGE_DATA_DIRECTORY {

  DWORD VirtualAddress; // RVA of the data

  DWORD Size; // Size of the data

 };

The Section Table

紧接着IMAGE_NT_HEADERS后的就是节表,节表就是IMAGE_SECTION_HEADER的数组。IMAGE_SECTION_HEADER包含了它所关联的节的信息,如位置,长度,特征;该数组的数目由IMAGE_NT_HEADERS.FileHeader.NumberOfSections指出。具体见下图

PE中的节的大小的总和最后是要对齐的,Visual Studio 6.0中的默认值是4k,除非你使用/OPT:NOWIN98 或/ALIGN开关;在.NET中,依然用了默认的/OPT:WIN98,但是如果文件小于一特定大小时,就会采用0X200为对齐值。

.NET文档中有关于对齐的另一件有趣的事。.NET文件的内存对齐值为8K而不是普通X86平台上的4K,这样就保证了在X86平台编译的程序可以在IA-64平台上运行。如果内存对齐值为4K,那么IA-64的载入器就不能载入这个程序,因为它的页为8K。

▲评论

X 正在回复:
姓 名: 留下更多信息
性 别:
邮 件:
主 页:
Q Q:
来 自:
职 业:
评 论:
验 证:


Valid HTML 4.01 Strict Valid CSS!
Copyleft.A!die Software Studio.ADSS
Power by webmaster@adintr.com