加载中...
返回

Linux动态链接二三事

0 动态链接概要

相信点开本文的读者朋友们知道, 模块化编程 是开发过程中的一个重要概念,其思想大致是将程序中的 功能上独立且可复用 的代码块封装为一个个模块,基于这些功能模块构建出一个完整的可执行程序。

库(libraries) 是实现模块化编程的重要基础。一个库就相当于是一个独立的模块,库的开发者将一系列功能封装成一个单元,开发人员可以在不同的库中找到不同的功能实现,以此简化代码并避免重复造轮子。

在Linux中,库分为 静态库(static libraries)动态库(dynamic libraries) ,前者是在 编译时 将库整合进可执行程序中,后者是在 运行时 才找到对应的库并加载执行。

静态库对应的静态链接发生在编译期间,这意味着在可执行程序中包含了主程序依赖的所有静态库。这样做的好处是对于一些小的功能模块可以省去动态链接所需的时间开销;坏处是静态库不可共用,假如有多个可执行程序使用了同一个静态库,那么磁盘和内存中就会有多个静态库副本。

动态库在我看来是更加优雅的模块化方式。其对应的 动态链接 发生在程序运行期间,当需要某一个功能函数时,系统自动查找提供这个功能的动态库并将其加载到内存中。其好处是不同的可执行程序依赖同一个动态库提供的功能时,只要内存中已经有了这个动态库就不需要重新加载,完美诠释了模块化+可复用的理念;坏处是动态链接一定程度上增加了运行耗时,且动态链接机制本身会使得错误的发现延后——编译时零告警零报错,等到运行时才发现跑挂了——这是很可能的事。

既然动态链接是程序在运行期间才发生的事情,那么系统将回答如下的问题:

  • 要怎么知道这个程序需要哪些库?

  • 怎么找到对应的库?

1 动态链接库的指定

对于第一个问题——要怎么知道这个程序需要哪些库——答案很简单:在编译的时候告诉系统。因此动态链接也不完全是在运行时才发生的事~至少在编译期间,我们会告诉编译器这个程序要链接哪些库,编译器在发现某些主程序没有实现的函数(符号)的时候,会到这些动态库里去找,假如在某个库里找到了这个函数,会在最终的可执行程序里标记出来,告诉系统在调用这个函数的时候动态加载一下所需的库。

文字描述过于抽象,举个例子。

假设我们实现了一个库,用来获取一个 [-100, 100] 之间的随机数,我们提供一个头文件供别人调用:

// get_random.h
#ifndef GET_RANDOM_H
#define GET_RANDOM_H

int GetRandom();

#endif

然后实现它:

#include <random>
#include "get_random.h"

int GetRandom()
{
    std::random_device rd;
    std::mt19937 mt(rd());
    std::uniform_real_distribution<double> dist(-100, 100);
    return static_cast<int>(dist(mt));
}

并编译出一个动态库:

$ g++ get_random.cpp -fPIC -shared -o libgetrd.so
# -fPIC     生成与位置无关的代码,全部使用相对地址,才能进行动态加载
# -shared   生成共享库
# -o        编译产生的目标文件

我们得到了 libgetrd.so 文件,它是一个共享对象( .so ,shared object),所有程序都可以链接它。现在有个主程序要打印一个随机数,它打算借助 GetRandom() 来获取随机数:

// main.cpp
#include <iostream>
#include "get_random.h"

int main()
{
    std::cout << "Random: " << GetRandom() << std::endl;
    return 0;
}

如果我们在编译的时候不告诉编译器 GetRandom() 来自于一个动态库,那么编译器就由于找不到这个函数的定义而报错:

$ g++ main.cpp -o main
/tmp/ccVJh4oD.o: In function `main':
main.cpp:(.text+0x1c): undefined reference to `GetRandom()'
collect2: error: ld returned 1 exit status

而只要把动态库的信息告诉编译器,情况就大为好转:

$ g++ main.cpp -L. -lgetrd -o main
# 编译通过
# -L        动态库的搜索路径
# -l        依赖的动态库,动态库的标准命名是 libxxx.so ,编译时指定 xxx 即可

编译出可执行程序 main 的历程如下:

编译源文件 main.cpp
发现一个函数 GetRandom()
main.cpp 当中没有定义这个函数
看看依赖的动态库 libgetrd.so 里有没有定义这个函数
找到函数定义,标记到可执行程序中,告诉系统运行到 GetRandom() 时加载 libgetrd.so

假如说 main.cpp 自己定义了 GetRandom() ,又想去链接 libgetrd.so ,情况会变成什么样子呢?

// main.cpp
#include <iostream>
#include "get_random.h"

int GetRandom() // fake GetRandom()
{
    return 1;
}

int main()
{
    std::cout << "Random: " << GetRandom() << std::endl;
    return 0;
}
$ g++ main.cpp -L. -lgetrd -o main
# 编译通过
# 搞一些小手段让 main 可以运行(详见第2章)

$ ./main 
Random: 1
$ ./main 
Random: 1
$ ./main 
Random: 1

