linux下动态库的版本号

据说linux下的动态库管理机制可以避免微软件DLL Hell的问题.

今天抽了点时间研究了一下, 发现其实本质上是利用别名(soname)技术来实现的.

在编译so文件时, 通过参数-soname将别名传入链接器ld(gcc可通过参数-Wl来将-soname参数传入链接器), 那么生成的动态库文件中Dynamic section中的SONAME将会被填入输入的别名. 如果没有-soname参数, 则Dynamic section中将不会有SONAME字段生成.

当使用命令gcc -g -Wall -o main -L. -lver时, 在进行链接时, 链接器ld会首先去当前目录查找libver.so, libver.so也被称为链接名字, 链接器会提取libver.so中的SONAME字段中的名字作为运行时要加载的动态库的名字, 如果没有SONAME字段则使用链接名字作为运行时要加载的动态库的名字(可通过readelf -d file来查看).

linux下为了更方便我们管理动态库, 提供了ldconfig程序. ldconfig的主要功能就是搜索/lib, /usr/lib, /etc/ld.so.conf内所列的目录, 提取动态库的SONAME,并将其作为软链接的名字来建立软链接, 以使程序运行时可以按别名加载.


一个具体例子来说明上述机制是怎么避免DLL Hell问题.

可执行文件main中依赖于一个动态库liba.so.
编译liba.so时可以使用gcc -g -Wall –shared -fPIC -o liba.so sourcefiles -Wl,-soname,liba.so.1来生成liba.so
编译main时可以使用gcc -g -Wall -o main -L. -la, 链接时依来liba.so动态库, 运行时依赖的实际上是liba.so.1文件(liba.so文件的别名)

当main程序发布时, 附带的动态库文件名为liba.so.1.1.3(SONAME为liba.so.1), 安装程序时将liba.so.1.1.3拷贝到/usr/lib/目录下, 然后运行ldconfig, 将会自动生成liba.so.1的软连接指向liba.so.1.1.3.
如果以后liba.so有bug修改且没有接口修改(兼容之前发布的main程序), 那么单独发布liba.so.1.2.1 (SONAME为liba.so.1) 文件并将其拷内到/usr/lib/目录下运行ldconfig, ldconfig会自动更新软连接liba.so.1指向liba.so.1.2.1.

如果是使用dlopen动态加载则直接使用dlopen(“liba.so.1”, xxx)来指定所需要的动态库的主版本号即可.

这样就避免了多个版本的动态库相互覆盖的问题, 也就从根本上避免了DLL Hell的问题.

linux下动态库

今天无意间发现在linux下share object(dynamic library)中的函数竟然可以不通过回调的方式直接访问主程序中的函数,瞬间颠覆以前对于动态库的观念.

代码所示libhi.so中有一个函数hello, 主程序main中有一个函数hi_out, 那么在main中调用libhi.so中的hello时,hello会自动找到main程序中的hi_output函数地址, 然后进行调用.

在感叹linux下动态库强大的同时, 对于其实现机制也产生了好奇. 经过一番努力终于在程序员的自我修养中第7.6.2章找到答案.
“动态链接器在完成基本自举后, 动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表中, 我们可以称它为全局符号表(Global Symbol Table)…..当一个新的share object被装载进来的时侯, 它的符号表会被合并到全局符号表中”, 因此其实libhi.so在调用hello函数时实际上是从全局符号表中找到hi_out函数的地址并进行调用, 本质上libhi.so并不知道这个hi_out是属于另一个share object还是属于main程序中.

但当我使用dlopen系列函数动态加载libhi.so时, 却总是加载失败提示找不到hi_out函数. 理论上静态加载与动态加载上的行为应该是一样的, 只不过静态加载时dlopen将会被隐式调用而已.

ld的手册找到了答案, ld在生成可执行文件时, 默认只导出被其他动态库使用的符号. 因为是使用dlopen去动态加载libhi.so, 那么链接时ld并不知道可执行文件中的hi_out会被外部引用, 也就不会导出hi_out到动态符号表去. 当dlopen打开libhi.so时, 动态链接器在全局符号表中找不到hi_out符号, 理所当然就报错了.

要解决这个问题只要给链接器加上参数-E将主程序中所有全局符号放到动态符号表中即可, 由于生成可执行文件一般都是gcc直接生成, 因此可以使用gcc -Wl,-E来将-E参数传给ld来完成创建一个可以被动态链接的可执行文件.

多线程调DLL

最近写代码一不小心又着了多线程的道, 背景如下:
前不久写了这样一个DLL:

const wchar_t *a = L"xxxx";
const wchar_t *b = L"xxxx";

int do_something_a(struct axx *param_a)
{
...
}
int do_something_b(struct bxx *param_b)
{
...
}


在do_something_a与do_something_b中分别用到了字符串a, b.本来这样相安无事, 可是很多地方会用到这个DLL的代码, 但是字符串a, b并不一样, 而字符中a, b可以根据param_a, param_b中的信息来生成, 本着代码正交性的原则, 将DLL重构如下:

wchar_t a[..];
wchar_t b[..];

int do_something_a(struct axx *param_a)
{
gen_a(param_a, a);
...
}
int do_something_b(struct bxx *param_b)
{
gen_b(param_b, b);
...
}

这样咋一看是没什么问题, 代码简洁了, 程序完美了. 可是我忽略了两个问题, 一个进程中不管调LoadLibrary多少次, DLL只会被加载一次. 而我这个DLL是会在多个线程同时加载使用的.
这样一来, 问题就来了, 由于全局数组a与b的存在, 所有的函数都不是线程安全的, 在低并发量的线程中冲突并不严重, 所以问题很难发现, 但是在高并发的线程中两个函数就会大量执行失败.

找到问题了后将代码重构如下:

int do_something_a(struct axx *param_a)
{
wchar_t a[..];
gen_a(param_a, a);
...
}
int do_something_b(struct bxx *param_b)
{
wchar_t b[..];
gen_b(param_b, b);
...
}

这次bug再次给我提了醒, 多线程代码要处处小心, 一不小心就会掉坑里.