Linux下静态库与动态库

发布时间 2023-11-15 17:16:58作者: Beasts777

环境:Ubuntu 18.04.6

文章参考:爱编程的大丙 (subingwen.cn)

简介:

所谓库文件,其实就是经过编译的二进制源文件,可以分为静态库动态库。在使用时需要搭配头文件。

在项目中使用库有两个目的:

  1. 使程序更加简洁,减少程序中的源文件数量。
  2. 避免源代码泄露。

1. 静态库

linux中静态库由ar(gcc内自带的程序)命令生成,现在已经使用的很少,大多数情况都是使用动态库。

命名规则如下:

  • Linux中,以lib为前缀,.a为后缀,中间随意,也就是libxxx.a的命名格式。
  • Windows中,以lib为前缀,以.lib为后缀,中间随意,也就是libxxx.lib的命名格式。

1.1 生成静态链接库

将源文件经过预编译、编译、汇编得到的二进制文件,通过ar工具打包即可得到静态库文件。

ar工具参数如下:

  • -c:创建一个库,不论库是否存在都将进行创建。
  • -s:创建目标文件索引,这样如果库较大时,能加快搜索速度。
  • -r:在库中插入模块。默认新成员是添加在库文件的结尾,但如果该模块名已经存在,那么就进行替换。

最后发布需要两个文件:

  • 制作的libxxx.a库文件,里面包含了具体实现的源代码。
  • 相应的头文件,相当于提供了源代码的接口。

1.2 实例

测试程序:

这里依旧使用一-->3-->3.1中的简单的计算机程序,结构如下:

.
├── add.c
├── include
│   └── head.h
├── main.c
├── sub.c

其中:

  • add.c和sub.c分别为加法和减法程序
  • include/head.h为头文件
  • main.c为测试文件

生成静态库:

  1. 将源文件进行汇编操作(前三步),得到二进制文件(注意指定头文件):

    gcc add.c sub.c -c -I include/
    

    得到二进制文件:

    add.o
    sub.o
    
  2. 将生成的目标文件通过ar工具打包为静态库(注意命名):

    ar -csr libcal.a add.o sub.o
    

    得到静态库文件:

    libcal.a
    
  3. 将头文件和静态库文件一起发给用户即可使用:

    include/head.h
    libcal.a
    

1.3 静态库的使用

首先要得到静态库和头文件,随后开始使用,当前文件结构如下:

head.h
libcal.h
main.c

错误示范:

gcc main.c -o cal
/tmp/ccT4oiqj.o:在函数‘main’中:
main.c:(.text+0x21):对‘add’未定义的引用
main.c:(.text+0x43):对‘sub’未定义的引用
collect2: error: ld returned 1 exit status

发现编译报错了,这是因为main.c中引入了头文件head.h,但编译器未能找到head.h中函数的具体实现,也就是找不到库文件,这与库文件的检索有关(后面会讲),简而言之就是找不到库文件,因此我们只需要在编译时指定库文件的路径和名字即可:

  • -L:指定库文件所在的目录,相对或绝对都可以。
  • -l(小写的l):指定库文件的名字(去掉前缀和后缀)。

正确示范:

gcc main.c -o test -L ./ -l cal

生成成功。得到可执行程序test。

该执行程序不依赖库文件和头文件即可运行,因为编译过程实际上是将库中的代码复制到了可执行程序中。

2. 动态库

简介:

与静态库不同,动态库是程序运行时才会加载的库,当动态链接成功部署后,多个程序可以使用同一个加载到内存中的动态库,因此在Linux中动态库也可以被称之为共享库。

动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊形式形成的。库中函数和变量使用的地址是相对地址(静态库中使用的是绝对地址),其真实地址是在应用程序加载动态库时形成的。

命名规则如下:

  • Linux中,以lib为前缀,以.so为后缀,中间是库的名字。也就是libxxx.so
  • Windows中,以lib为前缀,以.dll为后缀,中间是库的名字。也就是libxxx.dll

2.1 生成动态链接库

具体步骤如下:

  1. 通过-fpic参数在汇编时生成与位置无关的代码。
  2. 通过-shared参数告知编译器生成一个动态链接库。
  3. 发布头文件和动态链接库。

2.2 实例

实例代码:

依旧以一-->3-->3.1中的代码为例,其结构如下:

beasts777@ubuntu:~/coding/c++/cal$ tree
.
├── add.c
├── include
│   └── head.h
├── main.c
├── sub.c

生成动态链接库

  1. 使用gcc对源文件进行汇编(参数-c)生成与位置无关的目标文件,需要指定参数-fpic(注意指定头文件所在目录)

    gcc add.c sub.c -c -fpic -I include
    

    得到目标文件:

    add.o
    sub.o
    
  2. 使用gcc将二进制源文件打包成动态库,需要使用参数-shared

    gcc add.o sub.o -shared -o libcalc.so
    

    生成动态库文件:

    libcalc.so
    
  3. 发布动态库文件和头文件

    libcalc.so
    head.h
    

2.3 使用动态链接库

  1. 首先获取动态链接库和头文件:

    .
    ├── head.h
    ├── libcalc.so
    └── main.c		# 这是测试文件
    
  2. 编译测试文件(注意指定库文件的地址和名字):

    gcc main.c -L ./ -l calc -o app
    

    得到可执行文件app

  3. 执行文件:

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

    发现文件报错:找不到共享库libcal.so。这是为什么?明明在编译测试文件时制定了库文件的路径和名字,但实际运行时却记得名字却找不到目录。还有为什么静态库不会出现这一问题?答案见下一节?

2.4 解决动态库无法加载的问题