可以看到,主程序自己定义的 GetRandom() 把动态库里的 GetRandom() 隐藏掉了,真正运行的时候使用的是主程序里的 GetRandom()

有些富有经验的小伙伴就惊呆了,怎么好像以前编译项目的时候不是这个情况呀?或许你想到的是这个错误:

$ g++ main.cpp get_random.cpp -o main
/tmp/ccq8KOON.o: In function `GetRandom()':
get_random.cpp:(.text+0x0): multiple definition of `GetRandom()'
/tmp/ccHVl0yK.o:main.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

显然:动态链接时查找符号的机制比较宽松,出现同名函数时不会报重复定义的错误,只是在运行时会出现函数被隐藏的情况;但如果想把多个源文件编在一个二进制程序中,就不能出现函数的重复定义。

多个动态库分别实现了同名的函数时,情况也差不多,最先被链接的那个动态库里的函数会得到执行,这里就不做演示了。

2 动态链接库的找到

程序运行过程中,系统怎么知道要去哪里找这个程序依赖的动态库呢?我们编译时通过 -L-l 参数告诉编译器的信息,有被记录到可执行程序中吗?

2.1 编译时动态库路径

答案是没有。编译时通过 -l 告诉编译器我们需要什么库,通过 -L 告诉编译器去哪里找这个库,当编译器完成编译之后,会将 -l 提供的库信息写到可执行程序里,但会立刻将 -L 提供的路径信息抛诸脑后。

我们重新编译一下 main ,并尝试运行它:

$ g++ main.cpp -L. -lgetrd -o main
$ ./main
./main: error while loading shared libraries: libgetrd.so: cannot open shared object file: No such file or directory

这次,编译通过之后直接运行 main ,但得到了一个错误信息,告诉我们找不到 libgetrd.so

显然,编译时通过 -L 指定的搜索路径在运行时就失效了,这个信息并没有被写入可执行程序中。

Linux为我们提供了一个很不错的工具: ldd ,它可以查看一个可执行程序依赖的所有动态库,并按照动态库的搜索规则来尝试找到这些动态库的位置。我们尝试通过这个工具查看 main 依赖的动态库:

$ ldd main
        linux-vdso.so.1 =>  (0x00007ffee10d1000)
        libgetrd.so => not found
        libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fdf4bf46000)
        libm.so.6 => /lib64/libm.so.6 (0x00007fdf4bc44000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fdf4ba2e000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fdf4b660000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fdf4c24e000)

可见,按照默认的规则来看,我们编译出的 main 所依赖的 libgetrd.so 确实无法被正确加载。

2.2 LD_LIBRARY_PATH

大部分小伙伴对 LD_LIBRARY_PATH 还是比较熟悉的,这个环境变量通常用于指定 系统默认路径之外 的动态库搜索路径。

以我们的 libgetrd.so 为例,我们可以通过在 LD_LIBRARY_PATH 当中加上这个库所在的路径,来让我们的主程序在运行过程中能找到这个库。

$ echo $LD_LIBRARY_PATH
/opt/rh/devtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib:/opt/rh/devtoolset-8/root/usr/lib64/dyninst:/opt/rh/devtoolset-8/root/usr/lib/dyninst:/opt/rh/devtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib

# 把当前路径加到动态库搜索路径
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:`pwd`

# 注意最后一项
$ echo $LD_LIBRARY_PATH
/opt/rh/devtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib:/opt/rh/devtoolset-8/root/usr/lib64/dyninst:/opt/rh/devtoolset-8/root/usr/lib/dyninst:/opt/rh/devtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib:/home/dg/exp

# 注意 libgetrd.so 的路径
$ ldd main
        linux-vdso.so.1 =>  (0x00007ffc90d28000)
        libgetrd.so => /home/dg/exp/libgetrd.so (0x00007f51448ce000)
        libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f51445c6000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f51442c4000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f51440ae000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f5143ce0000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f5144ad2000)

2.3 rpath和runpath

除了 LD_LIBRARY_PATH 之外,是否还有其他指定动态库查找路径的方法呢?试想,我们需要发布一个程序,并将程序所需的所有动态库都放在我们指定的某个特定路径下,由于我们指定的路径不属于系统默认的动态库路径,难道只能委屈用户在使用我们的程序之前自己去指定 LD_LIBRARY_PATH 吗?这也太不友好了~

2.3.1 rpath

事实上,就像我们可以指定编译时的动态库路径那样,我们也可以指定运行时的动态库路径,这个路径信息会被写入到编译出来的可执行文件中。通过编译参数 -Wl 表示将后面的信息传递给 链接器 ,通过 -rpath,[path] 来告诉链接器将动态库的搜索路径写入到可执行文件中。

$ g++ main.cpp -L. -lgetrd -Wl,-rpath,./ -o main

动态库的信息记录在可执行文件的动态段里,我们可以通过 readelf 工具,并指定 -d 选项来查看 main 程序的动态段。

$ readelf -d main

Dynamic section at offset 0xdd8 contains 29 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libgetrd.so]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000f (RPATH)              Library rpath: [./]
# ====== 以下省略若干行 ======

