Linux下动态链接库的2种链接方式

方式一

通过 dlopen, dlsym, dlerror, dlclose 在代码中直接打开与使用动态链接库

  • dlopen 用于打开动态链接库,返回句柄
  • dlsym 使用dlopen返回的句柄与函数名来获得函数位置,返回函数指针
  • dlclose 关闭动态链接库
  • dlerror 当动态链接库函数操作失败时,返回出错信息;NULL表示成功

看以下示例程序

// fibo.cpp

#include <iostream>
#include <vector>

using namespace std;

// Unmangling the function name for dlsym convenience, as this is C++ file
extern "C" {
    unsigned long fibo(int n);
}

unsigned long fibo(int n)
{
    unsigned long vec[94] = {0, 1, 1};
    long result = 0;

    if (n <= 0 || n > 93) { // know why choose 93? Haha
        result = 0;
    }
    else { // 1 <= n <= 93
        for (int i=3; i<=n; i++) {
            vec[i] = vec[i-1] + vec[i-2];
        }
        result = vec[n];
    }

    return result;
}

该程序需使用如下命令编译成动态链接库

g++ -fPIC -shared fibo.cpp -o libfibo.so

在程序中,之所以使用 extern “C” 来声明 fibo 函数,是因为这个源代码文件是一个C++文件,默认编译出来的so文件中的fibo函数的名字是会被 mangling 的。这就会引起使用者在调用 dlsym 函数获取 fibo 函数地址的时候,需要填写一个mangling的名称,而不是直接写 “fibo”, 从而造成不便。为了使用者的方便,这里用 extern “C” 来声明 fibo,以去除名称的mangling.

另外,我们可以使用以下2个命令中的任何一个来验证fibo函数的命令在库中已经被unmangling了。

objdump -t libfibo.so | grep fibo 

nm libfibo.so | grep fibo

以下是调用者程序:

// use_libfibo_way1.cpp

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

#include <iostream>
using namespace std;

#define LIB_FIBO_PATH "./libfibo.so"

typedef unsigned long (*FUNC_PTR)(int);

int main()
{
    char * error;
    void * handler = dlopen(LIB_FIBO_PATH, RTLD_LAZY);
    if (!handler) {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE);
    }
    
    FUNC_PTR fibo = (FUNC_PTR)(dlsym(handler, "fibo"));
    if ((error = dlerror()) != NULL)  {
        fprintf(stderr, "%s\n", error);
        exit(EXIT_FAILURE);
    }
    
    cout << "fibo[3] = " << fibo(3) << endl;
    cout << "fibo[5] = " << fibo(5) << endl;
    cout << "fibo[93] = " << fibo(93) << endl;
    
    dlclose(handler);
    return 0;
}

该程序需使用如下命令编译:

g++ -rdynamic use_libfibo_way1.cpp -ldl -o a1.out

运行时,直接运行 a1.out 即可。

可以看出,这种调用动态链接的方式的缺点是:

  1. 需要在程序中或配置文件里注明库文件的位置
  2. 需使用额外的库函数 dlopen, dlsym, dlerror, dlclose 来辅助工作

优点是:
可以不用include头文件

方式二

编译时使用链接选项指定动态链接库

调用者程序如下:

// use_libfibo_way2.cpp

#include <iostream>
using namespace std;

extern "C" {
    unsigned long fibo(int n);
}

int main()
{
    cout << "fibo[3] = " << fibo(3) << endl;
    cout << "fibo[5] = " << fibo(5) << endl;
    cout << "fibo[93] = " << fibo(93) << endl;
    
    return 0;
}

该程序使用如下命令进行编译:

g++ use_libfibo_way2.cpp -L. -lfibo -o a2.out

而运行 a2.out 时,假如 libfibo.so 并不在系统搜索路径中,即不在环境变量 LD_LIBRARY_PATH 中,则需加入,然后运行:

export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

./a2.out

如果不想运行依赖于环境变量,可以在编译的时候使用这个命令:

g++ way2.cpp -lfibo -o a2.out -L. -Wl,-rpath=.

这里的 “-rpath=.” 就表示把”当前路径作为搜索路径“传递给了链接器。于是直接运行 ./a2.out 而不用改变环境变量 LD_LIBRARY_PATH 了。

这种方式的优点是:
程序简洁,不用依赖于其他库,即不用依赖 dlopen, dlsym, dlerror, dlclose 所在的 libdl.so 库

缺点是:
一般都需要include头文件(虽然本例中没有include头文件,那只是因为程序简单)

其他

方式二是常用的方式,但是方式一也需要知道。为何呢?
除了因为方式一可能有其特定的使用场合之外,还有一个原因:
如果使用 ldd 命令来查看一个二进制文件都链接了哪些动态链接库,你会发现方式二的 a2.out 的输出中有 libfibo.so , 而方式一的 a1.out 的输出中没有 libfibo.so!
这是意料之中的,因为a1.out在其编译选项中本就没有用到 libfibo.so, 不过它用到了 libdl.so , 因此可以看到这个库。
这就意味着 ldd 命令无法看出方式一的二进制文件究竟链接了哪些动态链接库,唯有看源码才能看到。

ldd的输出如下:

$ldd a1.out
        linux-vdso.so.1 (0x00007ffc4a3fd000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007fc1f87b8000)
        libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x00007fc1f842f000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fc1f8075000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fc1f8bbf000)
        libm.so.6 => /lib64/libm.so.6 (0x00007fc1f7d2a000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fc1f7b13000)

$ldd a2.out
        linux-vdso.so.1 (0x00007ffddf591000)
        libfibo.so => not found     >>> 因为没有运行 export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH  否则可以找到
        libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x00007f3266225000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f3265e6b000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f3265b20000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f32667b1000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f3265909000)

(完)