JIURL PE 格式學習總結(一)-- PE文件概述

零 前言

    PE格式,是Windows的可執行文件的格式。Windows中的 exe文件,dll文件,都是PE格式。PE 就是Portable Executable 的縮寫。Portable 是指對於不同的Windows版本和不同的CPU類型上PE文件的格式是一樣的,當然CPU不一樣了,CPU指令的二進制編碼是不一樣的。只是文件中各種東西的佈局是一樣的。

CSDN_Dev_Image_2003-4-242245030.gif 

圖 1.1

    圖1.1是 JIURL PEDUMP 打開 Win2K 中的 explorer.exe 的截圖。JIURL PEDUMP 是我寫的一個小工具,從文件開始的 Dos Header 一直到 Section Table,打開PE文件之後,點擊相應結構,就會高亮顯示文件中相應的部分。不過沒有Sections。對了解 PE 格式有所幫助,可以很好的配合後面的介紹。可以到我的主頁 http://jiurl.yeah.net 上下載。

一   PE文件格式概述

PE文件結構的總體層次分佈如下所示

 --------------
|DOS MZ Header |
|--------------|
|DOS Stub      |
|--------------|
|PE Header     |
|--------------|
|Section Table |
|--------------|
|Section 1     |
|--------------|
|Section 2     |
|--------------|
|Section ...   |
|--------------|
|Section n     |
 --------------

 

1.1 DOS Header

    PE文件最開始是一個簡單的 DOS MZ header,它是一個 IMAGE_DOS_HEADER 結構。有了它,一旦程序在DOS下執行,DOS就能識別出這是有效的執行體,然後運行緊隨 MZ Header 之後的 DOS Stub。

 1.2  DOS Stub   

    DOS Stub 是一個有效的 DOS 程序。當程序在DOS下運時,輸出象 "This program cannot be run in DOS mode" 這樣的提示。在 圖1.1中就可以看到字符串 "This program cannot be run in DOS mode"。這是編譯器生成的默認stub程序。你也可以通過鏈接選項 /STUB:filename 指定任何有效的MS-DOS可執行文件來替換它。

1.3 PE Header

    緊接着 DOS Stub 的是 PE Header。它是一個 IMAGE_NT_HEADERS 結構。其中包含了很多PE文件被載入內存時需要用到的重要域。執行體在支持PE文件結構的操作系統中執行時,PE裝載器將從 DOS MZ header 中找到 PE header 的起始偏移量。因而?DOS stub 直接定位到真正的文件頭 PE header。

1.4 Section Table

    PE Header 接下來的數組結構 Section Table (節表)。如果PE文件裏有5個節,那麼此 Section Table 結構數組內就有5個成員,每個成員包含對應節的屬性、文件偏移量、虛擬偏移量等。圖1中的節表有4個成員。

1.5 Sections

    PE文件的真正內容劃分成塊,稱之爲sections(節)。Sections 是以其起始位址來排列,而不是以其字母次序來排列。通過節表提供的信息,我們可以找到這些節。圖1.1所示的 explorer.exe 中有4個節。程序的代碼,資源等等就放在這些節中。

二  PE文件格式中的結構及其作用

這部分內容請參考下面的幾篇文章,使用工具 JIURL PEDUMP 有助於快速瞭解。
大家不要因此,而失望不看,本文重點在後三篇,本篇只是爲了有個交代,和介紹些相關內容。
注意,在WINNT.H中,有所有PE相關結構的定義。我們用到的結構定義都來自那裏。

Microsoft Portable Executable and Common Object File Format Specification
MSDN 

《Windows95系統程式設計大奧祕》
第8章 PE 與COFF OBJ 檔案格式
Matt Pietrek 著 侯傑譯

Iczelion的PE教程

PE學習筆記(一) rivershan
PE學習筆記(二) rivershan

Inside Windows 
An In-Depth Look into the Win32 Portable Executable File Format 
Matt Pietrek 
已經被人翻譯了。

Inside Windows 
An In-Depth Look into the Win32 Portable Executable File Format 
Matt Pietrek

三 幾個要注意的問題

3.1 文件中大量的空白

    在 PE Header結構 中的 OptionalHeader 結構中的成員 FileAlignment 的值是文件中節的對齊粒度,單位是字節,這個值應該是2的n次方,範圍從512到64k。如果這裏的值是512,那麼PE文件中的節的長度都是512字節的整數倍,內容不夠的部分用0填充。比如一個PE文件的 FileAlignment 爲200h(十進制512),它的第一個節在400h處,長度爲100h,那麼從文件400h到500h中爲這一節的內容,而文件對齊粒度是200h,所以爲了使這一節長度爲FileAlignment的整數倍,500h到600h會被用零填充。而下一個節的開始地址爲600h。用16進制編輯器打開PE文件,就可以看到這種情況,PE文件頭的內容結束到第一個節開始之間的地方,每一個節中內容結束到下一節開始的地方都會有大量的空白。VC6編譯鏈接時默認的FileAlignment爲1000h(4k),可以使用鏈接選項 /ALIGN:number 來改變這個值。比如把4k改成512時,可以明顯減小生成文件的大小。

3.2 big-endian和little-endian

    PE Header中的 FileHeader 的成員 Machine 中的值,根據WINNT.H中的定義,對於 Intel CPU 應該爲 0x014c。但是你用16進制編輯器打開PE文件,看到這個WORD顯示的卻是 4c 01 。你看到的並沒有錯,你看到的 4c 01 就是 0x014c,只不過由於 intel cpu 是ittle-endian,所以顯示出來是這樣的。對於 big-endian 和 little-endian,請看下面的例子。

比如一個整形int變量。長爲四個字節。
這個變量的地址比如爲n。
則這個變量的4個字節地址分別爲n,n+1,n+2,n+3。

