天天看點

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

1.Windows的二進制檔案格式PE/COFF

在32位Windows平台下,微軟引入了一種叫PE(Protable Executable)的可執行格式。PE檔案格式和ELF都是由COFF格式發展而來的。而對于VISUALC++編譯器産生的目标檔案仍然使用COFF格式。由于PE是COFF的一種擴充,是以它們的結構在很大程度上相同,甚至跟ELF檔案的基本結構也相同。即Windows下目标檔案預設為COFF格式,而可執行檔案為PE格式。可以将它們統稱為PE/COFF格式。

随着64位Windows的釋出,PE檔案結構對應的新的檔案格式叫做PE32+。新的PE32+并沒有添加任何結構,最大的變化就是把那些原來32位的字段變成了64位。絕大部分情況下,PE32+與PE的格式一緻。

在VISUALC++中可以使用“#pragma”這個編譯器訓示,将變量或函數放到自定義的段。

#pragma data_seg("FOO")
int global=1;
#pragma data_seg(".data")
           

表示将把全局變量“global”放到“FOO”段裡面去,然後再使用“#pragma”将這個編譯器訓示換回來,恢複到“.data”。

2.PE的前身——COFF

使用SimpleSection.c為例子。

int printf(const char * format, ...);

int global_init_var=84;
int global_uninit_var;

void func1(int i) {
    printf("%d\n",i);
}

int main() {
    static int static_var=85;
    static int static_var2;

    int a=1;
    int b;
    func1(static_var+static_var2+a+b);

    return a;
}
           

在Windows平台下下載下傳Visual studio後,Visual C++編譯器等也就安裝好了。之後在控制台就可以直接使用,“cl“是Visual C++的編譯器。使用以下編譯指令:

cl /c /Za SimpleSection.c
           

/c參數表示隻編譯,不連結。VISUAL C++有一些C和C++語言的專有擴充,這些擴充并沒有定義ANSI C标準或ANSI C++标準。/Za參數表示禁用這些擴充,使得我們的程式跟标準的C/C++相容。使用/Za參數時,編譯器自動定義了__STDC__這個宏,可以在程式中通過判斷這個宏是否被定義而确定編譯器是否禁用了Microsoft C/C++文法擴充。

該指令執行後會生成一個同名的.obj目标檔案。使用dumpbin檢視目标檔案的結構:

dumpbin /ALL SimpleSection.obj > SimpleSection.txt
           

/ALL參數表示列印目标檔案的所有相關資訊,我們把輸出資訊重定向了一個文本檔案中。/SUMMARY參數可以檢視整個檔案的基本資訊,它隻輸出所有段的段名和長度:

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

COFF幾乎和ELF一樣,也是由檔案頭及後面的若幹個段組成,再加上檔案末尾的符号表,調試資訊的内容,就構成了COFF檔案的基本結構。COFF檔案的檔案頭包括了兩部分,一個是描述檔案總體結構和屬性的映像頭(Image Header),另外一個是描述該檔案中包含的段屬性的段表(Section Table)。如圖所示:

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

描述檔案總體結構和屬性的映像頭的結構是IMAGE_FILE_HEADER,如下所示:

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

在SimpleSection.txt中的輸出資訊中可以看到,開始一段”FILE HEADER VALUES“中的内容和COFF映像頭中的成員是一一對應的。

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

字段含義從字面意思很好了解,依次是機器類型,段的數量,PE或COFF檔案建立的時間,符号表在PE/COFF中的位置,Optional Header的大小(這個結構隻存在于PE可執行檔案,COFF目标檔案中該結構不存在,是以這裡是0),屬性。

映像頭後面緊跟着的是COFF檔案的段表,它是一個類型位”IMAGE_SECTION_HEADER“結構的數組,數組裡面每個元素代表一個段,這個數組元素的個數就檔案頭中NumberOfSections的值。IMAGE_SECTION_HEADER這個結構是用來描述段的屬性的,如下所示:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE Name[8];   //段名
    union {
        DWORD PhysicalAddress;  //實體位址
        DWORD VirtualSize;  
    }Misc;
    DWORD VirtualAddress;   //虛拟位址
    DWORD SizeOfRawData;    //原始資料大小
    DWORD PointerToRawData;     //段在檔案中的位置
    DWORD PointerToRelocations;     //該段的重定位表在檔案中的位置
    DWORD PointerToLinenumbers;     //該段的行号表在檔案中的位置
    WORD NumberOfRelocations;
    WORD NumberOfLinenumbers;
    DWORD Characteristics;  //标志位
}IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
           

