Linux Dynamic Library (.so) 使用指南

1. Dynamic Library的編譯

假設我們有下面兩個文件a.h, a.cpp,放在同一目錄下。兩個文件的內容分別是:

// a.h

extern "C" void foo();
// a.cpp
 
#include <iostream>
#include "a.h"
 
using namespace std;
 
extern "C" void foo() {
    cout << "a.foo" << endl;
}

使用下面的命令行可以產生liba.so動態鏈接庫:

g++ -fPIC -c a.cpp
g++ -shared -o liba.so a.o

上面第一行的-fPIC是要求編譯器生成位置無關代碼(Position Independent Code),這對于動態庫來說是必須的。關于位置無關代碼的細節,可以查看后面列出的參考文獻,不再贅述。第二行使用-shared要求編譯器生成動態庫,而不是一個可執行文件。

另外,我們聲明和定義foo函數時使用了extern "C",這是希望c++編譯器不要對函數名進行改名(mangle)。對于共享庫來說,這樣定義接口函數更容易在Dynamic Loading時使用。至于什么是Dynamic Loading,在2.2節描述。

2. 動態庫的使用

2.1 Dynamic Linking方式

Dynamic Linking方式,是指在鏈接生成可執行文件時,通過-l指定要連接的共享庫,這種方式和使用靜態庫非常相似。

假設我們有一個main_dyn_link.cpp文件,內容如下:

// main_dyn_link.cpp
 
#include "a.h"
 
int main(int argc, char *argv[]) {
    foo();
    return 0;
}

我們可以使用下面的命令,將和其liba.so一起編譯鏈接為可執行文件test:

g++ main_dyn_link.cpp -o test -L`pwd` -la

當我們運行這個test程序時,會報錯,因為系統找不到liba.so文件。默認情況下,系統只會在/usr/lib、/usr/local/lib目錄下查找.so文件。為了能夠讓系統找到我們的liba.so,我們要么把liba.so放到上述兩個目錄中,要么使用LD_LIBRARY_PATH環境變量將liba.so所在的目錄添加為.so搜索目錄。這里我們使用第二種方法,在命令行輸入:

export LD_LIBRARY_PATH=`pwd`

這時,程序就能正常運行了。
此外還有其他方法能夠讓系統找到liba.so,可以查看下面的參考文檔1,不再贅述。

2.2 Dynamic Loading方式

使用dlopen、dlsym等函數,我們可以在運行期加載任意一個共享庫。我們把前面的main.cpp改為使用Dynamic Loading的方式:

// main_dyn_load.cpp
 
#include <dlfcn.h>
#include <iostream>
#include "a.h"
 
using namespace std;
 
typedef void (*Foo)();
 
Foo get_foo() {
    void *lib_handle = dlopen("liba.so", RTLD_LAZY);
    if (!lib_handle) {
        cerr << "load liba.so failed (" << dlerror() << ")" << endl;
        return 0;
    }
 
    char *error = 0;
    Foo foo_a = (Foo) dlsym(lib_handle, "foo");
    if ((error = dlerror()) != NULL) {
        cerr << "get foo failed (" << error << ")" << endl;
        return 0
    }
 
    return foo_a;
}
 
int main(int argc, char *argv[]) {
    Foo foo_a = get_foo();
    foo_a();
    return 0;
}

首先,為了使用dlopen、dlsym、dlerror等函數,我們需要包含dlfcn.h頭文件。

第12行,我們使用dlopen函數,傳遞liba.so的路徑名(本例是當前目錄),系統會嘗試加載liba.so。如果成功,返回給我們一個句柄。RTLD_LAZY是說加載時不處理unresolved symbols。對于本例,就是加載liba.so時,不會去查找foo的地址,只有在第一次調用foo時才會去找foo的實際地址。需要了解進一步詳細信息可以查找手冊(命令行輸入:man dlopen)。

第19行,我們使用dlsym函數,傳遞dlopen返回的句柄和我們想要獲取的函數名稱。如果這個
名稱是存在的,dlsym會返回其相應的地址。這就是為什么我們需要把.so的接口函數聲明為extern "C",否則,我們就必須給dlsym傳遞經過c++編譯器mingle之后的奇怪名字,才能找到相應的函數。

出現任何錯誤的時候,dlerror會返回相應的錯誤信息字符串;否則它會返回一個空指針。dlerror提供的信息對我們定位問題是非常有幫助的。

一旦獲取了函數地址,我們可以把它保存在函數指針中(第29行),隨后就可以像使用函數一樣來使用它(第30行)。

接著,我們編譯main.cpp,并生成可執行文件:

g++ main_dyn_load.cpp -o test -ldl

因為我們使用的是Dynamic Loading,因此就不需要在編譯時鏈接liba.so了(去掉了-la),因為我們使用了dlxxx函數,所以需要增加鏈接-ldl。