當 這個整形變量 的值爲 0x12345678 時,

對於 big-endian 來說
地址n+0的那個字節中的值爲 0x12
地址n+1的那個字節中的值爲 0x34
地址n+2的那個字節中的值爲 0x56
地址n+3的那個字節中的值爲 0x78

按如下方式就會顯示爲
n n+1 n+2 n+3 
12 34 56 78

對於 ittle-endian 來說
地址n+0的那個字節中的值爲 0x78
地址n+1的那個字節中的值爲 0x56
地址n+2的那個字節中的值爲 0x34
地址n+3的那個字節中的值爲 0x12

按如下方式就會顯示爲
n n+1 n+2 n+3 
78 56 34 12

Intel使用的是 ittle-endian 。

一個整形 int 變量 i,的地址是&i,那麼這個i的四個字節是&i,&i+1,&i+2,&i+3。
可以用這樣一個程序看到。

#include <stdio.h>
#include <conio.h>

void main()
{
int i;
char* p;
p=(char*)&i;

printf("i: ");
scanf("%x",&i);
printf("/n");

printf("&i+0: %x/n",*p);
printf("&i+1: %x/n",*(p+1));
printf("&i+2: %x/n",*(p+2));
printf("&i+3: %x/n",*(p+3));

printf("/n");
printf("&i-4: %x/n",*(p-4));
printf("&i-3: %x/n",*(p-3));
printf("&i-2: %x/n",*(p-2));
printf("&i-1: %x/n",*(p-1));

printf("/n");
printf("&i+4: %x/n",*(p+4));
printf("&i+5: %x/n",*(p+5));
printf("&i+6: %x/n",*(p+6));
printf("&i+7: %x/n",*(p+7));

getch();

}

當我們輸入 12345678 的時候可以看到,輸出

i: 12345678

&i+0: 78
&i+1: 56
&i+2: 34
&i+3: 12

&i-4: 7c
&i-3: ffffffff
&i-2: 12
&i-1: 0

&i+4: ffffffc0
&i+5: ffffffff
&i+6: 12
&i+7: 0
正是&i,&i+1,&i+2,&i+3這四個字節中儲存了i的值。

對於int,WORD,DWORD等等都要注意 big-endian 和 little-endian 。

3.3 RVA (Relative Virtual Address) 相對虛擬地址

    RVA是一個簡單的相對於PE載入點的內存偏移。比如,PE載入點爲0X400000,那麼代碼節中的地址0X401000的RVA爲(target address) 0x401000 - (load address)0x400000 = (RVA)0x1000.換句話說 RVA是0x1000,載入點爲0X400000,那麼該RVA的在內存中的實際地址就是0X401000。注意一下RVA是指內存中,不是指文件中。是指相對於載入點的偏移而不是一個內存地址,只有RVA加上載入點的地址,纔是一個實際的內存地址。

3.4 三種不同的地址

    PE的各種結構中,涉及到很多地址,偏移。有些是指在文件中的偏移,有的是指在內存中的偏移。一定要搞清楚,這個地址或者是偏移,是指在文件中,還是指在內存中。第一種,文件中的地址。比如用16進制編輯器打開PE文件,看到的地址(偏移)就是文件中的地址,我們使用某個結構的文件地址,就可以在文件中找到該結構。第二種,文件被整個映射到內存時,比如某些PE分析軟件,把整個PE文件映射到內存中,這時是內存中的地址,如果知道某一個結構在文件中的地址的話,那麼這個PE文件被映射到內存之後該結構的在內存中的地址,可以用文件中的地址加上映射內存的地址,就可以得到在該結構內存中的地址。第三種,執行PE時,PE文件會被載入器載入內存,這時經常需要的是RVA。比如知道一個結構的RVA,那麼載入點加上RVA就可以得到內存中該結構的實際地址。比如,某個程序,我們用16進制編輯器打開它,看到PE Header開始在16進制編輯器顯示爲000000C8的地方。於是我們在16進制編輯器顯示爲000000FC的地方找到了OptionalHeader的ImageBase,值爲400000h,那麼當這個程序被執行時,如果內存中400000h處沒有使用,該程序就會被載入到那裏。而我用CreateFileMapping將這個PE文件映射到內存中時,可以得到塊內存的地址爲5505024。對於映射入內存的這個PE文件,我們就可以在內存中000000FCh+05505024h=5505120處找到這個PE的OptionalHeader的ImageBase。

3.5 幾個重要結構的說明

PE Header 的 FileHeader 的 NumberOfSections:這是一個很重要的字段,用來確定文件中節的數目。

PE Header 的 OptionalHeader 的 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:一個IMAGE_DATA_DIRECTORY 結構數組。到目前爲止這個數組的長度是固定的,有16個元素,這16個元素分別代表
#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
每個元素是一個IMAGE_DATA_DIRECTORY結構,IMAGE_DATA_DIRECTORY定義如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
第一個字段是一個RVA,第二個字段是一個大小。

Section Table 節表緊跟在OptionalHeader之後,是一個IMAGE_SECTION_HEADER結構的數組。該數組中成員的個數由 File Header (IMAGE_FILE_HEADER) 結構中 NumberOfSections 域的域值來定。節表中的成員是IMAGE_SECTION_HEADER 結構,IMAGE_SECTION_HEADER 結構的長度固定,長40個字節。整個Section Table 的長度不固定,等於 NumberOfSections*sizeof(IMAGE_SECTION_HEADER)。IMAGE_SECTION_HEADER 結構中,
VirtualAddress:本節的RVA(相對虛擬地址)。
PointerToRawData:這是本節基於文件的偏移量。

3.6 DOS MZ Header 中的 MZ

    MZ是MZ格式的主要作者 Mark Zbikowski 的名字的縮寫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章