Linux之动态链接库

1. 什么是动态链接库?

动态链接库正确的名字叫共享库,英文 Shared Library。在windows下表现为 .dll 文件,在linux下表现为 .so 文件。

之所以叫共享库,是因为多个独立的程序可以共同使用同一个共享库,达到减少执行文件的大小,直到运行时才动态加载,节省磁盘空间和内存空间。

下面通过一个案例深入了解动态库的使用。

2. 动态库案例

main.cpp 用来生成可执行程序。

// main.cpp
#include "random.h"

int main() {
    return get_random_number();
}

该程序依赖于一个random库,库的源码如下:

// random.h
int get_random_number();
// random.cpp
#include "random.h"

int get_random_number(void) {
    return 4;
}

现在,我们用clang++编译器编译这个程序。(clang++与g++类似,但更适合于开发,可以sudo apt install clang安装。)

先编译random这个动态链接库:

clang++ -o random.o -c random.cpp

其中,-o指定输出文件的名称,我们把源文件random.cpp编译成目标文件random.o,-c表示只编译、不链接。然后再把目标文件编译成动态库:

clang++ -shared -o librandom.so random.o

其中,-shared表示生成动态链接库而不是静态库,-o指定输出文件名为librandom.so。注意该名称不是随便定的,而是遵守了动态链接的惯例——所有库的命名形式都为lib.so。

接下来,我们编译可执行程序,首先生成目标文件main.o:

clang++ -o main.o -c main.cpp

然后生成可执行文件main:

clang++ -o main main.o
main.o:在函数‘main’中:
main.cpp:(.text+0x10):对‘get_random_number()’未定义的引用
clang: error: linker command failed with exit code 1 (use -v to see invocation)

不出意外地出错了,因为我们没有链接到random库,所以出现“未定义的引用”。

💡 在开发C++程序的时候,如果看到错误信息 “未定义的引用”,一般有两种情况:一是忘记链接某库,cmake中就是target_link_libraries没有链接该库;二是动态库调用不正确,例如:ABI版本不正确,例如第三方库是低版本ABI,你的程序是高版本,此时可以设置 GLIBCXX_USE_CXX11_ABI=1来解决。

这次我们指定需要链接的库:

clang++ -o main main.o -lrandom
/usr/bin/ld: 找不到 -lrandom
clang: error: linker command failed with exit code 1 (use -v to see invocation)

又出错了。倒也不难想象,虽然我们指定了库的名称,但并没有指定库的路径,链接器/usr/bin/ld不知道去哪里找random这个库。所以我们得指定搜索路径。

clang++ -o main main.o -lrandom -L.

其中-L后面紧跟着路径.,表示在当前目录下查找库。现在,终于编译成功了,让我们运行main这个程序:

./main
./main: error while loading shared libraries: librandom.so: cannot open shared object file: No such file or directory

好了,这又是一个常见的错误。当我们满心欢喜准备见证运行结果的时候,它却告诉我们程序根本不能运行。这是因为动态链接的可执行程序在运行前,需要先加载所需的动态链接库。

虽然我们刚刚用-L.指定了链接路径,但该路径只对编译期生效。为了弄清楚运行时动态链接的方式,我们需要更加深入。

2.1 ELF文件格式

目前在Linux系统下可执行文件和库文件格式是ELF(Executable Linkable Format)格式。与此对应,windows系统可执行文件和库文件是PE(Portable Executable)格式。这些格式定义了可执行文件的二进制结构,库文件内并不全是机器码,还有很多其他信息,如:需要的共享库、运行路径、调试信息等。

在linux下我们可以通过 readelf 工具查看 ELF 文件内的信息:

$ readelf -d main
Dynamic section at offset 0xde8 contains 28 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[librandom.so]
 0x0000000000000001 (NEEDED)             共享库:[libstdc++.so.6]
 0x0000000000000001 (NEEDED)             共享库:[libm.so.6]
 0x0000000000000001 (NEEDED)             共享库:[libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]
 0x000000000000000c (INIT)               0x400568
 0x000000000000000d (FINI)               0x400764
 0x0000000000000019 (INIT_ARRAY)         0x600dd0
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x600dd8
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x400298
 0x0000000000000005 (STRTAB)             0x4003f0
 0x0000000000000006 (SYMTAB)             0x4002d0
 0x000000000000000a (STRSZ)              241 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x601000
 0x0000000000000002 (PLTRELSZ)           48 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x400538
 0x0000000000000007 (RELA)               0x400520
 0x0000000000000008 (RELASZ)             24 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x400500
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x4004e2
 0x0000000000000000 (NULL)               0x0

-d 表示查看动态库链接信息。根绝上面显示的内容,我们可以看到 main运行需要的动态库,一共是5个,这里除了 librandom其他是一些C、C++运行的环境,例如libstdc++是C++依赖库,并且一般它会在系统环境下。

