1. 程式人生 > >淺析靜態庫連結原理

淺析靜態庫連結原理

靜態庫的連結基本上同連結目標檔案.obj/.o相同,但也有些不同的地方。本文簡要描述linux下靜態庫在連結過程中的一些細節。

靜態庫檔案格式

靜態庫遠遠不同於動態庫,不涉及到符號重定位之類的問題。靜態庫本質上只是將一堆目標檔案進行打包而已。靜態庫沒有標準,不同的linux下都會有些細微的差別。大致的格式wiki上描述的較清楚:

Global header
-----------------        +-------------------------------
File header 1       ---> | File name
File content 1  |        | File modification timestamp 
-----------------        | Owner ID
File header 2            | Group ID
File content 2           | File mode
-----------------        | File size in bytes
...                      | File magic
                         +-------------------------------

File header很多欄位都是以ASCII碼錶示,所以可以用文字編輯器開啟。

靜態庫本質上就是使用ar命令打包一堆.o檔案。我們甚至可以用ar隨意打包一些檔案:

$ echo 'hello' > a.txt && echo 'world' > b.txt
$ ar -r test.a a.txt b.txt
$ cat test.a
!<arch>
a.txt/          1410628755  60833 100   100644  6         `
hello
b.txt/          1410628755  60833 100   100644  6         `
world

連結過程

連結器在連結靜態庫時,同連結一般的.o基本相似。連結過程大致可以歸納下圖:

總結為:

  • 所有傳入連結器的.o都會被連結進最終的可執行程式;連結.o時,會將.o中的global symbolunresolved symbol放入一個臨時表
  • 如果多個.o定義了相同的global symbol,那麼就會得到多重定義的連結錯誤
  • 如果連結結束了,unresolved symbol表不為空,那麼就會得到符號未定義的連結錯誤
  • .a靜態庫處理本質上就是處理其中的每一個.o,不同的是,如果某個.o中沒有一個符號屬於unresolved symbol表,也就是連結器此時懷疑該.o沒有必要,那麼其就會被忽略

可以通過一些程式碼來展示以上過程。在開發C++程式時,可以利用檔案靜態變數會先於main之前執行做一些可能利於程式結構的事情。如果某個.o(包含靜態庫中打包的.o)被連結程序序,那麼其檔案靜態變數就會先於main初始化。

// test.cpp
#include <stdio.h>

class Test {
public:
    Test() {
        printf("Test ctor\n");
    }
};

static Test s_test;

// lib.cpp
#include <stdio.h>

class Lib {
public:
    Lib() {
        printf("Lib ctor\n");
    }
};

static Lib s_lib;

// main.cpp
#include <stdio.h>

int main() {
    printf("main\n");
    return 0;
}

以上程式碼main.cpp中未引用任何test.cpp``lib.cpp中的符號:

$ g++ -o test test.o lib.o main.o
$ ./test
Lib ctor
Test ctor
main

生成的可執行程式執行如預期,其連結了test.o``lib.o。但是如果把lib.o以靜態庫的形式進行連結,情況就不一樣了:為了做對比,基於以上的程式碼再加一個檔案,及修改main.cpp

// libfn.cpp
int sum(int a, int b) {
    return a + b;
}

// main.cpp
#include <stdio.h>

int main() {
    printf("main\n");
    extern int sum(int, int);
    printf("sum: %d\n", sum(2, 3));
    return 0;
}

libfn.olib.o建立為靜態庫:

$ ar -r libfn.a libfn.o lib.o
$ g++ -o test main.o test.o -lfn -L.
$ ./test
Test ctor
main
sum: 5

因為lib.o沒有被連結,導致其檔案靜態變數也未得到初始化。

調整連結順序,可以進一步檢驗前面的連結過程:

# 將libfn.a的連結放在main.o前面

$ g++ -o test test.o -lfn main.o  -L.
main.o: In function `main':
main.cpp:(.text+0x19): undefined reference to `sum(int, int)'
collect2: ld returned 1 exit status

這個問題遇到得比較多,也有點讓人覺得莫名其妙。其原因就在於連結器在連結libfn.a的時候,發現libfn.o依然沒有被之前連結的*.o引用到,也就是沒有任何符號在unresolved symbol table,所以libfn.o也被忽略。

一些實踐

在實際開發中還會遇到一些靜態庫相關的問題。

連結順序問題

前面的例子已經展示了這個問題。調整庫的連結順序可以解決大部分問題,但當靜態庫之間存在環形依賴時,則無法通過調整順序來解決。

-whole-archive

-whole-archive選項告訴連結器把靜態庫中的所有.o都進行連結,針對以上例子:

$ g++ -o test -L. test.o -Wl,--whole-archive -lfn main.o -Wl,--no-whole-archive
$ ./test
Lib ctor
Test ctor
main
sum: 5

lib.o也被連結了進來。-Wl選項告訴gcc將其作為連結器引數傳入;之所以在命令列結尾加上--no-whole-archive是為了告訴編譯器不要連結gcc預設的庫

可以看出這個方法還是有點暴力了。

–start-group

格式為:

--start-group archives --end-group

位於--start-group --end-group中的所有靜態庫將被反覆搜尋,而不是預設的只搜尋一次,直到不再有新的unresolved symbol產生為止。也就是說,出現在這裡的.o如果發現有unresolved symbol,則可能回到之前的靜態庫中繼續搜尋。

$ g++ -o test -L. test.o -Wl,--start-group -lfn main.o -Wl,--end-group
$ ./test
Test ctor
main
sum: 5

檢視ldd關於該引數的man page還可以一窺連結過程的細節:

The specified archives are searched repeatedly until no new undefined references are created. Normally, an archive is searched only once in the order that it is specified on the command line. If a symbol in that archive is needed to resolve an undefined symbol referred to by an object in an archive that appears later on the command line, the linker would not be able to resolve that reference. By grouping the archives, they all be searched repeatedly until all possible references are resolved.

巢狀靜態庫

由於ar建立靜態庫時本質上只是對檔案進行打包,所以甚至可以建立一個巢狀的靜態庫,從而測試連結器是否會遞迴處理靜態庫中的.o

$ ar -r libfn.a libfn.o
$ ar -r liboutfn.a libfn.a lib.o
$ g++ -o test -L. test.o main.o -loutfn
main.o: In function `main':
main.cpp:(.text+0x19): undefined reference to `sum(int, int)'
collect2: ld returned 1 exit status

可見連結器並不會遞迴處理靜態庫中的檔案

之所以要提到巢狀靜態庫這個問題,是因為我發現很多時候我們喜歡為一個靜態庫工程連結其他靜態庫。當然,這裡的連結並非真正的連結(僅是打包),這個過程當然可以聰明到將其他靜態庫裡的.o提取出來然後打包到新的靜態庫。

如果我們使用的是類似scons這種封裝更高的依賴項管理工具,那麼它是否會這樣幹呢?

基於之前的例子,我們使用scons來建立liboutfn.a

# Sconstruct
StaticLibrary('liboutfn.a', ['libfn.a', 'lib.o'])

使用文字編輯器開啟liboutfn.a就可以看到其內容,或者使用:

$ ar -tv liboutfn.a
rw-r--r-- 60833/100   1474 Sep 14 02:59 2014 libfn.a
rw-r--r-- 60833/100   2448 Sep 14 02:16 2014 lib.o

可見scons也只是單純地打包。所以,在scons中構建一個靜態庫時,再連結其他靜態庫是沒有意義的

參考文件