3. 使用Dynamic Library的注意事項

Dynamic Library使用要比Static Library復雜,下面是一些需要注意的問題。

3.1 不同的.so內包含同名全局函數

3.1.1 Dynamic Linking

.so允許出現同名的強符號。因此,如果不同的.so包含同名的全局函數,鏈接時編譯器不會報錯。編譯器會使用命令行中先鏈接的那個庫的版本。例如,我們再增加一個b.cpp文件:

// b.cpp
  
#include <iostream>
#include "a.h"
  
using namespace std;
  
extern "C" void foo() {
    cout << "b.foo" << endl;
}

將其編譯、生成為libb.so:

g++ -fPIC -c b.cpp
g++ main_dyn_link.cpp -o test -shared -L`pwd` -la -lb

這時,test將使用liba.so版本的foo,也就是將打印a.foo。如果我們把上面第二行的-la -lb倒過來:

g++ main_dyn_link.cpp -o test -shared -L`pwd` -lb -la

這時,test將使用libb.so版本的foo,也就是將打印b.foo。
這個不會成為太大的問題,因為使用靜態庫也是這樣的。

3.1.2 Dynamic Loading

使用Dynamic Loading,我們可以從兩個.so中分別取出不同的版本,并按照自己的意圖來使用。我們修改一下main_dyn_load.cpp文件,使之使用兩個foo版本:

// main_dyn_load.cpp
 
#include <dlfcn.h>
#include <iostream>
#include "a.h"
 
using namespace std;
  
typedef void (*Foo)();
 
Foo get_foo(const char *lib_path) {
    void *lib_handle = dlopen(lib_path, RTLD_LAZY);
    if (!lib_handle) {
        cerr << "load liba.so failed (" << dlerror() << ")" << endl;
        return 0;
    }
  
    char *error = 0;
    Foo foo_a = (Foo) dlsym(lib_handle, "foo");
    if ((error = dlerror()) != NULL) {
        cerr << "get foo failed (" << error << ")" << endl;
        return 0;
    }
  
    return foo_a;
}
 
int main(int argc, char *argv[]) {
    Foo foo_a = get_foo("liba.so");
    Foo foo_b = get_foo("libb.so");
    foo_a();
    foo_b();
    return 0;
}

首先,稍微重構了一下get_foo函數,使之能夠接收一個.so路徑作為參數,然后它回取出相應.so里面的foo函數的地址。

第29和第30行,我們分別從liba.so和libb.so中取出了foo函數地址,將他們保存在foo_a和foo_b兩個函數指針中,并在第31和第32行分別進行了調用。

最后,程序將會打印a.foo和b.foo。

3.2 .so反向調用bin里面的函數

bin可以調用.so定義的函數,以及.so可以調用其它.so定義的函數,這是毫無疑問的。那么,.so能反過來調用bin里面的函數么?答案是肯定的,只要我們在編譯bin時制定-rdynamic選項就可以了。

我們只舉Dynamic Linking的例子,因為Dynamic Loading也是一樣的。

我們在main_dyn_linking里面定義一個新的函數bar:

// main_dyn_link.cpp
 
#include <iostream>
#include "a.h"
 
using namespace std;
extern "C" void bar() {
    cout << "main.bar" << endl;
}
 
int main(int argc, char *argv[]) {
    foo();
    return 0;
}

然后,我們在a.cpp里面調用這個函數:

// a.cpp
  
#include <iostream>
#include "a.h"
  
using namespace std;
 
extern "C" void bar();
extern "C" void foo() {
    cout << "a.foo" << endl;
    bar();
}

編譯,注意增加-rdynamic選項:

g++ -fPIC -c a.cpp
g++ -shared -o liba.so a.o
g++ main_dyn_link.cpp -o test -L`pwd` -la -rdynamic

執行程序,將會打印:
a.foo main.bar

3.3 不同的.so內出現同名的全局變量

終于要面對這個非常tricky的場景了。這里說的全局變量,既包括通常意義的『全局變量』,也包括類的靜態成員變量,因為后者本質上就是改了名字全局變量。

3.3.1 Dynamic Linking

我們先來考慮Dynamic Linking的情況。我首先添加一個類:MyClass,并把它實現為singleton。因為singleton模式是使用類靜態成員最常見的場景之一。
先來定義MyClass的頭文件:

// my_class.h
 
class MyClass {
public:
    MyClass();
    ~MyClass();
    void what();
    static MyClass &get_instance();
private:
    int _count;
    static MyClass _instance;
};

接著定義MyClass的源文件:

// my_class.cpp
 
#include <iostream>
#include "my_class.h"
 
using namespace std;
 
MyClass MyClass::_instance;
 
