LIB/PE/COFF檔案分析開源專案
這裡有一個開源專案:PE-MASTER,用來進行OBJ檔案的抽取,PE/LIB檔案分析。PE檔案修改。SVN:https://pe-master.googlecode.com/svn/trunk,如果大家對這個專案有興趣,希望加入的話,請聯絡我。
這裡是對LIB檔案格式的解析(轉載):
上一篇文章介紹了COFF目標檔案的結構。如果你試著做一個應用程式的聯結器(Linker),就會發現,僅僅有目標檔案是不夠的。我們在連線程式時,不僅僅要用到目標檔案,庫檔案也是必不可少的。
庫檔案是怎麼樣的結構呢?
其實,庫檔案的結構也很簡單。它就是“一堆”目標檔案的集合。把目標檔案做成庫以後,我們在使用目標檔案中所實現的功能時,連線程式會自動在庫檔案裡查詢相應的目標檔案,並使用它。這大大減少了我們對目標檔案的管理工作,減輕了程式碼重用的負擔。
Lib檔案中的節
COFF格式中所用到的“節”的概念再次出現在Lib格式中。不過,Lib檔案的節要簡單得多。先讓我們來看看它的整體結構:
如右圖所示: Lib格式只有四種類型的節(Section),即First Sec,Second Sec,Longname Sec和Obj Sec;其中Second Sec與Longname Sec是可選節,很多Lib檔案中都沒有。而開頭的Singature只是一個標識,它相當於COFF目標檔案中的魔法數字。它是一個長度為8的字串,值為“!<arch>/n”。 First Sec,顧名思義,就是第一個節。它包含了庫中所有的符號名以及這些符號所在的目標檔案在庫中的位置(絕對偏移)。 Second Sec就是第二節。它的內容和First Sec是相同的。不同的是,Second Sec是一個有序表,通過它來查詢庫中的符號比通過First Sec來查詢要快很多。 |
|
Longname Sec是長名稱節。這一節是一個字串表。它包含了所有長目標檔名。如果後面的Obj Sec中沒有給出相應的目標檔名,我們就要到這一節中來查詢。
Obj Sec就是目標檔案節。這些節中儲存著不同的目標檔案的原始資料。 在庫檔案中,每一節都有兩個部分。一個部分是頭,另一個部分才是該節的資料;資料緊跟在頭的後面。頭描述了該節資料的型別、長度等資訊。這些頭的格式都是相同的。其結構用C語言描述如下:
typedef struct {
char Name[16]; // 名稱
char Time[12]; // 時間
char UserID[6]; // 使用者ID
char GroupID[6]; // 組ID
char Mode[8]; // 模式
char Size[10]; // 長度
char EndOfHeader[2];// 結束符
} SectionHeader;
可以看到,頭中的資料全都是字串。用字串的好處是可以提高格式的相容性,因為在不同的機器上,資料的排列方式是不同的。有的機器是以Little-Endian方式工作,還有的是以Big-Endian方式工作,它們互不相容(這兩種方式的區別!?請看我的《COFF格式》一文,其中的檔案頭一節有說明)。用字串就不會有這種問題(後面我們將會遇到)。但它也有不方便的地方,就是必須把字串轉換成數值,多了一個步驟。
在這個結構中,最常用的Name、Size以及EndOfHeader三個成員。Name就是節的名稱啦!Size也很好理解,就是該節資料的長度。現在要注意的就是這個EndOfHeader成員了!這個成員標誌著頭的結束,其內容為“`/n”(注意,這裡沒有打錯,是兩個字元“`”和“/n”)。怎麼樣?有點奇怪吧?為什麼要有這個結束符?每一節的頭長度一定,每節中的資料長度也知道。按順序向下讀不行嗎?答案是:不行!因為每一節之間存在間隙!通常是一個位元組或零個位元組。如果是零個位元組倒好,按順序向下讀是OK的。可是如果不為零的話,這樣讀就要錯位了。要知道錯位沒有,只好用一個結束符來定位了。如果在讀頭的時候發現結束符不對,那就要一個位元組一個位元組地向下查詢,直到找到結束符,才能算是對齊了。切記!切記!
當然,通過First Sec或Second Sec中給出的偏移來讀資料就不存在這個問題。不會發生錯位,放心讀吧!
現在讓我們來看看每一節中的資料是什麼樣子。 First Sec
第一節,通常就是Lib中的每一個小節。它的名稱是“/”。其資料部分的結構如下:
typedef struct {
unsigned long SymbolNum; // 庫中符號的數量
unsigned long SymbolOffset[n]; // 符號所在目標節的偏移
char StrTable[m]; // 符號名稱字串表
}FirstSec;
第一個成員SymbolNum是符號的數量。注意!它是以Big-Endian方式儲存的(x86平臺上的資料是以Little-Endian方式儲存的。這裡應該注意轉換。後面給出的convert函式可以在Little-Endian格式與Big-Endian格式之間進行相互轉換)。
第二個成員SymbolOffset是一個數組,它的長度n就是符號的數量,也就是SymbolNum。這個陣列儲存了每一個符號所在的目標節的偏移。我們可以方便地通過它來查詢符號所在的目標檔案。注意!它也是以Big-Endian格式儲存的。
第三個成員StrTable是一個字串表,它的長度m就是SectionHeader.Size的值減去(SymbolNum+1)*4。其結構很簡單,就是一堆以‘/0’結尾的字串(和COFF檔案中的字串表結構相同)。在有的系統中,它還可能是以“//n”這兩個字元結尾的字串的集合。
很簡單的一個結構,不過有兩個成員的長度是不定的。怎麼才能方便地從Lib中讀出這些資料,留給大家自己想吧!下面我只給出一個進行Little-Endian與Big-Endian互轉的函式。
inline void convert(void * p // 要轉換的資料的指標
,size_t size = 4 // 資料的長度,long為4,short為2
) {
char * buf=(char*)p;
char temp;
for ( size_t i=0;i<size/2;i++ ) {
temp=buf[i];
buf[i]=buf[size-i-1];
buf[size-i-1]=temp;
}
} Second Sec
現在看看第二節。
這一節與第一節很相似!它通常也就是Lib檔案的第二個節。它的名字也是“/”(注意:檔案中第一個叫“/”的節是第一節,第二個就是第二節)。不過它的結構與第一節有些不同,如下:
typedef struct {
unsigned long ObjNum; // Obj Sec的數量
unsigned long ObjOffset[x]; // 每一個Obj Sec的偏移
unsigned long SymbolNum; // 庫中符號的數量
unsigned short SymbolIdx[n]; // 符號在ObjOffset表中的索引
char StrTable[m]; // 符號名稱字串表
}SecondSec;
第一個成員ObjNum是庫中Obj Sec的數量。
第二個成員ObjOffset是一個偏移表,它記錄了庫中所有Obj Sec的偏移。這個表的記錄數x就是ObjNum。
第三個成員SymbolNum與First Sec中的SymbolNum意義相同。
第四個成員SymbolIdx變成了一個索引,它記錄了相應名稱字串在ObjOffset這個表中的位置,我們要通過兩次索引才能找到我們所要符號的Obj Sec位置。它的專案數n為SymbolNum。但請注意,這個索引是unsigned short型,不再是unsigned long型。
第五個成員StrTable結構與First Sec中的一樣。不過,它的長度m為SectionHeader.Size的值減去((ObjNum+1)*4+(SymbolNum+2)*2)。
值得注意的是,這裡的所有資料都是Little-Endian格式的。千萬不要弄錯了!
Longname Sec
這個小節就是一個字串表,它的名稱為“//”,其結構同FirstSec.StrTable。這裡就不多說了。
Obj Sec
這一節中的資料就是COFF檔案的原始資料,把它讀出來存成檔案,就是一個COFF檔案。它的格式請參考《COFF格式》一文。
要指出的是它的命名方式有些特殊。如果Obj檔案的名稱少於16個字元,它就會被儲存在SectionHeader的Name成員中,以‘/’字元結尾。如果無法儲存在Name成員中,則Name成員的第一個字元就為‘/’,之後再跟上這個名稱在Longname Sec中的偏移。
例如:
!<arch>/n
……
LongName Sec:
This_Is_Long_Name0001/0
This_Is_Long_Name0002/0
……
Obj Sec1:
Name[16]:“shortname/”
……
Obj Sec2:
Name[16]:“/0” // 這裡使用了第一個長檔名This_Is_Long_Name0001
……
Obj Sec3:
Name[16]:“/22” // 這裡使用了第二個長檔名This_Is_Long_Name0002
…… OK!現在已經介紹完了Lib檔案的結構。大家的聯結器可以加新功能了。不過這裡只給出了最基本的Lib檔案結構,動態連線庫(DLL)的匯出庫有點特別,我將在PE檔案格式中進行詳細介紹。