其中幾個比較重要的字段:、

(1)VirtualSize:該段被加載至記憶體後的大小。

(2)VirtualAddress:該段被加載至記憶體後的虛拟位址。

(3)SizeOfRawData:該段在檔案中的大小,這個值可能和VirtualSize不一樣,比如.bss段的SizeOfRawData是0,而VirtualSize值是.bss段的大小,另外涉及一些記憶體對齊等問題,導緻SizeOfRawData一般比VirtualSize小。

(4)Characteristics:段的屬性。屬性裡主要包括段的類型(代碼,資料,bss),對齊方式及可讀可寫可執行等權限。段的屬性是一些标志位的組合。

段表之後就是一個個段的實際内容了,如代碼段,資料段和BSS段,它們的内容和存儲方式和ELF檔案幾乎一樣。這裡主要介紹兩個ELF檔案中不存在的段:“.drectve”和“.debug$S”。

3.連結訓示資訊

SimpleSection.txt中關于“.drectve”段的内容如下:

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

.drectve的内容是編譯器傳遞給連結器的指令。段名後面是段的屬性,注意到最後一個段的屬性是标志位flags,标志位為“0x100A00”,含義如下:

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

dumpin也把這三個組合屬性列印出來了,緊跟在flags之後。

緊跟着屬性的就是該段在檔案中的原始資料,用十六進制顯示的原始資料及相應的ASCII字元。從圖可知,表示的内容是“/DEFAULTLIB:'LIBCMT'”的連結指令。這個參數表示編譯器希望告訴連結器,該目标檔案需要LIBCMT這個預設庫。是以在連結過程中,連結器看到輸入檔案中有這個段,會将”/DEFAULTLIB:'LIBCMT'”參數添加到連結參數中,即将libcmt.lib加傳入連結接輸入檔案中。

我們可以在cl編譯器參數裡面加入/Zl來關閉預設C庫的連結指令。

4.調試資訊

COFF檔案中所有以“.debug”開始的段都包含着調試資訊。如,“.debug$S”表示包含的是符号相關的調試資訊段,“.debug$P”表示包含預編譯頭檔案相關的調試資訊段,“.debug$T”表示包含類型相關的調試資訊段。可以從該段的文本資訊中看到目标檔案的原始路徑,編譯器資訊等。内容如下:

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

5.符号表

“SimpleSection.txt”的最後部分是COFF符号表,和ELF檔案的符号表一樣,主要就是符号名,符号類型,所在的位置等。内容如下:

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

最左列是符号的編号,然後是符号的大小,即符号所表示的對象所占用的空間,第三列是符号所在的位置,ABS表示符号是個常量,不在任何段中,SECT1表示符号定義在第一個段中,依次類推,UNDEF表示符号未定義,即該符号定義在其他目标檔案中。第四列是符号類型,對于C語言的符号,COFF隻區分兩種,一種是變量和其他符号,類型為notype,另外一種是函數,類型為notype()。第五列是符号的可見範圍,Static表示符号是局部變量,隻有目标檔案内部可見,External表示符号是全局變量,可以被其他目标檔案引用。最後一列是符号名,會把修飾前後的名字都列印出來,後面括号裡面的是未修飾的符号名。

可以看到有個比較特殊的符号$SG4198,它表示的其實是“%d\n”字元串常量。對比之下,ELF檔案并沒有為字元串常量自動生成符号。另外所有的段名都是一個符号,dumpbin如果碰到某個符号是一個段的段名,那麼它還會解析該符号所表示的段的基本屬性,可以看到每個段名符号後緊跟着一行段的基本屬性,分别是段長,重定位數,行号數和校驗和。

6.Windows下的ELF——PE

PE檔案是基于COFF的擴充,它比COFF檔案多了幾個結構。主要有兩個變化:

(1)檔案最開始的部分不是COFF檔案頭,而是DOS MZ可執行檔案格式的檔案頭和樁代碼。

