Linux静态库和动态库

Linux静态库和动态库

转载自subingwen老师

不管是Linux还是Windows中的库文件其本质和工作模式都是相同的, 只不过在不同的平台上库对应的文件格式和文件后缀不同。程序中调用的库有两种静态库动态库,不管是哪种库文件本质是还是源文件,只不过是二进制格式只有计算机能够识别。

在项目中使用库一般有两个目的,一个是为了使程序更加简洁不需要在项目中维护太多的源文件,另一方面是为了源代码保密,毕竟不是所有人都想把自己编写的程序开源出来。

那么怎么使用呢,也很简单,拿到库,以及对应的头文件,在自己的代码中调用头文件的API即可。

1. 静态库

Linux中静态库由程序ar生成,现在基本使用动态库。静态库一般命名为libxxx.a

生成静态库

生成静态链接库,需要先对源文件进行汇编操作,gcc xxx.c -c得到一系列.o文件,再把这些.o文件进行打包,得到静态库文件

一般使用ar工具用到三个参数

  • c:创建一个库,不管库是否存在,都将创建。

  • s:创建目标文件索引,这在创建较大的库时能加快时间。

  • r:在库中插入模块(替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。

在某个目录中有如下的源文件, 用来实现一个简单的计算器

1
2
3
4
5
6
7
8
9
10
11
# 目录结构 add.c div.c mult.c sub.c -> 算法的源文件, 函数声明在头文件 head.h
# main.c中是对接口的测试程序, 制作库的时候不需要将 main.c 算进去
.
├── add.c
├── div.c
├── include
│ └── head.h
├── main.c
├── mult.c
└── sub.c

add.c

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

int add(int a, int b)
{
return a+b;
}

sub.c

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

int subtract(int a, int b)
{
return a-b;
}

mult.c

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

int multiply(int a, int b)
{
return a*b;
}

div.c

1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"

double divide(int a, int b)
{
return (double)a/b;
}

head.h

1
2
3
4
5
6
7
8
9
10
11
#ifndef _HEAD_H
#define _HEAD_H
// 加法
int add(int a, int b);
// 减法
int subtract(int a, int b);
// 乘法
int multiply(int a, int b);
// 除法
double divide(int a, int b);
#endif

测试程序main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include "head.h"

int main()
{
int a = 20;
int b = 12;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", subtract(a, b));
printf("a * b = %d\n", multiply(a, b));
printf("a / b = %f\n", divide(a, b));
return 0;
}

第一步: 将源文件add.c, div.c, mult.c, sub.c 进行汇编, 得到二进制目标文件 add.o, div.o, mult.o, sub.o

1
2
# 注意必须要指定头文件路径,否则会找不到报错
gcc add.c div.c mult.c sub.c -c -I ./include

第二步: 将生成的目标文件通过 ar工具打包生成静态库

1
ar rcs libcalc.a a.o b.o c.o    # a.o b.o c.o在同一个目录中可以写成 *.o

第三步:拿到静态库和相关的头文件以后,就可以进行使用了

1
2
# -L指定库路径 -l指定库名字 -I指定头文件路径
gcc main.c -o main -L. -lcalc -I ./include/

2. 动态库

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

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

关于动态库的命名规则如下:在Linux中动态库以lib作为前缀, 以.so作为后缀, 中间是库的名字自己指定即可, 即: libxxx.so

2.1 生成动态库

生成动态库是使用gcc命令加上-fPIC(-fpic)以及-shared参数

  • -fPIC 或 -fpic的意义是让gcc生成的代码是与位置无关的,也就是使用相对位置

  • -shared的意义是告诉编译器生成一个动态库

第一步:将源文件进行汇编,得到.o文件

1
2
# 得到若干个 .o文件
$ gcc 源文件(*.c) -c -fpic

第二步:将得到的.o文件打包成动态库, 还是使用gcc, 使用参数 -shared 指定生成动态库(位置没有要求)

1
gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libxxx.so)

第三步:拿到静态库和相关的头文件以后,就可以进行使用了

1
gcc main.c -o main -L. -lcalc -I ./include/

注意这里如果同时存在libcalc.a和libcalc.so,那么编译器会优先使用动态库!

2.2 动态库加载问题⭐

2.2.1 库的工作原理

上面第三步的命令运行成功以后,会在本地生成可执行文件main,运行会报错

