本文內容基於《CSAPP》第7章,只是符號解析的一部分,從使用的角度闡述了靜態庫的由來和使用,僅僅是個人見解,可能從編譯的角度看有不嚴謹的地方,如發現錯誤,還請指正,謝謝!
1 靜態庫
首先我們要知道,連結器將一組可重定位目標檔案連結起來可以組成一個可執行檔案,如
$ 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;
}
我們只需要簡單的執行
$ gcc -o a.out main.c
這是因為,標準庫中的函式都被編譯成了獨立的目標模組,然後相關模組會被封裝成一個單獨的靜態庫檔案,如libc.a包含了C標準庫中的標準I/O、字串操作等函式,libm.a包含了C標準庫中的整數數學函式,在執行連結操作時,編譯器的驅動程式會將這些標準靜態庫傳送給連結器,連結器會從中選擇適當的模組同我們自己編寫的目標模組(main.o)連結起來得到可執行檔案。
在Linux系統中,靜態庫以一種稱為存檔(archive)的檔案格式儲存,字尾名.a,它由一個頭和一系列的目標模組構成,頭負責描述每個成員目標模組的位置和大小。
2 使用靜態庫
既然有標準庫,那我們也可以把自己編寫的函式、全域性變數、巨集等封裝成靜態庫。
例如我們實現兩個自定義的整型操作函式,分別定義在下面兩個原始檔中,
// 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所在的目標檔案到可執行檔案。
3 連結器如何使用靜態庫來解析引用
命令列上庫和目標檔案的順序非常重要,如果我們對上一條命令做一些小小的改動,使之變為
$ gcc -static -o prog2c ./libcal.a main2.o
這條命令的執行就會報錯“undefined reference to 'add'”,之所以出現這樣的情況,是連結器解析外部引用的方式導致的。
連結器是按照命令列上從左到右的順序來掃描檔案的,在掃描檔案時,連結器會維護三個集合:E(這個集合中的檔案會被合併起來形成可執行檔案)、U(未解析的符號)以及D(在前面輸入檔案中已定義的符號集合),三個集合初始為空。
- 對於命令列上的每個檔案f,連結器會首先判斷這一檔案是目標檔案還是靜態庫檔案。若該檔案是一個目標檔案,則放入E中,並修改U和D來反映f中的符號定義和引用。
- 但如果f是一個靜態庫檔案,那麼連結器就試圖對U中未解析的符號和f的成員所定義的符號進行匹配。如果f中的某一成員m定義了一個符號來解析U中的一個引用,那麼就將m加入E中,再相應地修改U和D中的內容來反映m中的符號定義和引用,對f中的所有成員逐個進行匹配操作直至U和D不再發生變化,聯結器便開始處理下一個檔案。
- 當連結器掃描完所有命令列中的檔案後,若U是空的,那麼連線及就會合並和重定位E中的檔案,得到一個可執行檔案;否則,連結器就會報錯並終止。
現在,是不是理解了上面的錯誤了呢,連結器掃描到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
當然,把兩者合併為單獨的一個靜態庫也不失為一種好方法。