可以看到,动态段首先记录了这个程序运行所需要的所有动态库,这和我们前文提及的一致;在上面列出的最后一行中,还记录了一个 RPATH 字段,这个字段提供了程序运行时可以查找的动态库路径。

ldd 也是没有问题的:

$ ldd main
        linux-vdso.so.1 =>  (0x00007ffc90d28000)
        libgetrd.so => /home/dg/exp/libgetrd.so (0x00007f51448ce000)
        libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f51445c6000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f51442c4000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f51440ae000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f5143ce0000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f5144ad2000)
2.3.2 runpath

使用与 RPATH 类似的编译命令,但额外添加一个标记 --enable-new-dtags 来指定使用 runpath

$ g++ main.cpp -L. -lgetrd -Wl,-rpath,./,--enable-new-dtags -o main


$ readelf -d main

Dynamic section at offset 0xdd8 contains 29 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libgetrd.so]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000001d (RUNPATH)            Library runpath: [./]
# ====== 以下省略若干行 ======

readelf 的输出跟前面看到的基本一致,但得益于 --enable-new-dtags 标记,最后一行的 rpath 标记变为了 runpath

至此,我们产生了很大的疑问: LD_LIBRARY_PATHrpathrunpath 都用于指定动态链接库的搜索路径,为什么需要有这么多的机制呢?尤其是 rpathrunpath ,同作为可执行文件动态段的一个成员,它们的区别在什么地方呢?

2.4 动态链接库查找规则

2.4.1 查找规则

不必多言,直接摘录 ld.so官方文档

If a shared object dependency does not contain a slash, then it
is searched for in the following order:

o  Using the directories specified in the DT_RPATH dynamic
    section attribute of the binary if present and DT_RUNPATH
    attribute does not exist.  Use of DT_RPATH is deprecated.

o  Using the environment variable LD_LIBRARY_PATH, unless the
    executable is being run in secure-execution mode (see below),
    in which case this variable is ignored.

o  Using the directories specified in the DT_RUNPATH dynamic
    section attribute of the binary if present.  Such directories
    are searched only to find those objects required by DT_NEEDED
    (direct dependencies) entries and do not apply to those
    objects' children, which must themselves have their own
    DT_RUNPATH entries.  This is unlike DT_RPATH, which is applied
    to searches for all children in the dependency tree.

o  From the cache file /etc/ld.so.cache, which contains a
    compiled list of candidate shared objects previously found in
    the augmented library path.  If, however, the binary was
    linked with the -z nodeflib linker option, shared objects in
    the default paths are skipped.  Shared objects installed in
    hardware capability directories (see below) are preferred to
    other shared objects.

o  In the default path /lib, and then /usr/lib.  (On some 64-bit
    architectures, the default paths for 64-bit shared objects are
    /lib64, and then /usr/lib64.)  If the binary was linked with
    the -z nodeflib linker option, this step is skipped.

简言之,链接器在查找程序依赖的动态库时,搜索的顺序是:

  • RPATH

  • LD_LIBRARY_PATH

  • RUN_PATH

  • /etc/ld.so.cache 文件中记载的动态链接库路径缓存

  • 系统默认搜索路径 /lib/use/lib64 位的动态库默认是 /lib64/usr/lib64

这个查找规则,也解释了为什么既需要 RPATH 又需要 RUNPATH 。实际上,在历史中早先只有 RPATH ,但由于 RPATH 设置的路径优先级高于 LD_LIBRARY_PATH ,会导致调试过程中无法轻松加载不同路径下的动态库(只能固定取到 RPATH 路径下的那个库),后续才引入了 RUNPATH ,以允许在程序运行期间通过 LD_LIBRARY_PATH 覆盖编译时设定的动态库路径。

这两个东东的新旧顺序,也从我们的编译选项中可以看出来——默认情况下使用的是 rpath ,而通过使用新版标签 --enable-new-dtags 来启用 runpath 。假如说编译器是比较新的,则默认情况下使用的是 runpath ,用编译选项 --disable-new-dtags 禁用掉新版标签来回退到 rpath

2.4.2 LD_DEBUG

我们可以通过 LD_DEBUG 环境变量来控制链接器输出动态链接过程中的相关信息。通过将它的值设为 help 并运行任意程序来查看它的使用说明:

$ LD_DEBUG=help ls
Valid options for the LD_DEBUG environment variable are:

  libs        display library search paths
  reloc       display relocation processing
  files       display progress for input file
  symbols     display symbol table processing
  bindings    display information about symbol binding
  versions    display version dependencies
  scopes      display scope information
  all         all previous options combined
  statistics  display relocation statistics
  unused      determined unused DSOs
  help        display this help message and exit

To direct the debugging output into a file instead of standard output
a filename can be specified using the LD_DEBUG_OUTPUT environment variable.

将该变量设置为说明中给出的不同值,就能得到不同的输出。例如,值 libs 会打印动态链接库的搜索路径,则我们尝试通过指定 LD_DEBUG=libs 并运行 main 来观察我们的随机数库是怎么被找到的:

# 首先观察 RPATH 版本
$ g++ main.cpp -L. -lgetrd -Wl,-rpath,./ -o main
$ LD_DEBUG=libs ./main
     17058:     find library=libgetrd.so [0]; searching
     17058:      search path=./tls/x86_64:./tls:./x86_64:.              (RPATH from file ./main)
     17058:       trying file=./tls/x86_64/libgetrd.so
     17058:       trying file=./tls/libgetrd.so
     17058:       trying file=./x86_64/libgetrd.so
     17058:       trying file=./libgetrd.so
     17058:
     17058:     find library=libstdc++.so.6 [0]; searching
# ====== 以下省略若干行 ======
# 再观察 RUNPATH 版本
$ g++ main.cpp -L. -lgetrd -Wl,-rpath,./,--enable-new-dtags -o main
$ LD_DEBUG=libs ./main     20474:     find library=libgetrd.so [0]; searching
     20474:      search path=/opt/rh/devtoolset-8/root/usr/lib64/tls/x86_64:/opt/rh/devtoolset-8/root/usr/lib64/tls:/opt/rh/devtoolset-8/root/usr/lib64/x86_64:/opt/rh/devtoolset-8/root/usr/lib64:/opt/rh/devtoolset-8/root/usr/lib/tls/x86_64:/opt/rh/devtoolset-8/root/usr/lib/tls:/opt/rh/devtoolset-8/root/usr/lib/x86_64:/opt/rh/devtoolset-8/root/usr/lib:/opt/rh/devtoolset-8/root/usr/lib64/dyninst/tls/x86_64:/opt/rh/devtoolset-8/root/usr/lib64/dyninst/tls:/opt/rh/devtoolset-8/root/usr/lib64/dyninst/x86_64:/opt/rh/devtoolset-8/root/usr/lib64/dyninst:/opt/rh/devtoolset-8/root/usr/lib/dyninst/tls/x86_64:/opt/rh/devtoolset-8/root/usr/lib/dyninst/tls:/opt/rh/devtoolset-8/root/usr/lib/dyninst/x86_64:/opt/rh/devtoolset-8/root/usr/lib/dyninst            (LD_LIBRARY_PATH)
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib64/tls/x86_64/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib64/tls/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib64/x86_64/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib64/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib/tls/x86_64/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib/tls/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib/x86_64/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib64/dyninst/tls/x86_64/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib64/dyninst/tls/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib64/dyninst/x86_64/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib64/dyninst/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib/dyninst/tls/x86_64/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib/dyninst/tls/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib/dyninst/x86_64/libgetrd.so
     20474:       trying file=/opt/rh/devtoolset-8/root/usr/lib/dyninst/libgetrd.so
     20474:      search path=./tls/x86_64:./tls:./x86_64:.              (RUNPATH from file ./main)
     20474:       trying file=./tls/x86_64/libgetrd.so
     20474:       trying file=./tls/libgetrd.so
     20474:       trying file=./x86_64/libgetrd.so
     20474:       trying file=./libgetrd.so
     20474:
     20474:     find library=libstdc++.so.6 [0]; searching
# ====== 以下省略若干行 ======

两次不同的输出充分反映了 RPATHLD_LIBRARY_PATHRUN_PATH 的搜索顺序。

3 CMake与动态链接库

由于我所在的项目中已经不使用原始的编译命令,前文提及的 -L 也好、-rpath 也好,在日常工作中都无法感知了。当前使用较多的构建工具是 CMake ,因此想探寻一下前文内容在 CMake 中的体现。如果读者朋友平时不用 CMake ,可直接略过本节。

3.1 制作动态库

通过 cmake官方文档 我们知道,可以通过下面这句简单的命令来生成一个库:

add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            [<source>...])

这里指定的 <name> 会被自动纳入编译对象中,在Linux环境下会编译得到 lib<name>.alib<name>.so ;将标记设置为 SHARED ,就可以使我们编译得到的对象是一个动态库;将标记设置为 STATIC ,就可以使我们编译得到的对象是一个静态库,二者的区别已在 第0节 进行了简单介绍。 MODULE 标记用于编译模块化库,这种库一般由程序本身主动通过 dlopen 等方式来加载,本文不予探究。

现在对我们的demo进行一些小的改动,在源文件之外再添加一份 CMakeLists.txt ,然后创建一个 build 目录用于容纳后续的编译产物。

