1. 程式人生 > >linux動態庫的種種要點

linux動態庫的種種要點

統一 pri ont http 指向 try 找到 track linux

linux下使用動態庫,基本用起來還是非常easy。但假設我們的程序中大量使用動態庫來實現各種框架/插件,那麽就會遇到一些坑,掌握這些坑才有利於程序更穩健地執行。

本篇先談談動態庫符號方面的問題。

測試代碼能夠在github上找到

符號查找

一個應用程序test會鏈接一個動態庫libdy.so,假設一個符號。比如函數callfn定義於libdy.so中,test要使用該函數,簡單地聲明就可以:

// dy.cpp libdy.so
void callfn() {
    ...
}

// main.cpp test
extern void callfn();

callfn();

在鏈接test的時候,鏈接器會統一進行檢查。

相同,在libdy.so中有相同的規則,它能夠使用一個外部的符號,在它被鏈接/加載進一個可運行程序時才會進行符號存在與否的檢查。這個符號甚至能夠定義在test中,形成一種雙向依賴,或定義在其它動態庫中:

// 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被鏈接進test時則會進行檢查,試著把mfunc

函數的定義去掉,就會得到一個鏈接錯誤:

./libdy.so: undefined reference to `mfunc()'

相同。假設我們動態加載libdy.so,此時當然能夠鏈接通過,可是在加載時相同得到找不到符號的錯誤:

#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

結論:基於以上,我們知道,假設一個動態庫依賴了一些外部符號,這些外部符號能夠位於其它動態庫甚至應用程序中。

我們能夠再鏈接這個動態庫的時候就把依賴的其它庫也鏈接上,或者推遲到鏈接應用程序時再鏈接。而動態載入的庫,則要保證在載入該庫時,進程中載入的其它動態庫裏已經存在該符號。

比如。通過LD_PRELOAD環境變量能夠讓一個進程先載入指定的動態庫,上面那個動態載入啟動失敗的樣例,能夠通過預先載入包括mfunc符號的動態庫解決:

$ LD_PRELOAD=libmfun.so ./test
...

可是假設這個符號存在於可運行程序中則不行:

$ nm test | grep mfunc
0000000000400a00 T _Z5mfuncv
$ nm test | grep mfunc
0000000000400a00 T _Z5mfuncv
$ ./test
...
./test: symbol lookup error: ./libdy.so: undefined symbol: _Z5mfuncv

符號覆蓋

前面主要講的是符號缺少的情況。假設同一個符號存在多分。則更能引發問題。這裏談到的符號都是全局符號。一個進程中某個全局符號始終是全局唯一的。為了保證這一點。在鏈接或動態加載動態庫時。就會出現忽略反復符號的情況。

這裏就不提同一個鏈接單位(如可運行程序、動態庫)裏符號反復的問題了

函數

當動態庫和libdy.so可運行程序test中包括同名的函數時會如何?依據是否動態載入情況還有所不同。

當直接鏈接動態庫時,libdy.so和test都會鏈接包括func函數的fun.o。為了區分。我把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中都會調用func函數:

// main.cpp test
int main(int argc, char **argv) {
    func();
    ...
    callfn(); // 調用libdy.so中的函數
    ...
}

// dy.cpp libdy.so
extern "C" void callfn() {
    ... 
    printf("callfn\n");
    func();
    ...
}

執行後發現。都調用的是同一個func

$ ./test
...
func v1
...
callfn
func v1

結論,直接鏈接動態庫時,整個程序執行的時候符號會發生覆蓋,僅僅有一個符號被使用。在實踐中,假設程序和鏈接的動態庫都依賴了一個靜態庫,而後他們鏈接的這個靜態庫版本號不同,則非常有可能由於符號發生了覆蓋而導致問題。(靜態庫同普通的.o性質一樣,參考淺析靜態庫鏈接原理)

更復雜的情況中。多個動態庫和程序都有同樣的符號。情況也是一樣。會發生符號覆蓋。假設程序裏沒有這個符號,而多個動態庫裏有同樣的符號,也會覆蓋。

可是對於動態加載的情況則不同。相同的libdy.so我們在test中不鏈接,而是動態加載:

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

都正確地調用到各自鏈接的func

結論。實踐中,動態加載的動態庫通常會作為插件使用,那麽其同程序鏈接不同版本號的靜態庫(同樣符號不同實現),是沒有問題的。

變量

變量本質上也是符號(symbol),但其處理規則和函數還有點不一樣(是不是有點想吐槽了)。

// 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和動態庫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被構造了兩次,但地址一樣。全局變量僅僅有一個實例,似乎在情理之中。

動態加載libdy.so,變量地址還是同樣的:

$ ./test
ctor 0x6012a8
...
ctor 0x6012a8
...
dtor 0x6012a8
dtor 0x6012a8

結論,不同於函數,全局變量符號反復時。不論動態庫是動態加載還是直接鏈接,變量始終僅僅有一個。

但詭異的情況是,對象被構造和析構了兩次。構造兩次倒無所謂,浪費點空間,可是析構兩次就有問題。由於析構時都操作的是同一個對象。那麽假設這個對象內部有分配的內存,那就會對這塊內存造成double free,由於指針同樣。打開DF宏實驗下:

$ ./test
s addr 0x20de010
ctor 0x6012b8
s addr 0x20de040
ctor 0x6012b8
...
dtor 0x6012b8
s addr 0x20de040
dtor 0x6012b8
s addr 0x20de040

由於析構的兩次都是同一個對象,所以其成員s指向的內存被釋放了兩次。從而產生了double free,讓程序coredump了。

總結。全局變量符號反復時。始終會僅僅使用一個,而且會被初始化/釋放兩次,是一種較危急的情況,應當避免在使用動態庫的過程中使用全局變量。

linux動態庫的種種要點