在程序编译的最后一个阶段,也就是链接成可执行文件。我们使用gcc -L指定库路径

  • 如果是使用静态库,那么静态库会被打包到可执行文件中,当可执行文件被执行,静态库中的代码也会被加载到内存中,因此不会出现静态库找不到而无法被加载的问题。

  • 如果是使用动态库呢?我们只会检查这个-L指定的路径下库文件是否存在,同样对应的动态库文件也没有被打包到可执行文件中,只是在可执行程序中记录了库的那个名字。

    • 当可执行文件被执行的时候,会先去检测这个动态库是否可以被加载,加载不到就会提示上面的报错。
    • 当动态库中的函数被调用了,这个时候动态库才会被加载到内存,如果不调用则不加载
    • 动态库的检测和内存加载操作是由动态链接器完成的
2.2.2 动态链接器

动态链接器是一个独立于应用程序的进程, 属于操作系统, 当用户的程序需要加载动态库的时候动态链接器就开始工作了,很显然动态链接器根本就不知道用户通过 gcc 编译程序的时候通过参数 -L指定的路径。

那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序分别是:

  • 可执行文件内部的 DT_RPATH 段

  • 系统的环境变量 LD_LIBRARY_PATH

  • 系统动态库的缓存文件 /etc/ld.so.cache

  • 存储动态库/静态库的系统目录 /lib//usr/lib

按照以上四个顺序, 依次搜索, 找到之后结束遍历, 最终还是没找到, 动态连接器就会提示动态库找不到的错误信息。

2.2.3 解决方案

可执行程序生成之后, 根据动态链接器的搜索路径, 我们可以提供三种解决方案,我们只需要将动态库的路径放到对应的环境变量或者系统配置文件中,同样也可以将动态库拷贝到系统库目录(或者是将动态库的软链接文件放到这些系统库目录中)。

方案1:将库路径添加到环境变量LD_LIBRARY_PATH

  • 找到相关配置文件

    • 用户级别:~/.bashrc
    • 系统级别:/etc/profile
  • 在文件最后一行添加

    1
    export LD_LIBRARY_PATH =$LD_LIBRARY_PATH :动态库的绝对路径
  • 让修改的配置文件生效

    • 修改了用户级别的配置文件,关闭当前终端,打开一个新的终端配置就生效了

    • 修改了系统级别的配置文件,注销或关闭系统,再开机配置就生效了

    • 不想执行上面的操作,可以执行一个命令让配置重新被加载

      1
      2
      3
      4
      # 修改的是哪一个就执行对应的那个命令
      # source 可以简写为一个 . , 作用是让文件内容被重新加载
      source ~/.bashrc (. ~/.bashrc)
      source /etc/profile (. /etc/profile)

方案2:更新/etc/ld.so.cache文件

  • 找到动态库所在的绝对路径(不包括库的名字)比如:/home/jia/code/

  • 修改/etc/ld.so.conf这个文件,将上边的路径添加到文件中(独自占一行)

  • 更新/etc/ld.so.conf中的数据到/etc/ld.so.cache中

    1
    sudo ldconfig

方案3:拷贝动态库文件到系统目录/lib或者/usr/lib中,或者软链接

1
2
3
4
5
# 库拷贝
sudo cp /xxx/xxx/libxxx.so /usr/lib

# 创建软连接
sudo ln -s /xxx/xxx/libxxx.so /usr/lib/libxxx.so
2.2.4 验证

在启动可执行程序之前, 或者在设置了动态库路径之后, 我们可以通过一个命令检测程序能不能够通过动态链接器加载到对应的动态库, 这个命令叫做 ldd

1
2
3
4
5
6
7
8
9
# 语法:
$ ldd 可执行程序名

# 举例:
$ ldd app
linux-vdso.so.1 => (0x00007ffe8fbd6000)
libcalc.so => /home/robin/Linux/3Day/calc/test/libcalc.so (0x00007f5d85dd4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5d85a0a000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5d85fd6000) ==> 动态链接器, 操作系统提供

3. 优缺点

3.1 静态库

优点

  • 静态库被打包到应用程序中,加载速度快
  • 发布程序无需提供静态库,移植方便

缺点

  • 相同的库文件数据可能在内存中被加载多份,消耗系统资源,浪费内存
  • 库文件更新需要重新编译项目文件,生成新的可执行程序,浪费时间

3.2 动态库

优点:

  • 可实现不同进程间的资源共享
  • 动态库升级简单, 只需要替换库文件, 无需重新编译应用程序
  • 可以控制何时加载动态库, 不调用库函数动态库不会被加载

缺点:

  • 加载速度比静态库慢, 以现在计算机的性能可以忽略
  • 发布程序需要提供依赖的动态库