(2)原來的COFF檔案頭中的“IMAGE_FILE_HEADER”部分擴充成了PE檔案檔案頭結構“IMAGE_NT_HEADERS”,這個結構包括了原來的“Image Header”及新增的PE擴充頭部結構(PE Optional Header)。

PE檔案的結構如下所示:

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

DOS下的執行檔案的擴充名和Windows下的可執行檔案擴充名一樣,都是“.exe”。但DOS下的可執行檔案格式是MZ,和Windows下的PE完全不同,雖然使用的是相同的擴充名。實際上,在Windows發展的早期,DOS系統還如日中天,Windows還不能脫離DOS環境獨立運作,為Windows編寫的程式應盡量相容于DOS系統,

PE檔案中的“Image DOS Header”和“DOS Stub”這兩個結構就是為了相容DOS系統而設計的。其中IMAGE_DOS_HEADER”結構其實和DOS的“MZ”可執行結構的頭部完全一樣。是以PE檔案也算是一個“MZ”檔案。“IMAGE_DOS_HEADER”的結構中前兩個位元組是“e_magic”結構,裡面包含的“MZ”這兩個字母的ASCII碼,“e_cs”和“e_ip”兩個成員指向程式的入口位址。

當PE可執行映像在DOS下被加載時,DOS系統檢測該檔案的前兩個位元組是“MZ”,于是認為它是一個“MZ”可執行檔案。然後DOS将這個PE檔案當作正常的MZ檔案執行。接着DOS讀取e_cs和e_ip這兩個值,跳轉到程式的入口位址。然而在PE檔案中,e_cs和e_ip并不指向程式真正的入口位址,而是指向檔案中的DOS Stub。DOS Stub是一段可以在DOS下運作的一小段代碼,這段代碼作用隻是在終端輸出“This program cannot be run in DOS”,然後退出程式。是以如果在DOS下運作Windows的程式,會看到這段話,這是因為PE要相容DOS的緣故。

IMAGE_DOS_HEADER結構中,我們主要關心e_lfanew成員,它表明了PE檔案頭IMAGE_NT_HEADERS在PE檔案中的偏移,需要用這個值來定位到PE檔案頭。這個成員在DOS的MZ格式中為0。是以擋Windows執行一個.exe時,會判斷e_lfanew是否為0,如果為0,則會啟動DOS子系統來執行它。

IMAGE_NT_HEADERS是PE真正的檔案頭,包含了一個标記和兩個結構體。标記是一個常量,對于一個合法的PE檔案來說,它的值是0x00004550,在小端序中對應的是'P','E','\0','\0'這四個字元的ASCII碼。

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

IMAGE_FILE_HEADER前面已介紹。IMAGE_OPTIONAL_HEADER對于PE可執行檔案(包括DLL)來說,是必需的。定義如下:

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE
Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

在64位Windows下,結構名位IMAGE_OPTIONAL_HEADER64。但可以直接使用IMAGE_OPTIONAL_HEADER作為Optional Image Header的定義。在64位下,Visual C++編譯器編譯時會自動把IMAGE_OPTIONAL_HEADER定義成IMAGE_OPTIONAL_HEADER64,在32位下同理。

這裡主要說一下結構裡最後一個字段DataDirectory。

6.1 PE資料目錄

Windows系統裝載PE可執行檔案時,需要很快找到一些裝載所需要的資料結構,比如導入表,導出表,資源,重定位表等。這些常用的資料的位置和長度都被儲存在了資料目錄DataDirectory成員中。這個成員是IMAGE_DATA_DIRECTORY結構的一個數組,IMAGE_DATA_DIRECTORY定義如下:

Windows PE/COFF1.Windows的二進制檔案格式PE/COFF2.PE的前身——COFF3.連結訓示資訊4.調試資訊5.符号表6.Windows下的ELF——PE

數組大小為16,結構裡兩個成員分别是虛拟位址和長度。DataDirectory數組裡每個元素對應一個包含一定含義的表。

其中還定義了一些以“IMAGE_DIRECTORY_ENTRY_”開頭的宏,數值從0到15。實際含義是相關的表的宏定義在數組中的下标。比如IMAGE_DIRECTORY_ENTRY_EXPORT被定義為0,表示這個數組的第一個元素所包含的位址和長度就是導出表(Export Table)所在的位址和長度。

繼續閱讀