程序员的自我修养--链接、装载与库笔记:Windows PE/COFF
1. Windows的二進制文件格式PE/COFF
在32位Windows平臺下,微軟引入了一種叫PE(Portable Executable)的可執(zhí)行格式。作為Win32平臺的標(biāo)準(zhǔn)可執(zhí)行文件格式,PE有著跟ELF一樣良好的平臺擴展性和靈活性。PE文件格式事實上與ELF同根同源,它們都是由COFF(Common Object File Format)格式發(fā)展而來的,更加具體地講是來源于當(dāng)時著名的DEC(Digital Equipment Corporation)的VAX/VMS上的COFF文件格式。微軟將它的可執(zhí)行文件格式命名為”Portable Executable”,從字面意義上講是希望這個可執(zhí)行文件格式能夠在不同版本的Windows平臺上使用,并且可以支持各種CPU。
在Windows平臺,VISUAL C++編譯器產(chǎn)生的目標(biāo)文件仍然使用COFF格式,而可執(zhí)行文件為PE格式。
隨著64位Windows的發(fā)布,微軟對64位Windows平臺上的PE文件結(jié)構(gòu)稍微做了一些修改,這個新的文件格式叫做PE32+。新的PE32+并沒有添加任何結(jié)構(gòu),最大的變化就是把那些原來32位的字段變成了64位。絕大部分情況下,PE32+與PE的格式一致,我們可以將它看作是一般的PE文件。
???????? 與ELF文件相同,PE/COFF格式也是采用了那種基于段的格式。一個段可以包含代碼、數(shù)據(jù)或其它信息,在PE/COFF文件中,至少包含一個代碼段,這個代碼段的名字往往叫做”.code”,數(shù)據(jù)段叫做”.data”。不同的編譯器產(chǎn)生的目標(biāo)文件的段名不同,VISUAL C++使用”.code”和”.data”,而Borland的編譯器使用”CODE”, “DATA”。也就是說跟ELF一樣,段名只有提示性作用,并沒有實際意義。
跟ELF一樣,PE中也允許程序員將變量或函數(shù)放到自定義的段。在GCC中我們使用”__attribute__((section(“name”)))”擴展屬性,在VISUAL C++中可以使用”#pragma”編譯器指示,如”#pragma data_set(“name”)”。
2. PE的前身----COFF
SimpleSection.c內(nèi)容如下:
int printf(const char* format, ...);int global_init_var = 84;
int global_uninit_var;void func1(int i)
{printf("%d\n", i);
}int main(void)
{static int static_var = 85;static int static_var2;int a = 1;int b;func1(static_var + static_var2 + a + b);return a;
}
以管理員身份打開cmd.exe,然后在C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64目錄下,先執(zhí)行vcvars64.bat,然后定位到SimpleSection.c所在的目錄E:\test\下,執(zhí)行”cl.exe /c /Za SimpleSection.c”,會生成目標(biāo)文件SimpleSection.obj,執(zhí)行結(jié)果如下圖所示:
“cl.exe”是VISUAL C++的編譯器,即”Compiler”的縮寫。”/c”參數(shù)表示只編譯,不鏈接,即將.c文件編譯成.obj文件,而不調(diào)用鏈接器生成.exe文件。如果不加這個參數(shù),cl.exe會在編譯”SimpleSection.c”文件以后,再調(diào)用link.exe鏈接器將該產(chǎn)生的SimpleSection.obj文件與默認(rèn)的C運行庫鏈接,產(chǎn)生可執(zhí)行文件SimpleSection.exe。
VISUAL C++有一些C和C++語言的專有擴展,這些擴展并沒有定義ANSI C標(biāo)準(zhǔn)或ANSI C++標(biāo)準(zhǔn)。”/Za”參數(shù)禁用這些擴展,使得我們的程序跟標(biāo)準(zhǔn)的C/C++兼容。使用”/Za”參數(shù)時,編譯器自動定義了__STDC__這個宏,我們可以在程序里通過判斷這個宏是否被定義而確定編譯器是否禁用了Microsoft C/C++語法擴展。
跟GNU的工具鏈中的”objdump”一樣,Visual C++也提供了一個用于查看目標(biāo)文件和可執(zhí)行文件的工具,就是”dumpbin.exe”。通過這個命令可以查看SimpleSection.obj的結(jié)構(gòu),”/ALL”參數(shù)是將打印輸出目標(biāo)文件的所有相關(guān)信息,包括文件頭、每個段的屬性和段的原始數(shù)據(jù)及符號表。也可以用”/SUMMARY”選項來查看整個文件的基本信息,它只輸出所有段的段名和長度,執(zhí)行結(jié)果如下圖所示:
COFF文件結(jié)構(gòu):幾乎跟ELF文件一樣,COFF也是由文件頭及后面的若干個段組成,再加上文件末尾的符號表、調(diào)試信息的內(nèi)容,就構(gòu)成了COFF文件的基本結(jié)構(gòu)。COFF文件的文件頭部包括了兩部分,一個是描述文件總體結(jié)構(gòu)和屬性的映像頭(Image Header),另外一個是描述該文件中包含的段屬性的段表(Section Table)。文件頭后面緊跟著的就是文件的段,包括代碼段、數(shù)據(jù)段等,最后還有符號表等。
映像(Image):因為PE文件在裝載時被直接映射到進程的虛擬空間中運行,它是進程的虛擬空間的映像。所以PE可執(zhí)行文件很多時候被叫做映像文件(Image File)。
文件頭里描述COFF文件總體屬性的映像頭是一個”IMAGE_FILE_HEADER”的結(jié)構(gòu),它跟ELF中的”Elf64_Ehdr”結(jié)構(gòu)的作用相同。這個結(jié)構(gòu)及相關(guān)常數(shù)被定義在” C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include”里面:
//
// File header format.
//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;#define IMAGE_SIZEOF_FILE_HEADER 20
對照前面”SimpleSection.txt”中的輸出信息,我們可以看到輸出的信息里面最開始一段”FILE HEADER VALUES”中的內(nèi)容跟COFF映像頭中的成員是一一對應(yīng)的,如下圖所示:
可以看到這個目標(biāo)文件的文件類型是”COFF OBJECT”,也就是COFF目標(biāo)文件格式。文件頭里面還包含了目標(biāo)機器類型,例子里的類型是0x8664。按照微軟的預(yù)想,PE/COFF結(jié)構(gòu)的可執(zhí)行文件應(yīng)該可以在不同類型的硬件平臺上使用,所以預(yù)留了該字段。在WinNT.h里面可以找到相應(yīng)的以”IMAGE_FILE_MACHINE_”開頭的目標(biāo)機器類型的定義。文件頭里面的”Number of Sections”是指該PE所包含的”段”的數(shù)量。”Time date stamp”是指PE文件的創(chuàng)建時間。“File pointer to symbol table”是符號表在PE中的位置。”Size of optional header”是指Optional Header的大小,這個結(jié)構(gòu)只存在于PE可執(zhí)行文件,COFF目標(biāo)文件中該結(jié)構(gòu)不存在,所以為0.
映像頭后面緊跟著的就是COFF文件的段表,它是一個類型為”IMAGE_SECTION_HEADER”結(jié)構(gòu)的數(shù)組,數(shù)組里面每個元素代表一個段,這個結(jié)構(gòu)跟ELF文件中的”Elf64_Shdr”很相似。這個數(shù)組元素的個數(shù)剛好是該COFF文件所包含的段的數(shù)量,也就是映像頭里面的”number of sections”。這個結(jié)構(gòu)是用來描述每個段的屬性的,它也被定義在WinNT.h里面:
//
// Section header format.
//#define IMAGE_SIZEOF_SHORT_NAME 8typedef struct _IMAGE_SECTION_HEADER {BYTE Name[IMAGE_SIZEOF_SHORT_NAME];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;#define IMAGE_SIZEOF_SECTION_HEADER 40
可以看到每個段所擁有的屬性包括段名(Section Name)、物理地址(Physical address)、虛擬地址(Virtual address)、原始數(shù)據(jù)大小(Size of raw data)、段在文件中的位置(File pointer ro raw data)、該段的重定位表在文件中的位置(File pointer to relocation table)、該段的行號表在文件中的位置(File pointer to line numbers)、標(biāo)志位(Characteristics)等。VirtualSize:該段被加載至內(nèi)存后的大小;VirtualAddress:該段被加載至內(nèi)存后的虛擬地址;SizeOfRawData:該段在文件中的大小。注意:這個值有可能跟VirtualSize的值不一樣,比如.bss段的SizeOfRawData是0,而VirtualSize值是.bss段的大小。另外涉及一些內(nèi)存對齊等問題,這個值往往比VirtualSize小。Characteristics:段的屬性,屬性里包含的主要是段的類型(代碼、數(shù)據(jù)、bss)、對齊方式及可讀可寫可執(zhí)行等權(quán)限。段的屬性是一些標(biāo)志位的組合,這些標(biāo)志位被定義在WinNT.h里,比如IMAGE_SCN_CNF_CODE(0x00000020)表示該段里面包含的是代碼,等等。
段表以后就是一個個的段的實際內(nèi)容了。COFF中的代碼段、數(shù)據(jù)段、BSS段的內(nèi)容及它們的存儲方式與ELF中幾乎一樣。
3. 鏈接指示信息
SimpleSection.txt中”.drectve”段相關(guān)的內(nèi)容如下圖所示:
“.drectve”段實際上是”Directive”的縮寫,它的內(nèi)容是編譯器傳遞給鏈接器的指令(Directive),即編譯器希望告訴鏈接器應(yīng)該怎樣鏈接這個目標(biāo)文件。段名后面就是段的屬性,包括地址、長度、位置等與ELF中存在的相同的屬性,最后一個屬性是標(biāo)志位”flags”,即IMAGE_SECTION_HEADERS里面的Characteristics成員。”.drectve”段的標(biāo)志位為”0x100A00”,它是下表中標(biāo)志位的組合:
“dumpbin”已經(jīng)為我們打印出了標(biāo)志位的三個組合屬性:Inof、Remove、1 byte align。即該段是信息段,并非程序數(shù)據(jù);該段可以在最后鏈接成可執(zhí)行文件的時候被拋棄;該段在文件中的對齊方式是1個字節(jié)對齊。
輸出信息中緊隨其后的是該段在文件中的原始數(shù)據(jù)(RAW DATA #1,用十六進制顯示的原始數(shù)據(jù)及相應(yīng)的ASCII字符)。”dumpbin”知道該段是個”.drectve”段,并且對段的內(nèi)容進行了解析,解析結(jié)果為一個”/DEFAULTLIB:’LIBCMT’”的鏈接指令(Linker Directives),實際上它就是”cl.exe”編譯器希望傳給”link.exe”鏈接器的參數(shù)。這個參數(shù)表示編譯器希望告訴鏈接器,該目標(biāo)文件需要LIBCMT這個默認(rèn)庫。LIBCMT的全稱是Library C Multithread,它表示VC的靜態(tài)鏈接的多線程C庫,對應(yīng)的文件在VC安裝目錄下的lib/libcmt.lib。我們可以在cl.exe編譯器里面加入/Zl來關(guān)閉默認(rèn)C庫的鏈接指令。
4. 調(diào)試信息
COFF文件中所有以”.debug”開始的段都包含著調(diào)試信息。比如”.debug$S”表示包含的是符號(Symbol)相關(guān)的調(diào)試信息段;”.debug$P”表示包含預(yù)編譯頭文件(Precompiled Header Files)相關(guān)的調(diào)試信息段;”.debug$T”表示包含類型(Type)相關(guān)的調(diào)試信息段。在”SimpleSection.obj”中,我們只看到了”.debug$S”段,也就是只有調(diào)試時的相關(guān)信息。我們可以從該段的文本信息中看到目標(biāo)文件的原始路徑,編譯器信息等。調(diào)試信息段的具體格式被定義在PE格式文件標(biāo)準(zhǔn)中。調(diào)試段相關(guān)信息在”SimpleSection.txt”中的內(nèi)容如下:
5. 大家都有符號表
?“SimpleSection.txt”的最后部分是COFF符號表(Symbol table),COFF文件的符號表包含的內(nèi)容幾乎跟ELF文件的符號表一樣,主要就是符號名、符號的類型、所在的位置。”SimpleSection.txt”關(guān)于符號表的輸出如下所示:
在輸出結(jié)果的最左列是符號的編號,也就是符號在符號表中的下標(biāo)。接著是符號的大小,即符號所表示的對象所占用的空間。第三列是符號所在的位置,ABS(Absolute)表示符號是個絕對值,即一個常量,它不存在于任何段中;SECT1(Section #1)表示符號所表示的對象定義在本COFF文件的第一個段中,即本例中的”.drectve”段;UNDEF(Undefined)表示符號是未定義的,即這個符號被定義在其它目標(biāo)文件中。第四列是符號類型,可以看到對于C語言的符號,COFF只區(qū)分了兩種,一種是變量和其它符號,類型為notype,另外一種是函數(shù),類型為notype (),這個符號類型值可以用于其它一些需要強符號類型的語言或系統(tǒng)中,可以給鏈接器更多的信息來識別符號的類型。第五列是符號的可見范圍,Static表示符號是局部變量,只有目標(biāo)文件內(nèi)部是可見的;External表示符號是全局變量,可以被其它目標(biāo)文件引用。最后一列是符號名,對于不需要修飾的符號名,”dumpbin”直接輸出原始的符號名;對于那些經(jīng)過修飾的符號名,它會把修飾前和修飾后的名字都打印出來,后面括號里面的就是未修飾的符號名。
從符號表的dump輸出信息中,我們可以看到”global_init_var”這個符號位于Section #3,即”.data”段。另外還有一個$SG1326的符號,其實它表示的是程序中的那個”%d\n”字符串常量。因為程序中要引用到這個字符串常量,而該字符串常量又沒有名字,所以編譯器自動為它生成了一個名字,并且作為符號放在符號表里面,可以看到這個符號對外都是不可見的。ELF文件中并沒有為字符串常量自動生成的符號,另外所有的段名都是一個符號,”dumpbin”如果碰到某個符號是一個段的段名,那么它還會解析該符號所表示的段的基本屬性,每個段名符號后面緊跟著一行就是段的基本屬性,分別是段長度、重定位數(shù)、行號數(shù)和校驗和。
?6. Windows下的ELF----PE
PE文件是基于COFF的擴展,它比COFF文件多了幾個結(jié)構(gòu)。最主要的變化有兩個:第一個是文件最開始的部分不是COFF文件頭,而是DOS MZ可執(zhí)行文件格式的文件頭和樁代碼(DOS MZ File Header and Stub);第二個變化是原來的COFF文件頭中的”IMAGE_FILE_HEADER”部分?jǐn)U展成了PE文件文件頭結(jié)構(gòu)”IMAGE_NT_HEADERS”,這個結(jié)構(gòu)包括了原來的”Image Header”及新增的PE擴展頭部結(jié)構(gòu)(PE Optional Header)。PE文件的結(jié)構(gòu)如下圖所示:
DOS下的可執(zhí)行文件的擴展名與Windows下的可執(zhí)行文件擴展名一樣,都是”.exe”,但是DOS下的可執(zhí)行文件格式是”MZ”格式,與Windows下的PE格式完全不同,雖然它們使用相同的擴展名。PE文件中的”Image DOS Header”和”DOS Stub”這兩個結(jié)構(gòu)就是為了兼容DOS系統(tǒng)而設(shè)計的,其中”IMAGE_DOS_HEADER”結(jié)構(gòu)其實跟DOS的”MZ”可執(zhí)行結(jié)構(gòu)的頭部完全一樣,所以從某個角度看,PE文件其實也是一個”MZ”文件。”IMAGE_DOS_HEADER”的結(jié)構(gòu)中有的前兩個字節(jié)是”e_magic”結(jié)構(gòu),它是里面包含了”MZ”這兩個字母的ASCII碼;”e_cs”和”e_ip”兩個成員指向程序的入口地址。
當(dāng)PE可執(zhí)行映像在DOS下被加載的時候,DOS系統(tǒng)檢測該文件,發(fā)現(xiàn)最開始兩個字節(jié)是”MZ”,于是認(rèn)為它是一個”MZ”可執(zhí)行文件。然后DOS系統(tǒng)就將PE文件當(dāng)作正常的”MZ”文件開始執(zhí)行。DOS系統(tǒng)會讀取”e_cs”和”e_ip”這兩個成員的值,以跳轉(zhuǎn)到程序的入口地址。然而PE文件中,”e_cs”和”e_ip”這兩個成員并不指向程序真正的入口地址,而是指向文件中的”DOS Stub”。”DOS Stub”是一段可以在DOS下運行的一小段代碼,這段代碼的唯一作用是向終端輸出一行字:”This program cannot be run in DOS”,然后退出程序,表示該程序不能在DOS下運行。所以我們?nèi)绻贒OS系統(tǒng)下運行Windows的程序就可以看到上面這句話,這是因為PE文件結(jié)構(gòu)兼容DOS “MZ”可執(zhí)行文件結(jié)構(gòu)的緣故。
?“IMAGE_DOS_HEADER”結(jié)構(gòu)也被定義在WinNT.h里面,里面的”e_lfanew”成員表明了PE文件頭(IMAGE_NT_HEADERS)在PE文件中的偏移,我們需要使用這個值來定位PE文件頭。這個成員在DOS的”MZ”文件格式中它的值永遠(yuǎn)為0,所以當(dāng)Windows開始執(zhí)行一個后綴名為”.exe”的文件時,它會判斷”e_lfanew”成員是否為0。如果為0,則該”.exe”文件是一個DOS ?“MZ”可執(zhí)行文件,Windows會啟動DOS子系統(tǒng)來執(zhí)行它;如果不為0,那么它就是一個Windows的PE可執(zhí)行文件,”e_lfanew”的值表示”IMAGE_NT_HEADERS”在文件中的偏移。
“IMAGE_NT_HEADERS”是PE真正的文件頭,它包含了一個標(biāo)記(Signature)和兩個結(jié)構(gòu)體。標(biāo)記是一個常量。文件頭包含的兩個結(jié)構(gòu)分別是映像頭(Image Header)、PE擴展頭部結(jié)構(gòu)(Image Optional Header)。
Windows中把32位的PE文件格式叫做PE32,把64位的PE文件格式叫做PE32+。這兩種格式就是ELF32和ELF64一樣,都大同小異,只不過關(guān)于地址和長度的一些成員從32位擴展成了64位,還增加了若干個額外的成員之外,沒有其它區(qū)別。
我們平時可以使用”IMAGE_OPTIONAL_HEADER”作為”O(jiān)ptional Image Header”的定義。它是一個宏,在64位的Windows下,Visual C++在編譯時會定義”_WIN64”這個宏,那么”IMAGE_OPTIONAL_HEADER”就被定義成”IMAGE_OPTIONAL_HEADER64”;32位Windows下沒有定義”_WIN64”這個宏,那么它就是”IMAGE_OPTIONAL_HEADER32”。
PE數(shù)據(jù)目錄:在Windows系統(tǒng)裝載PE可執(zhí)行文件時,往往需要很快地找到一些裝載所需要的數(shù)據(jù)結(jié)構(gòu),比如導(dǎo)入表、導(dǎo)出表、資源、重定位表等。這些常用的數(shù)據(jù)的位置和長度都被保存在了一個叫做數(shù)據(jù)目錄(Data Directory)的結(jié)構(gòu)里面,其實它就是”IMAGE_OPTIONAL_HEADER”結(jié)構(gòu)里面的”DataDirectory”成員。這個成員是一個”IMAGE_DATA_DIRECTORY”的結(jié)構(gòu)數(shù)組,相關(guān)的定義如下:
//
// Directory format.
//typedef struct _IMAGE_DATA_DIRECTORY {DWORD VirtualAddress;DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
可以看到這個數(shù)組的大小為16,IMAGE_DATA_DIRECTORY結(jié)構(gòu)有兩個成員,分別是虛擬地址以及長度。DataDirectory數(shù)組里面每一個元素都對應(yīng)一個包含一定含義的表。”WinNT.h”里面定義了一些以”IMAGE_DIRECTORY_ENTRY_”開頭的宏,數(shù)值從0到14,它們實際上就是相關(guān)的表的宏定義在數(shù)組中的下標(biāo)。比如”IMAGE_DIRECTORY_ENTRY_EXPORT”被定義為0,所以這個數(shù)組的第一個元素所包含的地址和長度就是導(dǎo)出表(Export Table)所在的地址和長度。這個數(shù)組中還包含其它的表,不如導(dǎo)入表、資源表、異常表、重定位表、調(diào)試信息表、線程私有存儲(TLS)等的地址和長度。這些表多數(shù)跟裝載和DLL動態(tài)鏈接有關(guān),與靜態(tài)鏈接沒什么關(guān)系。
GitHub:?https://github.com/fengbingchun/Messy_Test?
總結(jié)
以上是生活随笔為你收集整理的程序员的自我修养--链接、装载与库笔记:Windows PE/COFF的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序员的自我修养--链接、装载与库笔记:
- 下一篇: libjpeg-turbo介绍及测试代码