MyClass::MyClass()
    : _count(0) {
    cout << "the count init to 0" << endl;
}
 
MyClass::~MyClass() {
    cout << "(" << this << ") destory" << endl;
}
 
void MyClass::what() {
    _count++;
    cout << "(" << this << ") the count is " << _count << endl;
}
 
MyClass &MyClass::get_instance() {
    return _instance;
}

每次調用what方法,MyClass對象內部計數會加1,并隨后打印對象的地址和當前的計數值。
我們在a.cpp和b.cpp里面分別調用MyClass::what方法。

// a.cpp
  
#include <iostream>
#include "a.h"
#include "my_class.h"
  
using namespace std;
  
extern "C" void bar();
extern "C" void foo() {
    cout << "a.foo" << endl;
    bar();
    MyClass::get_instance().what();
}

我們需要把my_class.cpp編譯到liba.so和libb.so中:

g++ -fPIC -c a.cpp
g++ -fPIC -c my_class.cpp
g++ -shared -o liba.so a.o my_class.o
 
g++ -fPIC -c b.cpp
g++ -shared -o libb.so b.o my_class.o
 
g++ main_dyn_link.cpp -o test -L\`pwd\` -la -lb -rdynamic

執行這個程序,我們發現,盡管在不同的.so內都包含了my_class.cpp(里面定義了_instance靜態靜態變量),但最終全局只有一個_instance實例。但是,這個實例被初始化了兩次和析構了兩次。重復析構可能會導致core,因此在.so場景下使用單例模式要更加小心(或選擇其它的單例實現方法)。

3.3.2 Dynamic Loading

現在我們看看Dynamic Loading的情況。這次,我們使用main_dyn_load.cpp進行編譯:

g++ main_dyn_load.cpp -o test -ldl -rdynamic

這次,我們驚訝的發現,居然存在兩個不同的_instance實例!當然,重復初始化和析構不存在了,每個對象上都只進行了一次初始化和析構。

這說明,在Dynamic Loading情況下,不同的.so中同名全局變量都會是不同的實例。

等等,如果你以為這是全部真相那就錯了。如果我們在bin中也定義同名的全局變量會怎么樣呢?我們修改一下main_dyn_load.cpp中的bar函數,使之也調用MyClass::get_instance().what()方法:

// main_dyn_load.cpp
 
#include <dlfcn.h>
#include <iostream>
#include "a.h"
#include "my_class.h"
using namespace std;
  
typedef void (*Foo)();
 
extern "C" void bar() {
    cout << "main.bar" << endl;
    MyClass::get_instance().what();
} 
Foo get_foo(const char *lib_path) {
    void *lib_handle = dlopen(lib_path, RTLD_LAZY);
    if (!lib_handle) {
        cerr << "load liba.so failed (" << dlerror() << ")" << endl;
        return 0;
    }
  
    char *error = 0;
    Foo foo_a = (Foo) dlsym(lib_handle, "foo");
    if ((error = dlerror()) != NULL) {
        cerr << "get foo failed (" << error << ")" << endl;
        return 0;
    }
  
    return foo_a;
}
int main(int argc, char *argv[]) {
 
    Foo foo_a = get_foo("liba.so");
    Foo foo_b = get_foo("libb.so");
    foo_a();
    foo_b();
    return 0;
}

我們還需要把my_class.cpp也直接編譯到bin里面,否則會找不到get_instance()、what()等符號。

g++ main_dyn_load.cpp my_class.o -o test -ldl -rdynamic

執行程序,結果再次令人意外:

全局變量再次合為一個,而且被重復初始化-析構了三次。

總結上述規律,在Dynamic Loading場景下,如果.so中出現了同名全局變量,那么每個.so都會有其單獨的全局變量實例,每個實例單獨初始化/析構;如果bin中也包括同名的全局變量,那么系統將只有唯一一份實例,在這個實例上會出現多次重復的初始化/析構。

這再次說明,在.so中使用全局變量(以及類的靜態成員變量)要非常謹慎,整個系統也要形成統一的規范,否則很可能出現未預期的行為。

3.4 dynamic_cast

從一個.so中創建的對象,在另外一個.so中進行dynamic_cast,即使第二個.so完全編譯了子類的定義,dynamic_cast也可能會失敗。為了演示,先修改一下MyClass的定義:

// my_class.h
 
class MyBase {
public:
    virtual ~MyBase() {}
};
class MyClass : public MyBase {
public:
    MyClass(const char *name);
    ~MyClass();
    void what();
private:
    int _count;
    const char *_name;
};

接著修改MyClass的實現:

// my_class.cpp
  
#include <iostream>
#include "my_class.h"
  
using namespace std;
  
MyClass::MyClass(const char *name)
    : _count(0), _name(name) {
    cout << "the count init to 0" << endl;
}
  
MyClass::~MyClass() {
    cout << "(" << this << ") destory" << endl;
}
  
void MyClass::what() {
    _count++;
    cout << "(" << this << ") created in " << _name << ", the _count is " << count << endl;
}

為了能夠讓.so產生出MyClass對象,我們給.so增加一個接口函數:create。此外,我們把foo改為接收一個MyBase對象的指針。

// a.h
 
class MyBase;
extern "C" void foo(MyBase*);
extern "C" MyBase *create();

在a.cpp和b.cpp中實現create函數。并且,在foo函數中使用dynamic_cast強制向下轉型:

// a.cpp
  
#include <iostream>
#include "a.h"
#include "my_class.h"
  
using namespace std;
  
extern "C" void bar();
 
extern "C" void foo(MyBase* base) {
    cout << "a.foo" << endl;
    bar();
    MyClass *cls = dynamic_cast<MyClass*>(base);
    if (!cls) {
        cerr << "dynamic_cast failed" << endl;
        return;
    }
    cls->what();    
}
 
extern "C" MyBase *create() {
    return new MyClass("liba.so");
} 
// b.cpp
  
#include <iostream>
#include "a.h"
#include "my_class.h"
using namespace std;
  
extern "C" void foo(MyBase *base) {
    cout << "b.foo" << endl;
    MyClass *cls = dynamic_cast<MyClass*>(base);
    if (!cls) {
        cerr << "dynamic_cast failed" << endl;
        return;
    }
    cls->what();   
}
 
extern "C" MyBase *create() {
    return new MyClass("libb.so");
}

最后,修改main_dyn_load.cpp文件,使之從liba.so創建對象,再libb.so中轉型、使用;然后反方向再來一次。

#include <dlfcn.h>
#include <iostream>
#include "a.h"
#include "my_class.h"
#include "fn.h"
 
using namespace std;
 
typedef void (*Foo)(MyBase*);
typedef MyBase *(*Create)();
 
extern "C" void bar() {
    cout << "main.bar" << endl;
}
 
int main(int argc, char *argv[]) {
    Foo foo_a = get_fn<Foo>("liba.so", "foo");
    Foo foo_b = get_fn<Foo>("libb.so", "foo");
    Create create_a = get_fn<Create>("liba.so", "create");
    Create create_b = get_fn<Create>("libb.so", "create");
    MyBase *base_a = create_a();
    MyBase *base_b = create_b();
    foo_a(base_a);
    foo_b(base_b);
    foo_a(base_b);
    foo_b(base_a);
    return 0;
}

第17到第20行,使用工具函數get_fn從.so中獲取函數地址,get_fn的源碼在附件中。第21行和第22行分別在liba.so和libb.so中創建了對象。第23行,liba.so創建的對象在liba.so中轉型,第24行同樣測試了libb.so的情形。第25行和第26行測試了交叉轉型的情況。

編譯:

g++ -fPIC -c a.cpp
g++ -fPIC -c my_class.cpp
g++ -shared -o liba.so a.o my_class.o

g++ -fPIC -c b.cpp
++ -shared -o libb.so b.o my_class.o

g++ main_dyn_load.cpp -o test -L`pwd` -ldl -rdynamic

程序運行結果如下:

可以看到,出錯的代碼就是第25和第26行,說明在一個.so中創建的對象無法在另一個.so中轉型成功。

怎樣解決這個問題呢?答案是把my_class.o也編譯到bin里面。如下:

g++ main_dyn_load.cpp my_class.o -o test -L`pwd` -ldl -rdynamic

編譯、運行,可以看到這次轉型成功了:

為什么會這樣呢?這其實和3.3.2的情景是一樣的:dynamic_cast時使用的類的虛函數表和RTTI元數據也是全局變量。當bin沒有同名的全局變量時,各個.so擁有各自獨立的虛函數表實例,導致轉型時認為不是同一個繼承體系而失敗。而當bin也編譯了同樣的虛函數表時,所有的虛函數表就只會出現為同一個實例了。

5. 總結

.so帶來了靈活性的同時,也使我們要面對很多tricky的場景,一不小心就可能落到坑里。因此,使用.so必須小心,只在安全的范圍內應用,并且在整個系統要有統一的規范。如果在使用.so的過程中發現了任何問題,歡迎隨時與作者交流。

6. 參考資料

  1. Static, Shared Dynamic and Loadable Linux Libraries
  2. Program Library HOWTO Shared Libraries
  3. Shared libraries with GCC on Linux
  4. Anatomy of Linux dynamic libraries
  5. Resolving ELF Relocation Name / Symbols
  6. PLT and GOT - the key to code sharing and dynamic libraries
  7. Linkers and Loaders
  8. C++ dynamic_cast實現原理
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容