庫文件是怎么樣的結(jié)構(gòu)呢?
其實,庫文件的結(jié)構(gòu)也很簡單。它就是“一堆”目標文件的集合。把目標文件做成庫以后,我們在使用目標文件中所實現(xiàn)的功能時,連接程序會自動在庫文件里查找相應(yīng)的目標文件,并使用它。這大大減少了我們對目標文件的管理工作,減輕了代碼重用的負擔(dān)。
Lib文件中的節(jié)
COFF格式中所用到的“節(jié)”的概念再次出現(xiàn)在Lib格式中。不過,Lib文件的節(jié)要簡單得多。先讓我們來看看它的整體結(jié)構(gòu):
如右圖所示: Lib格式只有四種類型的節(jié)(Section),即First Sec,Second Sec,Longname Sec和Obj Sec;其中Second Sec與Longname Sec是可選節(jié),很多Lib文件中都沒有。而開頭的Singature只是一個標識,它相當(dāng)于COFF目標文件中的魔法數(shù)字。它是一個長度為8的字符串,值為“!<arch>\n”。 First Sec,顧名思義,就是第一個節(jié)。它包含了庫中所有的符號名以及這些符號所在的目標文件在庫中的位置(絕對偏移)。 Second Sec就是第二節(jié)。它的內(nèi)容和First Sec是相同的。不同的是,Second Sec是一個有序表,通過它來查找?guī)熘械姆柋韧ㄟ^First Sec來查找要快很多。 |
|
Longname Sec是長名稱節(jié)。這一節(jié)是一個字符串表。它包含了所有長目標文件名。如果后面的Obj Sec中沒有給出相應(yīng)的目標文件名,我們就要到這一節(jié)中來查找。
Obj Sec就是目標文件節(jié)。這些節(jié)中存儲著不同的目標文件的原始數(shù)據(jù)。
在庫文件中,每一節(jié)都有兩個部分。一個部分是頭,另一個部分才是該節(jié)的數(shù)據(jù);數(shù)據(jù)緊跟在頭的后面。頭描述了該節(jié)數(shù)據(jù)的類型、長度等信息。這些頭的格式都是相同的。其結(jié)構(gòu)用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];// 結(jié)束符
} SectionHeader;
可以看到,頭中的數(shù)據(jù)全都是字符串。用字符串的好處是可以提高格式的兼容性,因為在不同的機器上,數(shù)據(jù)的排列方式是不同的。有的機器是以Little-Endian方式工作,還有的是以Big-Endian方式工作,它們互不兼容(這兩種方式的區(qū)別!?請看我的《COFF格式》一文,其中的文件頭一節(jié)有說明)。用字符串就不會有這種問題(后面我們將會遇到)。但它也有不方便的地方,就是必須把字符串轉(zhuǎn)換成數(shù)值,多了一個步驟。
在這個結(jié)構(gòu)中,最常用的Name、Size以及EndOfHeader三個成員。Name就是節(jié)的名稱啦!Size也很好理解,就是該節(jié)數(shù)據(jù)的長度。現(xiàn)在要注意的就是這個EndOfHeader成員了!這個成員標志著頭的結(jié)束,其內(nèi)容為“`\n”(注意,這里沒有打錯,是兩個字符“`”和“\n”)。怎么樣?有點奇怪吧?為什么要有這個結(jié)束符?每一節(jié)的頭長度一定,每節(jié)中的數(shù)據(jù)長度也知道。按順序向下讀不行嗎?答案是:不行!因為每一節(jié)之間存在間隙!通常是一個字節(jié)或零個字節(jié)。如果是零個字節(jié)倒好,按順序向下讀是OK的。可是如果不為零的話,這樣讀就要錯位了。要知道錯位沒有,只好用一個結(jié)束符來定位了。如果在讀頭的時候發(fā)現(xiàn)結(jié)束符不對,那就要一個字節(jié)一個字節(jié)地向下查找,直到找到結(jié)束符,才能算是對齊了。切記!切記!
當(dāng)然,通過First Sec或Second Sec中給出的偏移來讀數(shù)據(jù)就不存在這個問題。不會發(fā)生錯位,放心讀吧!
現(xiàn)在讓我們來看看每一節(jié)中的數(shù)據(jù)是什么樣子。
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];// 結(jié)束符
} SectionHeader;
可以看到,頭中的數(shù)據(jù)全都是字符串。用字符串的好處是可以提高格式的兼容性,因為在不同的機器上,數(shù)據(jù)的排列方式是不同的。有的機器是以Little-Endian方式工作,還有的是以Big-Endian方式工作,它們互不兼容(這兩種方式的區(qū)別!?請看我的《COFF格式》一文,其中的文件頭一節(jié)有說明)。用字符串就不會有這種問題(后面我們將會遇到)。但它也有不方便的地方,就是必須把字符串轉(zhuǎn)換成數(shù)值,多了一個步驟。
在這個結(jié)構(gòu)中,最常用的Name、Size以及EndOfHeader三個成員。Name就是節(jié)的名稱啦!Size也很好理解,就是該節(jié)數(shù)據(jù)的長度。現(xiàn)在要注意的就是這個EndOfHeader成員了!這個成員標志著頭的結(jié)束,其內(nèi)容為“`\n”(注意,這里沒有打錯,是兩個字符“`”和“\n”)。怎么樣?有點奇怪吧?為什么要有這個結(jié)束符?每一節(jié)的頭長度一定,每節(jié)中的數(shù)據(jù)長度也知道。按順序向下讀不行嗎?答案是:不行!因為每一節(jié)之間存在間隙!通常是一個字節(jié)或零個字節(jié)。如果是零個字節(jié)倒好,按順序向下讀是OK的。可是如果不為零的話,這樣讀就要錯位了。要知道錯位沒有,只好用一個結(jié)束符來定位了。如果在讀頭的時候發(fā)現(xiàn)結(jié)束符不對,那就要一個字節(jié)一個字節(jié)地向下查找,直到找到結(jié)束符,才能算是對齊了。切記!切記!
當(dāng)然,通過First Sec或Second Sec中給出的偏移來讀數(shù)據(jù)就不存在這個問題。不會發(fā)生錯位,放心讀吧!
現(xiàn)在讓我們來看看每一節(jié)中的數(shù)據(jù)是什么樣子。
First Sec
第一節(jié),通常就是Lib中的每一個小節(jié)。它的名稱是“/”。其數(shù)據(jù)部分的結(jié)構(gòu)如下:
typedef struct {
unsigned long SymbolNum; // 庫中符號的數(shù)量
unsigned long SymbolOffset[n]; // 符號所在目標節(jié)的偏移
char StrTable[m]; // 符號名稱字符串表
}FirstSec;
第一個成員SymbolNum是符號的數(shù)量。注意!它是以Big-Endian方式儲存的(x86平臺上的數(shù)據(jù)是以Little-Endian方式儲存的。這里應(yīng)該注意轉(zhuǎn)換。后面給出的convert函數(shù)可以在Little-Endian格式與Big-Endian格式之間進行相互轉(zhuǎn)換)。
第二個成員SymbolOffset是一個數(shù)組,它的長度n就是符號的數(shù)量,也就是SymbolNum。這個數(shù)組儲存了每一個符號所在的目標節(jié)的偏移。我們可以方便地通過它來查找符號所在的目標文件。注意!它也是以Big-Endian格式儲存的。
第三個成員StrTable是一個字符串表,它的長度m就是SectionHeader.Size的值減去(SymbolNum+1)*4。其結(jié)構(gòu)很簡單,就是一堆以‘\0’結(jié)尾的字符串(和COFF文件中的字符串表結(jié)構(gòu)相同)。在有的系統(tǒng)中,它還可能是以“/\n”這兩個字符結(jié)尾的字符串的集合。
很簡單的一個結(jié)構(gòu),不過有兩個成員的長度是不定的。怎么才能方便地從Lib中讀出這些數(shù)據(jù),留給大家自己想吧!下面我只給出一個進行Little-Endian與Big-Endian互轉(zhuǎn)的函數(shù)。
inline void convert(void * p // 要轉(zhuǎn)換的數(shù)據(jù)的指針
,size_t size = 4 // 數(shù)據(jù)的長度,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;
}
}
第一節(jié),通常就是Lib中的每一個小節(jié)。它的名稱是“/”。其數(shù)據(jù)部分的結(jié)構(gòu)如下:
typedef struct {
unsigned long SymbolNum; // 庫中符號的數(shù)量
unsigned long SymbolOffset[n]; // 符號所在目標節(jié)的偏移
char StrTable[m]; // 符號名稱字符串表
}FirstSec;
第一個成員SymbolNum是符號的數(shù)量。注意!它是以Big-Endian方式儲存的(x86平臺上的數(shù)據(jù)是以Little-Endian方式儲存的。這里應(yīng)該注意轉(zhuǎn)換。后面給出的convert函數(shù)可以在Little-Endian格式與Big-Endian格式之間進行相互轉(zhuǎn)換)。
第二個成員SymbolOffset是一個數(shù)組,它的長度n就是符號的數(shù)量,也就是SymbolNum。這個數(shù)組儲存了每一個符號所在的目標節(jié)的偏移。我們可以方便地通過它來查找符號所在的目標文件。注意!它也是以Big-Endian格式儲存的。
第三個成員StrTable是一個字符串表,它的長度m就是SectionHeader.Size的值減去(SymbolNum+1)*4。其結(jié)構(gòu)很簡單,就是一堆以‘\0’結(jié)尾的字符串(和COFF文件中的字符串表結(jié)構(gòu)相同)。在有的系統(tǒng)中,它還可能是以“/\n”這兩個字符結(jié)尾的字符串的集合。
很簡單的一個結(jié)構(gòu),不過有兩個成員的長度是不定的。怎么才能方便地從Lib中讀出這些數(shù)據(jù),留給大家自己想吧!下面我只給出一個進行Little-Endian與Big-Endian互轉(zhuǎn)的函數(shù)。
inline void convert(void * p // 要轉(zhuǎn)換的數(shù)據(jù)的指針
,size_t size = 4 // 數(shù)據(jù)的長度,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
現(xiàn)在看看第二節(jié)。
這一節(jié)與第一節(jié)很相似!它通常也就是Lib文件的第二個節(jié)。它的名字也是“/”(注意:文件中第一個叫“/”的節(jié)是第一節(jié),第二個就是第二節(jié))。不過它的結(jié)構(gòu)與第一節(jié)有些不同,如下:
typedef struct {
unsigned long ObjNum; // Obj Sec的數(shù)量
unsigned long ObjOffset[x]; // 每一個Obj Sec的偏移
unsigned long SymbolNum; // 庫中符號的數(shù)量
unsigned short SymbolIdx[n]; // 符號在ObjOffset表中的索引
char StrTable[m]; // 符號名稱字符串表
}SecondSec;
第一個成員ObjNum是庫中Obj Sec的數(shù)量。
第二個成員ObjOffset是一個偏移表,它記錄了庫中所有Obj Sec的偏移。這個表的記錄數(shù)x就是ObjNum。
第三個成員SymbolNum與First Sec中的SymbolNum意義相同。
第四個成員SymbolIdx變成了一個索引,它記錄了相應(yīng)名稱字符串在ObjOffset這個表中的位置,我們要通過兩次索引才能找到我們所要符號的Obj Sec位置。它的項目數(shù)n為SymbolNum。但請注意,這個索引是unsigned short型,不再是unsigned long型。
第五個成員StrTable結(jié)構(gòu)與First Sec中的一樣。不過,它的長度m為SectionHeader.Size的值減去((ObjNum+1)*4+(SymbolNum+2)*2)。
值得注意的是,這里的所有數(shù)據(jù)都是Little-Endian格式的。千萬不要弄錯了!
Longname Sec
這個小節(jié)就是一個字符串表,它的名稱為“//”,其結(jié)構(gòu)同F(xiàn)irstSec.StrTable。這里就不多說了。
Obj Sec
這一節(jié)中的數(shù)據(jù)就是COFF文件的原始數(shù)據(jù),把它讀出來存成文件,就是一個COFF文件。它的格式請參考《COFF格式》一文。
要指出的是它的命名方式有些特殊。如果Obj文件的名稱少于16個字符,它就會被保存在SectionHeader的Name成員中,以‘/’字符結(jié)尾。如果無法保存在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
……
現(xiàn)在看看第二節(jié)。
這一節(jié)與第一節(jié)很相似!它通常也就是Lib文件的第二個節(jié)。它的名字也是“/”(注意:文件中第一個叫“/”的節(jié)是第一節(jié),第二個就是第二節(jié))。不過它的結(jié)構(gòu)與第一節(jié)有些不同,如下:
typedef struct {
unsigned long ObjNum; // Obj Sec的數(shù)量
unsigned long ObjOffset[x]; // 每一個Obj Sec的偏移
unsigned long SymbolNum; // 庫中符號的數(shù)量
unsigned short SymbolIdx[n]; // 符號在ObjOffset表中的索引
char StrTable[m]; // 符號名稱字符串表
}SecondSec;
第一個成員ObjNum是庫中Obj Sec的數(shù)量。
第二個成員ObjOffset是一個偏移表,它記錄了庫中所有Obj Sec的偏移。這個表的記錄數(shù)x就是ObjNum。
第三個成員SymbolNum與First Sec中的SymbolNum意義相同。
第四個成員SymbolIdx變成了一個索引,它記錄了相應(yīng)名稱字符串在ObjOffset這個表中的位置,我們要通過兩次索引才能找到我們所要符號的Obj Sec位置。它的項目數(shù)n為SymbolNum。但請注意,這個索引是unsigned short型,不再是unsigned long型。
第五個成員StrTable結(jié)構(gòu)與First Sec中的一樣。不過,它的長度m為SectionHeader.Size的值減去((ObjNum+1)*4+(SymbolNum+2)*2)。
值得注意的是,這里的所有數(shù)據(jù)都是Little-Endian格式的。千萬不要弄錯了!
Longname Sec
這個小節(jié)就是一個字符串表,它的名稱為“//”,其結(jié)構(gòu)同F(xiàn)irstSec.StrTable。這里就不多說了。
Obj Sec
這一節(jié)中的數(shù)據(jù)就是COFF文件的原始數(shù)據(jù),把它讀出來存成文件,就是一個COFF文件。它的格式請參考《COFF格式》一文。
要指出的是它的命名方式有些特殊。如果Obj文件的名稱少于16個字符,它就會被保存在SectionHeader的Name成員中,以‘/’字符結(jié)尾。如果無法保存在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!現(xiàn)在已經(jīng)介紹完了Lib文件的結(jié)構(gòu)。大家的連接器可以加新功能了。不過這里只給出了最基本的Lib文件結(jié)構(gòu),動態(tài)連接庫(DLL)的導(dǎo)出庫有點特別,我將在PE文件格式中進行詳細介紹。