$ tree /home/dg/exp/
/home/dg/exp/
|-- build
|-- CMakeLists.txt
|-- get_random.cpp
|-- get_random.h
`-- main.cpp

1 directory, 4 files

CMakeLists.txt 中,我们准备把 get_random.cpp 编译成 libgetrd.so

PROJECT(DEMO)

# 设置c++编译器为g++
set(CMAKE_CXX_COMPILER /opt/rh/devtoolset-8/root/usr/bin/g++)

# 开启编译时的详细输出,好让我们知道一会儿发生了什么
set(CMAKE_VERBOSE_MAKEFILE ON)

# 动态库的源码路径,跟 CMakeLists.txt 在同级目录
SET(SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR})

# 编译对象 getrd
ADD_LIBRARY(getrd SHARED ${SRC_DIR}/get_random.cpp)

直接准备编译:

$ pwd
/home/dg/exp/build

$ cmake ..
====== 省略若干行 =====
-- Configuring done
-- Generating done
-- Build files have been written to: /home/dg/exp/build

$ make getrd
/usr/local/bin/cmake -S/home/dg/exp -B/home/dg/exp/build --check-build-system CMakeFiles/Makefile.cmake 0
make  -f CMakeFiles/Makefile2 getrd
make[1]: Entering directory '/home/dg/exp/build'
/usr/local/bin/cmake -S/home/dg/exp -B/home/dg/exp/build --check-build-system CMakeFiles/Makefile.cmake 0
/usr/local/bin/cmake -E cmake_progress_start /home/dg/exp/build/CMakeFiles 2
make  -f CMakeFiles/Makefile2 CMakeFiles/getrd.dir/all
make[2]: Entering directory '/home/dg/exp/build'
make  -f CMakeFiles/getrd.dir/build.make CMakeFiles/getrd.dir/depend
make[3]: Entering directory '/home/dg/exp/build'
cd /home/dg/exp/build && /usr/local/bin/cmake -E cmake_depends "Unix Makefiles" /home/dg/exp /home/dg/exp /home/dg/exp/build /home/dg/exp/build /home/dg/exp/build/CMakeFiles/getrd.dir/DependInfo.cmake --color=
make[3]: Leaving directory '/home/dg/exp/build'
make  -f CMakeFiles/getrd.dir/build.make CMakeFiles/getrd.dir/build
make[3]: Entering directory '/home/dg/exp/build'
[ 50%] Building CXX object CMakeFiles/getrd.dir/get_random.o
/opt/rh/devtoolset-8/root/usr/bin/g++ -Dgetrd_EXPORTS  -fPIC -MD -MT CMakeFiles/getrd.dir/get_random.o -MF CMakeFiles/getrd.dir/get_random.o.d -o CMakeFiles/getrd.dir/get_random.o -c /home/dg/exp/get_random.cpp
[100%] Linking CXX shared library libgetrd.so
/usr/local/bin/cmake -E cmake_link_script CMakeFiles/getrd.dir/link.txt --verbose=1
/opt/rh/devtoolset-8/root/usr/bin/g++ -fPIC -shared -Wl,-soname,libgetrd.so -o libgetrd.so CMakeFiles/getrd.dir/get_random.o 
make[3]: Leaving directory '/home/dg/exp/build'
[100%] Built target getrd
make[2]: Leaving directory '/home/dg/exp/build'
/usr/local/bin/cmake -E cmake_progress_start /home/dg/exp/build/CMakeFiles 0
make[1]: Leaving directory '/home/dg/exp/build'

由于在 CMakeLists.txt 中开启了编译时的详细输出,我们完全可以知道我们的一句 ADD_LIBRARY 最后自动演变成了怎样的编译命令:

[ 50%] Building CXX object CMakeFiles/getrd.dir/get_random.o
/opt/rh/devtoolset-8/root/usr/bin/g++ -Dgetrd_EXPORTS  -fPIC -MD -MT CMakeFiles/getrd.dir/get_random.o -MF CMakeFiles/getrd.dir/get_random.o.d -o CMakeFiles/getrd.dir/get_random.o -c /home/dg/exp/get_random.cpp

[100%] Linking CXX shared library libgetrd.so
/usr/local/bin/cmake -E cmake_link_script CMakeFiles/getrd.dir/link.txt --verbose=1
/opt/rh/devtoolset-8/root/usr/bin/g++ -fPIC -shared -Wl,-soname,libgetrd.so -o libgetrd.so CMakeFiles/getrd.dir/get_random.o 

这里发生的事情,跟此前我们一句 g++ get_random.cpp -fPIC -shared -o libgetrd.so 所做的基本一致,无非是将编译过程展开并增加了一些中间产物。源文件首先被编译成 .o 文件,然后再将所有依赖(在我们的例子中并没有其他依赖)链接得到 .so 文件。

3.2 使用动态库

仍然根据 CMake官方文档 得知如何在编译过程中链接动态库:

target_link_libraries(<target> ... <item>... ...)

我们在主程序的编译过程中链接 libgetrd.so

PROJECT(DEMO)

# 设置c++编译器为g++
set(CMAKE_CXX_COMPILER /opt/rh/devtoolset-8/root/usr/bin/g++)

# 开启编译时的详细输出,好让我们知道一会儿发生了什么
set(CMAKE_VERBOSE_MAKEFILE ON)

# 动态库和主程序的源码路径,跟 CMakeLists.txt 在同级目录
SET(SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR})

# 编译对象 getrd
ADD_LIBRARY(getrd SHARED ${SRC_DIR}/get_random.cpp)

# 编译主程序
ADD_EXECUTABLE(main ${SRC_DIR}/main.cpp)

# 主程序要链接getrd
TARGET_LINK_LIBRARIES(main getrd)
$ cmake ..
===== 输出省略 =====
$ make main
===== 省略若干行 =====
[ 50%] Built target getrd
===== 省略若干行 =====
[ 75%] Building CXX object CMakeFiles/main.dir/main.o
/opt/rh/devtoolset-8/root/usr/bin/g++    -MD -MT CMakeFiles/main.dir/main.o -MF CMakeFiles/main.dir/main.o.d -o CMakeFiles/main.dir/main.o -c /home/dg/exp/main.cpp
[100%] Linking CXX executable main
/usr/local/bin/cmake -E cmake_link_script CMakeFiles/main.dir/link.txt --verbose=1
/opt/rh/devtoolset-8/root/usr/bin/g++ -rdynamic CMakeFiles/main.dir/main.o -o main  -Wl,-rpath,/home/dg/exp/build libgetrd.so 
make[3]: Leaving directory '/home/dg/exp/build'
[100%] Built target main
make[2]: Leaving directory '/home/dg/exp/build'
/usr/local/bin/cmake -E cmake_progress_start /home/dg/exp/build/CMakeFiles 0
make[1]: Leaving directory '/home/dg/exp/build'

可以看到,生成 main 的编译命令跟前几节里手动编译的命令基本一致:

# 2.3.1小节里使用的编译命令
$ g++ main.cpp -L. -lgetrd -Wl,-rpath,./ -o main
# 通过 CMake TARGET_LINK_LIBRARIES 自动生成的编译命令
/opt/rh/devtoolset-8/root/usr/bin/g++ -rdynamic CMakeFiles/main.dir/main.o -o main  -Wl,-rpath,/home/dg/exp/build libgetrd.so 

值得注意的是,由于我们在同一个CMake项目中通过 ADD_LIBRARY 制作了一个动态库,又通过 TARGET_LINK_LIBRARIES 使主程序链接了这个动态库,每次编译时会自动重新生成这个动态库以保证库是最新的,体现在 make main 的输出当中: [ 50%] Built target getrd

考虑另一种场景,假如我们使用的不是自己生成的动态库,而是周边预先提供的动态库,又如何呢?

我们先稍稍更改目录结构,把 libgetrd.so 放到源文件同级目录下:

$ tree /home/dg/exp/
/home/dg/exp/
|-- build
|-- CMakeLists.txt
|-- get_random.cpp
|-- get_random.h
|-- libgetrd.so
`-- main.cpp

