COFF檔案的格式 (轉)

gugu99發表於2007-08-17
COFF檔案的格式 (轉)[@more@]COFF格式
 
 COFF – 通用格式(Common File Format),是一種很流行的物件檔案格式(注意:這裡不說它是“目標”檔案,是為了和產生的目標檔案(*.o/*.obj)相區別,因為這種格式不只用於目標檔案,庫檔案、可檔案也經常是這種格式)。大家可能會經常使用VC吧?它所產生的目標檔案(*.obj)就是這種格式。其它的編譯器,如GCC( Compiler Collection)、ICL( C/C++ Compiler)、VectorC,也使用這種格式的目標檔案。不僅僅是C/C++,很多其它語言也使用這種格式的物件檔案。統一格式的目標檔案為混合語言帶來了極大的方便。
 當然,並不是只有這一種物件檔案格式。常用格式的還有OMF-物件模型檔案(Object Module File)以及ELF-可執行及連線檔案格式(Executable and Linking Format)。OMF是一大群IT巨頭在n年制定的一種格式,在平臺上很常見。大家喜歡的Borland公司現在使用的目標檔案就是這種格式。MS和Intel在n年前用的也是這種格式,現在都改投異側,用COFF格式了。ELF格式在非Windows平臺上使用得比較多,在Windows平臺基本上沒見過。做為員,很有必要認識一下這些你經常打交道的傢伙!不過這次讓我介紹COFF先!
 
 COFF的檔案結構
 讓我們先來看一下COFF檔案的整體結構,看看它到底長得什麼樣!

File Header

Optional Header

Section Header 1

......

Section Header n

Section Data

Relocation Directives

Line Numbers

Symbol Table

String Table

如左圖:
COFF檔案一共有8種資料,自上而下分別為:
1. 檔案頭(File Header)
2. 可選頭(Optional Header)
3. 段落頭(Section Header)
4. 段落資料(Section Data)
5. 重定位表(Relocation Directives)
6. 行號表(Line Numbers)
7. 符號表(Symbol Table)
8. 字串表(String Table)
 其中,除了段落頭可以有多個節(因為可以有多個段落)以外,其它的所有型別的節最多隻能有一個。
 檔案頭:顧名思義,它就是COFF檔案的頭,它用來儲存COFF檔案的基本資訊,如檔案標識,各個表的位置等等。
 可選頭:再顧名思義,它也是一個頭,還是可選的,而且可有可無。在目標檔案中,基本上都沒有這個頭;但在其它的檔案中(如:可執行檔案)這個段用來儲存在檔案頭中沒有描述到的資訊。
 段落頭:又顧……(不顧了,再顧有人要打我了J),這個頭(怎麼這麼多的頭啊?!)是用來描述段落資訊的,每個段落都有一個段落頭來描述。段落的數目在檔案頭中會指出。
 段落資料:這通常是COFF檔案中最大的資料段,每個段落真正的資料就儲存在這個位置。至於怎麼區分這些資料是哪個段落的,不要問我,去問段落頭。
 重定位表:這個表通常只存在於目標檔案中,它用來描述COFF檔案中符號的重定位資訊。至於為什麼要重定位,請回家看看你的操作的書籍。
 符號表:這個表用來儲存COFF檔案中所用到的所有符號的資訊,連線多個COFF檔案時,這個表幫助我們重定位符號。程式時也要用到它。
 字串表:不用我說,大家也知道它用來儲存字串的。可是字串儲存給誰看呢?不知道了吧!?問我啊!J符號表是以記錄的形式來描述符號資訊的,但它只為符號名稱留置了8個字元的空間,早期的小程式還將就能行,可在現在的程式中,一個符號名動不動就數十個字元,8個字元怎麼能夠?沒辦法,只好把這些名稱存在字串表中。而符號表中只記錄這些字串的位置。
 檔案的結構大體上就是這樣了。長得是醜了點,不過還算它的設計者有點遠見。可擴充性設計得不錯,以致於沿用至今。瞭解了檔案的整體結構,現在讓我們來逐個段落分析它。

 檔案頭
 檔案頭,自然是從檔案的0偏移處開始,它的結構很簡單。用C的結構描述如下:
typedef struct {
  unsigned short usMagic; // 魔法數字
  unsigned short usNumSec; // 段落(Section)數
  unsigned long  ulTime; // 時間戳
  unsigned long  ulSymbolOffset; // 符號表偏移
  unsigned long  ulNumSymbol; // 符號數
  unsigned short usOptHdrSZ; // 可選頭長度
  unsigned short ulags; // 檔案標記
} FILEHDR;
 結構中usMagic成員是一個魔法數字(Magic Number),在I386平臺上的COFF檔案中它的值為0x014c。如果COFF檔案頭中魔法數字不為0x014c,那就不用看了,這不是一個I386平臺的COFF檔案。其實這就是一個平臺標識。
 第二個成員usNumSec是一個無符號短整型,它用來描述段落的數量。段落頭(Section Header)的數目就是它。
 ulTime成員是一個時間戳,它用來描述COFF檔案的建立時間。當COFF檔案為一個可執行檔案時,這個時間戳經常用來當做一個用的比對標識。
 ulSymbolOffset是符號表在檔案中的偏移量,這是一個絕對偏移量,要從檔案頭開始計數。在COFF檔案的其它節中,也存在這種偏移量,它們都是絕對偏移量。
 ulNumSymbol成員給出了符號表中符號記錄的數量。
 usOptHdrSZ是可選頭的長度,通常它為0。而可選頭的型別也是從這個長度得知的,針對不同的長度,我們就要選擇不同的處理方式。
 usFlag是COFF檔案的屬性標記,它標識了COFF檔案的型別,COFF檔案中所儲存的資料等等資訊。
 其值如下:
值名稱 說明 0x0001 F_RELFLG 無重定位資訊標記。這個標記指出COFF檔案中沒有重定位資訊。通常在目標檔案中這個標記們為0,在可執行檔案中為1。 0x0002 F_EXEC 可執行標記。這個標記指出 COFF 檔案中所有符號已經解析, COFF 檔案應該被認為是可執行檔案。 0x0004 F_LNNO 檔案中所有行號已經被去掉。 0x0008 F_LSYMS 檔案中的符號資訊已經被去掉。 0x0100 F_AR32WR 些標記指出檔案是 32 位的 Little-Endian COFF 檔案。
 注:Little-Endian,記不得它的中文名稱了。它是指資料的排列方式。比如:十六進位制的0x1234以Little-Endian方式在中的順序為0x34 0x12。與之相反的是Big-Endian,這種方式下,在記憶體中的順序是0x12 0x34。
這個表的內容並不全面,但在目標檔案中,常用的也就只有這些。其它的標記我將在以後介紹PE格式時給出。
可選頭
 可選頭接在檔案頭的後面,也就是從COFF檔案的0x0014偏移處開始。長度可以為0。不同長度的可選頭,其結構也不同。標準的可選頭長度為24或28位元組,通常是28啦。這裡我就只介紹長度為28的可選頭。(因為這種頭的長度是自定義的,不同的人定義的結果就不一樣,我只能選一種最常用的頭來介紹,別的我也不知道)
這種頭的結構如下:
typedef struct {
  unsigned short usMagic; // 魔法數字
  unsigned short usVersion; // 版本標識
  unsigned long  ulTextSize; // 正文(text)段大小
  unsigned long  ulInitDataSZ; // 已初始化資料段大小
  unsigned long  ulUninitDataSZ; // 未初始化資料段大小
  unsigned long  ulEntry; // 入口點
  unsigned long  ulTextBase; // 正文段基址
  unsigned long  ulDataBase; // 資料段基址(在PE32中才有)
} OPTHDR;
 第一個成員usMagic還是魔法數字,不過這回它的值應該為0x010b或0x0107。當值為0x010b時,說明COFF檔案是一個一般的可執行檔案;當值為,0x0107時,COFF則為一個ROM映象檔案。
 usVersion是COFF檔案的版本,ulTextSize是這個可執行COFF的正文段長度,ulInitDataSZ和ulUninitDataSZ分別為已初始化資料段和未初始化資料段的長度。
 ulEntry是程式的入口點,也就是COFF載入記憶體時正文段的位置(EIP暫存器的值),當COFF檔案是一個動態庫時,入口點也就是動態庫的入口。
 ulTextBase是正文段的基址。
 ulDataBase是資料段基址。
 其實在這些成員中,只要注意usMagic和ulEntry就可以了。

 段落頭
 段落頭緊跟在可選頭的後面(如果可選頭的長度為0,那麼它就是緊跟在檔案頭後)。它的長度為36個位元組,如下:
typedef struct {
  char  cName[8]; // 段名
  unsigned long  ulVSize; // 虛擬大小
  unsigned long  ulVAddr; // 虛擬地址
  unsigned long  ulSize; // 段長度
  unsigned long  ulSecOffset; // 段資料偏移
  unsigned long  ulRelOffset; // 段重定位表偏移
  unsigned long  ulLNOffset; // 行號表偏移
  unsigned short ulNumRel; // 重定位表長度
  unsigned short ulNumLN; // 行號表長度
  unsigned long  ulFlags; // 段標識
} SECHDR;
 這個頭可是個重要的頭頭,我們要用到的最終資訊就由它來描述。一個COFF檔案可以不要其它的節,但檔案頭和段落頭這兩節是必不可少的。
 cName用來儲存段名,常用的段名有.text,.data,.comment,.bss等。.text段是正文段,通常也就是程式碼段;.data是資料段,在這個資料段中所儲存的資料是初始化過的資料;.bss段也可以用來儲存資料,不過這裡的資料是未初始化的,這個段也是一個空段;.comment段,看名字也知道,它是註釋段,用來儲存一些編譯資訊,算是對COFF檔案的註釋。
 ulVSize是段資料載入記憶體時的大小。只在可執行檔案中有效,在目標檔案中總為0。如果它的長度大於段的實際長度,則多的部分將用0來填充。
 ulVAddr是段資料載入或連線時的虛擬地址。對於可執行檔案來說,這個地址是相對於它的地址空間而言。當可執行檔案被載入記憶體時,這個地址就是段中資料的第一個位元組的位置。而對於目標檔案而言,這只是重定位時,段資料當前位置的一個偏移量。為了計算方便,便定位的計算簡化,它通常設為0。
 ulSize這才是段中資料的實際長度,也就是段資料的長度,在讀取段資料時就由它來確定要讀多少位元組。
 ulSecOffset是段資料在COFF檔案中的偏移量。
 ulRelOffset是該段的重定位資訊的偏移量。它指向了重定位表的一個記錄。
 ulLNOffset是該段的行號表的偏移量。它指向的是行號表中的一個記錄。
 ulNumRel是重定位資訊的記錄數。從ulRelOffset指向的記錄開始,到第ulNumRel個記錄為止,都是該段的重定位資訊。
 ulNumLN和ulNumRel相似。不過它是行號資訊的記錄數。
 ulFlags是該段的屬性標識。其值如下表:
值名稱 說明 0x0020 STYP_TEXT 正文段標識,說明該段是程式碼。 0x0040 STYP_DATA 資料段標識,有些標識的段將用來儲存已初始化資料。 0x0080 STYP_BSS 有這個標識段也是用來儲存資料,不過這裡的資料是未初始化資料。 注意,在BSS段中,ulVSize、ulVAddr、ulSize、ulSecOffset、ulRelOffset、ulLNOffset、ulNumRel、ulNumLN的值都為0。(上表只是部分值,其它值在PE格式中介紹,後同)

 段資料
 “人”如其名,這裡是儲存各個段的資料的位置。不同型別的段,資料的內容、結構也不盡相同。但在目標檔案中,這些資料都是原始資料(Raw Data)。不存在什麼特別的格式。

 重定位表
 這個表所儲存的是各個段的重定位資訊。這是一張很大的表,因為所有段的重定位資訊都在這個表裡。各個段落頭記錄了自己的重定位資訊的偏移和數量。要用到重定位資訊時就到這個表裡來讀。當然,你也可以把整個重定位表看成多個重定位表,每個段落都有一個自己的重定位表。這個表只在目標檔案中有,可執行檔案中是不存在這個表的。
 既然有表,那麼就會有記錄。重定位表中的每一條記錄就是一條重定位資訊。這個記錄的結構很簡單,如下:
typedef struct {
  unsigned long  ulAddr; // 定位偏移
  unsigned long  ulSymbol; // 符號
  unsigned short usType; // 定位型別
} RELOC;
 有夠簡單吧,一共只三個成員!ulAddr是要定位的內容在段內偏移。比如:一個正文段,起始位置為0x010,ulAddr的值為0x05,那你的定位資訊就要寫在0x15處。而且資訊的長度要看你的程式碼的型別,32位的程式碼要寫4個位元組,16位的就只要字2個位元組。
 ulSymbol是符號,它指向符號表中的一個記錄。注意,這裡是索引,不是偏移!它只是符號表中的一個記錄的記錄號。這個成員指明瞭重定位資訊所對映的符號。
usType是重定位型別的標識。32位程式碼中,通常只用兩種定位方式。一是絕對定位,二是相對定位。其程式碼如下:
值名稱 說明 6 RELOC_ADDR32 32位絕對定位。 20 RELOC_REL32 32位相對定位。 對於不同的,這些值也不盡相同。這裡給出的是i386平臺上最常用的兩個種定位方式的標識。
 其定位方式如下:
 絕對定位
 在絕對定位方式下,你要給出符號的絕對地址(注意,有時候這裡可能不是地址,而是值,對於常量來說,你不用給出它的地值,只用給出它的值)。當然,這個地址也不是現成的,你要用符號的相對地址+它所在段的相對地址來得到它的絕對地址。
 公式:符號絕對地址=段偏移+符號偏移
 這些偏移量你要分別從段落頭和符號表中得到。當段落要重定位時,當然還要先重定位段落,才能定位其中的符號。
 相對定位
 相對定位要複雜一些。它所要的地址資訊是相對於當前位置的偏移,這個當前位置就是ulAddr所指向的這個偏移的絕對地址後四個位元組(32位程式碼是四個位元組,16位是兩個位元組)的位置。也就是用定位偏移+當前段偏移+機器字長÷8
 公式:當前地址=定位偏移+當前段偏移+機器字長÷8
 有了當前地址,相對地址就好計算了。只要用符號的絕對地址減去當前地址就可以了。
 公式:相對地址=符號絕對地址-當前地址
 計算好了地址,把它寫到ulAddr所指向的位置,就一切OK!你已經完成了重定位的工作了。

 行號表
 行號表在除錯時很有用。它把可執行的二進位制程式碼與的行號之間建立了對映關係。這樣,當程式執行不正確時(其實正確的也可以J),我們就可以根據當前執行程式碼的位置得知出錯原始碼的行號,再加以修改。如果沒有它的話,鬼才知道是哪一行出了毛病!
 它的格式也很簡單。只有兩個成員,如下:
typedef struct {
 unsigned long ulAddrORSymbol; // 程式碼地址或符號索引
 unsigned short usLineNo; // 行號
} LINENO;
 讓我們先看第二個成員,usLineNo。這是一個從1開始計數的計數器,它代表原始碼的行號。第一個成員ulAddrORSymbol在行號大於0時,代表原始碼的地址;而當行號為0時,它就成了行號所對映的符號在符號表中的索引。下面讓我們來看看符號表吧!

 符號表
 符號表是物件檔案中用來儲存符號資訊的一張表,也是COFF檔案中最為複雜的一張表。所有段落使用到的符號都在這個表裡。它也是由很多條記錄組成,每條記錄都以如下結構儲存:
typedef struct {
  union {
  char cName[8]; // 符號名稱
  struct {
  unsigned long ulZero; // 字串表標識
  unsigned long ulOffset; // 字串偏移
  } e;
  } e;
  unsigned long ulValue;   // 符號值
  short iSection; // 符號所在段
  unsigned short usType;    // 符號型別
  unsigned char usClass;   // 符號型別
  unsigned char usNumAux;   // 符號附加記錄數
} SYMENT;
 cName符號名稱,和前面所有的名稱一樣,它也是8個位元組,但不同的是它在一個聯合體中。和它佔相同的儲存空間的還有ulZero和ulOffset這兩個成員。如果符號的名稱只有8個字元,那很好,可以直接放到這個cName中;可是,如果名稱的長度大於8個位元組,這裡就放不下了,只好放到字串表中。這時候,ulZero的值就會為0,而在ulOffset中會給出我們所用的符號的名稱在字串表中的偏移。
 一個符號有了名稱不夠,它還要有值!ulValue就是這個符號所代表的值。
 iSection成員指出了這個符號所在的段落。如果它的值為0,那麼這個符號就是一個外部符號,要從其它的COFF檔案中解析(連線多個目標檔案就是要解析這種符號)。當它的值為-1時,說明這個符號的值是一個常量,不是它在段落中的偏移。而當它的值為-2時,這個符號只是一個除錯符號,只有在除錯時才會用到它。當它大於0時,才是符號所在段的索引值。
 usType是符號的型別標識。它用來說明這個符號的型別,是函式?整型?還是其它什麼。這個標識是兩個位元組。
 低位元組的低四位是基本標識,它指出了符號的基本型別,如整型,字元,結構,聯合等。高四位指出了符號的高階型別,如指標(0001b),函式(0010b),陣列(0011b),無型別(0000b)等。現在的編譯器,通常不使用基本型別,只使用高階型別。所以,符號的基本型別通常被設為0。
高位元組通常未用。
 usClass是符號的儲存型別標識。它指明瞭符號的儲存方式。
 其值與意義見下表:
值名稱 說明 NULL 0 無儲存型別。 AUTOMATIC 1 自動型別。通常是在棧中分配的變數。 EXTERNAL 2 外部符號。當為外部符號時,iSection的值應該為0,如果不為0,則ulValue為符號在段中的偏移。 STATIC 3 靜態型別。ulValue為符號在段中的偏移。如果偏移為0,那麼這個符號代表段名。 REGISTER 4 暫存器變數。 MEMBER_OF_STRUCT 8 結構成員。ulValue值為該符號在結構中的順序。 STRUCT_TAG 10 結構識別符號。 MEMBER_OF_UNION 11 聯合成員。ulValue值為該符號在聯合中的順序。 UNION_TAG 12 聯合識別符號。 TYPE_DEFINITION 13 型別定義。 FUNCTION 101 函式名。 FILE 102 檔名。
 最後一個成員usNumAux是附加記錄的數量。附加記錄是用來描述符號的一些附加資訊,為了便於儲存,這些附加記錄通常選擇成為一條符號資訊記錄的整數倍(多數為1)。所以,如果這個成員的值為1,那麼就表示在當前符號資訊記錄後附加了一條記錄,用來儲存附加資訊。
 附加資訊的結構是與符號的型別以及儲存型別相關的。不同的型別的符號,其附加資訊(如果有的話)的結構也不同。如果你不在意這些內容,也可以把它們乎略。
 當段的型別為FILE時,附加資訊就是一個字串,它是目標檔案對應原始檔的名稱。其它型別在介紹PE時再進行詳細討論。

 字串表
 不用多說,瞎子也能看出這個表是用來儲存字串的。它緊接在符號表後。至於為什麼要儲存字串,前面已經說過了。這裡就不再多說了,只說說字串的儲存格式。
 字串表是所有節中最簡單一節。如下圖: 0 4 
字串表長度字串1 .... 字串n

 字串表的前四個位元組是字串表的長度,以位元組為單位。其後就是以0結尾的字串(C風格字串)。要注意的是,字串表的長度不僅僅是字串的長度(這個長度要包括每個字串後的‘’)的總合,它還包括這個長度域的四個位元組。符號表中ulOffset成員所指出的偏移就是從字串表起始處的偏移。比如:指像每一個字串的符號,ulOffset的值總為4。
 下面給出的程式碼,是從字串表中讀取字串的典型C程式碼。

int iStrlen,iCur=4;  // iStrLen是字串表的長度,iCur是當前字串偏移
char *str; // 字串表
read(fn, &iStrlen, 4); // 得到字串表長度
str = (char *)malloc(iStrlen); // 為字串表分配空間
while (iCur iCur+=read(fn, str+iCur, iStrlen- iCur);
iCur=4; // 把當前字串偏移指到每一個字串
while (iCur printf("String offset 0x%04X : %s ", iCur, str + iCur);
 iCur+=(strlen(str+iCur)+1);    // 計算偏移時不要忘了計算‘’字元所佔的1個位元組!
}
free(str); // 釋放字串表空間
 
 直到這裡,整個COFF的結構已經全部介紹完了。很多瞭解PE格式的朋友一定會奇怪,好像少了很多內容!?是的,標準的COFF檔案只有這麼多的東西。但MS為了和DOS的可執行檔案相容,以及對可執行檔案功能的擴充套件,在COFF格式中加了很多它自己的標準。讓我差點就認不出COFF了。但瞭解了COFF檔案以後,再來學習PE檔案的格式,那就很簡單了。
 想了解PE檔案的格式?網上有很多它的資料,我將在本文的基礎上再寫幾篇文章,分別介紹PE,OMF以及ELF的格式。
 現在大家可以自己動手,寫一個COFF檔案解析器或是一個簡單的連線程式了!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-963008/,如需轉載,請註明出處,否則將追究法律責任。

COFF檔案的格式 (轉)
請登入後發表評論 登入
全部評論

相關文章