2.4.1 库的工作原理

静态库:

在程序编译的最后一个阶段,也就是链接阶段,提供的静态库会被打包进可执行程序中。也就是说:此时可执行程序内已经包含了静态库中的代码,当可执行程序执行时,其拷贝的静态库的代码也会加载到进程的代码区,因此也就不需要再去寻找静态库了。

动态库:

  • 在链接阶段,虽然使用gcc命令的-L-l指定了动态库的目录和名字,但此时:
    • 这一步只检查了动态库是否存在,并未将动态库中的代码拷贝到可执行程序中,因此运行时仍需要依赖动态库。
    • 虽然链接时指定了动态库的目录和名字,但可执行程序中只保留了库名,而未保留库的路径,它寻找库实际上是通过程序链接器按照指定顺序在固定目录寻找的。
  • 在可执行程序执行阶段:
    • 程序执行时会先检测需要的动态库是否存在,加载不到就会报错,显示无法加载到动态库。
    • 当动态库中的函数在程序中被调用了,这时动态库才会加载到内存中,不调用就不加载。
    • 动态库的检测和内存加载操作都是通过动态链接器完成的。

2.4.2 动态链接器

简介:动态链接器是一个独立于应用程序的进程,其本身属于操作系统,搜索动态库的依照一定策略,优先级从高到低依次是:

  1. 可执行文件内部的DT_RPATH
  2. 系统的环境变量:LD_LIBRARY_PATH
  3. 系统动态库的缓存文件:/etc/ld.so.cache
  4. 存储动态库、静态库的系统目录:/lib//usr/lib

按照以上顺序,依次搜索动态库是否存在,如果都搜索不到,那么动态链接器就会报错,提示无法找到动态库。

由此,便可以得到找不到动态库的解决方案。

2.4.3 解决方案

一共有三种方法:

  • 方案一:将库的路径添加到环境变量LD_LIBRARY_PATH中。具体步骤如下:

    1. 找到配置文件:

      • 用户级别:~/.bashrc。该设置仅对当前用户有效。
      • 全局级别:/etc/profile。该设置对所有用户都有效。
    2. 打开配置文件,添加一句话:

      export LD_LIBRARY_PATH =$LD_LIBRARY_PATH :动态库的绝对路径
      # eg: export LD_LIBRARY_PATH =$LD_LIBRARY_PATH :/home/beasts777/coding/c++/activeLib/libcalc.so
      
    3. 令配置的文件生效:

      • 用户级别的修改:重启终端即可。(因为用户配置文件是在打开终端时加载的)

      • 系统级别的修改:重启系统即可。(全局配置文件在开机时加载)

      • 也可以用命令让操作系统重新加载配置文件,无需重启终端或系统:

        # 用户级
        source ~/.bashrc
        # 系统级
        source /etc/profile
        
  • 方案二:更新系统动态库的缓存文件:/etc/ld.so.cache(需要注意的是,我们无法直接更改缓存文件,应当更改:/etc/ld.so.conf配置文件,随后再同步到缓存文件):

    1. 打开/etc/ld.so.conf,将动态库的目录(注意这里时目录,不要添加库的名字)添加到最后一行,保存退出

      /home/beasts777/coding/c++/activeLib/
      
    2. ld.so.conf同步到ld.so.cache中:

      sudo ldconfig
      

      不需要进行其它操作即可生效。

  • 方案三:将动态库文件拷贝到系统库目录/lib//usr/lib/,或者在里面创建库的软连接(更推荐,因为这样如果后续库被修改了,就不用再拷贝一次了)

    # 拷贝库
    sudo cp /home/beasts777/coding/c++/activeLib/libcalc.so /usr/lib/libcal.so
    # 创建软链接(推荐)
    sudo ln -s /home/beasts777/coding/c++/activeLib/libcalc.so /usr/lib/libcal.so
    

2.4.4 验证是否能够链接到动态库文件

语法:ldd 可执行程序名

EG:

ldd app
linux-vdso.so.1 (0x00007ffe5ffbb000)
libcalc.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3a8b488000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3a8ba7b000)

如果可以链接,那么会显示地址,否则会显示not found。

2.4.5 实操

通过在系统动态库内添加软连接实现。

当前文件结构如下:

.
├── app			# 可执行文件
├── head.h		# 头文件
├── libcalc.so	# 动态库文件
  1. /usr/lib下创建动态库文件的软链接:

    sudo ln -s ~/coding/c++/cal/activeLib/libcalc.so /usr/lib/libcalc.so
    
  2. 查看可执行程序是否可以读取到动态库文件:

    ldd app
    linux-vdso.so.1 (0x00007ffe3e3ee000)
    libcalc.so => /usr/lib/libcalc.so (0x00007f29c7c68000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f29c7877000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f29c806c000)
    

    读取成功。

  3. 直接运行程序即可。

3. 优缺点

3.1 静态库

优点:

  • 静态库被直接打包到应用程序中,因此加载速度更快。
  • 发布程序时无需发布静态库。

缺点:

  • 相同的库文件可能在内存中被加载多份,浪费内存。
  • 如果库文件更新,就需要对项目进行重新编译,将新的库文件代码打包到可执行程序中。

3.2 动态库

优点:

  • 不同进程可使用同一动态库,实现不同进程间的资源共享,无需多次复制。
  • 修改动态库时,只需替换库文件,无需重新编译应用程序。
  • 因为动态库只有在使用库函数时才会被调用,因此程序员可以控制何时加载动态库。

缺点:

  • 加载速度比静态库慢,但当今计算机基本可以忽略。
  • 发布应用程序时需要发布以来的动态库。