1 directory, 5 files

然后在 CMakeLists.txt 里删除 ADD_LIBRARY ,表明这个库现在不由我们提供:

PROJECT(DEMO)

# 设置c++编译器为g++
set(CMAKE_CXX_COMPILER /opt/rh/devtoolset-8/root/usr/bin/g++)

# 开启编译时的详细输出,好让我们知道一会儿发生了什么
set(CMAKE_VERBOSE_MAKEFILE ON)

# 主程序的源码路径,跟 CMakeLists.txt 在同级目录
SET(SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR})

# 编译主程序
ADD_EXECUTABLE(main ${SRC_DIR}/main.cpp)

# 主程序要链接getrd
TARGET_LINK_LIBRARIES(main getrd)

愉快地编译运行!

$ cmake ..
$ make main
===== 省略若干行 =====
[100%] Linking CXX executable main
/usr/local/bin/cmake -E cmake_link_script CMakeFiles/main.dir/link.txt --verbose=1
/opt/rh/devtoolset-8/root/usr/bin/g++ -rdynamic CMakeFiles/main.dir/main.cpp.o -o main  -lgetrd 
/opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/8/ld: cannot find -lgetrd
collect2: error: ld returned 1 exit status
make[3]: *** [CMakeFiles/main.dir/build.make💯 main] Error 1
make[3]: Leaving directory '/home/dg/exp/build'
make[2]: *** [CMakeFiles/Makefile2:86: CMakeFiles/main.dir/all] Error 2
make[2]: Leaving directory '/home/dg/exp/build'
make[1]: *** [CMakeFiles/Makefile2:93: CMakeFiles/main.dir/rule] Error 2
make[1]: Leaving directory '/home/dg/exp/build'
make: *** [Makefile:127: main] Error 2

oho,似乎缺少了一点东西。

实际上,正如我们在 第一节 当中所说的,我们需要在编译时告诉编译器应该去哪里找动态库,在CMake中,承担这个任务的是 target_link_directories

# 设置c++编译器为g++
set(CMAKE_CXX_COMPILER /opt/rh/devtoolset-8/root/usr/bin/g++)

# 开启编译时的详细输出,好让我们知道一会儿发生了什么
set(CMAKE_VERBOSE_MAKEFILE ON)

# 主程序的源码路径,跟 CMakeLists.txt 在同级目录
SET(SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR})

# 编译主程序
ADD_EXECUTABLE(main ${SRC_DIR}/main.cpp)

# 指定动态库的路径,跟 CMakeLists.txt 在同级目录
TARGET_LINK_DIRECTORIES(main PUBLIC ${SRC_DIR})

