1. Dynamic Library的編譯
假設我們有下面兩個文件a.h, a.cpp际度,放在同一目錄下愉择。兩個文件的內(nèi)容分別是:
// a.h
extern "C" void foo();
// a.cpp
#include <iostream>
#include "a.h"
using namespace std;
extern "C" void foo() {
cout << "a.foo" << endl;
}
使用下面的命令行可以產(chǎn)生liba.so動態(tài)鏈接庫:
g++ -fPIC -c a.cpp
g++ -shared -o liba.so a.o
上面第一行的-fPIC是要求編譯器生成位置無關代碼(Position Independent Code),這對于動態(tài)庫來說是必須的盯蝴。關于位置無關代碼的細節(jié)碘耳,可以查看后面列出的參考文獻胁黑,不再贅述。第二行使用-shared要求編譯器生成動態(tài)庫貌踏,而不是一個可執(zhí)行文件十饥。
另外,我們聲明和定義foo函數(shù)時使用了extern "C"祖乳,這是希望c++編譯器不要對函數(shù)名進行改名(mangle)逗堵。對于共享庫來說,這樣定義接口函數(shù)更容易在Dynamic Loading時使用眷昆。至于什么是Dynamic Loading蜒秤,在2.2節(jié)描述。
2. 動態(tài)庫的使用
2.1 Dynamic Linking方式
Dynamic Linking方式亚斋,是指在鏈接生成可執(zhí)行文件時垦藏,通過-l指定要連接的共享庫,這種方式和使用靜態(tài)庫非常相似伞访。
假設我們有一個main_dyn_link.cpp文件掂骏,內(nèi)容如下:
// main_dyn_link.cpp
#include "a.h"
int main(int argc, char *argv[]) {
foo();
return 0;
}
我們可以使用下面的命令,將和其liba.so一起編譯鏈接為可執(zhí)行文件test:
g++ main_dyn_link.cpp -o test -L`pwd` -la
當我們運行這個test程序時厚掷,會報錯弟灼,因為系統(tǒng)找不到liba.so文件级解。默認情況下,系統(tǒng)只會在/usr/lib田绑、/usr/local/lib目錄下查找.so文件勤哗。為了能夠讓系統(tǒng)找到我們的liba.so,我們要么把liba.so放到上述兩個目錄中掩驱,要么使用LD_LIBRARY_PATH環(huán)境變量將liba.so所在的目錄添加為.so搜索目錄芒划。這里我們使用第二種方法,在命令行輸入:
export LD_LIBRARY_PATH=`pwd`
這時欧穴,程序就能正常運行了民逼。
此外還有其他方法能夠讓系統(tǒng)找到liba.so,可以查看下面的參考文檔1涮帘,不再贅述拼苍。
2.2 Dynamic Loading方式
使用dlopen、dlsym等函數(shù)调缨,我們可以在運行期加載任意一個共享庫疮鲫。我們把前面的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等函數(shù)基茵,我們需要包含dlfcn.h頭文件。
第12行,我們使用dlopen函數(shù)闷愤,傳遞liba.so的路徑名(本例是當前目錄)课蔬,系統(tǒng)會嘗試加載liba.so何暇。如果成功句柠,返回給我們一個句柄。RTLD_LAZY是說加載時不處理unresolved symbols桃序。對于本例杖虾,就是加載liba.so時,不會去查找foo的地址媒熊,只有在第一次調(diào)用foo時才會去找foo的實際地址奇适。需要了解進一步詳細信息可以查找手冊(命令行輸入:man dlopen)。
第19行芦鳍,我們使用dlsym函數(shù)嚷往,傳遞dlopen返回的句柄和我們想要獲取的函數(shù)名稱。如果這個
名稱是存在的柠衅,dlsym會返回其相應的地址皮仁。這就是為什么我們需要把.so的接口函數(shù)聲明為extern "C",否則,我們就必須給dlsym傳遞經(jīng)過c++編譯器mingle之后的奇怪名字贷祈,才能找到相應的函數(shù)趋急。
出現(xiàn)任何錯誤的時候,dlerror會返回相應的錯誤信息字符串势誊;否則它會返回一個空指針呜达。dlerror提供的信息對我們定位問題是非常有幫助的。
一旦獲取了函數(shù)地址粟耻,我們可以把它保存在函數(shù)指針中(第29行)查近,隨后就可以像使用函數(shù)一樣來使用它(第30行)。
接著挤忙,我們編譯main.cpp霜威,并生成可執(zhí)行文件:
g++ main_dyn_load.cpp -o test -ldl
因為我們使用的是Dynamic Loading,因此就不需要在編譯時鏈接liba.so了(去掉了-la)饭玲,因為我們使用了dlxxx函數(shù)侥祭,所以需要增加鏈接-ldl叁执。
3. 使用Dynamic Library的注意事項
Dynamic Library使用要比Static Library復雜茄厘,下面是一些需要注意的問題。
3.1 不同的.so內(nèi)包含同名全局函數(shù)
3.1.1 Dynamic Linking
.so允許出現(xiàn)同名的強符號谈宛。因此次哈,如果不同的.so包含同名的全局函數(shù),鏈接時編譯器不會報錯吆录。編譯器會使用命令行中先鏈接的那個庫的版本窑滞。例如,我們再增加一個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侄柔。
這個不會成為太大的問題共啃,因為使用靜態(tài)庫也是這樣的。
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;
}
首先纵苛,稍微重構(gòu)了一下get_foo函數(shù),使之能夠接收一個.so路徑作為參數(shù),然后它回取出相應.so里面的foo函數(shù)的地址攻人。
第29和第30行幔虏,我們分別從liba.so和libb.so中取出了foo函數(shù)地址,將他們保存在foo_a和foo_b兩個函數(shù)指針中贝椿,并在第31和第32行分別進行了調(diào)用想括。
最后,程序?qū)蛴.foo和b.foo烙博。
3.2 .so反向調(diào)用bin里面的函數(shù)
bin可以調(diào)用.so定義的函數(shù)瑟蜈,以及.so可以調(diào)用其它.so定義的函數(shù),這是毫無疑問的渣窜。那么铺根,.so能反過來調(diào)用bin里面的函數(shù)么?答案是肯定的乔宿,只要我們在編譯bin時制定-rdynamic選項就可以了位迂。
我們只舉Dynamic Linking的例子,因為Dynamic Loading也是一樣的详瑞。
我們在main_dyn_linking里面定義一個新的函數(shù)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里面調(diào)用這個函數(shù):
// 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
執(zhí)行程序坝橡,將會打有喊铩:
a.foo main.bar
3.3 不同的.so內(nèi)出現(xiàn)同名的全局變量
終于要面對這個非常tricky的場景了。這里說的全局變量计寇,既包括通常意義的『全局變量』锣杂,也包括類的靜態(tài)成員變量,因為后者本質(zhì)上就是改了名字全局變量番宁。
3.3.1 Dynamic Linking
我們先來考慮Dynamic Linking的情況元莫。我首先添加一個類:MyClass,并把它實現(xiàn)為singleton蝶押。因為singleton模式是使用類靜態(tài)成員最常見的場景之一踱蠢。
先來定義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;
}
每次調(diào)用what方法,MyClass對象內(nèi)部計數(shù)會加1播聪,并隨后打印對象的地址和當前的計數(shù)值朽基。
我們在a.cpp和b.cpp里面分別調(diào)用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
執(zhí)行這個程序离陶,我們發(fā)現(xiàn)稼虎,盡管在不同的.so內(nèi)都包含了my_class.cpp(里面定義了_instance靜態(tài)靜態(tài)變量),但最終全局只有一個_instance實例招刨。但是霎俩,這個實例被初始化了兩次和析構(gòu)了兩次。重復析構(gòu)可能會導致core,因此在.so場景下使用單例模式要更加小心(或選擇其它的單例實現(xiàn)方法)打却。
3.3.2 Dynamic Loading
現(xiàn)在我們看看Dynamic Loading的情況杉适。這次,我們使用main_dyn_load.cpp進行編譯:
g++ main_dyn_load.cpp -o test -ldl -rdynamic
這次柳击,我們驚訝的發(fā)現(xiàn)猿推,居然存在兩個不同的_instance實例!當然捌肴,重復初始化和析構(gòu)不存在了蹬叭,每個對象上都只進行了一次初始化和析構(gòu)。
這說明状知,在Dynamic Loading情況下秽五,不同的.so中同名全局變量都會是不同的實例。
等等饥悴,如果你以為這是全部真相那就錯了坦喘。如果我們在bin中也定義同名的全局變量會怎么樣呢?我們修改一下main_dyn_load.cpp中的bar函數(shù)西设,使之也調(diào)用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
執(zhí)行程序坯沪,結(jié)果再次令人意外:
全局變量再次合為一個绿映,而且被重復初始化-析構(gòu)了三次擒滑。
總結(jié)上述規(guī)律,在Dynamic Loading場景下叉弦,如果.so中出現(xiàn)了同名全局變量丐一,那么每個.so都會有其單獨的全局變量實例,每個實例單獨初始化/析構(gòu)淹冰;如果bin中也包括同名的全局變量库车,那么系統(tǒng)將只有唯一一份實例,在這個實例上會出現(xiàn)多次重復的初始化/析構(gòu)樱拴。
這再次說明柠衍,在.so中使用全局變量(以及類的靜態(tài)成員變量)要非常謹慎,整個系統(tǒng)也要形成統(tǒng)一的規(guī)范晶乔,否則很可能出現(xiàn)未預期的行為珍坊。
3.4 dynamic_cast
從一個.so中創(chuàng)建的對象,在另外一個.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的實現(xiàn):
// 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產(chǎn)生出MyClass對象履怯,我們給.so增加一個接口函數(shù):create回还。此外,我們把foo改為接收一個MyBase對象的指針叹洲。
// a.h
class MyBase;
extern "C" void foo(MyBase*);
extern "C" MyBase *create();
在a.cpp和b.cpp中實現(xiàn)create函數(shù)柠硕。并且,在foo函數(shù)中使用dynamic_cast強制向下轉(zhuǎn)型:
// 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創(chuàng)建對象,再libb.so中轉(zhuǎn)型糙捺、使用诫咱;然后反方向再來一次。
#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行洪灯,使用工具函數(shù)get_fn從.so中獲取函數(shù)地址坎缭,get_fn的源碼在附件中。第21行和第22行分別在liba.so和libb.so中創(chuàng)建了對象签钩。第23行掏呼,liba.so創(chuàng)建的對象在liba.so中轉(zhuǎn)型,第24行同樣測試了libb.so的情形铅檩。第25行和第26行測試了交叉轉(zhuǎn)型的情況憎夷。
編譯:
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
程序運行結(jié)果如下:
可以看到,出錯的代碼就是第25和第26行昧旨,說明在一個.so中創(chuàng)建的對象無法在另一個.so中轉(zhuǎn)型成功拾给。
怎樣解決這個問題呢?答案是把my_class.o也編譯到bin里面兔沃。如下:
g++ main_dyn_load.cpp my_class.o -o test -L`pwd` -ldl -rdynamic
編譯蒋得、運行,可以看到這次轉(zhuǎn)型成功了:
為什么會這樣呢乒疏?這其實和3.3.2的情景是一樣的:dynamic_cast時使用的類的虛函數(shù)表和RTTI元數(shù)據(jù)也是全局變量额衙。當bin沒有同名的全局變量時,各個.so擁有各自獨立的虛函數(shù)表實例怕吴,導致轉(zhuǎn)型時認為不是同一個繼承體系而失敗窍侧。而當bin也編譯了同樣的虛函數(shù)表時,所有的虛函數(shù)表就只會出現(xiàn)為同一個實例了转绷。
5. 總結(jié)
.so帶來了靈活性的同時伟件,也使我們要面對很多tricky的場景,一不小心就可能落到坑里暇咆。因此锋爪,使用.so必須小心丙曙,只在安全的范圍內(nèi)應用,并且在整個系統(tǒng)要有統(tǒng)一的規(guī)范其骄。如果在使用.so的過程中發(fā)現(xiàn)了任何問題亏镰,歡迎隨時與作者交流。
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實現(xiàn)原理