摘要 Windows NT
3.1引入了一種名為PE文件格式的新可執行文件格式。PE文件格式的規范包含在了MSDN的CD中(Specs and Strategy,
Specifications, Windows NT File Format Specifications),但是它非常之晦澀。
然而這一的文檔并未提供足夠的信息,所以開發者們無法很好地弄懂PE格式。本文旨在解決這一問題,它會對整個的PE文件格式作一個十分徹底的解釋,另外,本文中還帶有對所有必需結構的描述以及示范如何使用這些信息的源碼示例。
為了獲得PE文件中所包含的重要信息,我編寫了一個名為PEFILE.DLL的動態鏈接庫,本文中所有出現的源碼示例亦均摘自于此。這個DLL和它的源代
碼都作為PEFile示例程序的一部分包含在了CD中(譯注:示例程序請在MSDN中尋找,本站恕不提供),你可以在你自己的應用程序中使用這個DLL;
同樣,你亦可以依你所愿地使用并構建它的源碼。在本文末尾,你會找到PEFILE.DLL的函數導出列表和一個如何使用它們的說明。我覺得你會發現這些函
數會讓你從容應付PE文件格式的。
介紹
Windows操作系統家族最近增加的Windows
NT為開發環境和應用程序本身帶來了很大的改變,這之中一個最為重大的當屬PE文件格式了。新的PE文件格式主要來自于UNIX操作系統所通用的COFF
規范,同時為了保證與舊版本MS-DOS及Windows操作系統的兼容,PE文件格式也保留了MS-DOS中那熟悉的MZ頭部。
在本文之中,PE文件格式是以自頂而下的順序解釋的。在你從頭開始研究文件內容的過程之中,本文會詳細討論PE文件的每一個組成部分。
許多單獨的文件成分定義都來自于Microsoft Win32
SDK開發包中的WINNT.H文件,在這個文件中你會發現用來描述文件頭部和數據目錄等各種成分的結構類型定義。但是,在WINNT.H中缺少對PE文
件結構足夠的定義,在這種情況下,我定義了自己的結構來存取文件數據。你會在PEFILE.DLL工程的PEFILE.H中找到這些結構的定義,整套的
PEFILE.H開發文件包含在PEFile示例程序之中。
本文配套的示例程序除了PEFILE.DLL示例代碼之外,還有一個單獨的Win32示例應用程序,名為EXEVIEW.EXE。創建這一示例目的有二:
首先,我需要測試PEFILE.DLL的函數,并且某些情況要求我同時查看多個文件;其次,很多解決PE文件格式的工作和直接觀看數據有關。例如,要弄懂
導入地址名稱表是如何構成的,我就得同時查看.idata段頭部、導入映像數據目錄、可選頭部以及當前的.idata段實體,而EXEVIEW.EXE就
是查看這些信息的最佳示例。
閑話少敘,讓我們開始吧。
PE文件結構
PE文件格式被組織為一個線性的數據流,它由一個MS-DOS頭部開始,接著是一個是模式的程序殘余以及一個PE文件標志,這之后緊接著PE文件頭和可選
頭部。這些之后是所有的段頭部,段頭部之后跟隨著所有的段實體。文件的結束處是一些其它的區域,其中是一些混雜的信息,包括重分配信息、符號表信息、行號
信息以及字串表數據。我將所有這些成分列于圖1。
圖1.PE文件映像結構
從MS-DOS文件頭結構開始,我將按照PE文件格式各成分的出現順序依次對其進行討論,并且討論的大部分是以示例代碼為基礎來示范如何獲得文件的信息
的。所有的源碼均摘自PEFILE.DLL模塊的PEFILE.C文件。這些示例都利用了Windows
NT最酷的特色之一——內存映射文件,這一特色允許用戶使用一個簡單的指針來存取文件中所包含的數據,因此所有的示例都使用了內存映射文件來存取PE文件
中的數據。
注意:請查閱本文末尾關于如何使用PEFILE.DLL的那一段。
MS-DOS頭部/實模式頭部
如上所述,PE文件格式的第一個組成部分是MS-DOS頭部。在PE文件格式中,它并非一個新概念,因為它與MS-DOS
2.0以來就已有的MS-DOS頭部是完全一樣的。保留這個相同結構的最主要原因是,當你嘗試在Windows 3.1以下或MS-DOS
2.0以上的系統下裝載一個文件的時候,操作系統能夠讀取這個文件并明白它是和當前系統不相兼容的。換句話說,當你在MS-DOS
6.0下運行一個Windows NT可執行文件時,你會得到這樣一條消息:“This program cannot be run in DOS
mode.”如果MS-DOS頭部不是作為PE文件格式的第一部分的話,操作系統裝載文件的時候就會失敗,并提供一些完全沒用的信息,例如:“The
name specified is not recognized as an internal or external command,
operable program or batch file.”
MS-DOS頭部占據了PE文件的頭64個字節,描述它內容的結構如下:
//WINNT.H
typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE頭部
USHORT e_magic; // 魔術數字
USHORT e_cblp; // 文件最后頁的字節數
USHORT e_cp; // 文件頁數
USHORT e_crlc; // 重定義元素個數
USHORT e_cparhdr; // 頭部尺寸,以段落為單位
USHORT e_minalloc; // 所需的最小附加段
USHORT e_maxalloc; // 所需的最大附加段
USHORT e_ss; // 初始的SS值(相對偏移量)
USHORT e_sp; // 初始的SP值
USHORT e_csum; // 校驗和
USHORT e_ip; // 初始的IP值
USHORT e_cs; // 初始的CS值(相對偏移量)
USHORT e_lfarlc; // 重分配表文件地址
USHORT e_ovno; // 覆蓋號
USHORT e_res[4]; // 保留字
USHORT e_oemid; // OEM標識符(相對e_oeminfo)
USHORT e_oeminfo; // OEM信息
USHORT e_res2[10]; // 保留字
LONG e_lfanew; // 新exe頭部的文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
第一個域e_magic,被稱為魔術數字,它被用于表示一
個MS-DOS兼容的文件類型。所有MS-DOS兼容的可執行文件都將這個值設為0x5A4D,表示ASCII字符MZ。MS-DOS頭部之所以有的時候
被稱為MZ頭部,就是這個緣故。還有許多其它的域對于MS-DOS操作系統來說都有用,但是對于Windows
NT來說,這個結構中只有一個有用的域——最后一個域e_lfnew,一個4字節的文件偏移量,PE文件頭部就是由它定位的。對于Windows
NT的PE文件來說,PE文件頭部是緊跟在MS-DOS頭部和實模式程序殘余之后的。
實模式殘余程序
實模式殘余程序是一個在裝載時能夠被MS-DOS運行的實際程序。對于一個MS-DOS的可執行映像文件,應用程序就是從這里執行的。對于
Windows、OS/2、Windows
NT這些操作系統來說,MS-DOS殘余程序就代替了主程序的位置被放在這里。這種殘余程序通常什么也不做,而只是輸出一行文本,例如:“This
program requires Microsoft Windows v3.1 or
greater.”當然,用戶可以在此放入任何的殘余程序,這就意味著你可能經常看到像這樣的東西:“You can''t run a Windows
NT application on OS/2, it''s simply not possible.”
當為Windows
3.1構建一個應用程序的時候,鏈接器將向你的可執行文件中鏈接一個名為WINSTUB.EXE的默認殘余程序。你可以用一個基于MS-DOS的有效程序
取代WINSTUB,并且用STUB模塊定義語句指示鏈接器,這樣就能夠取代鏈接器的默認行為。為Windows
NT開發的應用程序可以通過使用-STUB:鏈接器選項來實現。
PE文件頭部與標志 PE文件頭部是由MS-DOS頭部的e_lfanew域定位的,這個域只是給出了文件的偏移量,所以要確定PE頭部的實際內存映射地址,就需要添加文件的內存映射基地址。例如,以下的宏是包含在PEFILE.H源文件之中的:
//PEFILE.H
#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + \
((PIMAGE_DOS_HEADER)a)->e_lfanew))
在處理PE文件信息的時候,我發現文件之中有些位置需要經常查閱。既然這些位置僅僅是對文件的偏移量,那么用宏來實現這些定位就比較容易,因為它們較之函數有更好的表現。
請注意這個宏所獲得的是PE文件標志,而并非PE文件頭部的偏移量。那是由于自Windows與OS/2的可執行文件開始,.EXE文件都被賦予了目標操
作系統的標志。對于Windows
NT的PE文件格式而言,這一標志在PE文件頭部結構之前。在Windows和OS/2的某些版本中,這一標志是文件頭的第一個字。同樣,對于PE文件格
式,Windows NT使用了一個DWORD值。
以上的宏返回了文件標志的偏移量,而不管它是哪種類型的可執行文件。所以,文件頭部是在DWORD標志之后,還是在WORD標志處,是由這個標志是否
Windows NT文件標志所決定的。要解決這個問題,我編寫了ImageFileType函數(如下),它返回了映像文件的類型:
//PEFILE.C
DWORD WINAPI ImageFileType (LPVOID lpFile)
{
/* 首先出現的是DOS文件標志 */
if (*(USHORT *)lpFile == IMAGE_DOS_SIGNATURE)
{
/* 由DOS頭部決定PE文件頭部的位置 */
if (LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) ==
IMAGE_OS2_SIGNATURE ||
LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) ==
IMAGE_OS2_SIGNATURE_LE)
return (DWORD)LOWORD(*(DWORD *)NTSIGNATURE (lpFile));
else if (*(DWORD *)NTSIGNATURE (lpFile) ==
IMAGE_NT_SIGNATURE)
return IMAGE_NT_SIGNATURE;
else
return IMAGE_DOS_SIGNATURE;
}
else
/* 不明文件種類 */
return 0;
}
以上列出的代碼立即告訴了你NTSIGNATURE宏有多么有用。對于比較不同文件類型并且返回一個適當的文件種類來說,這個宏就會使這兩件事變得非常簡單。WINNT.H之中定義的四種不同文件類型有:
//WINNT.H
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
#define IMAGE_OS2_SIGNATURE 0x454E // NE
#define IMAGE_OS2_SIGNATURE_LE 0x454C // LE
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00
首先,Windows的可執行文件類型沒有出現在這一列表中,這一點看起來很奇怪。但是,在稍微研究一下之后,就能得到原因了:除了操作
系統版本規范的不同之外,Windows的可執行文件和OS/2的可執行文件實在沒有什么區別。這兩個操作系統擁有相同的可執行文件結構。
現在把我們的注意力轉向Windows NT PE文件格式,我們會發現只要我們得到了文件標志的位置,PE文件之后就會有4個字節相跟隨。下一個宏標識了PE文件的頭部:
//PEFILE.C
#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a + \
((PIMAGE_DOS_HEADER)a)->e_lfanew + \
SIZE_OF_NT_SIGNATURE))
這個宏與上一個宏的唯一不同是這個宏加入了一個常量SIZE_OF_NT_SIGNATURE。不幸的是,這個常量并未定義在WINNT.H之中,于是我將它定義在了PEFILE.H中,它是一個DWORD的大小。
既然我們知道了PE文件頭的位置,那么就可以檢查頭部的數據了。我們只需要把這個位置賦值給一個結構,如下:
PIMAGE_FILE_HEADER pfh;
pfh = (PIMAGE_FILE_HEADER)PEFHDROFFSET(lpFile);
在這個例子中,lpFile表示一個指向可執行文件內存映像基地址的指針,這就顯出了內存映射文件的好處:不需要執行文件的I/O,只需使用指針pfh就能存取文件中的信息。PE文件頭結構被定義為:
//WINNT.H
typedef struct _IMAGE_FILE_HEADER {
USHORT Machine;
USHORT NumberOfSections;
ULONG TimeDateStamp;
ULONG PointerToSymbolTable;
ULONG NumberOfSymbols;
USHORT SizeOfOptionalHeader;
USHORT Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
#define IMAGE_SIZEOF_FILE_HEADER 20
請注意這個文件頭部的大小已經定義在這個包含文件之中了,這樣一來,想要得到這個結構的大小就很方便了。但是我覺得對結構本身使用
sizeof運算符(譯注:原文為“function”)更簡單一些,因為這樣的話我就不必記住這個常量的名字
IMAGE_SIZEOF_FILE_HEADER,而只需要記住結構IMAGE_FILE_HEADER的名字就可以了。另一方面,記住所有結構的名字
已經夠有挑戰性的了,尤其在是這些結構只有WINNT.H中才有的情況下。
PE文件中的信息基本上是一些高級信息,這些信息是被操作系統或者應用程序用來決定如何處理這個文件的。第一個域是用來表示這個可執行文件被構建的目標機
器種類,例如DEC(R) Alpha、MIPS R4000、Intel(R)
x86或一些其它處理器。系統使用這一信息來在讀取這個文件的其它數據之前決定如何處理它。
Characteristics域表示了文件的一些特征。比如對于一個可執行文件而言,分離調試文件是如何操作的。調試器通常使用的方法是將調試信息從
PE文件中分離,并保存到一個調試文件(.DBG)中。要這么做的話,調試器需要了解是否要在一個單獨的文件中尋找調試信息,以及這個文件是否已經將調試
信息分離了。我們可以通過深入可執行文件并尋找調試信息的方法來完成這一工作。要使調試器不在文件中查找的話,就需要用到
IMAGE_FILE_DEBUG_STRIPPED這個特征,它表示文件的調試信息是否已經被分離了。這樣一來,調試器可以通過快速查看PE文件的頭部
的方法來決定文件中是否存在著調試信息。
WINNT.H定義了若干其它表示文件頭信息的標記,就和以上的例子差不多。我把研究這些標記的事情留給讀者作為練習,由你們來看看它們是不是很有趣,這些標記位于WINNT.H中的IMAGE_FILE_HEADER結構之后。
PE文件頭結構中另一個有用的入口是NumberOfSections域,它表示如果你要方便地提取文件信息的話,就需要了解多少個段——更明確一點來
說,有多少個段頭部和多少個段實體。每一個段頭部和段實體都在文件中連續地排列著,所以要決定段頭部和段實體在哪里結束的話,段的數目是必需的。以下的函
數從PE文件頭中提取了段的數目:
PEFILE.C
int WINAPI NumOfSections(LPVOID lpFile)
{
/* 文件頭部中所表示出的段數目 */
return (int)((PIMAGE_FILE_HEADER)
PEFHDROFFSET (lpFile))->NumberOfSections);
}
如你所見,PEFHDROFFSET以及其它宏用起來非常方便。
PE可選頭部 PE可執行文件中接下來的224個字節組成了PE可選頭部。雖然它的名字是“可選頭部”,但是請確信:這個頭部并非“可選”,而是“必需”的。OPTHDROFFSET宏可以獲得指向可選頭部的指針:
//PEFILE.H
#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + \
((PIMAGE_DOS_HEADER)a)->e_lfanew + \
SIZE_OF_NT_SIGNATURE + \
sizeof(IMAGE_FILE_HEADER)))
可選頭部包含了很多關于可執行映像的重要信息,例如初始的堆棧大小、程序入口點的位置、首選基地址、操作系統版本、段對齊的信息等等。IMAGE_OPTIONAL_HEADER結構如下:
//WINNT.H
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// 標準域
//
USHORT Magic;
UCHAR MajorLinkerVersion;
UCHAR MinorLinkerVersion;
ULONG SizeOfCode;
ULONG SizeOfInitializedData;
ULONG SizeOfUninitializedData;
ULONG AddressOfEntryPoint;
ULONG BaseOfCode;
ULONG BaseOfData;
//
// NT附加域
//
ULONG ImageBase;
ULONG SectionAlignment;
ULONG FileAlignment;
USHORT MajorOperatingSystemVersion;
USHORT MinorOperatingSystemVersion;
USHORT MajorImageVersion;
USHORT MinorImageVersion;
USHORT MajorSubsystemVersion;
USHORT MinorSubsystemVersion;
ULONG Reserved1;
ULONG SizeOfImage;
ULONG SizeOfHeaders;
ULONG CheckSum;
USHORT Subsystem;
USHORT DllCharacteristics;
ULONG SizeOfStackReserve;
ULONG SizeOfStackCommit;
ULONG SizeOfHeapReserve;
ULONG SizeOfHeapCommit;
ULONG LoaderFlags;
ULONG NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
如你所見,這個結構中所列出的域實在是冗長得過分。為了不讓你對所有這些域感到厭煩,我會僅僅討論有用的——就是說,對于探究PE文件格式而言有用的。
標準域 首先,請注意這個結構被劃分為“標準域”和“NT附加域”。所謂標準域,就是和UNIX可執行文件的COFF格式所公共的部分。雖然標準域保留了COFF中定義的名字,但是Windows NT仍然將它們用作了不同的目的——盡管換個名字更好一些。
·Magic。我不知道這個域是干什么的,對于示例程序EXEVIEW.EXE示例程序而言,這個值是0x010B或267(譯注:0x010B為.EXE,0x0107為ROM映像,這個信息我是從eXeScope上得來的)。
·MajorLinkerVersion、MinorLinkerVersion。表示鏈接此映像的鏈接器版本。隨Window NT build 438配套的Windows NT SDK包含的鏈接器版本是2.39(十六進制為2.27)。
·SizeOfCode。可執行代碼尺寸。
·SizeOfInitializedData。已初始化的數據尺寸。
·SizeOfUninitializedData。未初始化的數據尺寸。
·AddressOfEntryPoint。在標準域中,AddressOfEntryPoint域是對PE文件格式來說最為有趣的了。這個域表示應用程
序入口點的位置。并且,對于系統黑客來說,這個位置就是導入地址表(IAT)的末尾。以下的函數示范了如何從可選頭部獲得Windows
NT可執行映像的入口點。
//PEFILE.C
LPVOID WINAPI GetModuleEntryPoint(LPVOID lpFile)
{
PIMAGE_OPTIONAL_HEADER poh;
poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET(lpFile);
if (poh != NULL)
return (LPVOID)poh->AddressOfEntryPoint;
else
return NULL;
}
·BaseOfCode。已載入映像的代碼(“.text”段)的相對偏移量。
·BaseOfData。已載入映像的未初始化數據(“.bss”段)的相對偏移量。
Windows NT附加域 添加到Windows NT PE文件格式中的附加域為Windows NT特定的進程行為提供了裝載器的支持,以下為這些域的概述。
·ImageBase。進程映像地址空間中的首選基地址。Windows NT的Microsoft Win32 SDK鏈接器將這個值默認設為0x00400000,但是你可以使用-BASE:linker開關改變這個值。
·SectionAlignment。從ImageBase開始,每個段都被相繼的裝入進程的地址空間中。SectionAlignment則規定了裝載時段能夠占據的最小空間數量——就是說,段是關于SectionAlignment對齊的。
Windows NT虛擬內存管理器規定,段對齊不能少于頁尺寸(當前的x86平臺是4096字節),并且必須是成倍的頁尺寸。4096字節是x86鏈接器的默認值,但是它可以通過-ALIGN: linker開關來設置。
·FileAlignment。映像文件首先裝載的最小的信息塊間隔。例如,鏈接器將一個段實體(段的原始數據)加零擴展為文件中最接近的
FileAlignment邊界。早先提及的2.39版鏈接器將映像文件以0x200字節的邊界對齊,這個值可以被強制改為512到65535這么多。
·MajorOperatingSystemVersion。表示Windows NT操作系統的主版本號;通常對Windows NT 1.0而言,這個值被設為1。
·MinorOperatingSystemVersion。表示Windows NT操作系統的次版本號;通常對Windows NT 1.0而言,這個值被設為0。
·MajorImageVersion。用來表示應用程序的主版本號;對于Microsoft Excel 4.0而言,這個值是4。
·MinorImageVersion。用來表示應用程序的次版本號;對于Microsoft Excel 4.0而言,這個值是0。
·MajorSubsystemVersion。表示Windows NT Win32子系統的主版本號;通常對于Windows NT 3.10而言,這個值被設為3。
·MinorSubsystemVersion。表示Windows NT Win32子系統的次版本號;通常對于Windows NT 3.10而言,這個值被設為10。
·Reserved1。未知目的,通常不被系統使用,并被鏈接器設為0。
·SizeOfImage。表示載入的可執行映像的地址空間中要保留的地址空間大小,這個數字很大程度上受SectionAlignment的影響。例
如,考慮一個擁有固定頁尺寸4096字節的系統,如果你有一個11個段的可執行文件,它的每個段都少于4096字節,并且關于65536字節邊界對齊,那
么SizeOfImage域將會被設為11 * 65536 =
720896(176頁)。而如果一個相同的文件關于4096字節對齊的話,那么SizeOfImage域的結果將是11 * 4096 =
45056(11頁)。這只是個簡單的例子,它說明每個段需要少于一個頁面的內存。在現實中,鏈接器通過個別地計算每個段的方法來決定
SizeOfImage確切的值。它首先決定每個段需要多少字節,并且最后將頁面總數向上取整至最接近的SectionAlignment邊界,然后總數
就是每個段個別需求之和了。
·SizeOfHeaders。這個域表示文件中有多少空間用來保存所有的文件頭部,包括MS-DOS頭部、PE文件頭部、PE可選頭部以及PE段頭部。文件中所有的段實體就開始于這個位置。
·CheckSum。校驗和是用來在裝載時驗證可執行文件的,它是由鏈接器設置并檢驗的。由于創建這些校驗和的算法是私有信息,所以在此不進行討論。
·Subsystem。用于標識該可執行文件目標子系統的域。每個可能的子系統取值列于WINNT.H的IMAGE_OPTIONAL_HEADER結構之后。
·DllCharacteristics。用來表示一個DLL映像是否為進程和線程的初始化及終止包含入口點的標記。
·SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、
SizeOfHeapCommit。這些域控制要保留的地址空間數量,并且負責棧和默認堆的申請。在默認情況下,棧和堆都擁有1個頁面的申請值以及16個
頁面的保留值。這些值可以使用鏈接器開關-STACKSIZE:與-HEAPSIZE:來設置。
·LoaderFlags。告知裝載器是否在裝載時中止和調試,或者默認地正常運行。
·NumberOfRvaAndSizes。這個域標識了接下來的DataDirectory數組。請注意它被用來標識這個數組,而不是數組中的各個入口數字,這一點非常重要。
·DataDirectory。數據目錄表示文件中其它可執行信息重要組成部分的位置。它事實上就是一個IMAGE_DATA_DIRECTORY結構的數組,位于可選頭部結構的末尾。當前的PE文件格式定義了16種可能的數據目錄,這之中的11種現在在使用中。
數據目錄 WINNT.H之中所定義的數據目錄為:
//WINNT.H
// 目錄入口
// 導出目錄
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
// 導入目錄
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
// 資源目錄
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
// 異常目錄
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
// 安全目錄
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
// 重定位基本表
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
// 調試目錄
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
// 描述字串
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
// 機器值(MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
// TLS目錄
#define IMAGE_DIRECTORY_ENTRY_TLS 9
// 載入配置目錄
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
基本上,每個數據目錄都是一個被定義為IMAGE_DATA_DIRECTORY的結構。雖然數據目錄入口本身是相同的,但是每個特定的目錄種類卻是完全唯一的。每個數據目錄的定義在本文的以后部分被描述為“預定義段”。
//WINNT.H
typedef struct _IMAGE_DATA_DIRECTORY {
ULONG VirtualAddress;
ULONG Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
每個數據目錄入口指定了該目錄的尺寸和相對虛擬地址。如果你要定義一個特定的目錄的話,就需要從可選頭部中的數據目錄數組中決定相對的地址,
然后使用虛擬地址來決定該目錄位于哪個段中。一旦你決定了哪個段包含了該目錄,該段的段頭部就會被用于查找數據目錄的精確文件偏移量位置。
所以要獲得一個數據目錄的話,那么首先你需要了解段的概念。我在下面會對其進行描述,這個討論之后還有一個有關如何定位數據目錄的示例。
PE文件段
PE文件規范由目前為止定義的那些頭部以及一個名為“段”的一般對象組成。段包含了文件的內容,包括代碼、數據、資源以及其它可執行信息,每個段都有一個
頭部和一個實體(原始數據)。我將在下面描述段頭部的有關信息,但是段實體則缺少一個嚴格的文件結構。因此,它們幾乎可以被鏈接器按任何的方法組織,只要
它的頭部填充了足夠能夠解釋數據的信息。
段頭部 PE文件格式中,所有的段頭部位于可選頭部之后。每個段頭部為40個字節長,并且沒有任何的填充信息。段頭部被定義為以下的結構:
//WINNT.H
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
UCHAR Name[IMAGE_SIZEOF_SHORT_NAME];
union {
ULONG PhysicalAddress;
ULONG VirtualSize;
} Misc;
ULONG VirtualAddress;
ULONG SizeOfRawData;
ULONG PointerToRawData;
ULONG PointerToRelocations;
ULONG PointerToLinenumbers;
USHORT NumberOfRelocations;
USHORT NumberOfLinenumbers;
ULONG Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
你如何才能獲得一個特定段的段頭部信息?既然段頭部是被連續的組織起來的,而且沒有一個特定的順序,那么段頭部必須由名稱來定位。以下的函數示范了如何從一個給定了段名稱的PE映像文件中獲得一個段頭部:
//PEFILE.C
BOOL WINAPI GetSectionHdrByName(LPVOID lpFile, IMAGE_SECTION_HEADER *sh, char *szSection)
{
PIMAGE_SECTION_HEADER psh;
int nSections = NumOfSections (lpFile);
int i;
if ((psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(lpFile))
!= NULL)
{
/* 由名稱查找段 */
for (i = 0; i < nSections; i++)
{
if (!strcmp(psh->Name, szSection))
{
/* 向頭部復制數據 */
CopyMemory((LPVOID)sh, (LPVOID)psh,
sizeof(IMAGE_SECTION_HEADER));
return TRUE;
}
else
psh++;
}
}
return FALSE;
}
這個函數通過SECHDROFFSET宏將第一個段頭部定位,然后它開始在所有段中循環,并將要尋找的段名稱和每個段的名稱相比較,直到找
到了正確的那一個為止。當找到了段的時候,函數將內存映像文件的數據復制到傳入函數的結構中,然后IMAGE_SECTION_HEADER結構的各域就
能夠被直接存取了。
段頭部的域 ·Name。每個段都有一個8字符長的名稱域,并且第一個字符必須是一個句點。
·PhysicalAddress或VirtualSize。第二個域是一個union域,現在已不使用了。
·VirtualAddress。這個域標識了進程地址空間中要裝載這個段的虛擬地址。實際的地址由將這個域的值加上可選頭部結構中的ImageBase
虛擬地址得到。切記,如果這個映像文件是一個DLL,那么這個DLL就不一定會裝載到ImageBase要求的位置。所以一旦這個文件被裝載進入了一個進
程,實際的ImageBase值應該通過使用GetModuleHandle來檢驗。
·SizeOfRawData。這個域表示了相對FileAlignment的段實體尺寸。文件中實際的段實體尺寸將少于或等于
FileAlignment的整倍數。一旦映像被裝載進入了一個進程的地址空間,段實體的尺寸將會變得少于或等于FileAlignment的整倍數。
·PointerToRawData。這是一個文件中段實體位置的偏移量。
·PointerToRelocations、PointerToLinenumbers、NumberOfRelocations、NumberOfLinenumbers。這些域在PE格式中不使用。
·Characteristics。定義了段的特征。這些值可以在WINNT.H及本光盤(譯注:MSDN的光盤)的PE格式規范中找到。
值 定義
0x00000020 代碼段
0x00000040 已初始化數據段
0x00000080 未初始化數據段
0x04000000 該段數據不能被緩存
0x08000000 該段不能被分頁
0x10000000 共享段
0x20000000 可執行段
0x40000000 可讀段
0x80000000 可寫段
定位數據目錄 數據目錄存在于它們相應的數據段中。典型地來說,數據目錄是段實體中的第一個結構,但不是必需的。由于這個緣故,如果你需要定位一個指定的數據目錄的話,就需要從段頭部和可選頭部中獲得信息。
為了讓這個過程簡單一點,我編寫了以下的函數來定位任何一個在WINNT.H之中定義的數據目錄。
// PEFILE.C
LPVOID WINAPI ImageDirectoryOffset(LPVOID lpFile,
DWORD dwIMAGE_DIRECTORY)
{
PIMAGE_OPTIONAL_HEADER poh;
PIMAGE_SECTION_HEADER psh;
int nSections = NumOfSections(lpFile);
int i = 0;
LPVOID VAImageDir;
/* 必須為0到(NumberOfRvaAndSizes-1)之間 */
if (dwIMAGE_DIRECTORY >= poh->NumberOfRvaAndSizes)
return NULL;
/* 獲得可選頭部和段頭部的偏移量 */
poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET(lpFile);
psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(lpFile);
/* 定位映像目錄的相對虛擬地址 */
VAImageDir = (LPVOID)poh->DataDirectory
[dwIMAGE_DIRECTORY].VirtualAddress;
/* 定位包含映像目錄的段 */
while (i++ < nSections)
{
if (psh->VirtualAddress <= (DWORD)VAImageDir &&
psh->VirtualAddress +
psh->SizeOfRawData > (DWORD)VAImageDir)
break;
psh++;
}
if (i > nSections)
return NULL;
/* 返回映像導入目錄的偏移量 */
return (LPVOID)(((int)lpFile +
(int)VAImageDir. psh->VirtualAddress) +
(int)psh->PointerToRawData);
}
該函數首先確認被請求的數據目錄入口數字,然后它分別獲取指向可選頭部和第一個段頭部的兩個指針。它從可選頭部決定數據目錄的虛擬地址,
然后它使用這個值來決定數據目錄定位在哪個段實體之中。如果適當的段實體已經被標識了,那么數據目錄特定的位置就可以通過將它的相對虛擬地址轉換為文件中
地址的方法來找到。
posted @
2010-10-12 16:38 小果子 閱讀(303) |
評論 (0) |
編輯 收藏
在STL中為了提供通用的操作而又不損失效率,我們用到了一種特殊的技巧,叫traits編程技巧。具體的來說,traits就是
通過定義一些結構體或類,并利用模板類特化和偏特化的能力,給類型賦予一些特性,這些特性根據類型的不同而異。在程序設計中可以使用這些traits來判
斷一個類型的一些特性,引發C++的函數重載機制,實現同一種操作因類型不同而異的效果。traits的編程技巧極度彌補了C++語言的不足 。
舉例:
現在定義一個__type_traits可以獲得類型的如下屬性:
1. 是否存在non-trivial default constructor
2. 是否存在non-trivial copy constructor
3. 是否存在non-trivial assignment operator
4. 是否存在non-trivial destructor
struct __true_type {
};
struct __false_type {
};
template <class _Tp>
struct __type_traits {
typedef __false_type has_trivial_default_constructor;
typedef __false_type has_trivial_copy_constructor;
typedef __false_type has_trivial_assignment_operator;
typedef __false_type has_trivial_destructor;
};
問題:為什么把對象的所有的屬性都定義為__false_type?
這樣是采用最保守的做法,先把所有的對象屬性都設置為__false_type,然后在針對每個基本數據類型設計特化的__type_traits,就可以達到預期的目的,如可以定義__type_traits<int>如下:
template <>
struct __type_traits<int> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
};
template <>
struct __type_traits<char> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
};
......
......
其他基本類型的traits也可以有相應的定義
__type_traits的偏特化版本
template <class _Tp>
struct __type_traits<_Tp*> {
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
typedef __true_type is_POD_type;
};
我們可以自定義__type_traits的特化版本
比如對與自定義的Shape類型,我們可以這樣定義__type_traits<Shape>
struct __type_traits<Shape> {
typedef __false_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
typedef __true_type is_POD_type;
};
如果編譯器夠厲害,我們甚至可以不用自己去定義特化的__type_traits,編譯器就能夠幫我們搞定:)
如何使用呢?
假設現在用個模板函數fun需要根據類型T是否有non-trivial constructor來進行不同的操作,可以這樣來實現:
template<class T>
void fun()
{
typedef typename __type_traits<T>::has_trivial_constructor _Trivial_constructor;
__fun(_Trivial_constructor()); // 根據得到的_Trivial_constructor來調用相應的函數
}
// 兩個重載的函數
void _fun(_true_type)
{
cout<<"fun(_true_type)called"<<endl;
}
void _fun(_false_type)
{
cout<<"fun(_false_type) called"<<endl;
}
//測試代碼
int main()
{
fun<char>();
fun<int>();
fun<char *>();
fun<double>();
}
posted @
2010-10-11 09:24 小果子 閱讀(1614) |
評論 (0) |
編輯 收藏
用template要求寫一個模板函數,返回值要求是參數類型,初步設計
template<typename T>
class AIter{
public:
AIter(T* p=0):ptr(p){};
T* ptr;
typedef T value_type;
T& operator*()const{
return *ptr;
}
T* operator->()const{
return ptr;
}
};
template<typename T>
typename T::value_type
func(T val){
return *val;
}
這方法一個缺陷就是對于不是class type的類型無能為力,比如原生指針,只有class type類型才能內嵌類型
改進--模板偏特化(template partial specialization)
聲明一個類型
template<typename T>
struct stl_iterator_traits{
typedef typename T::value_type value_type;
};
原先的func可以寫成這樣
template<typename T>
typename stl_iterator_traits<T>::value_type
func(T val){
return *val;
}
這樣還是處理不了
int* p=new int(3);
func(p);
原生指針類型,為其提供特化版本
template<typename T>
struct stl_iterator_traits<T*>{
typedef T value_type;
};
這樣就能完美解決剛才問題
但是對于指向常數對象的指針
stl_iterator_traits<const int*>::value_type
我們希望暫時存儲一個變量,但是我們獲取的類型是const int,聲明一個無法賦值的臨時變量無意義,所以我們在提供一個特化版本
template<typename T>
struct stl_iterator_traits<const T*>{
typedef T value_type;
};
iterator example:
#include <iterator>
//#using <mscorlib.dll>
#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>
//using namespace System;
using namespace std;
template<typename T>
class ListItem{
public:
ListItem(T value){
_value=value;
_next=NULL;
}
ListItem(){
_next=NULL;
_value=0;
}
T value()const{
return _value;
}
ListItem<T>* _next;
T _value;
};
template<class Item>
class ListIter:public iterator<std::forward_iterator_tag,Item>{
public:
Item* ptr;
ListIter(Item* p=0):ptr(p){};
Item& operator*()const{
return *ptr;
}
Item* operator->()const{
return ptr;
}
ListIter& operator++(){
ptr=ptr->_next;
return *this;
}
ListIter operator++(int){
ListIter tmp=*this;
++(*this);
return tmp;
}
bool operator==(const ListIter& iter)const{
return ptr==iter.ptr;
}
bool operator!=(const ListIter& iter)const{
return ptr!=iter.ptr;
}
};
template<typename T>
bool operator==(ListItem<T>& item,T value){
return item.value()==value;
}
template<typename T>
class List{
public:
typedef ListIter<ListItem<T> > iterator;
List(){
_end=new ListItem<T>();
_front=0;
}
void insert_front(T value){
ListItem<T>* item=new ListItem<T>(value);
if(empty()){
item->_next=_end;
_front=item;
}else{
item->_next=_front;
_front=item;
}
};
bool empty(){
return _front==NULL;
}
void insert_end(T value){
//ListItem<T>* item=new ListItem<T>(value);
if(empty()){
_front=_end;
_end->_value=value;
_end->_next=new ListItem<T>();
_end=_end->_next;
}else{
_end->_value=value;
_end->_next=new ListItem<T>();
_end=_end->_next;
}
};
void display(ostream& os=cout){
ListItem<T>* head=_front;
while(head!=_end){
cout<<head->value()<<endl;
head=head->_next;
}
};
ListItem<T>* front(){
return _front;
}
private:
ListItem<T>* _end;
ListItem<T>* _front;
long _size;
};
template<typename T>
struct stl_iterator_traits{
typedef typename T::value_type value_type;
};
template<typename T>
struct stl_iterator_traits<T*>{
typedef T value_type;
};
template<typename T>
class AIter{
public:
AIter(T* p=0):ptr(p){};
T* ptr;
typedef T value_type;
T& operator*()const{
return *ptr;
}
T* operator->()const{
return ptr;
}
};
template<typename T>
typename stl_iterator_traits<T>::value_type
func(T val){
return *val;
}
int _tmain(int argc, _TCHAR* argv[])
{
List<int> list;
for(int i=0;i<5;i++){
list.insert_front(i);
list.insert_end(i+2);
}
list.display();
List<int>::iterator begin(list.front());
List<int>::iterator end;
List<int>::iterator iter;
//vector<int>::iterator itere;
AIter<int> it(new int(2));
iter=find(begin,end,2);
cout<<iter->value()<<endl;
//list.insert_end(1);
//list.insert_end(2);
//list.display();
//list.insert_end(
return 0;
}
現在對于class type 迭代器AIter,還是原生指針int* 或const int*,都能獲取正確類型int
stl規定,每個迭代器都要自己內嵌型別定義的方式定義出相應型別
(待續...)
posted @
2010-10-09 13:23 小果子 閱讀(227) |
評論 (0) |
編輯 收藏
摘要: 第一章 從C轉向C++
對每個人來說,習慣C++需要一些時間,對于已經熟悉C的程序員來說,這個過程尤其令人苦惱。因為C是C++的子集,所有的C的技術都可以繼續使用,但很多用起來又不太合適。例如,C++程序員會認為指針的指針看起來很古怪,他們會問:為什么不用指針的引用來代替呢?C
是一種簡單的語言。它真正提供的只有有宏、指針、結構、數組和函數。不管什么問題,C都靠宏、指針、結構、數組和函數來解決...
閱讀全文
posted @
2010-10-08 09:19 小果子 閱讀(804) |
評論 (0) |
編輯 收藏
今天看STL源碼遇到一個問題:
template <class _T1, class _T2>
inline void _Construct(_T1* __p, const _T2& __value) {
new ((void*) __p) _T1(__value);
}
上網搜到了一些文章,分享了:
原帖地址: http://www.ksarea.com/articles/20080124_cc.html
"placement new"?
它
到底是什么東東呀?我也是最近幾天才聽說,看來對于C++我還差很遠呀!placement new 是重載operator
new的一個標準、全局的版本,它不能被自定義的版本代替(不像普通的operator new和operator
delete能夠被替換成用戶自定義的版本)。
它的原型如下:
void *operator new( size_t, void *p ) throw() { return p; }
首先我們區分下幾個容易混淆的關鍵詞:new、operator new、placement new
new和delete操作符我們應該都用過,它們是對堆中的內存進行申請和釋放,而這兩個都是不能被重載的。要實現不同的內存分配行為,需要重載operator new,而不是new和delete。
看如下代碼:
class MyClass {…};
MyClass * p=new MyClass;
這里的new實際上是執行如下3個過程:
1. 調用operator new分配內存 ;2. 調用構造函數生成類對象;3. 返回相應指針。
operator new就像operator+一樣,是可以重載的,但是不能在全局對原型為void operator new(size_t
size)這個原型進行重載,一般只能在類中進行重載。如果類中沒有重載operator new,那么調用的就是全局的::operator
new來完成堆的分配。同理,operator new[]、operator delete、operator
delete[]也是可以重載的,一般你重載的其中一個,那么最后把其余的三個都重載一遍。
至于placement new才是本文的重點。其實它也只是operator
new的一個重載的版本,只是我們很少用到它。如果你想在已經分配的內存中創建一個對象,使用new時行不通的。也就是說placement
new允許你在一個已經分配好的內存中(棧或者堆中)構造一個新的對象。原型中void*p實際上就是指向一個已經分配好的內存緩沖區的的首地址。
我們知道使用new操作符分配內存需要在堆中查找足夠大的剩余空間,這個操作速度是很慢的,而且有可能出現無法分配內存的異常(空間不夠)。
placement
new就可以解決這個問題。我們構造對象都是在一個預先準備好了的內存緩沖區中進行,不需要查找內存,內存分配的時間是常數;而且不會出現在程序運行中途
出現內存不足的異常。所以,placement new非常適合那些對時間要求比較高,長時間運行不希望被打斷的應用程序。
使用方法如下:
1. 緩沖區提前分配
可以使用堆的空間,也可以使用棧的空間,所以分配方式有如下兩種:
class MyClass {…};
char *buf=new char[N*sizeof(MyClass)+sizeof(int)];或者char buf[N*sizeof(MyClass)+sizeof(int)];
2. 對象的構造
MyClass * pClass=new(buf) MyClass;
3. 對象的銷毀
一旦這個對象使用完畢,你必須顯式的調用類的析構函數進行銷毀對象。但此時內存空間不會被釋放,以便其他的對象的構造。
pClass->~MyClass();
4. 內存的釋放
如果緩沖區在堆中,那么調用delete[] buf;進行內存的釋放;如果在棧中,那么在其作用域內有效,跳出作用域,內存自動釋放。
注意:
- 在C++標準中,對于placement operator new []有如下的說明: placement operator
new[] needs implementation-defined amount of additional storage to save a
size of array. 所以我們必須申請比原始對象大小多出sizeof(int)個字節來存放對象的個數,或者說數組的大小。
- 使用方法第二步中的new才是placement new,其實是沒有申請內存的,只是調用了構造函數,返回一個指向已經分配好的內存的一個指針,所以對象銷毀的時候不需要調用delete釋放空間,但必須調用析構函數銷毀對象。
placement new 是重載operator new 的一個標準、全局的版本,它不能夠被自定義的版本代替(不像普通版本的operator new 和 operator delete能夠被替換)。
void *operator new( size_t, void *p ) throw() { return p; }
placement new的執行忽略了size_t參數,只返還第二個參數。其結果是允許用戶把一個對象放到一個特定的地方,達到調用構造函數的效果。
和其他普通的new不同的是,它在括號里多了另外一個參數。比如:
Widget * p = new Widget; - - - - - - - - - //ordinary new
pi = new (ptr) int; pi = new (ptr) int; //placement new
括
號里的參數ptr是一個指針,它指向一個內存緩沖器,placement new將在這個緩沖器上分配一個對象。Placement new的返回值是這
個被構造對象的地址(比如括號中的傳遞參數)。placement new主要適用于:在對時間要求非常高的應用程序中,因為這些程序分配的時間是確定
的;長時間運行而不被打斷的程序;以及執行一個垃圾收集器 (garbage collector)。
? new 、operator new 和 placement new 區別
new :不能被重載,其行為總是一致的。它先調用operator new分配內存,然后調用構造函數初始化那段內存。
operator new:要實現不同的內存分配行為,應該重載operator new,而不是new。
delete和operator delete類似。
placement new:只是operator new重載的一個版本。它并不分配內存,只是返回指向已經分配好的某段內存的一個指針。因此不能刪除它,但需要調用對象的析構函數。
? new 操作符的執行過程
1. 調用operator new分配內存 ;
2. 調用構造函數生成類對象;
3. 返回相應指針。
operator new
就像operator+一樣,是可以重載的。如果類中沒有重載operator new,那么調用的就是全局的::operator new來完成堆的分
配。同理,operator new[]、operator delete、operator delete[]也是可以重載的,其實
operator new也是operator new的一個重載的版本,只是很少用而已。如果你想在已經分配的內存中創建一個對象,使用new時行不通
的。也就是說placement new允許你在一個已經分配好的內存中(棧或者堆中)構造一個新的對象。原型中void*p實際上就是指向一個已經分配
好的內存緩沖區的的首地址。
? Placement new 存在的理由
1.用Placement new 解決buffer的問題
問
題描述:用new分配的數組緩沖時,由于調用了默認構造函數,因此執行效率上不佳。若沒有默認構造函數則會發生編譯時錯誤。如果你想在預分配的內存上創建
對象,用缺省的new操作符是行不通的。要解決這個問題,你可以用placement new構造。它允許你構造一個新對象到預分配的內存上。
2.增大時空效率的問題
使用new操作符分配內存需要在堆中查找足夠大的剩余空間,顯然這個操作速度是很慢的,而且有可能出現無法分配內存的異常(空間不夠)。
placement new
就可以解決這個問題。我們構造對象都是在一個預先準備好了的內存緩沖區中進行,不需要查找內存,內存分配的時間是常數;而且不會出現在程序運行中途出現內
存不足的異常。所以,placement new非常適合那些對時間要求比較高,長時間運行不希望被打斷的應用程序。
? 使用步驟
在很多情況下,placement new的使用方法和其他普通的new有所不同。這里提供了它的使用步驟。
第一步 緩存提前分配
有三種方式:
1.為了保證通過placement new使用的緩存區的memory alignmen(內存隊列)正確準備,使用普通的new來分配它:在堆上進行分配
class Task ;
char * buff = new [sizeof(Task)]; //分配內存
(請注意auto或者static內存并非都正確地為每一個對象類型排列,所以,你將不能以placement new使用它們。)
2.在棧上進行分配
class Task ;
char buf[N*sizeof(Task)]; //分配內存
3.還有一種方式,就是直接通過地址來使用。(必須是有意義的地址)
void* buf = reinterpret_cast<void*> (0xF00F);
第二步:對象的分配
在剛才已分配的緩存區調用placement new來構造一個對象。
Task *ptask = new (buf) Task
第三步:使用
按照普通方式使用分配的對象:
ptask->memberfunction();
ptask-> member;
//...
第四步:對象的析構
一旦你使用完這個對象,你必須調用它的析構函數來毀滅它。按照下面的方式調用析構函數:
ptask->~Task(); //調用外在的析構函數
第五步:釋放
你可以反復利用緩存并給它分配一個新的對象(重復步驟2,3,4)如果你不打算再次使用這個緩存,你可以象這樣釋放它:
delete [] buf;
跳過任何步驟就可能導致運行時間的崩潰,內存泄露,以及其它的意想不到的情況。如果你確實需要使用placement new,請認真遵循以上的步驟。
posted @
2010-09-30 17:01 小果子 閱讀(1096) |
評論 (0) |
編輯 收藏