序文
現在のネットワークファイル構造上のPEの記事すぎて、あなたの記事は、単純に理解し、要約し、自分自身の学習のPEファイル構造を記録します。
基本的な考え方
PE(ポータブル実行:ポータブル実行可能)Win32環境では、独自の実行可能ファイル形式を持っています。その機能のいくつかは、Unix Coffを(共通オブジェクトファイル形式)ファイル形式から継承されました。ポータブル実行可能ファイル形式は、これは、インテル以外のCPU、任意のWin32プラットフォームを特定し、ファイル形式を使用することができますPEローダー上で実行しても、WindowsのクロスWin32プラットフォームであることを意味しています。もちろん、必ずしもいくつかの変更を持っている必要があり、異なるCPUのPE実行ファイルに移植。VxDの16 DLLに加えて、すべてのファイルがWin32のPEファイル形式を使用して実行されます。そこで、本研究PEファイル形式は、Windowsは、我々は機会の構造を知っています。
ファイル構造
構造チャート:
DOSヘッダは、MS-DOS互換のオペレーティングシステムに使用される
主含むNTヘッダ情報のWindows PEファイル
のセクションテーブルを:PEファイルは、次のセクションで説明されている
セクション:実際には、各容器のセクションは、コード、データなどを含むことができ、各セクションでは、デフォルトのコードセクションの読み取りなどの独立した権限のメモリを持つことができます/自分自身を定義することができる権限、名称およびセクションの数を実行します
アドレスファイル
1、ハードディスク上とメモリ内のPEファイルには、各セクションがハードドライブ上に連続しているため、より大きなハードディスク上のスペースを占有よりも、仮想アドレス空間によって占有メモリにロードされた後、まったく同じではありませんメモリ内のページ整列されています。
2、PEの内部構造を、2つの方法を使用して場所を示すアドレスは、オリジナルまたは物理メモリアドレスと呼ばれるファイルを格納するためのハードディスク上のアドレスは、ヘッダからのオフセットアドレスを表し、他方がローディングするためのものですアドレスのメモリマップした後、相対仮想アドレス(RVA)、ヘッドの相対オフセットを示すメモリマップと呼ばれます。
CPUの図3に示すように、特定の命令は、確かにアドレス伝達関数をコンパイルした後、グローバル変数のアドレスをとるように、絶対アドレスを使用する必要があるアセンブリ命令を使用する必要が絶対アドレスではなく、相対的オフセット画像ヘッドので、PEファイル(これは、ベースアドレスと呼ばれる)のメモリアドレスにロードされたオペレーティングシステムをお勧めします、この表現は、仮想アドレスと呼ばれている(VA)
4、PEファイルが予想されるアドレスにロードすることはできません、システムがここに彼をロードするための再選択、適切なベースアドレス彼を助けるだろう、この時間は、すべての既存のVAが失敗した上で、NTは、最初に必要なPEをロードするためにファイルを保存情報私はPEをベースアドレスにロードされているかわからない前に、VAは無効であるため、ほとんど使用RVAアドレスを表すために、PEファイルのヘッダー
実行可能なヘッダ
他のPEファイルもいくつかの他のPEファイルからインポートできるように、1、PEファイルには、機能をエクスポートすることができます
図2に示すように、テーブルを導出することにより、PEファイルは、テーブルをインポート機能モジュールをインポートする必要性を示しており、それらの機能が自分を導出示します。
図3は、DOSおよびNTヘッドが重要PEファイルヘッダファイルヘッダ2であります
DOSヘッド
typedefは構造体 _IMAGE_DOS_HEADER { // DOS .EXEヘッダ WORDのe_magic。 // マジックナンバー WORDのe_cblp。 // ファイルの最後のページにバイト WORDのe_cp。 // ファイルにあるページ WORDのe_crlc。 // 再配置 WORDのe_cparhdr。 // 段落におけるヘッダのサイズ WORDのe_minalloc。 // 最小の余分な段落では、必要な WORDのe_maxallocを。 // 必要に応じて最大の余分な段落 WORDのe_ss。 // 最初の(相対)SS値 WORD e_sp。 // 初期SP値 WORDのe_csum。 // チェックサム WORDのe_ip。 // 初期IP値 WORDのe_cs。 // 初期(相対)CS値 WORDのe_lfarlc。 // 再配置テーブルのファイルアドレス WORD e_ovno。 // オーバーレイ番号 WORDのe_res [ 4 ]。 // 予約語 WORDのe_oemid。 // (e_oeminfo用)OEM識別子 e_oeminfo WORD。 // OEM情報。特定e_oemid WORD e_res2 [ 10 ]。 // 予約語 LONG e_lfanew。 // 新しいexeファイルヘッダのファイルアドレス } IMAGE_DOS_HEADER、* PIMAGE_DOS_HEADER。
フィールドにフォーカス
e_magic:WORD型は、一定の値0x4D5Aで、「MZ」テキストエディタをビットの値を参照して、実行可能な「MZ」を開始する必要があります。
e_lfanew:拡張された32ビットのフィールドの実行可能ファイルは、DOSヘッダNTヘッド相対ファイル開始アドレスの後のオフセットを示すために使用しました。
NTヘッド
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature:类似于DOS头中的e_magic,其高16位是0,低16是0x4550,用字符表示是'PE‘。
IMAGE_FILE_HEADER是PE文件头
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;
PE文件头
Machine:该文件的运行平台,是x86、x64还是I64
NumberOfSections:该PE文件中有多少个节,也就是节表中的项数。
TimeDateStamp:PE文件的创建时间,一般有连接器填写。
PointerToSymbolTable:COFF文件符号表在文件中的偏移。
NumberOfSymbols:符号表的数量。
SizeOfOptionalHeader:紧随其后的可选头的大小。
Characteristics:可执行文件的属性,可以是下面这些值按位相或。
PE可选头
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;
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。
SizeOfImage:映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。
SizeOfHeaders:所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。
CheckSum:映象文件的校验和。
SizeOfStackReserve:运行时为每个线程栈保留内存的大小。
SizeOfStackCommit:运行时每个线程栈初始占用内存大小。
SizeOfHeapReserve:运行时为进程堆保留内存大小。
SizeOfHeapCommit:运行时进程堆初始占用内存大小。
NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数
DataDirectory:数据目录,这是一个数组,数组的项定义如下:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
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导出表
导出表是用来描述模块中的导出函数的结构,如果一个模块导出了函数,那么这个函数会被记录在导出表中,这样通过GetProcAddress函数就能动态获取到函数的地址。函数导出的方式有两种,一种是按名字导出,一种是按序号导出。这两种导出方式在导出表中的描述方式也不相同。
导出表定义:
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;
图表:
PE导入表
IMAGE_DIRECTORY_ENTRY_IMPORT就是导入表,在PE文件加载时,会根据这个表里的内容加载依赖的DLL,并填充所需函数的地址
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT叫做绑定导入表,在第一种导入表导入地址的修正是在PE加载时完成,如果一个PE文件导入的DLL或者函数多那么加载起来就会略显的慢一些,所以出现了绑定导入,在加载以前就修正了导入表,这样就会快一些。
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT叫做延迟导入表,一个PE文件也许提供了很多功能,也导入了很多其他DLL,但是并非每次加载都会用到它提供的所有功能,也不一定会用到它需要导入的所有DLL,因此延迟导入就出现了,只有在一个PE文件真正用到需要的DLL,这个DLL才会被加载,甚至于只有真正使用某个导入函数,这个函数地址才会被修正。
IMAGE_DIRECTORY_ENTRY_IAT是导入地址表,前面的三个表其实是导入函数的描述,真正的函数地址是被填充在导入地址表中的。
重定位
Windows使用重定位机制保证代码无论模块加载到哪个基址都能正确被调用。
编译的时候由编译器识别出哪些项使用了模块内的直接VA,比如push一个全局变量、函数地址,这些指令的操作数在模块加载的时候就需要被重定位。
链接器生成PE文件的时候将编译器识别的重定位的项纪录在一张表里,这张表就是重定位表,保存在DataDirectory中,序号是 IMAGE_DIRECTORY_ENTRY_BASERELOC。
PE文件加载时,PE 加载器分析重定位表,将其中每一项按照现在的模块基址进行重定位。
每个重定位项应该是一个DWORD,里面保存需要重定位的RVA,这样只需要简单操作便能找到需要重定位的项。
然而,Windows并没有这样设计,原因是这样存放太占用空间了,试想一下,加入一个文件有n个重定位项,那么就需要占用4*n个字节。
所以Windows采用了分组的方式,按照重定位项所在的页面分组,每组保存一个页面起始地址的RVA,页内的每项重定位项使用一个WORD保存重定位项在页内的偏移,这样就大大缩小了重定位表的大小。
定义:
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
VirtualAddress:页起始地址RVA。
SizeOfBlock:表示该分组保存了几项重定位项。
TypeOffset:这个域有两个含义,页内偏移用12位就可以表示,剩下的高4位用来表示重定位的类型。而事实上,Windows只用了一种类型IMAGE_REL_BASED_HIGHLOW数值是 3。
哪些项目需要被重定位呢??
1.代码中使用全局变量的指令,因为全局变量一定是模块内的地址,而且使用全局变量的语句在编译后会产生一条引用全局变量基地址的指令。
2.将模块函数指针赋值给变量或作为参数传递,因为赋值或传递参数是会产生mov和push指令,这些指令需要直接地址。
3.C++中的构造函数和析构函数赋值虚函数表指针,虚函数表中的每一项本身就是重定位项
区段名及其含义
.text默认的代码区块,它的内容全是指令代码,链接器把所有目标文件的text块连接成一个大的.text块,
.data默认的读/写数据块,全局变量,静态变量一般放在这个区段
.rdata默认只读数据区块,但程序中很少用到该块中的数据,一般两种情况用到,一是MS 的链接器产生EXE文件中用于存放调试目录,二是用于存放说明字符串,如果程序的DEF文件中指定了DESCRIPTION,字符串就会出现在rdata中
.idata包含其他外来的DLL的函数及数据信息,即输入表,将.idata区块合并成另一个区块已成为一种惯例
.edata输出表,当创建一个输出API或数据的可执行文件时,连接器会创建一个.EXP文件,这个.EXP文件包含一个.edata区块,其会被加载到可执行文件中,经常被合并到.text或.rdata 区块中
.rsrc资源,包括模块的全部资源,如图标,菜单,位图等,这个区块是只读的,无论如何不应该把它命名为.rsrc以外的名字,也不能合并到其他的区块里
.bss未初始化的数据,很少在用,取而代之的是执行文件的.data区块的的VirtualSize被扩展大的空间里用来装未初始化的数据.
.crt用于C++ 运行时(CRT)所添加的数据
.tlsTLS的意思是线程局部存储器,用于支持通过_declspec(thread)声明的线程局部存储变量的数据,这包括数据的初始化值,也包括运行时所需要的额外变量
.reloc可执行文件的基址重定位,基址重定位一般仅Dll需要的
.sdata相对于全局指针的可被定位的 短的读写数据
.pdata异常表,包含CPU特定的IAMGE_RUNTIME_FUNTION_ENTRY结构数组,DataDirectory中的IMAGE_DIRECTORY_ENTRY_EXCEPTION指向它.
.didat延迟装入输入数据,在非Release模式下可以找到
装载PE文件的主要步骤
第一:当PE文件被执行,PE装载器检查DOS MZ header里的PE header偏移量。如果找到,则跳转到PE header。
第二:PE装载器检查PE header的有效性。如果有效,就跳转到PE header的尾部。
第三:紧跟PE header的是节表。PE装载器读取其中的节索引信息,并采用文件映射方法将这些节映射到内存,同时附上节表里指定的节属性。
第四:PE文件映射入内存后,PE装载器将处理PE文件中类似import table(引入表)逻辑部分。