注: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 |
< FONT>有這個標識段也是用來保存數據,不過這裏的數據是未初始化數據。 |
注意,在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\0 |
.... |
字符串n\0 |
字符串表的前四個字節是字符串表的長度,以字節爲單位。其後就是以0結尾的字符串(C風格字符串)。要注意的是,字符串表的長度不只僅是字符串的長度(這個長度要包括每一個字符串後的‘\0’)的總合,它還包括這個長度域的四個字節。符號表中ulOffset成員所指出的偏移就是從字符串表起始處的偏移。好比:指像每個字符串的符號,ulOffset的值總爲4。
下面給出的代碼,是從字符串表中讀取字符串的典型C代碼。
int iStrlen,iCur=4; // iStrLen是字符串表的長度,iCur是當前字符串偏移
char *str; // 字符串表
read(fn, &iStrlen, 4); // 獲得字符串表長度
str = (char *)malloc(iStrlen); // 爲字符串表分配空間
while (iCur<iStrlen ) // 讀字符串表,直到所有讀入內存
iCur+=read(fn, str+iCur, iStrlen- iCur);
iCur=4; // 把當前字符串偏移指到每個字符串
while (iCur<iStrlen ) { // 顯示每個字符串
printf("String offset 0x%04X : %s\n", iCur, str + iCur);
iCur+=(strlen(str+iCur)+1); // 計算偏移時不要忘了計算‘\0’字符所佔的1個字節!
}
free(str); // 釋放字符串表空間
直到這裏,整個COFF的結構已經所有介紹完了。不少了解PE格式的朋友必定會奇怪,好像少了不少內容!?是的,標準的COFF文件只有這麼多的東西。但MS爲了和DOS的可執行文件兼容,以及對可執行文件功能的擴展,在COFF格式中加了不少它本身的標準。讓我差點就認不出COFF了。但瞭解了COFF文件之後,再來學習PE文件的格式,那就很簡單了。 想了解PE文件的格式?網上有不少它的資料,我將在本文的基礎上再寫幾篇文章,分別介紹PE,OMF以及ELF的格式。 如今你們能夠本身動手,寫一個COFF文件解析器或是一個簡單的鏈接程序了!