C語言程序的編譯和鏈接過程
1.程序的編譯
一般而言,大多數編譯系統都提供編譯驅動程序(complier driver),根據用戶需求調用語言預處理器,編譯器,匯編器和鏈接器.例如有如下歷程:
//main.c
void swap();
int buf[2]={1, 2};
int main()
{
swap();
return 0;
}
//swap.c
int *bufp0 = &buf[0]
int *bufp1;
void swap()
{
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
驅動程序首先運行C預處理器(cpp),它將C的源程序main.c翻譯成一個ASCII碼的中間文件main.i.接下來,驅動程序運行C編譯器(ccl),將main.i翻譯成一個ASCII匯編語言文件main.s.然后,驅動程序運行匯編器(as),它將main.s翻譯成一個可重定位的目標文件main.o.具體過程如下圖所示:
2.鏈接
鏈接就是將不同部分的代碼和數據收集和組合成為一個單一文件的過程,這個文件可被加載或拷貝到存儲器執行.
鏈接可以執行與編譯時(源代碼被翻譯成機器代碼時),也可以執行與加載時(在程序被加載器加載到存儲器並執行時),甚至執行與運行時,由應用程序來執行.在現代系統中,鏈接是由鏈接器自動執行的.
鏈接器分為:靜態鏈接器和動態鏈接器兩種.
2.1.靜態鏈接器
靜態鏈接器以一組可重定位目標文件和命令行參數作為輸入,生成一個完全鏈接的可以加載和運行的可執行目標文件作為輸出.
靜態鏈接器主要完成兩個任務:
1>符號解析:目標文件定義和引用符號.符號解析的目的在於將每個符號引用和一個符號定義聯系起來.
2>重定位:編譯器和匯編器生成從地址零開始的代碼和數據節.鏈接器通過把每個符號定義和一個存儲器位置聯系起來,然后修改所有對這些符號的引用,使得他們執行這個存儲位置,從而重定位這些節.
目標文件:
目標文件有三種形式:
1>可重定位的目標文件:
包含二進制代碼和數據,其形式可以再編譯時與其他可定位目標文件合並起來,創建一個可執行目標文件.
2>可執行目標文件:
包含二進制代碼和數據,其形式可以被直接拷貝到存儲器並執行.
3>共享目標文件:
一種特殊的可重定位目標文件,可以再加載或運行時,被動態地夾在到存儲器並執行.
編譯器和匯編器生成可重定位目標文件(包括共享目標文件),鏈接器生成可執行目標文件.
可重定位目標文件:
EF頭L以一個16字節的序列開始,這個序列描述了字的大小和生成該文件的系統字節順序.ELF頭剩下的部分包含幫助鏈接器解析和解釋目標文件的信息.其中包括ELF頭的大小,目標文件的類型(比如,可重定位,可執行,共享目標文件),機器類型,節頭部表的文件偏移,以及節頭部表中的表目大小和數量.不同節的位置和大小是節頭部表描述的,其中目標文件中的每個節都有一個固定大小的表目.ELF格式的可重定位目標文件結構如下圖:
.text:已編譯程序的機器代碼
.rodata:只讀數據
.data:已初始化的全局C變量
.bss:未初始化的全局C變量.在目標文件中這個節不占實際空間,僅是一個占位符.
.sysmtab:一個符號表,存放在程序中被定義和引用的函數和全局變量的信息.
.rel.text:當鏈接器把這個目標文件和其他文件結合時,.text節中的許多位置都需要修改.一般而言,任何調用外部函數或者引用全局變量的指令都要修改.另一個方面,調用本地函數的指令則不需要修改.
.rel.data:被模塊定義或引用的任何全局變量的信息.
.debug:一個調試符號表
.line:原始C源程序中的行號和.text節中機器指令之間的映射.
.strtab:一個字符串表,其中內容包括.symtab和.debug節中的符號表,以及節頭部中的節名字.
符號和符號表
每個可重定位目標模塊m都有一個符號表,它包含m所定義和引用的符號的信息.在鏈接器上下文中,有三種不同的符號:
1>由m定義並能被其他模塊引用的全局符號.全局鏈接器符號對應於非靜態的C函數以及被定義為不帶C的static屬性的全局變量.
2>由其他模塊定義並被模塊m引用的全局符號.這些符號成為外部符號,對應於定義在其他模塊中的C函數和變量.
3>只被模塊m定義和引用的本地符號.有的本地符號鏈接器符號對應於帶static屬性的C函數和全局變量.這些符號在模塊m中的任何地方都可見,但是不能被其他模塊引用.目標文件中對應於模塊m的節和相應的源文件的名字也能獲得本地符號.
符號表式有匯編器構造的,使用編譯器輸出到匯編語言.s文件中的符號.sysmab節中包含ELF符號表.這張符號表包含一個關於表目的數組.表目的格式如下:
typedef struct{
int name; //string table offset
int value; //section offset, or VM address
int size; //object size in bytes
char type:4, //data, func, section, or src file
binding:4; //local or global
char reserved; //unused
char section; //section header index, ABS, UNDEF, or COMMON
}Elf_Symbol;
2.1.1符號解析
鏈接器解析符號引用的方法是將每個引用和它輸入的可重定位目標文件按的符號表中的一個確定的符號定義聯系起來.
對於那些和引用定義在相同模塊的本地符號的引用,符號解析式非常簡單明了的.編譯器只允許每個模塊中的每個本地符號只有一個定義.編譯器還確保靜態本地變量,它們會有本地鏈接器符號,擁有唯一的名字.
對於全局符號的引用解析,當編譯器遇到一個不是在當前模塊中定義的符號(變量或函數名)時,它會假設該符號式在其他某個模塊中定義的,生成一個鏈接器符號表表目,並把它交給鏈接器處理.如果鏈接器在它的任何輸入模塊中都找不到這個被引用的符號,它就輸出一條錯誤信息並終止.
在編譯時,編譯器輸出的每個全局符號給匯編器,或者是強,或者是弱,而匯編器把這個信息隱含地編碼在可重定位目標文件的符號表中.函數和以初始化的全局變量是強符號,未初始化的全局變量是弱符號.
根據符號的強弱,有如下規則:
1>不允許有多個強符號
2>如果有一個強符號和多個弱符號,則選擇強符號
3>如果有多個弱符號,則任選一個弱符號
與靜態庫鏈接
所有編譯系統都提供一種機制,將所有相關的目標模塊打包為一個單獨的文件,稱為靜態庫,它可以用做鏈接器的輸入.當鏈接器構造一個輸出的可執行文件時,它只拷貝靜態庫里被應用程序引用的目標模塊.
在unix系統中,靜態庫以一種稱為存檔的特殊文件格式存放在磁盤中.存檔文件是一組連接起來的可重定位目標文件的集合,有一個頭部描述每個成員目標文件的大小和位置.
鏈接器如何使用靜態庫來解析引用
在符號解析階段,鏈接器從左到右按照它們在編譯驅動程序命令行上出現的相同順序來掃描可重定位目標文件和存檔文件.在這次掃描中,鏈接器位置一個可重定位目標文件集合E,這個集合中的文件會被合並起來形成可執行文件,和一個未解析的符號集合U,以及一個在前面輸入文件中已定義的符號結合D.初始時,E,U,D都是空的.
1>對於命令行上的每個輸入文件f,鏈接器會判斷f是一個目標文件還是一個存檔文件.如果是一個目標文件,那么鏈接器把f添加到E,修改U和D來反映f中的符號定義和引用,並繼續下一個輸入文件.
2>如果f是一個存檔文件,那么鏈接器就嘗試匹配U中未解析的符號由存檔文件成員定義的符號.如果某個存檔文件成員m,定義了一個符號來解析U中的一個引用,那么就將m加到E中,並且鏈接器修改U和D來反映m中的符號定義和引用.對存檔文件中的所有成員目標文件都反復進行這個過程,知道U和D都不再發生變化.在此時,任何不包含在E中的成員目標文件都會被丟棄,而鏈接器將繼續到下一個輸入文件.
3>如果當鏈接器完成對輸入命令行的掃描后,U是非空的,那么鏈接器就會輸出一個錯誤並終止.否則,它會合並重定位E中的目標文件,從而構建輸出的可執行文件.
這種方式,導致了在輸入命令時要考慮到,靜態庫和目標文件的位置,庫文件放在目標文件的后面,如果庫文件之間有引用關系,則被引用的庫放在后面.
2.1.2重定位
當鏈接器完成了符號解析這一步時,它就把代碼中的每個符號引用和確定的一個符號定義(也就是,它的一個輸入目標模塊中的一個符號表表目)聯系起來.此時,鏈接器就知道它的輸入目標模塊中的代碼節和數據解的確切大小.然后就開始重定位步驟.重定位由兩步組成:
1>重定位節和符號定義:
在這一步中,鏈接器將所有相同類型的節合並為一個新的聚合節.然后,鏈接器將運行時存儲器地址賦值給新的聚合節,賦給輸入模塊定義的每個節,以及賦給輸入模塊定義的每個符號.當這一步完成時,程序中的每個指令和全局變量都一個唯一的運行時存儲器地址.
2>重定位節中的符號引用:
在這一步中,鏈接器修改代碼節和數據節中對每個符號的引用,使得它們指向正確的運行時地址.為了執行這一步,鏈接器依賴於稱為重定位表目的可重定位目標模塊中的數據結構.
重定位表目:
當匯編器生成一個目標模塊時,它並不知道數據和代碼最終將存放在存儲器中的什么位置.它也不知道這個模塊引用的任何外部定義的函數或者全局變量的位置.所以,無論何時匯編器遇到對最終位置未知的目標引用,它就會生成一個重定位表目,告訴鏈接器在將目標文件合並為可執行文件時,如何修改這個引用.代碼的重定位表目放在.rel.text中.已初始化數據的重定位表目放在rel.data中.
ELF重定位表目的格式如下:
typedef struct{
int offset; //offset of the reference to relocate
int symbol:24, //symbol the reference point to
type:8; //relocation type
} Elf32_Rel;
ELF定義了11中不同的重定位類型,其中最基本的兩種重定位類型是:R_386_PC32(重定位一個使用32PC相關的地址引用)和R_386_32(重定位一個使用32位絕對地址的引用).
2.2.動態鏈接器
共享庫是一個目標模塊,在運行時,可以加載到任意的存儲器地址,並在存儲器中和一個程序鏈接起來.這個過程稱為動態鏈接,是由動態鏈接器完成的.
共享庫的共享在兩個方面有所不同.首先,在任何給定的文件系統中,對於一個庫只有一個.so文件.所有引用該庫德可執行目標文件共享這個.so文件中的代碼和數據,而不是像靜態庫德內容那樣被拷貝和嵌入到引用它們的可執行的文件中.其次,在存儲器中,一個共享庫的.text節只有一個副本可以被不同的正在運行的進程共享.