# 主程序要链接getrd
TARGET_LINK_LIBRARIES(main getrd)
$ cmake ..
$ make main
===== 省略若干行 =====
[ 50%] Linking CXX executable main
/usr/local/bin/cmake -E cmake_link_script CMakeFiles/main.dir/link.txt --verbose=1
/opt/rh/devtoolset-8/root/usr/bin/g++ -rdynamic CMakeFiles/main.dir/main.o -o main   -L/home/dg/exp  -Wl,-rpath,/home/dg/exp -lgetrd 
make[3]: Leaving directory '/home/dg/exp/build'
[100%] Built target main
make[2]: Leaving directory '/home/dg/exp/build'
/usr/local/bin/cmake -E cmake_progress_start /home/dg/exp/build/CMakeFiles 0
make[1]: Leaving directory '/home/dg/exp/build'

TARGET_LINK_DIRECTORIES 在我们的编译命令当中增加了 -L 参数,指定了动态库的搜索路径,帮助我们顺利找到了 libgetrd.so

3.3 动态库依赖传递

主程序依赖动态库——动态库的实现又依赖另一个动态库,这是很常见的事。在一个复杂的系统中,我们的业务程序已经不知道位于多么高的层次了,下层的动态库总是搭建在很多更下层的基础库上的。

对我们的demo进行一些小小的改造,现在我们的主程序不再使用 GetRandom 来获取单个随机数了,而通过 libgetrdlist.so 当中定义的 GetRandomListOfSize(size_t) 来获取一个随机数的 vector ,而这个库的实现就只是通过多次调用 libgetrd.so 当中定义的 GetRandom 来生成随机数。

// get_random_list.cpp
#include "get_random_list.h"
#include "get_random.h"
#include <vector>

std::vector<int> GetRandomListOfSize(size_t size)
{
    std::vector<int> res;
    constexpr size_t MAX_LIST_SIZE = 10; // 规定随机数列表的最大长度不超过10
    while ((size-- > 0) && (res.size() < MAX_LIST_SIZE)) {
        res.push_back(GetRandom());
    }
    return res;
}
// main.cpp
#include <iostream>
#include "get_random_list.h"

int main()
{
    auto randomList = GetRandomListOfSize(5);
    std::cout << "Random list: ";
    for (const auto &r : randomList) {
        std::cout << r << ' ';
    }
    std::cout << std::endl;
    return 0;
}

源码列表当中增加了两个文件,一个 libs 目录用来放置我们的动态库:

$ tree /home/dg/exp/
/home/dg/exp/
|-- build
|-- CMakeLists.txt
|-- get_random.cpp
|-- get_random.h
|-- get_random_list.cpp
|-- get_random_list.h
|-- libs
|   `-- libgetrd.so
`-- main.cpp

2 directories, 7 files

生成 libgetrdlist.soCMakeLists.txt 用的均是本节介绍过的东西:

PROJECT(DEMO)

# 设置c++编译器为g++
set(CMAKE_CXX_COMPILER /opt/rh/devtoolset-8/root/usr/bin/g++)

# 开启编译时的详细输出,好让我们知道一会儿发生了什么
set(CMAKE_VERBOSE_MAKEFILE ON)

# 主程序的源码路径,跟 CMakeLists.txt 在同级目录
SET(SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR})

# 动态库的路径,在 CMakeLists.txt 同级目录的 libs 目录下
SET(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libs)

# 编译 getrdlist
ADD_LIBRARY(getrdlist SHARED ${SRC_DIR}/get_random_list.cpp)
TARGET_LINK_DIRECTORIES(getrdlist PUBLIC ${LIB_DIR})
TARGET_LINK_LIBRARIES(getrdlist getrd)
$ cmake ..
$ make getrdlist
===== 省略若干行 =====
[100%] Linking CXX shared library libgetrdlist.so
/usr/local/bin/cmake -E cmake_link_script CMakeFiles/getrdlist.dir/link.txt --verbose=1
/opt/rh/devtoolset-8/root/usr/bin/g++ -fPIC -shared -Wl,-soname,libgetrdlist.so -o libgetrdlist.so CMakeFiles/getrdlist.dir/get_random_list.o   -L/home/dg/exp/libs  -Wl,-rpath,/home/dg/exp/libs -lgetrd 
make[3]: Leaving directory '/home/dg/exp/build'
[100%] Built target getrdlist
make[2]: Leaving directory '/home/dg/exp/build'
/usr/local/bin/cmake -E cmake_progress_start /home/dg/exp/build/CMakeFiles 0
make[1]: Leaving directory '/home/dg/exp/build'

我们把编译得到的 libgetrdlist.so 也挪到动态库的路径下,然后在 CMakeLists.txt 里把相关的编译指令删掉,还是只保留编译主程序的内容。

PROJECT(DEMO)

# 设置c++编译器为g++
set(CMAKE_CXX_COMPILER /opt/rh/devtoolset-8/root/usr/bin/g++)

# 开启编译时的详细输出,好让我们知道一会儿发生了什么
set(CMAKE_VERBOSE_MAKEFILE ON)

# 主程序的源码路径,跟 CMakeLists.txt 在同级目录
SET(SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR})

# 动态库的路径,在 CMakeLists.txt 同级目录的 libs 目录下
SET(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libs)

