C++應用程式在Windows下的編譯、連結:第二部分COFF/PE檔案結構

yangxi_001發表於2016-11-17

2.1概述

在windows作業系統下,可執行檔案的儲存格式是PE格式;在Linux作業系統下,可執行檔案的儲存格式的WLF格式。它們都是COFF格式檔案的變種,都是從COFF格式的檔案演化而來的。

在windows平臺下,目標檔案(.obj),靜態庫檔案(.lib)使用COFF格式儲存;而可執行檔案(.exe),動態連結庫檔案(.dll)使用PE格式儲存。靜態庫檔案其實就是一堆目標檔案的集合。

在“WinNT.h”標頭檔案中定義了COFF格式檔案,以及PE格式檔案的資料結構。這些定義是一系列的結構體,列舉,以及#define巨集定義。在ImageHlp.dll中定義了編輯和讀取PE檔案內容的Win32API。

在64位Windows作業系統下,PE格式檔案被做了少部分修改。沒有新的欄位定義被加入,並且去除了一些欄位的定義,同時將欄位的寬度從32位擴充到64位。64位windows作業系統下的PE格式檔案被命名為:PE32+。

2.2COFF檔案的結構

2.2.1總體結構圖

COFF檔案的總體結構如下圖所示:

從檔案內容上來看,COFF檔案由二進位制資料組成。這些二進位制資料從檔案的零位置開始,依次儲存,直到檔案末尾。從資料結構的角度來看,這些二進位制資料又分別屬於不同的結構體或者結構體陣列。這些結構體被定義在“WinNT.h”標頭檔案中。

在COFF檔案中,這些結構體或結構體陣列分別表示不同的含義,記錄著COFF檔案中的不同內容。從檔案的頂端開始,依次儲存了檔案頭,可選頭,段表,段資料,重定位表,行號表,符號表,以及字串表的資訊。這些結構體資料之間存在關聯關係。比如:檔案頭資訊中儲存了符號表的開始位置,以及段表中陣列元素的個數;在段表中儲存了各個段的位置,重定位表的位置,行號表的位置;重定位表中的項會關聯到符號表中的某個符號;而符號表中某個符號的名稱可能會儲存在字串表中。

使用dumpbin工具可以將目標檔案的內容匯出,具體的命令格式如下:

Dumpbin /all DemoMath.obj >DemoMath.txt

    在上面的命令中,將目標檔案“DemoMath.obj”的所有內容匯出到文字檔案“DemoMath.txt”中。命令選項“/all”表示匯出所有內容,命令選項“>”表示將匯出的內容儲存到檔案中。

2.2.2檔案頭

檔案頭以一個結構體的形式儲存在COFF檔案的開始位置,佔20個位元組的大小。每一個COFF格式的二進位制檔案都必須包含一個檔案頭,它用來儲存COFF檔案的基本資訊,如:檔案標識,各個表的位置等。

使用dumpbin工具匯出“DemoMath.obj”目標檔案的內容後,檔案頭部分的資訊內容如下:

Dump of file demomath.obj

File Type: COFF OBJECT                                //表示該檔案格式為COFF格式

FILE HEADER VALUES                                 //以下依次是檔案頭中各項的值

             14C machine (x86)                      //魔數

              20 number of sections                  //段的數量

        519AFB7E time date stamp Tue May 21 12:43:42 2013   //建立時間

            288A file pointer to symbol table           //符號表的位置

              83 number of symbols                 //符號的數量

               0 size of optional header              //可選頭的大小

               0 characteristics                     //檔案屬性標記。零表示有重定位資訊,有符號表,有行號,不可執行,具體解釋可見“Characteristics欄位的取值情況表”的描述。

在“WinNT.h”標頭檔案中,檔案頭被定義為IMAGE_FILE_HEADER型別,具體的定義形式如下:

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        //檔案頭的大小

   在檔案頭中,各個欄位的詳細解釋如下表所示:

欄位名稱

型別

描述

Machine

Word

魔法數字,在i386平臺中,該值為0x014c。這是一個平臺的標識。

NumberOfSections

Word

段的數量。段表的大小由它確定。

段表的大小 =  Sizeof(IMAGE_SECTION_HEADER)

 * NumberOfSections

TimeDateStamp

Dword

該欄位是一個時間戳,用來記錄COFF檔案建立的時間。當COFF檔案作為一個可執行檔案的時候,該值被用來當作加密用的比對標識。

PointerToSymbolTable

Dword

符號表在檔案中的偏移量,該偏移量從檔案的零位置為基準。使用該值可以確定符號表的第一個位元組的位置。

NumberOfSymbols

Dword

符號表中符號的個數。

SizeOfOptionalHeader

Word

可選頭的大小。通常為零。通過此值可定位段表。

Characteristics

Word

檔案的屬性標記,它標記了檔案的型別,以及檔案中所儲存的資料的資訊。該標記的詳細說明見下表。

   Characteristics欄位的取值情況如下表所示:

名稱

說明

F_RELFLG

0x0001

無重定位資訊標記。值為1表示無重定位資訊。在目標檔案中,該值為1,可執行檔案中,該值為零。

F_EXEC

0x0002

可執行標記。值為2表示該檔案中所有符號都已經被解析完畢,可以被執行。在目標檔案中,該值為零。

F_LNNO

0x0004

無行號標記。值為4表示該檔案中沒有行號表

F_LSYMS

0x0008

無符號標記。值為8表示該檔案中沒有符號表

F_AR32WR

0x0100

該標記指出檔案是 32 位的 Little-Endian COFF 檔案。

 

2.2.3可選頭

   該資料結構為可選資料,在目標檔案中不存在此資料結構。只有當COFF檔案作為可執行檔案存在的時候,該資料結構才有意義。

2.2.4段表

段表是各個段的目錄,用於檢索各個段的資訊。它以結構體陣列的形式儲存在可選頭或者檔案頭的後面。在段表中,每一項的大小是36個位元組,陣列元素的個數記錄在檔案頭的“NumberOfSections”欄位中。

段的劃分是基於各組資料的共同屬性,而不是邏輯概念。每段是一塊擁有共同屬性的資料,比如程式碼/資料、讀/寫等。如果COFF檔案中的資料/程式碼擁有相同屬性,它們就能被歸入同一段中。

在段表中記錄了各個段在段資料區域中的位置(相對檔案首位置的絕對偏移),以及各段重定位資訊在重定位表中的位置。

 在COFF格式的目標檔案中,每一個函式形成一個.text段,因此會有多個名為.text的段。在使用工具dumpbin匯出“DemoMath.obj”目標檔案的內容後,除了列出.text段的同時,也將與該段相對應的重定位段一起列出。具體內容如下:

SECTION HEADER #9

   .text name                     //段表資訊的內容

       0 physical address

       0 virtual address

      2A size of raw data

    16B0 file pointer to raw data (000016B0 to 000016D9)

    16DA file pointer to relocation table

       0 file pointer to line numbers

       1 number of relocations

       0 number of line numbers

60501020 flags

         Code

         COMDAT; sym= "int __cdecl GetOperTimes(void)" (?GetOperTimes@@YAHXZ)

         16 byte align

         Execute Read

RAW DATA #9                      //段的二進位制資料

  00000000: 55 8B EC 81 EC C0 00 00 00 53 56 57 8D BD 40 FF  U.ì.ìà...SVW.?@?

  00000010: FF FF B9 30 00 00 00 B8 CC CC CC CC F3 AB A1 00  ??10...?ììììó??.

  00000020: 00 00 00 5F 5E 5B 8B E5 5D C3                    ..._^[.?]?

 

RELOCATIONS #9                   //段的重定位資訊

                                                Symbol    Symbol

 Offset    Type              Applied To         Index     Name

 --------  ----------------  -----------------  --------  ------

 0000001F  DIR32                      00000000         F  ?nOperTimes@@3HA (int nOperTimes)

在“WinNT.h”標頭檔案中,檔案頭被定義為IMAGE_SECTION_HEADER型別,具體的定義形式如下:

#define IMAGE_SIZEOF_SHORT_NAME              8

typedef 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

在段表中,各個欄位的詳細解釋如下表:

欄位名稱

型別

描述

Name

BYTE

節的ASCII名稱。節名不保證一定是以NULL結尾的。如果你指定了長於8個字元的節名,連結器會把它截短為8個字元。在OBJ檔案中存在一個機制允許更長的節名。節名通常以一個句點開始,但這並不是必須的。節名中有一個“$”時連結器會對之進行特殊處理。前面帶有“$”的相同名字的節將會被合併。合併的順序是按照“$”後面字元的字母順序進行合併的

PhysicalAddress

DWORD

 

VirtualSize

DWORD

指出實際被使用的節的大小。這個域的值可以大於或小於SizeOfRawData域的值。如果VirtualSize的值大,SizeOfRawData就是可執行檔案中已初始化資料的大小,剩下的位元組用0填充。在OBJ檔案中這個域被設為0。

VirtualAddress

DWORD

在可執行檔案中,是節被載入到記憶體中後的RVA。在OBJ檔案中應該被設為0

SizeOfRawData

DWORD

在可執行檔案或OBJ檔案中該節所佔用的位元組大小。對於可執行檔案,這個值必須是PE頭中給出的檔案對齊值的倍數。如果是0,則說明這個節中的資料是未初始的。

PointerToRawData

DWORD

節在磁碟檔案中的偏移。對於可執行檔案,這個值必須是PE頭部給出的檔案對齊值的倍數。

PointerToRelocations

DWORD

節的重定位資料的檔案偏移。只用於OBJ檔案,在可執行檔案中被設為0。對於OBJ檔案,如果這個域的值不為0的話,它就指向一個IMAGE_RELOCATION結構陣列。

PointerToLinenumbers

DWORD

