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.og++ -fPIC -c b.cpp
++ -shared -o libb.so b.o my_class.og++ 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. 參考資料
- Static, Shared Dynamic and Loadable Linux Libraries
- Program Library HOWTO Shared Libraries
- Shared libraries with GCC on Linux
- Anatomy of Linux dynamic libraries
- Resolving ELF Relocation Name / Symbols
- PLT and GOT - the key to code sharing and dynamic libraries
- Linkers and Loaders
- C++ dynamic_cast實現原理