【鏈接 1】與靜態鏈接庫鏈接

本文內容基於《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

當然,把兩者合併為單獨的一個靜態庫也不失為一種好方法。