本文內容基於《CSAPP》第7章,只是符號解析的一部分,從使用的角度闡述了靜態庫的由來和使用,僅僅是我的看法,可能從編譯的角度看有不嚴謹的地方,如發現錯誤,還請指正,謝謝!程序員
首先咱們要知道,連接器將一組可重定位目標文件連接起來能夠組成一個可執行文件,如shell
$ ld -o prog ./a.o ./b.o
但對於一些基礎的操做,如C標準庫中提供的printf、scanf、rand等一些列經常使用的函數,若是每次編譯,咱們都要操做帶有這些函數的可重定位目標文件,那麼一次簡單的編譯過程就會變成下面這樣:函數
$ gcc -o a.out main.c /usr/lib/printf.o /usr/lib/scanf.o /usr/lib/rand.o ...
這樣一來,不只每次都要編寫冗長的命令行,並且程序員還必須維護一個包含所需的源文件或目標文件的文件夾。工具
但實際上,咱們在編譯咱們的程序時,並無考慮過這樣的問題,對於一個僅僅使用了標準庫中函數的源文件而言,也並不須要程序員手動的進行額外的連接操做。如對於下面main.c這個源文件而言,命令行
// main.c #include<stdio.h> int main() { printf("Hello World!"); return 0; }
咱們只須要簡單的執行code
$ gcc -o a.out main.c
這是由於,標準庫中的函數都被編譯成了獨立的目標模塊,而後相關模塊會被封裝成一個單獨的靜態庫文件,如libc.a包含了C標準庫中的標準I/O、字符串操做等函數,libm.a包含了C標準庫中的整數數學函數,在執行連接操做時,編譯器的驅動程序會將這些標準靜態庫傳送給連接器,連接器會從中選擇適當的模塊同咱們本身編寫的目標模塊(main.o)連接起來獲得可執行文件。字符串
在Linux系統中,靜態庫以一種稱爲存檔(archive)的文件格式存儲,後綴名.a,它由一個頭和一系列的目標模塊構成,頭負責描述每一個成員目標模塊的位置和大小。編譯器
既然有標準庫,那咱們也能夠把本身編寫的函數、全局變量、宏等封裝成靜態庫。數學
例如咱們實現兩個自定義的整型操做函數,分別定義在下面兩個源文件中,io
// add.c int add(int a, int b){ return a+b }
// sub.c void sub(int a, int b){ return a-b; }
建立靜態庫須要使用AR工具,使用如下命令:
$ gcc -c add.c sub.c $ ar rcs libcal.a add.o sub.o
如此便獲得了一個靜態庫libcal.a,在源文件中引用,便可使用靜態庫中定義的符號(非static函數、全局變量等)。
// main2.c #include "cal.h" int main() { int a = 0, b = 3, c = 0; c = add(a, b); printf("%d", c); return 0; }
編譯該源文件,
$ gcc -c main2.c $ gcc -static -o prog2c main2.o
或者等價地使用,
$ gcc -c main2.c $ gcc -static -o prog2c main2.o -L. -lcal
連接器運行時,它就會斷定main2.o引用了add.o定義的add符號,因此複製add.o到可執行文件,此外,他也會從/usr/lib/libc.a中複製printf所在的目標文件到可執行文件。
命令行上庫和目標文件的順序很是重要,若是咱們對上一條命令作一些小小的改動,使之變爲
$ gcc -static -o prog2c ./libcal.a main2.o
這條命令的執行就會報錯「undefined reference to 'add'」,之因此出現這樣的狀況,是連接器解析外部引用的方式致使的。
連接器是按照命令行上從左到右的順序來掃描文件的,在掃描文件時,連接器會維護三個集合:E(這個集合中的文件會被合併起來造成可執行文件)、U(未解析的符號)以及D(在前面輸入文件中已定義的符號集合),三個集合初始爲空。
如今,是否是理解了上面的錯誤了呢,連接器掃描到libcal.a時,U中尚是空的,故直接繼續掃描後面的main2.o,而後,main2.o中的add符號未解析,被加入到U中,隨後,結束掃描,U中非空,連接器報錯。
須要注意的是,庫和庫之間也可能存在依賴關係,故使用多個庫時要注意其前後順序,若存在相互依賴的關係,則能夠選擇在命令行上重複庫,以下面一條命令中,libx.a調用了liby.a中的函數,liby.a又調用了libx.a中的函數,
$ gcc foo.c libx.a liby.a libx.a
固然,把二者合併爲單獨的一個靜態庫也不失爲一種好方法。