linux下使用動態(tài)庫,基本用起來還是很容易。但如果我們的程序中大量使用動態(tài)庫來實現(xiàn)各種框架/插件,那么就會遇到一些坑,掌握這些坑才有利于程序更穩(wěn)健地運行。
本篇先談?wù)剟討B(tài)庫符號方面的問題。
測試代碼可以在github上找到
符號查找
一個應(yīng)用程序test會鏈接一個動態(tài)庫libdy.so,如果一個符號,例如函數(shù)callfn定義于libdy.so中,test要使用該函數(shù),簡單地聲明即可:
// dy.cpp libdy.so
void callfn() {
...
}
// main.cpp test
extern void callfn();
callfn();
在鏈接test的時候,鏈接器會統(tǒng)一進(jìn)行檢查。
同樣,在libdy.so中有相同的規(guī)則,它可以使用一個外部的符號,在它被鏈接/載入進(jìn)一個可執(zhí)行程序時才會進(jìn)行符號存在與否的檢查。這個符號甚至可以定義在test中,形成一種雙向依賴,或定義在其他動態(tài)庫中:
// dy.cpp libdy.so
extern void mfunc();
mfunc();
// main.cpp test
void mfunc() {
...
}
在生成libdy.so時mfunc可以找不到,此時mfunc為未定義:
$ nm libdy.so | grep mfun
U _Z5mfuncv
但在libdy.so被鏈接進(jìn)test時則會進(jìn)行檢查,試著把mfunc函數(shù)的定義去掉,就會得到一個鏈接錯誤:
./libdy.so: undefined reference to `mfunc()'
同樣,如果我們動態(tài)載入libdy.so,此時當(dāng)然可以鏈接通過,但是在載入時同樣得到找不到符號的錯誤:
#ifdef DY_LOAD
void *dp = dlopen("./libdy.so", RTLD_LAZY);
typedef void (*callfn)();
callfn f = (callfn) dlsym(dp, "callfn");
f();
dlclose(dp);
#else
callfn();
#endif
得到錯誤:
./test: symbol lookup error: ./libdy.so: undefined symbol: _Z5mfuncv
結(jié)論:基于以上,我們知道,如果一個動態(tài)庫依賴了一些外部符號,這些外部符號可以位于其他動態(tài)庫甚至應(yīng)用程序中。我們可以再鏈接這個動態(tài)庫的時候就把依賴的其他庫也鏈接上,或者推遲到鏈接應(yīng)用程序時再鏈接。而動態(tài)加載的庫,則要保證在加載該庫時,進(jìn)程中加載的其他動態(tài)庫里已經(jīng)存在該符號。
例如,通過LD_PRELOAD環(huán)境變量可以讓一個進(jìn)程先加載指定的動態(tài)庫,上面那個動態(tài)加載啟動失敗的例子,可以通過預(yù)先加載包含mfunc符號的動態(tài)庫解決:
$ LD_PRELOAD=libmfun.so ./test
...
但是如果這個符號存在于可執(zhí)行程序中則不行:
$ nm test | grep mfunc
0000000000400a00 T _Z5mfuncv
$ nm test | grep mfunc
0000000000400a00 T _Z5mfuncv
$ ./test
...
./test: symbol lookup error: ./libdy.so: undefined symbol: _Z5mfuncv
符號覆蓋
前面主要講的是符號缺少的情況,如果同一個符號存在多分,則更能引發(fā)問題。這里談到的符號都是全局符號,一個進(jìn)程中某個全局符號始終是全局唯一的。為了保證這一點,在鏈接或動態(tài)載入動態(tài)庫時,就會出現(xiàn)忽略重復(fù)符號的情況。
這里就不提同一個鏈接單位(如可執(zhí)行程序、動態(tài)庫)里符號重復(fù)的問題了
函數(shù)
當(dāng)動態(tài)庫和libdy.so可執(zhí)行程序test中包含同名的函數(shù)時會怎樣?根據(jù)是否動態(tài)加載情況還有所不同。
當(dāng)直接鏈接動態(tài)庫時,libdy.so和test都會鏈接包含func函數(shù)的fun.o,為了區(qū)分,我把func按照條件編譯得到不同的版本:
// fun.cpp
#ifdef V2
extern "C" void func() {
printf("func v2\n");
}
#else
extern "C" void func() {
printf("func v1\n");
}
#endif
// Makefile
test: libdy obj.o mainfn
g++ -g -Wall -c fun.cpp -o fun.o # 編譯為fun.o
g++ -g -Wall -c main.cpp #-DDY_LOAD
g++ -g -Wall -o test main.o obj.o fun.o -ldl mfun.o -ldy -L.
libdy: obj
g++ -Wall -fPIC -c fun.cpp -DV2 -o fun-dy.o # 定義V2宏,編譯為fun-dy.o
g++ -Wall -fPIC -shared -o libdy.so dy.cpp -g obj.o fun-dy.o
這樣,test中的func就會輸出func v1;libdy.so中的func就會輸出func v2。test和libdy.o確實都有func符號:
$ nm libdy.so | grep func
0000000000000a60 T func
$nm test | grep func
0000000000400a80 T func
在test和libdy.so中都會調(diào)用func函數(shù):
// main.cpp test
int main(int argc, char **argv) {
func();
...
callfn(); // 調(diào)用libdy.so中的函數(shù)
...
}
// dy.cpp libdy.so
extern "C" void callfn() {
...
printf("callfn\n");
func();
...
}
運行后發(fā)現(xiàn),都調(diào)用的是同一個func:
$ ./test
...
func v1
...
callfn
func v1
結(jié)論,直接鏈接動態(tài)庫時,整個程序運行的時候符號會發(fā)生覆蓋,只有一個符號被使用。在實踐中,如果程序和鏈接的動態(tài)庫都依賴了一個靜態(tài)庫,而后他們鏈接的這個靜態(tài)庫版本不同,則很有可能因為符號發(fā)生了覆蓋而導(dǎo)致問題。(靜態(tài)庫同普通的.o性質(zhì)一樣,參考淺析靜態(tài)庫鏈接原理)
更復(fù)雜的情況中,多個動態(tài)庫和程序都有相同的符號,情況也是一樣,會發(fā)生符號覆蓋。如果程序里沒有這個符號,而多個動態(tài)庫里有相同的符號,也會覆蓋。
但是對于動態(tài)載入的情況則不同,同樣的libdy.so我們在test中不鏈接,而是動態(tài)載入:
int main(int argc, char **argv) {
func();
#ifdef DY_LOAD
void *dp = dlopen("./libdy.so", RTLD_LAZY);
typedef void (*callfn)();
callfn f = (callfn) dlsym(dp, "callfn");
f();
func();
dlclose(dp);
#else
callfn();
#endif
return 0;
}
運行得到:
$ ./test
func v1
...
callfn
func v2
func v1
都正確地調(diào)用到各自鏈接的func。
結(jié)論,實踐中,動態(tài)載入的動態(tài)庫一般會作為插件使用,那么其同程序鏈接不同版本的靜態(tài)庫(相同符號不同實現(xiàn)),是沒有問題的。
變量
變量本質(zhì)上也是符號(symbol),但其處理規(guī)則和函數(shù)還有點不一樣(是不是有點想吐槽了)。
// object.h
class Object {
public:
Object() {
#ifdef DF
s = malloc(32);
printf("s addr %p\n", s);
#endif
printf("ctor %p\n", this);
}
~Object() {
printf("dtor %p\n", this);
#ifdef DF
printf("s addr %p\n", s);
free(s);
#endif
}
void *s;
};
extern Object g_obj;
我們的程序test和動態(tài)庫libdy.so都會鏈接object.o。首先測試test鏈接libdy.so,test和libdy.so中都會有g_obj這個符號:
// B g_obj 表示g_obj位于BSS段,未初始化段
$ nm test | grep g_obj
0000000000400a14 t _GLOBAL__I_g_obj
00000000006012c8 B g_obj
$ nm libdy.so | grep g_obj
000000000000097c t _GLOBAL__I_g_obj
0000000000200f30 B g_obj
運行:
$ ./test
ctor 0x6012c8
ctor 0x6012c8
...
dtor 0x6012c8
dtor 0x6012c8
g_obj被構(gòu)造了兩次,但地址一樣。全局變量只有一個實例,似乎在情理之中。
動態(tài)載入libdy.so,變量地址還是相同的:
$ ./test
ctor 0x6012a8
...
ctor 0x6012a8
...
dtor 0x6012a8
dtor 0x6012a8
結(jié)論,不同于函數(shù),全局變量符號重復(fù)時,不論動態(tài)庫是動態(tài)載入還是直接鏈接,變量始終只有一個。
但詭異的情況是,對象被構(gòu)造和析構(gòu)了兩次。構(gòu)造兩次倒無所謂,浪費點空間,但是析構(gòu)兩次就有問題。因為析構(gòu)時都操作的是同一個對象,那么如果這個對象內(nèi)部有分配的內(nèi)存,那就會對這塊內(nèi)存造成double free,因為指針相同。打開DF宏實驗下:
$ ./test
s addr 0x20de010
ctor 0x6012b8
s addr 0x20de040
ctor 0x6012b8
...
dtor 0x6012b8
s addr 0x20de040
dtor 0x6012b8
s addr 0x20de040
因為析構(gòu)的兩次都是同一個對象,所以其成員s指向的內(nèi)存被釋放了兩次,從而產(chǎn)生了double free,讓程序coredump了。
總結(jié),全局變量符號重復(fù)時,始終會只使用一個,并且會被初始化/釋放兩次,是一種較危險的情況,應(yīng)當(dāng)避免在使用動態(tài)庫的過程中使用全局變量。
完