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实现原理