節的COFF樣式行號的檔案偏移。如果非0,則指向一個IMAGE_LINENUMBER結構陣列。只在COFF行號被生成時使用。

NumberOfRelocations

WORD

PointerToRelocations 指向的重定位的數目。在可執行檔案中應該是0。

NumberOfLinenumbers

WORD

NumberOfRelocations 域指向的行號的數目。只在COFF行號被生成時使用。

Characteristics

WORD

被或到一起的一些標記,用來表示節的屬性。這些標記中很多都可以通過連結器選項/SECTION來設定。

Characteristics欄位的取值情況如下表所示:

描述

IMAGE_SCN_CNT_CODE

節中包含程式碼。

IMAGE_SCN_MEM_EXECUTE

節是可執行的。

IMAGE_SCN_CNT_INITIALIZED_DATA

節中包含已初始化資料。

IMAGE_SCN_CNT_UNINITIALIZED_DATA

節中包含未初始化資料。

IMAGE_SCN_MEM_DISCARDABLE

節可被丟棄。用於儲存連結器使用的一些資訊,包括.debug$節。

IMAGE_SCN_MEM_NOT_PAGED

節不可被頁交換,因此它總是存在於實體記憶體中。經常用於核心模式的驅動程式。

IMAGE_SCN_MEM_SHARED

包含節的資料的實體記憶體頁在所有用到這個可執行體的程式之間共享。因此,每個程式看到這個節中的資料值都是完全一樣的。這對一個程式的所有例項之間共享全域性變數很有用。要使一個節共享,可使用/section:name,S 連結器選項。

IMAGE_SCN_MEM_READ

節是可讀的。幾乎總是被設定。

IMAGE_SCN_MEM_WRITE

節是可寫的。

IMAGE_SCN_LNK_INFO

節中包含連結器使用的資訊。只在OBJ檔案中存在。

IMAGE_SCN_LNK_REMOVE

節中的資料不會成為映像的一部分。只出現在OBJ檔案中。

IMAGE_SCN_LNK_COMDAT

節中的內容是公共資料(comdat)。公共資料是指可被定義在多個OBJ檔案中的資料。連結器將選擇一個包含到可執行檔案中。Comdat 對於支援C++模板函式和在函式級別上的連結是至關重要的。Comdat節只出現在OBJ檔案中。

IMAGE_SCN_ALIGN_XBYTES

在最終的可執行檔案中這個節中資料的對齊大小。它可有許多取值(_4BYTES,_8BYTES,_16BYTES等)。如果沒有被指定,預設是16位元組。這些標記只在OBJ檔案中被設定。

 

2.2.5重定位表

在編譯階段,將某些原始檔編譯成目標檔案的時候,在目標檔案中,某些被呼叫函式或者資料的位置是無法確定的。這時候,編譯器將這些被呼叫的函式或者資料的地址設定為一個預設的假值。在連結階段,當能夠確定這些被呼叫函式或資料的地址的時候,再用真實的地址來替換這些假值。我們將這個過程叫做重定位。

使用工具dumpbin將目標檔案main.obj的內容輸出為彙編格式的檔案後,可以觀察到這些假值的設定情況,以及需要重定位的位置。命令格式如下:

Dumpbin /disasm main.obj >mainasm.txt

輸入的彙編檔案的一部分內容如下:

//objMath.SubData(nGlobalData,3);以下是執行該函式呼叫的彙編程式碼

00000080: 8B F4              mov         esi,esp

  00000082: 83 EC 08           sub         esp,8

  00000085: DD 05 00 00 00 00   fld         qword ptr [__real@4008000000000000]

  0000008B: DD 1C 24           fstp        qword ptr [esp]

  0000008E: DB 05 00 00 00 00    fild        dword ptr [?nGlobalData@@3HA]

  00000094: 83 EC 08            sub         esp,8

  00000097: DD 1C 24           fstp        qword ptr [esp]

  0000009A: 8D 4D EC           lea         ecx,[ebp-14h]

  0000009D: FF 15 00 00 00 00    call dword ptr [__imp_?SubData@DemoMath@@QAEXNN@Z]

  000000A3: 3B F4              cmp         esi,esp

  000000A5: E8 00 00 00 00       call        __RTC_CheckEsp

在上面的程式碼中,地址0x0000008E處引用了全域性變數nGlobalData,指令格式為:DB 05 00 00 00 00。DB 05為fild彙編指令的二進位制碼,而後邊四個位元組的零(紅色表示)是nGlobalData的地址,這個地址是個臨時的假值。

在當前目標檔案中,如果被呼叫的函式或資料位於另外一個目標檔案中,那麼在連結的時候需要對被呼叫的函式或資料執行重定位;如果被呼叫的函式或資料是全域性函式或者全域性變數,那麼在連結的時候,需要對該全域性函式或全域性變數執行重定位。在示例程式碼中,全域性變數:nGlobalData, nOperTimes,全域性函式:GetOperTimes()在連結的時候需要執行重定位。

重定位表只存在於目標檔案中,它儲存了各個段的重定位資訊。在每個段的段表中,記錄了該段重定位資訊在重定位表中的位置(相對於檔案首位置的偏移)。

使用工具dumpbin將目標檔案的內容匯出後,如果某個程式碼段存在重定位資訊(該程式碼段引用過了全域性符號或者外部符號),那麼在該程式碼段的後面就會列出該程式碼段的重定位資訊。該重定位資訊是重定位表中的一個片段。示例如下:

SECTION HEADER #16           //程式碼段的資訊摘要。Subdata函式所在的程式碼段

   .text name

       0 physical address

       0 virtual address

      5C size of raw data

    2088 file pointer to raw data (00002088 to 000020E3)

    20E4 file pointer to relocation table

       0 file pointer to line numbers

       4 number of relocations

       0 number of line numbers

60501020 flags

         Code

         COMDAT; sym= "public: void __thiscall DemoMath::SubData(double,double)"

         16 byte align

         Execute Read

 

RAW DATA #16                  //程式碼段的二進位制資料內容,紅色字型表示需要重定位的位置。被//VirtualAddress欄位指定。

  00000000: 55 8B EC 81 EC CC 00 00 00 53 56 57 51 8D BD 34  U.ì.ìì...SVWQ.?4

  00000010: FF FF FF B9 33 00 00 00 B8 CC CC CC CC F3 AB 59  ???13...?ììììó?Y

  00000020: 89 4D F8 A1 00 00 00 00 83 C0 01 A3 00 00 00 00  .M??.....à.£....

  00000030: DD 45 08 DC 65 10 83 EC 08 DD 1C 24 8B 45 F8 8B  YE.üe..ì.Y.$.E?.

  00000040: 08 E8 00 00 00 00 5F 5E 5B 81 C4 CC 00 00 00 3B  .è...._^[.?ì...;

  00000050: EC E8 00 00 00 00 8B E5 5D C2 10 00              ìè.....?]?..

RELOCATIONS #16               //程式碼段的重定位資訊。

                                                   Symbol     Symbol

 Offset     Type                    Applied To        Index      Name

 --------  ----------------  -----------------  --------  ------

 00000024  DIR32                     00000000         F  ?nOperTimes@@3HA (int nOperTimes)

 0000002C  DIR32                     00000000         F  ?nOperTimes@@3HA (int nOperTimes)

 00000042  REL32                     00000000        59  ?OutPutInfo@DemoOutPut@@QAEXN@Z

 00000052  REL32                     00000000        3F  __RTC_CheckEsp

 

這是類DemoMath的成員函式:SubData()所在的程式碼段的重定位資訊,在該重定位資訊中,需要重定位的符號是:全域性變數nOperTimes和外部函式OutPutInfo()。在上面程式碼中,紅色字型的部分被重定位表中的欄位:VirtualAddress指向,標記了需要重定位的位置。

在“WinNT.h”標頭檔案中,檔案頭被定義為IMAGE_RELOCATION型別,具體的定義形式如下:

typedef struct _IMAGE_RELOCATION

{

union

{

        DWORD   VirtualAddress;

        DWORD   RelocCount;      // Set to the real count when IMAGE_SCN_LNK_NRELOC_OVFL is set

    };

    DWORD   SymbolTableIndex;

    WORD    Type;

} IMAGE_RELOCATION;

typedef IMAGE_RELOCATION UNALIGNED *PIMAGE_RELOCATION;

在重定位表中,各個欄位的詳細解釋如下表:

欄位名稱

型別

描述

VirtualAddress

DWORD

該欄位指向程式碼段中的一個地址。該地址所包含的資料是需要重定位的符號的地址。這部分資料將要被重定位修正。該欄位指向了這個資料的第一個位元組。上面的示例中,紅色字型標記的部分被該欄位指向。

RelocCount

DWORD

 

SymbolTableIndex

DWORD

需要重定位的符號在符號表中的索引,該值為符號表陣列的索引。通過該值可以檢索符號在符號表中的資訊。

Type

WORD

一般分兩種型別。DIR32表示32位絕對地址;REL32表示32位相對地址。在執行重定位的時候,對於絕對地址型別,將被替換為符號的絕對地址;而對於相對地址型別,將被替換為符號的相對地址,即:符號相對於被修正位置的地址差。

 

2.2.6行號表

行號表描述了二進位制程式碼與原始碼行號之間的關係,除錯階段使用。在“WinNT.h”標頭檔案中,檔案頭被定義為IMAGE_RELOCATION型別,具體的定義形式如下:

typedef struct _IMAGE_LINENUMBER

{

union

{

        DWORD   SymbolTableIndex;    // Symbol table index of function name if Linenumber is 0.

        DWORD   VirtualAddress;      // Virtual address of line number.

    } Type;

    WORD    Linenumber;             // Line number.

} IMAGE_LINENUMBER;