虽然main文件内记录了依赖的动态库,但是没有记录每个动态库的路径,所以 librandom 动态库找不到,我们看看main能够运行的动态库加载情况。

ldd main
    linux-vdso.so.1 =>  (0x00007ffcdde26000)
    librandom.so => not found
    libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f74aaf37000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f74aac2e000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f74aaa18000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f74aa64e000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f74ab2b9000)

果然,除了 random ,其他库都链接到了正确的位置,但是 librandom.so 为not found。由于我们没有指定该动态库的查找路径,连接器就会根据动态链接机制进行查找,一般顺序如下:

  1. ELF文件内rpath指定的路径
  2. LD_LIBRARY_PATH 环境变量中的路径
  3. ELF文件内runpath指定的路径
  4. /etc/ld.so.conf文件中列出的路径。该文件可包含子文件,因此也包括子文件中列出的路径。
  5. 默认的系统路径/lib和/usr/lib
  6. “not found”.

链接器找不到random库,说明它并不在以上这些路径中。最简单的做法,我们可以把random的路径添加到LD_LIBRARY_PATH环境变量中。

LD_LIBRARY_PATH=.
./main

这种方式不是最佳解决方案,最好的方式就是通过设置rpath或runpath来解决。那么什么是rpath和runpath?

2.2 RPATH和RUNPATH

rpathrunpath 是被记录在ELF文件格式内的。当没有设置时,通过 readelf 工具时查看不到该字段的。上面有提到 rpath 和 runpath 搜索路径是有区别的,并且在编译的时候就可以确定的,这样我们就不需要再设置其他搜索路径。

💡 rpath是优先级最高的,并且会依赖传递,例如:A→B→C,A的rpath/runpath里有C的路径,B没有C路径,那么rpath可以找到C,runpath就找不到,即RUNPATH仅用于查找顶层动态库所需的库,而不用于间接动态链接所需的后续库。而rpath可以传递到子依赖,runpath只负责当前依赖。

那么现在我们把 rpath 加进去

clang++ -o main main.o -lrandom -L. -Wl,-rpath,.

其中, -Wl 后面跟着逗号分隔的链接器参数 -rpath. 。表示main运行先到当前目录下查找所需要的库。此时我们再次运行就可以了,然后我们再次查看一下main内的ELF信息。

readelf -d main
Dynamic section at offset 0xdd8 contains 29 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[librandom.so]
 0x0000000000000001 (NEEDED)             共享库:[libstdc++.so.6]
 0x0000000000000001 (NEEDED)             共享库:[libm.so.6]
 0x0000000000000001 (NEEDED)             共享库:[libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]
 0x000000000000000f (RPATH)              Library rpath: [.]
 ...

可以发现多了一项RPATH,正是我们刚刚设置的值。rpath和runpath的区别仅仅是优先级不同,runpath中的路径可以被外部的环境变量LD_LIBRARY_PATH覆盖,而rpath则不会。

💡 在ubuntu 18.04 以前运行程序使用的是rpath,而在此之后使用的是runpath,并且如果runpath,则会忽略rpath。如果在使用库的时候出现导致找不到库,可以通过 设置 -Wl,--disable-new-dtags 参数使用rpath链接。cmake(老版本)在编译的时候会使用rpath,但是install时将会清除rpath,这是cmake认为库是可以被移植的,不应该有其他信息。

3. patchelf工具

patchelf工具可以帮助我们修改已经编译好的elf文件rpath。

  1. 安装
    sudo apt install patchelf

  2. 使用
    修改rpath

    patchelf --set-rpath 'xxx:xxx' libsdk.so
    

    查看

    readelf -d libsdk.so
    ldd libsdk.so
    

4. cmake之rpath

下面的网址是cmake关于rpath的一些说明,都是英文,建议阅读一下,可以加深理解。

https://gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/RPATH-handling

5. 错误

有时候编译会出现这样的错误 recompile with -fPIC

/usr/bin/ld: /usr/local/lib/libprotobuf.a(message_lite.o): relocation R_X86_64_32S against `_ZTVN6google8protobuf11MessageLiteE' can not be used when making a shared object; recompile with -fPIC
/usr/local/lib/libprotobuf.a: error adding symbols: Bad value
collect2: error: ld returned 1 exit status
which may bind externally can not be used when making a shared object; recompile with -fPIC

PIC是Position-Independent-Code的缩写。在计算机系统中,PIC和PIE(Position-IndependentExecutable)是可以在主存中不同位置执行的目标代码。PIC经常被用在共享库中,这样就能将相同的库代码为每个程序映射到一个位置,不用担心覆盖掉其他程序或共享库。

因为so动态库编译的时候加上了 -fPIC,但是连接的 libprotobuf.a文件并不是 -fPIC生成的,所以就报错了。那就是说连接的 libprotobuf.aa 文件,也需要加上 -fPIC 选项进行编译了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FlyWine

你的鼓励将是我创作的做大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值