# 编译主程序
ADD_EXECUTABLE(main ${SRC_DIR}/main.cpp)

# 指定动态库的路径
TARGET_LINK_DIRECTORIES(main PUBLIC ${LIB_DIR})

# 主程序要链接getrdlist
TARGET_LINK_LIBRARIES(main getrdlist)
$ cmake ..
$ make main
===== 省略若干行 =====
[100%] Linking CXX executable main
/usr/local/bin/cmake -E cmake_link_script CMakeFiles/main.dir/link.txt --verbose=1
/opt/rh/devtoolset-8/root/usr/bin/g++ -rdynamic CMakeFiles/main.dir/main.o -o main   -L/home/dg/exp/libs  -Wl,-rpath,/home/dg/exp/libs -lgetrdlist -lgetrd 
make[3]: Leaving directory '/home/dg/exp/build'
[100%] Built target main
make[2]: Leaving directory '/home/dg/exp/build'
/usr/local/bin/cmake -E cmake_progress_start /home/dg/exp/build/CMakeFiles 0
make[1]: Leaving directory '/home/dg/exp/build'

我们发现了一个怪异的现象:在编译 main 的命令中,把 libgetrd.so 这个最底层的库也带上了。

由此引出了 动态库依赖传递 的概念。在 target_link_libraries 中,可以通过 PUBLICPRIVATEINTERFACE 来指定动态库是否将自己的依赖传递给使用者,我们编译 libgetrdlist.so 时并没有指定这些关键字,默认地将 libgetrd.so 这个依赖传递给了 main

尝试将 libgetrd.so 的依赖改为 PRIVATE

==== 省略若干行 ====
# 编译 getrdlist
ADD_LIBRARY(getrdlist SHARED ${SRC_DIR}/get_random_list.cpp)
TARGET_LINK_DIRECTORIES(getrdlist PRIVATE ${LIB_DIR})
TARGET_LINK_LIBRARIES(getrdlist PRIVATE getrd)

拿着编译出来的 libgetrdlist.so 来编译 main

$ make main
===== 省略若干行 =====
[100%] Linking CXX executable main
/usr/local/bin/cmake -E cmake_link_script CMakeFiles/main.dir/link.txt --verbose=1
/opt/rh/devtoolset-8/root/usr/bin/g++ -rdynamic CMakeFiles/main.dir/main.o -o main   -L/home/dg/exp/libs  -Wl,-rpath,/home/dg/exp/libs -lgetrdlist 
make[3]: Leaving directory '/home/dg/exp/build'
[100%] Built target main
make[2]: Leaving directory '/home/dg/exp/build'
/usr/local/bin/cmake -E cmake_progress_start /home/dg/exp/build/CMakeFiles 0
make[1]: Leaving directory '/home/dg/exp/build'

在编译命令中已经看不到 libgetrd.so 的字样,说明这次修改成功将底层的依赖隐藏了。实际上,CMake文档中给出的建议是,假如一个依赖库只用于上层库的实现,那么就在 target_link_libraries 当中设置 PRIVATE 关键字;假如一个依赖库不仅用于上层库的实现,还用于上层库的头文件中,那么就应该将关键字设置为 PUBLIC ;最后假如一个依赖库不用于上层库的实现,而只用于头文件中,那么就将关键字设置为 INTERFACE

4 小结

这篇小文本质上是基于工作中与动态库所打的交道,选取了一些自觉得有价值的知识点进行的记录。文中的言语并不太流畅,实验设计并不太精巧,不过总的还是对 rpathrunpath 以及动态库相关的CMake知识进行了描述和测试,比文档搬运要来得直观一些。

由于篇幅、精力和个人能力的限制,还有很多复杂的内容没有展开,有遗漏之处、错误之处、没能满足读者的期望之处,均是欢迎指出的。

参考资料

[1] “Anatomy of Linux dynamic libraries - IBM Developer,” , https://developer.ibm.com/tutorials/l-dynamic-libraries/

[2] “ld.so(8) - Linux manual page”, https://www.man7.org/linux/man-pages/man8/ld.so.8.html

[3] 福豹, Runtime:RPATH/LD_LIBRARY_PATH/RUNPATH, https://zhuanlan.zhihu.com/p/534778561

[4] “The LD_DEBUG environment variable | B. Nikolic Software and Computing Blog,” Bnikolic, https://bnikolic.co.uk/blog/linux-ld-debug.html

[5] “add_library — CMake 3.26.3 Documentation,” Cmake, https://cmake.org/cmake/help/latest/command/add_library.html

[6] “target_link_libraries — CMake 3.26.3 Documentation,” Cmake, https://cmake.org/cmake/help/latest/command/target_link_libraries.html

[7] “target_link_directories — CMake 3.26.3 Documentation,” Cmake, https://cmake.org/cmake/help/latest/command/target_link_directories.html#command:target_link_directories

[8] “cmake-buildsystem(7) — CMake 3.26.3 Documentation,” Cmake, https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#target-usage-requirements

有朋自远方来,不亦说乎?
Built with Hugo
Theme Stack designed by Jimmy