typedef IMAGE_LINENUMBER UNALIGNED *PIMAGE_LINENUMBER;

在行號表中,各個欄位的詳細解釋如下表:

欄位名稱

型別

描述

SymbolTableIndex

DWORD  

符號在符號表中的索引

VirtualAddress

DWORD  

符號的地址值

Linenumber

WORD   

行號

 

2.2.7符號表

在編譯階段的詞法分析過程中,編譯器掃描整個C++原始碼,將原始碼中的函式名稱,變數名稱收集起來,然後寫入符號表中。在符號表中主要包含如下內容:函式名稱,變數名稱,段的名稱,以及一些常量資訊,這些名稱被統稱為符號。

符號表中的資訊被用於靜態連結階段,用來進行被引用的函式或變數的地址重定位。每一個目標檔案中都會包含一個符號表。在該符號表中的符號,要麼是在該目標檔案中定義的函式名稱或變數名稱;要麼是被該目標檔案引用的,定義於其他目標檔案中的函式名稱或變數名稱。在靜態連結階段,多個目標檔案進行連結的時候,存在於這些目標檔案中的符號表會被合併到一起,形成一個全域性符號表。在C++原始碼中出現的所有符號都應該能在全域性符號表中被查詢到。

將符號表中的符號進行分類,具體的分類情況如下:

  • 定義在本目標檔案中的全域性符號,該符號可能會被其他目標檔案引用;
  • 在本目標檔案中引用的全域性符號,該符號定義在其他目標檔案中,該符號被稱為外部符號;
  • 段的名稱,由編譯器加入到符號表中。該符號的值就是段的起始地址;
  • 區域性符號。在編譯單元內部可見,連結的時候忽略。

    在執行連結的時候,只關注前兩種型別的符號。

如果符號的名稱小於8個位元組,那麼將該符號的名稱直接儲存在符號表中;如果符號的名稱大於8個位元組,那麼將符號的名稱儲存在字串表中,原來符號表中儲存符號名稱的地方儲存了一個地址偏移量,該地址偏移量指向了字串表中符號名稱的位置。

根據符號儲存型別以及符號在段中位置的不同,符號的值有不同的解釋。

使用工具dumpbin將DemoMath.obj的內容匯出以後,其符號表中的一部分的內容描述如下:

000 00847809 ABS    notype       Static       | @comp.id    //絕對值常量

001 00000001 ABS    notype       Static       | @feat.00     //絕對值常量

002 00000000 SECT1  notype       Static       | .drectve      //段名稱

    //段名稱符號下面緊跟段的資訊。每行佔用一個符號索引的位置,所以符號索引不是連續的。

    Section length  201, #relocs    0, #linenums    0, checksum        0 

Relocation CRC 00000000

005 00000000 SECT4  notype       External     | ?nOperTimes@@3HA (int nOperTimes)  //變數

006 00000000 SECT1A  notype ()    External     | ?DivData@DemoMath@@QAEXNN@Z  //函式

007 00000000 UNDEF  notype ()    External     | ?OutPutInfo@DemoOutPut@@QAEXPBD@Z //外部函式

 

   在上面的示例中,從左到右各欄位的含義依次是:符號結構體所在陣列的索引,符號大小,符號在段中位置,符號型別,符號的儲存型別,符號名稱。在該符號表的內容中,列出了全域性變數名:nOperTimes,類成員函式名:DivData,被引用的外部函式名:OutPutInfo。段的名稱也被作為一個符號寫入到符號表中,上面示例中的“.drectve”即為一個段的名稱。

  在“WinNT.h”標頭檔案中,檔案頭被定義為IMAGE_SYMBOL型別,具體的定義形式如下:

typedef struct _IMAGE_SYMBOL

{

union

{

        BYTE    ShortName[8];

        Struct

 {               

            DWORD   Short;     // if 0, use LongName

            DWORD   Long;      // offset into string table

        } Name;

        DWORD   LongName[2];    // PBYTE [2]

    } N;

    DWORD   Value;

    SHORT   SectionNumber;

    WORD    Type;

    BYTE    StorageClass;

    BYTE    NumberOfAuxSymbols;

} IMAGE_SYMBOL;

typedef IMAGE_SYMBOL UNALIGNED *PIMAGE_SYMBOL;

在符號表中,各個欄位的詳細解釋如下表:

欄位名稱

型別

描述

ShortName

BYTE

小於8個位元組的符號名稱儲存於此。

Short

DWORD

0表示符號名稱位於字串表中。

Long

DWORD

符號名稱在字串表中的偏移量。

LongName

DWORD

 

Value

DWORD

符號的值。對於變數或函式來說,符號值就是它們的地址。根據符號儲存型別的不同,符號值有不同的解釋。

SectionNumber

SHORT

符號所在的段落。ABS表示符號是個絕對值,是個常量;UNDEF表示符號是未定義的,即該符號的定義在其他段中;SECT1表示該符號位於編號為1的段中。

Type

WORD

符號的型別。Notype表示變數;notype()表示函式。

StorageClass

BYTE

符號的儲存型別。Static表示區域性變數,檔案內部可見;external表示全域性變數,全域性範圍內可見。

NumberOfAuxSymbols

BYTE

附加記錄的數量。

 

符號的值的具體含義需要根據符號所在的段落(SectionNumber)以及符號的儲存型別(StorageClass)來確定,這三者之間的具體關係如下表所示:

StorageClass

SectionNumber

Value

Static

SECTn(n為1,2,3…)

如果值不為零,表示符號在段內偏移。

SECTn(n為1,2,3…)

如果值為零,表示這個符號為段名。

ABS

常量的值。

External

UNDEF

符號為全域性變數/函式,符號定義在外部檔案中,值待定。

SECTn(n為1,2,3…)

符號為全域性變數/函式,符號定義在當前檔案中,值表示符號在段內偏移。

 

2.2.8字串表

字串表用來儲存長度大於8個位元組的符號名稱。字串表的前4個位元組表示字串的長度,後面的緊跟字串的內容,它以位元組為單位,以’\0’作為字串的結束符。這裡的字串長度不僅僅是字串自身的長度(字串內容+’\0’),還包括前面4個位元組的該資料自身的長度。

2.2.9各資料結構之間的關係

在COFF檔案所包含的資料結構中,各個資料結構之間的關係如下圖所示:

重定位表和符號表之間通過符號表的索引進行關聯;在檔案頭中儲存了可選頭的大小和段表所包含專案的數量,通過計算可以確定段表的起始位置和結束位置。段表起始位置=檔案頭大小+可選頭大小;其他關係通過相對檔案首位置的偏移表示。

2.3Lib檔案的結構

2.3.1總體結構圖

靜態連結庫就是一組目標檔案的集合,當執行靜態連結的時候,被選定的目標檔案的內容就會被合併到相關的Pe檔案中去。靜態連結庫的總體結構如下圖所示:

靜態連結庫以簽名開始,簽名的資料內容為“(!<arch>\n”,長8個位元組。緊跟在簽名後面的是三個特別成員,分別是第一連結器節,第二連結器節,以及長名稱節。在這三個特別成員之後,直到檔案結束,儲存的都是目標檔案節的內容。

第一連結器節,第二連結器節,長名稱節,以及目標檔案節的資料結構都是由頭資料+節資料這樣的資料結構組成的。

第一連結器節。在靜態連結庫中必須存在該節,它包含了靜態連結庫中所有的符號名以及這些符號在靜態連結庫檔案中的偏移;

第二連結器節。在靜態連結庫中該節可選,它包含了與第一連結器節相同的內容,但是它的內容是有序的,通過它查詢符號要比在第一連結器節中查詢的快;

長名稱節。在靜態連結庫中該節可選,它是一個字串表,用於儲存名稱大於16個位元組的目標檔案的名稱。在目標檔案節中,如果目標檔案的名稱小於16個位元組,那麼這個名稱會被儲存在標頭檔案的名稱域;如果這個名稱大於16個位元組,那麼這個名稱就會被儲存到這裡。而在標頭檔案的名稱域儲存的則是該字串在長名稱節的偏移。

目標檔案節。該節是靜態連結庫的主要內容,節的數量不定,它儲存了若干各目標檔案的內容,每一節都是頭資訊+目標檔案的結構。目標檔案的結構與2.2節描述的一致。

在WinNT.h標頭檔案中,頭資訊被定義為IMAGE_ARCHIVE_MEMBER_HEADER型別,具體的定義內容如下:

typedef struct _IMAGE_ARCHIVE_MEMBER_HEADER

{

    BYTE     Name[16];                          // File member name - `/' terminated.

    BYTE     Date[12];                          // File member date - decimal.

    BYTE     UserID[6];                         // File member user id - decimal.

    BYTE     GroupID[6];                        // File member group id - decimal.

    BYTE     Mode[8];                           // File member mode - octal.

    BYTE     Size[10];                          // File member size - decimal.

    BYTE     EndHeader[2];                      // String to end header.

} IMAGE_ARCHIVE_MEMBER_HEADER, *PIMAGE_ARCHIVE_MEMBER_HEADER;

 

2.4PE檔案的結構

2.4.1總體結構圖

從檔案內容上來看,PE檔案由二進位制資料組成。這些二進位制資料從檔案的零位置開始,依次儲存,直到檔案末尾。從資料結構的角度來看,這些二進位制資料又分別屬於不同的結構體或者結構體陣列。這些結構體被定義在“WinNT.h”標頭檔案中。

在PE檔案中,這些結構體或結構體陣列分別表示不同的含義,記錄著PE檔案中的不同內容。從檔案的頂端開始,依次儲存了DOS頭,PE頭,段表,各段詳細資料等資訊。在進行資訊欄位定位的時候,PE檔案採用兩種方式:1利用指標。比如:在Dos頭中儲存一個指向PE頭的指標;2利用資料結構的大小。在PE的頭部資訊中,一些資料結構的大小是固定的。在資料儲存的時候,各個資料結構緊湊存放,中間沒有空隙。在這種情況下,以一個資料結構的欄位為基點,通過計算資料結構佔用空間的大小,就可以定位另外一個資料結構的位置。

使用dumpbin工具可以將PE檔案的內容匯出,具體的命令格式如下:

Dumpbin /all DemoDlld.dll >DemoDll.txt

    在上面的命令中,將PE檔案“DemoDlld.dll”的所有內容匯出到文字檔案“DemoDll.txt”中。命令選項“/all”表示匯出所有內容,命令選項“>”表示將匯出的內容儲存到檔案中。

 

2.4.2 DOS頭

2.4.2.1DOS MZ 頭

所有 PE檔案都必須以DOS MZ header開始,它是一個IMAGE_DOS_HEADER的結構。有了它,一旦程式在DOS下執行,DOS就能識別出這是有效的執行體,然後執行緊隨MZ Header之後的DOS Stub。

   在“WinNT.h”標頭檔案中,DOS MZ 頭被定義為IMAGE_DOS_HEADER型別,具體的定義形式如下:

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header

    WORD   e_magic;                     // 魔術數字

    WORD   e_cblp;                      // 檔案最後頁的位元組數

    WORD   e_cp;                        // 檔案頁數

    WORD   e_crlc;                      // 重定義元素個數

    WORD   e_cparhdr;                   // 頭部尺寸,以段落為單位

    WORD   e_minalloc;                  // 所需的最小附加段

    WORD   e_maxalloc;                  // 所需的最大附加段

    WORD   e_ss;                        // 初始的SS值

    WORD   e_sp;                        // 初始的SP值

    WORD   e_csum;                      // 校驗和

    WORD   e_ip;                        // 初始的IP值

    WORD   e_cs;                        // 初始的CS值

    WORD   e_lfarlc;                    // 重分配表檔案地址

    WORD   e_ovno;                      // 覆蓋號

    WORD   e_res[4];                    // 保留字

    WORD   e_oemid;                     // OEM 識別符號

    WORD   e_oeminfo;                   // OEM information; e_oemid specific

    WORD   e_res2[10];                  // 保留字

    LONG   e_lfanew;                    // PE頭的地址

  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

在DOS頭中,第一個域“e_magic”被稱為魔術數字,它用於表示一個MS-DOS相容的檔案型別。所有MS-DOS相容的可執行檔案都將這個值設為0x5A4D,表示ASCII字元MZ。MS-DOS頭部之所以有的時候被稱為MZ頭部,就是這個緣故。

對於MS-DOS作業系統來說,許多其他的域都是有用的。但是對於 Windows NT來說,只有最後一個域e_lfnew是有用的,該域是一個指標,佔用4個位元組,用於指明PE頭在檔案中的位置。

2.4.2.2DOS Stub

DOS Stub實際上是個有效的EXE,在不支援PE檔案格式的作業系統中,它將簡單顯示一個錯誤提示,類似於字串“This program requires Windows”,或者程式設計師可根據自己的意圖實現完整的DOS程式碼。大多數情況下DOS Stub由彙編器/編譯器自動生成。

2.4.3 PE頭

PE頭緊跟在DOS MS頭以及真實模式程式殘餘之後,在WinNt.h標頭檔案中,PE頭被定義為IMAGE_NT_HEADER型別,具體的定義內容如下所示:

typedef struct _IMAGE_NT_HEADERS

{

    DWORD Signature;                          //PE頭標識

    IMAGE_FILE_HEADER FileHeader;             //PE檔案物理分佈資訊

    IMAGE_OPTIONAL_HEADER32 OptionalHeader;   //PE檔案邏輯分佈資訊

} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

   在該結構體資料中,除了包含PE頭標識外,又巢狀了兩個結構體資料,分別是PE檔案頭的資訊,以及PE可選頭的資訊。

2.4.3.1PE頭標識

在一個有效的PE檔案中,PE頭標識欄位的值是0x00004550,用ASCII表示就是“PE00”。 #define IMAGE_NT_SIGNATURE定義了這個值。

2.4.3.2檔案頭

見2.2.2節描述。PE中的檔案頭與COFF中的檔案頭定義一致。

2.4.3.3可選頭

在PE檔案頭之後,是PE可選頭。該頭224位元組大小,它包含了許多重要的資訊,例如:初始堆疊大小,程式的入口地址,首選載入基地址,作業系統版本,段對齊等。該頭並非可選,而是必須要有的頭。

在可選頭中,包含了三類主要資訊,分別是:標準域資訊,WinNT附加資訊,以及資料目錄資訊。

所謂標準域就是指和UNIX可執行檔案的COFF格式所公共的部分,雖然標準域中保留了COFF檔案中定義的名稱,但是WindowsNT仍然將它用作了不同的目的。

在作業系統的載入器載入PE檔案的時候,WinNT附件域的資訊為載入器提供了支援。

在可執行檔案中有許多資料結構需要被快速定位,資料目錄提供了這種支援。資料目錄是一個指標列表,在該列表中儲存了一系列的指標值,這些指標指向了其他的資料表。如:匯入表,匯出表,資源表,重定位表等。資料目錄以指標的形式提供了一種資訊查詢的方式。

使用工具dumpbin可以將PE檔案的內容匯出,在該內容中包含了描述可選頭的摘要資訊,具體的資訊內容如下:

OPTIONAL HEADER VALUES

//以下為標準域的資訊

             10B magic # (PE32)

            9.00 linker version

            5800 size of code

            4800 size of initialized data

               0 size of uninitialized data

           11159 entry point (10011159) @ILT+340(__DllMainCRTStartup@12)

            1000 base of code

            1000 base of data

//以下為WinNT附加域的資訊

        10000000 image base (10000000 to 1001CFFF)

            1000 section alignment

             200 file alignment

            5.00 operating system version

            0.00 image version

            5.00 subsystem version

               0 Win32 version

           1D000 size of image

             400 size of headers

               0 checksum

               2 subsystem (Windows GUI)

             140 DLL characteristics

                   Dynamic base

                   NX compatible

          100000 size of stack reserve

            1000 size of stack commit

          100000 size of heap reserve

            1000 size of heap commit

               0 loader flags

              10 number of directories

//以下為資料目錄的資訊

           18AD0 [     2BA] RVA [size] of Export Directory

           1A000 [      50] RVA [size] of Import Directory

           1B000 [     C09] RVA [size] of Resource Directory

               0 [       0] RVA [size] of Exception Directory

               0 [       0] RVA [size] of Certificates Directory

           1C000 [     38C] RVA [size] of Base Relocation Directory

           17520 [      1C] RVA [size] of Debug Directory

               0 [       0] RVA [size] of Architecture Directory

               0 [       0] RVA [size] of Global Pointer Directory

               0 [       0] RVA [size] of Thread Storage Directory

               0 [       0] RVA [size] of Load Configuration Directory

               0 [       0] RVA [size] of Bound Import Directory

           1A224 [     1D4] RVA [size] of Import Address Table Directory

               0 [       0] RVA [size] of Delay Import Directory

               0 [       0] RVA [size] of COM Descriptor Directory

               0 [       0] RVA [size] of Reserved Directory

在WinNT.h標頭檔案中,可選頭被定義為IMAGE_OPTIONAL_HEADER型別,具體的定義內容描述如下:

//資料目錄中資料元素的個數

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

//可選頭的定義

typedef struct _IMAGE_OPTIONAL_HEADER

{

    //標準域

    WORD    Magic;  //魔法數字

    BYTE    MajorLinkerVersion;//連結器的最大版本號

    BYTE    MinorLinkerVersion;//連結器的最小版本號

    DWORD   SizeOfCode;//可執行程式碼長度

    DWORD   SizeOfInitializedData;//初始化資料的長度(data段)

    DWORD   SizeOfUninitializedData;//未初始化資料的長度(bss段)

    DWORD   AddressOfEntryPoint;//程式碼的入口地址,程式從此處開始執行

    DWORD   BaseOfCode;//可執行程式碼的起始位置

    DWORD   BaseOfData;//初始化資料的起始位置

    //NT附件域

    DWORD   ImageBase;//載入程式首選的相對虛擬地址

    DWORD   SectionAlignment;//段載入到記憶體以後的對齊方式

    DWORD   FileAlignment;//段在檔案中的對齊方式

    WORD    MajorOperatingSystemVersion;//作業系統最大版本號

    WORD    MinorOperatingSystemVersion;//作業系統最小版本號

    WORD    MajorImageVersion;//程式最大版本號

    WORD    MinorImageVersion;//程式最小版本號

    WORD    MajorSubsystemVersion;//子程式最大版本號

    WORD    MinorSubsystemVersion;//子程式最小版本號

    DWORD   Win32VersionValue;//這個值一直為零

    DWORD   SizeOfImage;//程式載入到記憶體以後,佔用記憶體的大小。

    DWORD   SizeOfHeaders;//檔案頭部總大小

    DWORD   CheckSum;//校驗和

    WORD    Subsystem;//一個標明可執行檔案所期望的子系統的列舉值

    WORD    DllCharacteristics;//dll狀態

    DWORD   SizeOfStackReserve;//保留棧大小

    DWORD   SizeOfStackCommit;//啟動後實際申請棧數

    DWORD   SizeOfHeapReserve;//保留堆的大小

    DWORD   SizeOfHeapCommit;//啟動後實際申請堆數

    DWORD   LoaderFlags;

DWORD   NumberOfRvaAndSizes;

//資料目錄的定義

    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

在可選頭中,各個欄位的詳細解釋如下表:

欄位名稱

型別

描述

Magic

WORD   

一個簽名,確定這是什麼型別的頭。兩個最常用的值是IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b

和IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b.

MajorLinkerVersion

BYTE   

建立可執行檔案的連結器的主版本號。對於Microsoft的連結器生成的PE檔案,這個版本號的Visual Studio的版本號相一致

MinorLinkerVersion

BYTE   

建立可執行檔案的連結器的次版本號

SizeOfCode

DWORD  

所有具有IMAGE_SCN_CNT_CODE屬性的節的總的大小

SizeOfInitializedData

DWORD  

所有包含已初始資料的節的總的大小。

SizeOfUninitializedData

DWORD  

所有包含未初始化資料的節的總的大小。這個域總是0,因為連結器可以把未初始化資料附加到常規資料節的末尾。

AddressOfEntryPoint

DWORD  

檔案中將被執行的第一個程式碼位元組的RVA。對於DLL,這個進入點將在程式初始化和關閉時,以及執行緒被建立和銷燬時呼叫。在大多數可執行檔案中,這個地址並不直接指向main,WinMain或DllMain函式,而是指向執行時庫程式碼,由執行時庫呼叫前述函式。在DLL中,這個域可以被設為0,這樣的話上面所說的通知就不能被接收到。連結器選項/NOENTRY可以設定這個域為0。

BaseOfCode

DWORD  

載入到記憶體後程式碼的第一個位元組的RVA。

BaseOfData

DWORD  

理論上,它表示載入到記憶體後資料的第一個位元組的RVA。然而,這個域的值對於不同版本的Microsoft連結器是不一致的。在64位的可執行檔案中這個域不出現。

ImageBase

DWORD  

檔案在記憶體中的首選載入地址。載入器儘可能地把PE檔案載入到這個地址(就是說,如果當前這塊記憶體沒有被佔用,它是對齊的並且是一個合法的地址,等等)。如果可執行檔案被載入到這個地址,載入器就可以跳過進行基址重定位(在這篇文章的第二部分描述)這一步。對於EXE,預設的ImageBase是0x400000。對於DLL,預設是0x10000000。在連結時可以通過/BASE 選項來指定ImageBase,或者以後用REBASE工具重新設定。

SectionAlignment

DWORD  

載入到記憶體後節的對齊大小。這個值必須大於等於FileAlignment(下一個域)。預設的對齊值是目標CPU的頁大上。對於執行在Windows 9x或Windows Me下的使用者模式可執行檔案,最小對齊大小是一頁(4KB)。這個域可以通過連結器選項/ALIGN來設定。

FileAlignment

DWORD  

在PE檔案中節的對齊大小。對於x86下的可執行檔案,這個值通常是0x200或0x1000。不同版本的Microsoft連結器預設值不同。這個值必須是2的冪,並且如果SectionAlignment小於CPU的頁大小,這個域必須和SectionAlignment相匹配。連結器選項/OPT:WIN98可設定x86可執行檔案的檔案對齊為0x1000,/OPT:NOWIN98設定檔案對齊為0x200。

MajorOperatingSystemVersion

WORD   

所要求的作業系統的主版本號。隨著那麼多版本Windows的出現,這個域的值就變得很不確切。

MinorOperatingSystemVersion

WORD   

所要求的作業系統的次版本號。

MajorImageVersion

WORD   

這個檔案的主版本號。不被系統使用並可設為0。可以通過連結器選項/VERSION來設定。

MinorImageVersion

WORD   

這個檔案的次版本號。

MajorSubsystemVersion

WORD   

可執行檔案所要求的操作子系統的主版本號。它曾經被用來表示需要較新的Windows 95或Windows NT使用者介面,而不是老版本的Windows NT介面。今天隨著各種不同版本Windows的出現,這個域已不被系統使用,並且通常被設為4。可通過連結器選項/SUBSYSTEM設定這個域的值。

MinorSubsystemVersion

WORD   

執行檔案所要求的操作子系統的次版本號。

Win32VersionValue

DWORD  

不被使用的域,通常設為0。

SizeOfImage

DWORD  

映像的大小。它表示了載入檔案到記憶體中時系統必須保留的記憶體的數量。這個域的值必須是SectionAlignmnet的倍數。

SizeOfHeaders

DWORD  

MS-DOS頭,PE頭和節表的總的大小。PE檔案中所有這些專案出現在任何程式碼或資料節之前。這個域的值被調整為檔案對齊大小的整數倍。

CheckSum

DWORD  

映像的校驗和。IMAGEHLP.DLL中的CheckSumMappedFile函式可以計算出這個值。校驗和用於核心模式的驅動和一些系統DLL。對於其它的,這個域可以為0。當使用連結器選項/RELEASE時校驗和被放入檔案中。

Subsystem

WORD   

指示可執行檔案期望的子系統(使用者介面型別)的列舉值。這個域只用於EXE。一些重要的值包括:

IMAGE_SUBSYSTEM_NATIVE

                                         // 映像不需要子系統

IMAGE_SUBSYSTEM_WINDOWS_GUI

                                         // 使用Windows GUI

IMAGE_SUBSYSTEM_WINDOWS_CUI

                                         // 作為控制檯程式執行。

                                         // 執行時,作業系統建立一個控制檯

                                         // 視窗並提供stdin,stdout和stderr

                                         // 檔案控制程式碼。

DllCharacteristics

WORD   

標記DLL的特性。對應於IMAGE_DLLCHARACTERISTICS_xxx定義。當前的值是:

IMAGE_DLLCHARACTERISTICS_NO_BIND

                                         // 不要繫結這個映像

IMAGE_DLLCHARACTERISTICS_WDM_DRIVER

                                         // WDM模式的驅動程式

IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE

                                         // 當終端服務載入一個不是

                                         // Terminal- Services-aware 的應用程

                                         // 序時,它也載入一個包含相容程式碼

                                         // 的DLL。

SizeOfStackReserve

DWORD  

在EXE檔案中,為執行緒保留的堆疊大小。預設是1MB,但並不是所有的記憶體一開始都被提交。

SizeOfStackCommit

DWORD  

在EXE檔案中,為堆疊初始提交的記憶體數量。預設情況下,這個域是4KB。

SizeOfHeapReserve

DWORD  

在EXE檔案中,為預設程式堆初始保留的記憶體大小。預設是1MB。然而在當前版本的Windows中,堆不經過使用者干涉就能超出這裡指定的大小。

SizeOfHeapCommit

DWORD  

在EXE檔案中,提交到堆的記憶體大小。預設情況下,這裡的值是4KB。

LoaderFlags

DWORD  

不使用。

NumberOfRvaAndSizes

DWORD  

在IMAGE_NT_HEADERS結構的末尾是一個IMAGE_DATA_DIRECTORY結構陣列。此域包含了這個陣列的元素個數。自從最早的Windows NT釋出以來這個域的值一直是16。

 

資料目錄一共有16項,每一項都儲存一個指向其他資料表的指標,資料目錄為資料的快速定位提供了支援。在WinNT.h標頭檔案中,資料目錄被定義為IMAGE_DATA_DIRECTORY,具體的定義內容描述如下:

typedef struct _IMAGE_DATA_DIRECTORY

{

    DWORD   VirtualAddress; //被指向元素表的起始RVA地址。

    DWORD   Size;           //被指向元素表的長度

} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

 

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

在資料目錄的16項元素中,每一項都預定義了它的含義,IMAGE_DIRECTORY_ENTRY_XXX定義了資料目錄陣列的索引。該索引的具體含義描述如下表:

序號

索引

描述

0

IMAGE_DIRECTORY_ENTRY_EXPORT

指向匯出表(一個IMAGE_EXPORT_DIRECTORY結構)。

1

IMAGE_DIRECTORY_ENTRY_IMPORT

指向匯入表(一個IMAGE_IMPORT_DESCRIPTOR結構陣列)。

2

IMAGE_DIRECTORY_ENTRY_RESOURCE

指向資源(一個IMAGE_RESOURCE_DIRECTORY結構。

3

IMAGE_DIRECTORY_ENTRY_EXCEPTION

指向異常處理表(一個IMAGE_RUNTIME_FUNCTION_ENTRY結構陣列)。CPU特定的並且基於表的異常處理。用於除x86之外的其它CPU上。

4

IMAGE_DIRECTORY_ENTRY_SECURITY

指向一個WIN_CERTIFICATE結構的列表,它定義在WinTrust.H中。不會被對映到記憶體中。因此,VirtualAddress域是一個檔案偏移,而不是一個RVA。

5

IMAGE_DIRECTORY_ENTRY_BASERELOC

指向基址重定位資訊。

6

IMAGE_DIRECTORY_ENTRY_DEBUG

指向一個IMAGE_DEBUG_DIRECTORY結構陣列,其中每個結構描述了映像的一些除錯資訊。早期的Borland連結器設定這個IMAGE_DATA_DIRECTORY結構的Size域為結構的數目,而不是位元組大小。要得到IMAGE_DEBUG_DIRECTORY結構的數目,用IMAGE_DEBUG_DIRECTORY 的大小除以這個Size域。

7

IMAGE_DIRECTORY_ENTRY_ARCHITECTURE

指向特定架構資料,它是一個IMAGE_ARCHITECTURE_HEADER結構陣列。不用於x86或IA-64,但看來已用於DEC/Compaq Alpha。

8

IMAGE_DIRECTORY_ENTRY_GLOBALPTR

在某些架構體系上VirtualAddress域是一個RVA,被用來作為全域性指標(gp)。不用於x86,而用於IA-64。Size域沒有被使用。參見2000年11月的Under The Hood 專欄可得到關於IA-64 gp的更多資訊。

9

IMAGE_DIRECTORY_ENTRY_TLS

指向執行緒區域性儲存初始化節。

10

IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG

指向一個IMAGE_LOAD_CONFIG_DIRECTORY結構。IMAGE_LOAD_CONFIG_DIRECTORY中的資訊是特定於Windows NT、Windows 2000和 Windows XP的(例如 GlobalFlag 值)。要把這個結構放到你的可執行檔案中,你必須用名字__load_config_used 定義一個全域性結構,型別是IMAGE_LOAD_CONFIG_DIRECTORY。對於非x86的其它體系,符號名是_load_config_used (只有一個下劃線)。如果你確實要包含一個IMAGE_LOAD_CONFIG_DIRECTORY,那麼在 C++ 中要得到正確的名字比較棘手。連結器看到的符號名必須是__load_config_used (兩個下劃線)。C++ 編譯器會在全域性符號前加一個下劃線。另外,它還用型別資訊修飾全域性符號名。因此,要使一切正常,在 C++ 中就必須像下面這樣使用:

extern "C"

IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {...}

11

IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT

指向一個 IMAGE_BOUND_IMPORT_DESCRIPTOR結構陣列,對應於這個映像繫結的每個DLL。陣列元素中的時間戳允許載入器快速判斷繫結是否是新的。如果不是,載入器忽略繫結資訊並且按正常方式解決匯入API。

12

IMAGE_DIRECTORY_ENTRY_IAT

指向第一個匯入地址表(IAT)的開始位置。對應於每個被匯入DLL的IAT都連續地排列在記憶體中。Size域指出了所有IAT的總的大小。在寫入匯入函式的地址時載入器使用這個地址和Size域指定的大小臨時地標記IAT為可讀寫。

13

IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT

指向延遲載入資訊,它是一個CImgDelayDescr結構陣列,定義在Visual C++的標頭檔案DELAYIMP.H中。延遲載入的DLL直到對它們中的API進行第一次呼叫發生時才會被裝入。Windows中並沒有關於延遲載入DLL的知識,認識到這一點很重要。延遲載入的特徵完全是由連結器和執行時庫實現的。

14

IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR

在最近更新的系統標頭檔案中這個值已被改名為IMAGE_DIRECTORY_ENTRY_COMHEADER。它指向可執行檔案中.NET資訊的最高階別資訊,包括後設資料。這個資訊是一個IMAGE_COR20_HEADER結構。

15

IMAGE_DIRECTORY_ENTRY_EXPORT

指向匯出表(一個IMAGE_EXPORT_DIRECTORY結構)。

 

2.4.4匯入表

當一個可執行程式或者動態連結庫呼叫另外一個動態連結庫中的變數或者函式的時候,必須將這些被呼叫函式或者變數匯入。這些被匯入的變數或函式是必須是被呼叫動態連結庫匯出表中的一個子集。也就是說,只有當這些變數或者函式被匯出以後,才能被其他可執行程式或者動態連結庫匯入。

在PE檔案中,我們將這些變數和函式統稱為符號,這些被匯入的符號的資訊被儲存在匯入表中。在PE檔案中,匯入表位於.idata段中,該段可以單獨存在。

使用工具dumpbin將可執行成DemoExe.exe中的內容匯出,.idata段的部分內容如下所示:

//.idata段的資訊摘要

SECTION HEADER #5

  .idata name

    1130 virtual size

   1A000 virtual address (0041A000 to 0041B12F)

    1200 size of raw data

    7A00 file pointer to raw data (00007A00 to 00008BFF)

       0 file pointer to relocation table

       0 file pointer to line numbers

       0 number of relocations

       0 number of line numbers

C0000040 flags

         Initialized Data

         Read Write

//idata段的二進位制資料內容

RAW DATA #5

  0041A000: 64 A0 01 00 00 00 00 00 00 00 00 00 B4 A5 01 00  d?..........′¥..

  0041A010: B0 A2 01 00 58 A1 01 00 00 00 00 00 00 00 00 00  °¢..X?..........

  0041A020: 30 AB 01 00 A4 A3 01 00 F4 A1 01 00 00 00 00 00  0?..¤£..??......

      …………….

//匯入表的資訊

Section contains the following imports:

 

    DemoDLLd.dll

                41A2B0 Import Address Table   //匯入地址表地址

                41A064 Import Name Table    //匯入名稱表地址

                     0 time date stamp

                     0 Index of first forwarder reference

  //匯出地址表陣列下標  符號名稱

                    6 ?GetOperTimes@@YAHXZ

                    4 ?Area@DemoMath@@QAEXN@Z

                    5 ?DivData@DemoMath@@QAEXNN@Z

                    8 ?SubData@DemoMath@@QAEXNN@Z

                    3 ?AddData@DemoMath@@QAEXNN@Z

                    0 ??0DemoMath@@QAE@XZ

                    1 ??1DemoMath@@QAE@XZ

對於每一個可執行程式或者動態連結庫,只要它呼叫了其他動態連結庫中的符號,那麼就會有一個或多個IMAGE_IMPORT_DESCRIPTOR型別的陣列與之對應。該陣列元素的個數與該可執行程式或者動態連結庫所依賴的動態連結庫的數量有關。

在IMAGE_IMPORT_DESCRIPTOR型別的資料結構中,儲存的是與匯入表相關的資訊。該資料結構與其他資料結構發生關聯,與該資料結構發生關聯的資料實體包括:資料目錄,字串表,匯入地址表,匯入名稱表。它們之間的關係如下圖所示:

在可選頭中包含了資料目錄,在資料目錄的第二項中,儲存了指向匯入表的位置的指標,以及該資料結構的大小。在資料目錄的第十二項中,儲存了指向第一個IAT的位置的指標,以及所有IAT陣列的大小。

匯入表是一個陣列,該資料元素的型別是IMAGE_IMPORT_DESCRIPTOR。在該陣列中,每一個陣列元素都會對應一個被匯入的DLL。在該陣列的末尾,儲存了一個所有的域為零的IMAGE_IMPORT_DESCRIPTOR型別的陣列元素,表示該匯入表結束。因此,在一個可執行程式或者動態連結庫的匯入表中,至少會包含兩個資料元素。一個對應被匯入符號的資訊,一個所有域為零。

在每個匯入表的陣列的元素中,擁有兩個地址欄位,它們分別指向了匯入地址表(IAT)和匯入名稱表(INT)。在匯入地址表和匯入名稱表中,儲存了被匯入符號的名稱或地址,它們是一個擁有多個資料元素的陣列。因為匯入表的陣列元素可能是多個(當前可執行程式或動態連結庫依賴多個其他的動態連結庫),所以在一個動態連結庫的匯入資訊中也會存在多個IAT陣列以及INT陣列。當PE檔案被載入到記憶體以後,這些IAT陣列會被儲存在一塊連續地記憶體區域中,它們首尾相連,中間沒有空隙。在資料目錄的第十二項中,儲存了第一個IAT的位置,以及所有IAT陣列的大小。匯入表的結構佈局如下圖所示:

匯入地址表和匯入名稱表的資料結構是一致的,它們均為IMAGE_THUNK_DATA型別,並以一個所有域均為零的陣列元素結尾。匯入地址表用於動態連結,而匯入名稱表用於符號地址繫結。在PE檔案中,匯入地址表和匯入名稱表中的資料內容是一樣的,它們都儲存著被匯入符號的名稱或者序號;在程式載入階段,匯入名稱表的資料內容不會發生變化,而匯入地址表中的被匯入符號的序號或者名稱將會被替換為該符號的真實虛擬記憶體地址。匯入名稱表的資料內容永遠不會發生變化。

在WinNT.h標頭檔案中,匯入表被定義為IMAGE_IMPORT_DESCRIPTOR型別,具體的定義內容如下所示:

typedef struct _IMAGE_IMPORT_DESCRIPTOR

{

union

{

        DWORD   Characteristics;      // 0 for terminating null import descriptor

        DWORD   OriginalFirstThunk;   // RVA to original unbound IAT (PIMAGE_THUNK_DATA)

    };

    DWORD   TimeDateStamp;           // 0 if not bound,

                                     // -1 if bound, and real date\time stamp

                                     //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)

                                     // O.W. date/time stamp of DLL bound to (Old BIND)

 

    DWORD   ForwarderChain;         // -1 if no forwarders

    DWORD   Name;

    DWORD   FirstThunk;            // RVA to IAT (if bound this IAT has actual addresses)

} IMAGE_IMPORT_DESCRIPTOR;

typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

在該定義中,各個欄位的詳細解釋如下表所示:

欄位名稱

型別

描述

Characteristics

DWORD  

 

OriginalFirstThunk

DWORD  

指向匯入名稱表的相對虛擬地址。

TimeDateStamp

DWORD  

如果可執行檔案不與被匯入DLL繫結時,該值為0;如果以舊的樣式繫結時,改值為時間戳;如果以新的樣式繫結時,該值為-1。

ForwarderChain

DWORD  

第一個被轉送符號的索引,如果沒有轉送,則設定為-1。

Name

DWORD  

該欄位儲存了一個相對虛擬地址。該地址指向字串表中某個字串,這個字串是匯入DLL的名稱。

FirstThunk

DWORD  

指向匯入地址表的相對虛擬地址。

在WinNT.h標頭檔案中,IAT以及INT被定義為IMAGE_THUNK_DATA型別,具體的定義內容如下所示:

typedef struct _IMAGE_THUNK_DATA32

{

union

{

        DWORD ForwarderString;      // PBYTE

        DWORD Function;             // PDWORD

        DWORD Ordinal;

        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME

    } u1;

} IMAGE_THUNK_DATA32;

typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

從上面的定義可以看出,IAT資料結構是一個聯合。在該聯合中,每一個欄位代表著在不同條件或者時間上,該資料結構可能儲存不同意義的資料。在該定義中,各個欄位的詳細解釋如下表所示:

欄位名稱

型別

描述

ForwarderString

DWORD

指向一個轉向字串的相對虛擬地址。轉向匯入的時候使用。

Function

DWORD

被匯入符號的虛擬地址。動態連結完成以後寫入該值。

Ordinal

DWORD

被匯入符號在匯出表中的序號。編譯連結完畢後,該值可能會被寫入到PE檔案中。該值與AddressofData欄位互斥。

AddressOfData

DWORD

指向一個IMAGE_IMPORT_BY_NAME型別的資料結構。編譯連結完畢後,該值可能會被寫入到PE檔案中。該值與Ordinal欄位互斥。

在WinNT.h標頭檔案中,IMAGE_IMPORT_BY_NAME的定義內容如下:

typedef struct _IMAGE_IMPORT_BY_NAME

{

    WORD    Hint;

    BYTE    Name[1];

} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

在上面的定義中,各欄位的詳細解釋如下表:

欄位名稱

型別

描述

Hint

WORD   

匯入符號最有可能的序號值。在動態連結的時候,當用符號名匯入時,首先用Hint值在匯出符號表中查詢該符號,如果能夠查詢到,則命中;如果不能查詢到,則使用符號名進行二分法查詢。

Name

BYTE   

符號名稱的字串

 

在IAT陣列中,每一個陣列元素都會對應一個被匯入符號的地址,陣列元素在不同的情況下有不同的含義。具體的含義形式有四種,它們分別是:

  • 轉向字串形式。
  • 符號虛擬地址形式。
  • 序號形式。
  • 符號名稱形式。

在不同的時間點上,或者在不同的情況下,被匯出符號的地址用不同的形式表示。因此在資料結構定義的時候使用了聯合。

在源程式被編譯、連結完畢以後生成的PE檔案中,以PE檔案被載入到記憶體中,尚未執行動態連結的時候,在IAT中符號的地址以序號或者符號名稱的形式表示;當執行完畢動態連結以後,使用符號的序號或者符號名稱,通過對相關DLL匯出表的查詢,這些符號的序號或者符號的名稱被替換成了符號的虛擬記憶體地址。

當IAT中的符號地址以序號或者符號名稱表示的時候,可以通過DWORD值的最高位來區分是使用序號表示還是使用符號名稱表示。如果最高位被置1,那麼該符號地址使用序號的形式表示,DWORD值的低31位表示序號;如果最高為被置0,那麼該符號地址使用符號名稱的形式表示,DWORD值表示一個地址,指向了一個IMAGE_IMPORT_BY_NAME型別的資料結構。

在IMAGE_IMPORT_BY_NAME型別的資料結構中,包含了一個符號的名稱字串,以及一個WORD型別的提示值,該提示值指示了符號在匯出表中最可能的序號。在符號查詢的時候,如果使用Hint值查詢不能命中,那麼就會使用符號名稱進行二分法查詢。

2.4.5匯出表

在一個動態連結庫檔案中,總會有一些變數或者函式被宣告為public,這些變數和函式被稱為該動態連結庫的介面,它們可以被其它可執行程式或者動態連結庫呼叫。在該動態連結庫中,並不是所有被宣告為public型別的介面都能被其他可執行程式或動態連結庫呼叫,只有當這些介面被匯出以後才能被其他可執行程式或者動態連結庫呼叫。

在PE檔案中,我們將變數和函式統稱為符號。這些被匯出的符號的資訊被儲存在匯出表。一般情況,匯出表不會單獨存在,在輸出PE檔案的時候,匯出表可能會被合併到.rata段中,該段是隻讀資料段。

使用工具dumpbin將動態連結庫DemoDlld.dll的內容匯出,.rdata段的部分內容如下所示:

//只讀段的摘要資訊

SECTION HEADER #3

  .rdata name

    1D8A virtual size

   17000 virtual address (10017000 to 10018D89)

    1E00 size of raw data

    5C00 file pointer to raw data (00005C00 to 000079FF)

       0 file pointer to relocation table

       0 file pointer to line numbers

       0 number of relocations

       0 number of line numbers

40000040 flags

         Initialized Data

         Read Only

//只讀段的二進位制資料內容

RAW DATA #3

  10017000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

  10017010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

  10017020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

…..

//匯出表的資訊

Section contains the following exports for DemoDLLd.dll

 

    00000000 characteristics

    51A86C37 time date stamp Fri May 31 17:24:07 2013

        0.00 version

           1 ordinal base

           9 number of functions  //符號地址表元素的數量

           9 number of names    //符號名稱表元素的數量

 

      ordinal  hint  RVA       name

//序號,陣列下標,相對虛擬地址,符號名稱

          1    0 00011145 ??0DemoMath@@QAE@XZ

          2    1 00011109 ??1DemoMath@@QAE@XZ

          3    2 0001101E ??4DemoMath@@QAEAAV0@ABV0@@Z

          4    3 000111C7 ?AddData@DemoMath@@QAEXNN@Z

          5    4 0001116D ?Area@DemoMath@@QAEXN@Z

          6    5 00011221 ?DivData@DemoMath@@QAEXNN@Z

          7    6 000110B4 ?GetOperTimes@@YAHXZ

          8    7 00011005 ?MulData@DemoMath@@QAEXNN@Z

          9    8 0001114A ?SubData@DemoMath@@QAEXNN@Z

 

對於每一個動態連結庫,都會有一個IMAGE_EXEPORT_DIRECTORY型別的資料結構與之對應。該資料結構儲存了與匯出表有關的資訊,並與其他資料結構發生關聯。與該資料結構有關聯的資料實體包括:資料目錄,字串表,符號地址表,符號名稱表,名稱序號對應關係表。這些實體之間的關係如下圖所示:

可選頭中包含了資料目錄,資料目錄的第一項指向了匯出表。匯出表是一個結構體型別,在該結構體的欄位中儲存了與匯出表相關的資訊,如:DLL名稱欄位指向了一個字串表,在該字串表中儲存了DLL的名稱;符號地址表地址欄位指向了一個地址陣列,該陣列中儲存了符號的地址;符號名稱表地址欄位指向了符號名稱陣列,該陣列中儲存了符號名稱字串的地址;名稱序號對應關係表地址指向了名稱序號對應關係的陣列,該陣列中儲存了符號的名稱與序號之間的對應關係。

     符號地址表是一個DWORD型別的陣列,在該陣列中儲存了被匯出符號的相對虛擬地址,陣列中每一個非零的陣列元素都指向了一個符號的相對虛擬地址;符號名稱表是一個DWORD型別的陣列,陣列中儲存的是相對虛擬地址,該相對虛擬地址指向了字串表中的相關位置,這些位置中儲存了符號的名稱;名稱序號表是一個WORD型別的陣列,該陣列中儲存了符號的序號。符號名稱表中的元素與名稱序號表中的元素通過陣列下標對應。比如:在符號名稱表中,陣列下標為1的元素,它的序號存在在名稱序號表中,陣列下標也為1。

     序號的計算公式為:序號 = 符號地址表陣列下標 + Base欄位值。在Windows16位時代,由於受到硬體大小的限制,在執行動態連結的時候,使用序號查詢符號的地址,即:用序號的值減去Base的值,獲得符號地址表陣列的下標,進而獲得符號的相對虛擬記憶體地址。這種方式節省了記憶體空間以及符號查詢的時間,但是易讀性差。隨著時間的發展,當硬體的實體記憶體不在是問題的時候,開始使用符號名稱查詢符號的地址,具體的查詢過程是:通過符號名稱在名稱序號對應關係表中查詢到符號的序號,然後再用符號的序號查詢符號的地址。雖然引入了符號名稱表,但是這個表不是必須的,依然可以通過序號查詢符號的地址。在一個DLL中,每一個匯出符號都有一個唯一對應的序號,而匯出符號名是可選的。

在動態連結的時候,可以通過兩種方式進行符號地址的查詢,一種是直接利用符號的序號直接查詢,另外一種是利用符號的名稱間接查詢。在進行符號地址查詢的時候,符號地址表,符號名稱表,名稱序號對應表之間的關係如下圖所示:

在WinNT.h標頭檔案中,匯出表被定義為IMAGE_EXPORT_DIRECTORY型別,具體的定義內容如下:

typedef struct _IMAGE_EXPORT_DIRECTORY

{

    DWORD   Characteristics;

    DWORD   TimeDateStamp;

    WORD    MajorVersion;

    WORD    MinorVersion;

    DWORD   Name;

    DWORD   Base;

    DWORD   NumberOfFunctions;

    DWORD   NumberOfNames;

    DWORD   AddressOfFunctions;     // RVA from base of image

    DWORD   AddressOfNames;         // RVA from base of image

    DWORD   AddressOfNameOrdinals;  // RVA from base of image

} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

在該定義中,各欄位的詳細解釋如下表所示:

欄位名稱

型別

描述

Characteristics

DWORD

關於匯出表的一些標記,目前沒有被定義。

TimeDateStamp

DWORD

匯出表被建立的時間。

MajorVersion

WORD   

匯出表的主版本號,目前沒有使用,設為零。

MinorVersion

WORD   

匯出表的次版本號,目前沒有使用,設為零。

Name

DWORD

一個相對虛擬地址,指向字串表的一個位置,該位置儲存了該Dll的名稱。

Base

DWORD

通常設定為1,但也不是必須的。

序號 = Base + 符號地址陣列下標。

NumberOfFunctions

DWORD

符號地址表中資料元素的個數。

NumberOfNames

DWORD

符號名稱表以及符號名稱序號對應關係表中資料元素的個數。由於符號名稱表和符號名稱序號對應關係表是一一對應的,所以它們的陣列元素個數相同;該值可能會小於NumberOfFunctions,如果小於這個值,表示有一部分符號只通過序號對應,沒有儲存它們的名稱。

AddressOfFunctions

DWORD

相對虛擬地址,指向符號地址表。

AddressOfNames

DWORD

相對虛擬地址,指向符號名稱表。

AddressOfNameOrdinals

DWORD

相對虛擬地址,指向符號名稱序號對應關係表。

 

2.4.6基址重定位表

在目標檔案中,所有符號的地址都是基於檔案頭或者檔案中某個位置的偏移地址。在將目標檔案連結以後,在輸出的PE檔案中,這些偏移地址會被轉化成相對虛擬記憶體地址,並且再加上一個預設記憶體載入位置的地址值,形成符號的基於預設記憶體載入位置的虛擬記憶體地址。基於預設記憶體載入位置的虛擬記憶體地址的計算公式為:

符號虛擬記憶體地址 = 預設記憶體載入位置 + 相對虛擬記憶體地址

可執行程式預設載入到記憶體的0x0400000位置,而動態連結庫預設載入到記憶體的0x10000000位置。

當PE檔案被載入到記憶體以後,如果該檔案不能被載入到預設的記憶體位置,那麼在指令程式碼中,所有使用絕對地址表示的符號的地址都需要被重定位。在Windows中,這一地址重定位的過程被叫做重定基地址。具體的操作過程是:在每一個需要進行地址重定位的符號處,將該符號當前地址的數值上再加上一個固定的數值,這個新獲得的地址值就是該符號正確的虛擬記憶體地址。

這個固定值的計算公式是:

固定值 = DLL當前記憶體載入的位置 – DLL預設記憶體載入位置(0x10000000)

地址重定位工作由作業系統的載入器來完成,在基地址重定位表中,記錄了每一個需要進行地址重定位的符號的地址。在地址重定位的時候,載入器讀取該表中的數值,然後查詢到需要進行地址重定位的符號的位置,最後修正該符號的虛擬記憶體地址。

在PE檔案被載入到記憶體以後,這些檔案內容是以頁為單位儲存在記憶體中,每個記憶體頁的大小是4KB。在基址重定位表中,資料表中的資料被分割成一個個資料塊,每一個資料塊會對應一個虛擬記憶體頁,表示在該虛擬記憶體頁中的符號的地址重定位資訊。

使用工具dumpbin將DemoDlld.dll中的內容匯出,涉及到基址重定位表的內容如下所示:

//基址重定位表的摘要資訊

SECTION HEADER #7

  .reloc name

     524 virtual size

   1C000 virtual address (1001C000 to 1001C523)

     600 size of raw data

    9A00 file pointer to raw data (00009A00 to 00009FFF)

       0 file pointer to relocation table

       0 file pointer to line numbers

       0 number of relocations

       0 number of line numbers

42000040 flags

         Initialized Data

         Discardable

         Read Only

//基址重定位表的二進位制資料內容

RAW DATA #7

  1001C000: 00 10 01 00 68 00 00 00 4F 35 76 35 9F 35 A4 37 

  1001C010: AC 37 24 38 2C 38 A4 38 AC 38 30 39 41 39 49 39

  1001C020: C4 39 CC 39 D8 39 C6 3A D7 3A DD 3A EE 3A FD 3A

     ………………

//以下是對二進位制內容的翻譯。

BASE RELOCATIONS #7

//基址重定位表的資料塊 相對虛擬地址為:11000,塊大小為68,該資料塊對應一個4KB大小的記憶體頁

   11000 RVA,       68 SizeOfBlock

低12位偏移  型別             符號的地址  符號名稱

     54F  HIGHLOW            10019140  ?nOperTimes@@3HA (int nOperTimes)

     576  HIGHLOW            100156AE

     59F  HIGHLOW            10019004  ___security_cookie

             …….

//下一個資料塊,相對虛擬地址為:12000,塊大小為F4,該資料塊對應一個4KB大小的記憶體頁

12000 RVA,       F4 SizeOfBlock

       C  HIGHLOW            10012010

      18  HIGHLOW            1001201C

     146  HIGHLOW            10015718

     16F  HIGHLOW            10019004  ___security_cookie

            ………

基址重定位表的結構如下圖所示:

可選頭中包含了資料目錄,資料目錄的第五項資料中包含了指向了基址重定位表的指標,以及基址重定位表的大小。基址重定位表以記憶體頁的大小為依據進行分塊,在每一個塊中,都以IMAGE_BASE_RELOCATION型別的資料結構開頭,後面跟隨著每個符號的基地址重定位資訊。這些符號的重定位資訊是一系列的WORD值。這些WORD值的高4位指出了重定位的型別,而低12位是一個地址偏移。將該地址偏移數值與資料塊的虛擬記憶體地址數值(即:IMAGE_BASE_RELOCATION. VirtualAddress)相加,可以得到該符號需要進行重定位的位置。

在基址重定位表的資料塊中,所包含的重定位資訊的個數的計算公式為:

重定位資訊個數 = (塊大小 – sizeof(IMAGE_BASE_RELOCATION))/2

因為塊大小以位元組為單位表示,而重定位資訊以字為單位表示,轉化成字需要除2

重定位的型別描述如下表:

型別

描述

IMAGE_REL_BASED_ABSOLUTE (0)

這種不需操作;用於將塊按32位邊界對齊。位置應該為0。

IMAGE_REL_BASED_HIGH (1)

重定位的高16位必須被用於被偏移量所指向的那個16位的WORD單元,此WORD是一個32位的DWORD的高位WORD。

IMAGE_REL_BASED_LOW (2)

重定位的低16位必須被用於被偏移量所指向的那個16位的WORD單元,此WORD是一個32位的DWORD的低位WORD。

IMAGE_REL_BASED_HIGHLOW (3)

重定位的全部32位必須應用於上面所說的全部32位。PE檔案採用該型別。

IMAGE_REL_BASED_HIGHADJ (4)

這種修正要求一個全32位值。高16位定位於偏移量處,低16位定位在下一個陣列元素(此陣列元素包括在大小的域中)的偏移量處。它們兩個需要被連成一個有符號的變數。加上32位的增量。然後加上0x8000 並將有符號變數的高16位儲存在偏移量處的16位域中。”

IMAGE_REL_BASED_MIPS_JMPADDR (5)

 

IMAGE_REL_BASED_SECTION (6)

 

IMAGE_REL_BASED_REL32 (7)

 

在WinNT.h標頭檔案中,基址重定位表被定義為IMAGE_BASE_RELOCATION型別,具體的定義內容如下:

typedef struct _IMAGE_BASE_RELOCATION

{

    DWORD   VirtualAddress;

    DWORD   SizeOfBlock;

//  WORD    TypeOffset[1];

} IMAGE_BASE_RELOCATION;

typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

在該定義中,各欄位的詳細解釋如下:

欄位名稱

型別

描述

VirtualAddress

DWORD  

資料塊的虛擬記憶體地址

SizeOfBlock

DWORD  

資料塊的大小

   在以下內容中,將以一個示例的形式來說明如何查詢符號重定位的位置。使用工具dumpbin將DemoDlld.dll的基址重定位表的內容匯出,其中一個資料塊的內容如下:

BASE RELOCATIONS #7

//基址重定位表的資料塊 相對虛擬地址為:11000,塊大小為68,該資料塊對應一個4KB大小的記憶體頁

   11000 RVA,       68 SizeOfBlock

低12位偏移  型別             符號的地址  符號名稱

     54F  HIGHLOW            10019140  ?nOperTimes@@3HA (int nOperTimes)

     576  HIGHLOW            100156AE

     59F  HIGHLOW            10019004  ___security_cookie

             …….

 

   在上面的內容中,紅色資訊表示引用了符號nOperTimes處的地址需要被重定位,該引用形式必然是使用了絕對地址。

重定位地址的計算公式為:預設載入位置 + 資料塊相對虛擬記憶體地址 + 偏移 = 0x10000000 + 0x11000 + 0x54F = 0x1001154F。處於虛擬記憶體地址0x1001154F處的地址值需要被重定位。

將DemoDlld.dll的內容匯出為彙編格式,與地址0x1001154F相關的內容如下:

?GetOperTimes@@YAHXZ:

  10011530: 55                 push        ebp

  10011531: 8B EC              mov         ebp,esp

  10011533: 81 EC C0 00 00 00  sub         esp,0C0h

  10011539: 53                 push        ebx

  1001153A: 56                 push        esi

  1001153B: 57                 push        edi

  1001153C: 8D BD 40 FF FF FF  lea         edi,[ebp-0C0h]

  10011542: B9 30 00 00 00     mov         ecx,30h

  10011547: B8 CC CC CC CC     mov         eax,0CCCCCCCCh

  1001154C: F3 AB              rep stos    dword ptr es:[edi]

  1001154E: A1 40 91 01 10     mov         eax,dword ptr [?nOperTimes@@3HA]

  10011553: 5F                 pop         edi

  10011554: 5E                 pop         esi

  10011555: 5B                 pop         ebx

  10011556: 8B E5              mov         esp,ebp

  10011558: 5D                 pop         ebp

  10011559: C3                 ret

上面紅色字型標記出了關鍵程式碼行,綠色的字型是需要被重定位的地址值,該地址的當前值為0x10019140。該值的第一個位元組正好對應地址0x1001154F,這就是需要被重定位的位置。該值是符號nOperTimes在PE檔案中被分配的虛擬記憶體地址,由於在此處使用了絕對地址的形式,所以當PE檔案被載入到記憶體以後,該符號的地址需要被重定位。

假設DemoDlld.dll被載入到了記憶體位置為:0x20000000,那麼該地址值將被修正為:0x20000000 – 0x10000000 + 0x10019140 = 0x20019140。

2.4.7各資料結構之間的關係

在PE檔案中,各個資料結構之間的關係如下圖所示:

相關文章