以下内容参考于MSDN:
为什么要去了解PE文件格式?我想作为每一个开发人员去了解对你的开发工作都是有好处的, 你会从中受益的.
PE格式来源于VAX/VMS系统的coff(Common Object File Format) 格式,为什么采用这种格式,查MSDN上介绍上说原来的NT开发组的很多成员是来自于DEC(Digital Equipment Corporation).自然他们利用现成的代码了.
平时我们开发软件,一般是写好程序以后(指C和C++)程序员,先用编译器进行编译成.obj文件,当然这指的是Windows平台下,你在Linux下一般是用gcc -c编译器生成.o文件,其实我想不论什么编译器它们的原理应该是一致的, 然后再用链接器将这些中间文件连接成一个可执行文件.那么.obj文件是究竟什么呢?我参考了MSDN, 大致是这样的:
我们用编译器编译生成.obj文件,其实就是把我们可读的代码,生成机器码和数据,这是机器能够识别的,那些二进制数据组成的代码和数据所组成的一个连续的区域我们称之为节,Microsoft 的编译器把代码放进称为.text的节中,把数据放进称为.data 的节中,可以利用Visual Studio的一个dumpbin 的工具来分析obj文件.Linker 的任务就是把不同的obj文件中的节连接生成一个PE文件,见下面
其实EXE文件格式与DLL文件格式都是一样的PE文件格式,只是不同的在文件中有一个位不同指示是DLL还是EXE,其它还是其它一些像.ocx(控件). cpl(控制面板扩展),它们只是换了一个扩展名而已,其它都是一样的PE文件格式.
但装载器装载PE 文件,它不是把整个PE文件全部映射到进行的地址空间中去的,而是由装载器决定需要映射哪一部分的需要被映射的,在映射的过程中,PE文件中相当偏移较大的映射到地址也是较高的地方的,见下图:
当一个PE文件被映射到地址空间比如用LoadLibraryA(W)以后,返回的HMODULE或者HINSTANCE 就是被映射的起始地址,只要我们对PE文件足够的了解的话,再加上就起始地址,我们就可以做很多事情了......
一个PE的节可以代表各种数据,比如代码(机器码),搞过单片机的可能都知道,写完的程序最后就是一个二进制文件,还有资源等等 ,还有输入/输出表,每一种有它自己的属性,有的只可以读,有的可读可写的,每一个节有一个独特的名字,比如.rdata就是只读的代码,当然你也可以生成自己的节,可以把数据放在其中,相信很多人都有过这样的用法:
#pragma data_seg(“Shared”)
int g_data;
#pragma data_seg();
#pragma comment(linker,”/section:Shared,rws”)
告诉编译器在PE中插入一个名叫Shared节,把g_data放在这个节中,而不是.data中.
一旦PE文件被映射到内存中,每个节起始总是在一个页的边界上,在不同的CPU上这个页面大小是不同的.在X86上是4KB.一个有意思的事情是可以合并节,为什么呢,比如一个节实际数据很少,可是最少也需要4KB,可以通过编译器把属性相似的节合并成一个节,在VC中可以这样做:
/MERGE:.rdata =.text
上面两个节的属性是相近的,都是只读的数据,所以可以合并到一个节中去,这样可以节约空间了.
相对虚拟地址:
为了避免在PE文件用一些硬编码的地址,使用一个称为RVA(Relative Virtual Address .一个相对虚拟地址仅仅是相对PE文件起始地址的一个偏移而已,比如说一般一个EXE一般会被加载到地址为0x00400000中(可以用/BASE:address来自己设定 ),可以用下面的公式来算出RVA:
目标地址 – 加载的起始地址 = 相对虚拟地址
比如说一个原来硬编码的地址:0x00410000,而加载的起始地址为0x00400000,所以相对的虚拟地址为0x10000.我们可以用GetModuleHandle 来获得一个模块的起始地址(在已经加载的情况下).
PE文件中有许多数据结构需要被快速的定位,例如很明显的例子如输入表,输出表,资源,这些数据结构是固定的.我们调用DLL是的函数可以分为两种方式,一种是隐式的,另一种是用诸如LoadLibraryA(W),GetProcAddress,其实用隐式这种方式的时候和显式一样也调用这些API,但是这些不需要你自己来动手的,当程序在启动的时候,装载器会自动检测需要装载哪些DLL,如果装载失败的话,程序会提示并退出,在VC6.0中新增加一个称为延时加载的特性,就是要真正调用DLL中函数是才真正去加载DLL,当然需要一些额外的工作:
/pragma comment(linker,”/DelayLoad:\”youdll.dll\”);
/pragma comment(linker,”/Delay:unload”);
这样会使用延时加载的特性,当然如果卸载下应该用__FunloadDelayLoadedDLL,关于详细的DLL延时加载可以参照<<Windows 核心编程>>中关于DLL高级技术一章.
在PE文件中存在这样一个数据结构数组,每个结构体包含DLL的名字和一个导出函数的地址,这个函数指针数组称之为输入地址表(IAT),有许多关于重建输入表的文章.一旦一个模块被装载以后, IAT中包含被调用的输入函数地址.
下面我们来看一看当调用一个输入表中函数会发生哪些事情,有两种情况可以考虑:一种为效率高(相对后面一种)的方法,一种为稍逊一筹的方法:先看一下第一种方法:
CALL DWORD PTR [0x00405030]
这种是直接通过一个函数指针事进行调用的,其中0x00405030是在IAT表中的.
第二种方法看起来有些笨的方法,却在实际中被采用的方法:
CALL 0x0040100C
.......................
0x0040100C:
JMP DWORD PTR [0x00405030]
为什么采用后一种方式呢,因为直接调用函数和调用输入表的的函数是不同的,编译器是无法直接来区别这两种,所以采用后一种方式,这也是为什么我们在调用一些写好的DLL文件的时候需要加上:__declspec(dllimport)的原因,告诉编译器这个函数其实现是在其它DLL模块中的同时生成这样的指令:
CALL DWORD PTR [XXXXXXXX]
而不是;
CALL [XXXXXXXX]
同时,编译器在目标文件增加一些信息以通知连接器定位函数的地址时去找名称为__imp_functionname的函数,大家可能会想这个名称会在什么地方.我们都知道当要使用DLL中函数,要用到.lib文件,这个lib文件中包括了这些信息,不信的话,你用一些十六进制编辑器来打开一些.lib文件,你会发现很多前面有__imp_+DLL中导出函数名.
下面看一下PE文件的格式内容:
首先是MS-DOS 头部,在MS-DOS头部的一些是一个称为IMAGE_DOS_HEADER的结构体,其中我们感兴趣的东西并不多,只有两个称为e_magic和e_lfanew的成员,e_magic固定为0x5a4d即ASCII码为(MZ),e_lfanew包括PE文件头的偏移.
IMAGE_NT_HEADERS的偏移是根据e_lfanew来确定的
例如我打开一个EXE文件部分如下:
00000000h:
4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 ; MZ?..........
00000010h: B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ; ?......@.......
00000020h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; ................
00000030h: 00 00 00 00 00 00 00 00 00 00 00 00
E8 00 00 00 ; ............?..
00000040h: 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68 ; ..?.???L?Th
00000050h: 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F ; is program canno
00000060h: 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 ; t be run in DOS
00000070h: 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00 ; mode....$.......
00000080h: B2 F7 DA E4 F6 96 B4 B7 F6 96 B4 B7 F6 96 B4 B7 ; 谗阡鰱捶鰱捶鰱捶
00000090h: A0 89 A7 B7 D3 96 B4 B7 F6 96 B4 B7 CE 96 B4 B7 ; 爥Х訓捶鰱捶螙捶
000000a0h: 75 8A BA B7 EA 96 B4 B7 1E 89 BE B7 68 96 B4 B7 ; u姾逢柎?壘穐柎?
000000b0h: F6 96 B5 B7 3A 97 B4 B7 94 89 A7 B7 E3 96 B4 B7 ; 鰱捣:棿窋墽枫柎?
000000c0h: 1E 89 BF B7 92 96 B4 B7 4E 90 B2 B7 F7 96 B4 B7 ; .壙窉柎種惒拂柎?
000000d0h: 52 69 63 68 F6 96 B4 B7 00 00 00 00 00 00 00 00 ; Rich鰱捶........
000000e0h: 00 00 00 00 00 00 00 00
50 45 00 00 4C 01 04 00 ; ........PE..L...
上面第一个橙色的地方即为MZ,第二地方即为IMAGE_DOS_HEADER中的最后一个域即PE头的偏移量,可以看出值为0x000000e8即为第三个橙色的地方,可以看到PE00,
今天就写的这里,下次再继续吧................