靜態(tài)庫的鏈接基本上同鏈接目標(biāo)文件.obj/.o
相同,但也有些不同的地方。本文簡要描述linux下靜態(tài)庫在鏈接過程中的一些細(xì)節(jié)。
靜態(tài)庫文件格式
靜態(tài)庫遠(yuǎn)遠(yuǎn)不同于動態(tài)庫,不涉及到符號重定位之類的問題。靜態(tài)庫本質(zhì)上只是將一堆目標(biāo)文件進(jìn)行打包而已。靜態(tài)庫沒有標(biāo)準(zhǔn),不同的linux下都會有些細(xì)微的差別。大致的格式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碼表示,所以可以用文本編輯器打開。
靜態(tài)庫本質(zhì)上就是使用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
鏈接過程
鏈接器在鏈接靜態(tài)庫時,同鏈接一般的.o
基本相似。鏈接過程大致可以歸納下圖:

總結(jié)為:
-
所有傳入鏈接器的
.o
都會被鏈接進(jìn)最終的可執(zhí)行程序;鏈接.o
時,會將.o
中的global symbol
和unresolved symbol
放入一個臨時表
- 如果多個
.o
定義了相同的global symbol
,那么就會得到多重定義的鏈接錯誤
- 如果鏈接結(jié)束了,
unresolved symbol
表不為空,那么就會得到符號未定義的鏈接錯誤
-
.a
靜態(tài)庫處理本質(zhì)上就是處理其中的每一個.o
,不同的是,如果某個.o
中沒有一個符號屬于unresolved symbol
表,也就是鏈接器此時懷疑該.o
沒有必要,那么其就會被忽略
可以通過一些代碼來展示以上過程。在開發(fā)C++程序時,可以利用文件靜態(tài)變量會先于main
之前執(zhí)行做一些可能利于程序結(jié)構(gòu)的事情。如果某個.o
(包含靜態(tài)庫中打包的.o
)被鏈接進(jìn)程序,那么其文件靜態(tài)變量就會先于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
生成的可執(zhí)行程序執(zhí)行如預(yù)期,其鏈接了test.o``lib.o
。但是如果把lib.o
以靜態(tài)庫的形式進(jìn)行鏈接,情況就不一樣了:為了做對比,基于以上的代碼再加一個文件,及修改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.o
和lib.o
創(chuàng)建為靜態(tài)庫:
$ 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
沒有被鏈接,導(dǎo)致其文件靜態(tài)變量也未得到初始化。
調(diào)整鏈接順序,可以進(jìn)一步檢驗前面的鏈接過程:
# 將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
的時候,發(fā)現(xiàn)libfn.o
依然沒有被之前鏈接的*.o
引用到,也就是沒有任何符號在unresolved symbol table
中,所以libfn.o
也被忽略。
一些實踐
在實際開發(fā)中還會遇到一些靜態(tài)庫相關(guān)的問題。
鏈接順序問題
前面的例子已經(jīng)展示了這個問題。調(diào)整庫的鏈接順序可以解決大部分問題,但當(dāng)靜態(tài)庫之間存在環(huán)形依賴時,則無法通過調(diào)整順序來解決。
-whole-archive
-whole-archive
選項告訴鏈接器把靜態(tài)庫中的所有.o
都進(jìn)行鏈接,針對以上例子:
$ 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
也被鏈接了進(jìn)來。-Wl
選項告訴gcc將其作為鏈接器參數(shù)傳入;之所以在命令行結(jié)尾加上--no-whole-archive
是為了告訴編譯器不要鏈接gcc默認(rèn)的庫
可以看出這個方法還是有點暴力了。
–start-group
格式為:
--start-group archives --end-group
位于--start-group
--end-group
中的所有靜態(tài)庫將被反復(fù)搜索,而不是默認(rèn)的只搜索一次,直到不再有新的unresolved symbol
產(chǎn)生為止。也就是說,出現(xiàn)在這里的.o
如果發(fā)現(xiàn)有unresolved symbol
,則可能回到之前的靜態(tài)庫中繼續(xù)搜索。
$ g++ -o test -L. test.o -Wl,--start-group -lfn main.o -Wl,--end-group
$ ./test
Test ctor
main
sum: 5
查看ldd
關(guān)于該參數(shù)的man page還可以一窺鏈接過程的細(xì)節(jié):
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.
嵌套靜態(tài)庫
由于ar
創(chuàng)建靜態(tài)庫時本質(zhì)上只是對文件進(jìn)行打包,所以甚至可以創(chuàng)建一個嵌套的靜態(tài)庫,從而測試鏈接器是否會遞歸處理靜態(tài)庫中的.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
可見鏈接器并不會遞歸處理靜態(tài)庫中的文件
之所以要提到嵌套靜態(tài)庫這個問題,是因為我發(fā)現(xiàn)很多時候我們喜歡為一個靜態(tài)庫工程鏈接其他靜態(tài)庫。當(dāng)然,這里的鏈接并非真正的鏈接(僅是打包),這個過程當(dāng)然可以聰明到將其他靜態(tài)庫里的.o
提取出來然后打包到新的靜態(tài)庫。
如果我們使用的是類似scons這種封裝更高的依賴項管理工具,那么它是否會這樣干呢?
基于之前的例子,我們使用scons來創(chuàng)建liboutfn.a
:
# Sconstruct
StaticLibrary('liboutfn.a', ['libfn.a', 'lib.o'])
使用文本編輯器打開liboutfn.a
就可以看到其內(nèi)容,或者使用:
$ 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中構(gòu)建一個靜態(tài)庫時,再鏈接
其他靜態(tài)庫是沒有意義的
參考文檔