参考文章

一、什么是库

​ 库是写好的,现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。

​ 本质上来说,库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库动态库。linux下的静态库为.a结尾的文件,动态库以.so结尾的文件;windows下的静态库为以.lib为结尾的文件,动态库为以.dll为结尾的文件。

​ 所谓静态、动态是指链接。静态库、动态库区别来自链接阶段如何处理库,链接成可执行程序。

# 二、静态库

1.命名规则

在Linux中静态库由程序ar生成,现在静态库已经不像之前那么普遍了,这主要是由于程序都在使用动态库。关于静态库的命名规则如下:

  • 在Linux中静态库以lib作为前缀, 以.a作为后缀中间是库的名字自己指定即可, 即: libxxx.a

  • 在Windows中静态库一般以lib作为前缀, 以lib作为后缀, 中间是库的名字需要自己指定, 即: libxxx.lib

2.Linux下生成静态库的步骤

生成静态库,需要先对源文件进行汇编操作 (使用参数 -c) 得到二进制格式的目标文件 (.o 格式), 然后在通过 ar工具将目标文件打包就可以得到静态库文件了 (libxxx.a)。

生成静态链接库的具体步骤:
  1. 将源文件进行汇编, 得到 .o 文件

    # 执行如下操作, 默认生成二进制的.o文件
    # 需要使用参数 -c,该参数位置没有要求
    $ gcc -c 源文件(*.c) 
    
  2. 将得到的 .o 进行打包, 得到静态库

    $ar -rcs 静态库的名字(libxxx.a) 目标文件(*.o)
    

    使用ar工具创建静态库的时候需要三个参数:

    • 参数c:创建一个库,不管库是否存在,都将创建。
    • 参数s:创建目标文件索引,这在创建较大的库时能节约时间。
    • 参数r:在库中插入模块(替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。
  3. 发布静态库

    1. 提供头文件 **.h
    2. 提供制作出来的静态库 libxxx.a
    

3.静态库的使用

当我们得到了一个可用的静态库之后, 需要将其放到一个目录中(头文件和静态库文件放在同一个文件夹下); 然后根据得到的头文件编写测试代码, 对静态库中的函数进行调用;最后编译测试程序, 得到可执行文件。

在编译的时将静态库的路径和名字都指定出来

# -L: 指定库所在的目录(相对或者绝对路径),与参数之间可以不加空格
# -l: 指定库的名字, 需要掐头(lib)去尾(.a) 剩下的才是需要的静态库的名字,与参数之间可以不加空格
gcc main.c -o app -L库所在目录 -l静态库名

4.静态库制作举例

1.源码

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

# 目录结构 add.c div.c mult.c sub.c -> 算法的源文件, 函数声明在头文件 head.h
.
├── add.c
├── div.c
├── include
│   └── head.h
├── mult.c
└── sub.c

算法文件add.c div.c mult.c sub.c

add.c

#include <stdio.h>
#include "head.h"

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

div.c

#include <stdio.h>
#include "head.h"

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

mult.c

#include <stdio.h>
#include "head.h"

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

sub.c

#include <stdio.h>
#include "head.h"

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

头文件head.h

#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

2.静态库的制作

# 将源文件进行汇编, 得到 .o 文件
$ gcc -c add.c div.c mult.c sub.c -I ./include/
# 将得到的 .o 进行打包, 得到静态库
$ ar rcs libcalc.a *.o
# 发布静态库
# 1. 提供头文件 **.h
# 2. 提供制作出来的静态库 libxxx.a

3.静态库的使用

测试文件main.c,main.c中是对接口的测试程序, 制作库的时候不需要将 main.c 算进去,需要提高头文件和静态库文件

目录结构

.
├── main.c
├── libcalc.a
├── include
   └── head.h

main.c

#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;
}

使用静态库,编译测试文件

$ gcc main.c -o app -L ./ -l calc -I./include/
$ ./app

三、动态库

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

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

1.命名规则

关于动态库的命名规则如下:

  • 在Linux中动态库以lib作为前缀, 以.so作为后缀, 中间是库的名字自己指定即可, 即: libxxx.so
  • 在Windows中动态库一般以lib作为前缀, 以dll作为后缀, 中间是库的名字需要自己指定, 即: libxxx.dll

2.Linux下生成动态库的步骤

生成动态链接库的具体步骤:

1.将源文件进行汇编操作

# 需要使用参数 -c, 还需要添加额外参数 -fpic / -fPIC
# -fPIC 或 -fpic 参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置
# 得到若干个 .o文件
$ gcc -c 源文件(*.c) -fpic

2.将得到的.o文件打包成动态库

# 使用gcc, 使用参数 -shared 指定生成动态库(位置没有要求)
# -shared参数的作用是告诉gcc编译器生成一个动态链接库
$ gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libxxx.so)

3.发布动态库和头文件

# 发布动态库
 	1. 提供头文件: xxx.h
 	2. 提供动态库: libxxx.so

3.动态库的使用

$ gcc main.c -o app -L ./ -l calc -I./include/
$ ./app
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory 
# 出现报错参考3.解决动态库无法加载的问题

4.动态库的制作举例

目录结构

.
├── add.c
├── div.c
├── include
│   └── head.h
├── mult.c
└── sub.c

1.动态库的制作

#1.将源文件进行汇编操作
$ gcc *.c -c -fpic -I ./include/

#2.将得到的.o文件打包成动态库
$ gcc -shared *.o -o libcalc.so 

#3. 发布库文件和头文件
	1. head.h
	2. libcalc.so

2.动态库的使用

​ 当我们得到了一个可用的动态库之后, 需要将其放到一个目录中, 然后根据得到的头文件编写测试代码, 对动态库中的函数进行调用。

目录结构

.
├── head.h          ==> 函数声明
├── libcalc.so      ==> 函数定义
└── main.c          ==> 函数测试

编译测试程序

# 在编译的时候指定动态库相关的信息: 库的路径 -L, 库的名字 -l
$ gcc main.c -o app -L./ -lcalc

# 执行生成的可执行程序, 错误提示 ==> 可执行程序执行的时候找不到动态库
$ ./app 
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory

gcc通过指定的动态库信息生成了可执行程序, 但是可执行程序运行却提示无法加载到动态库。

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

1.库的工作原理
链接的三种方式:

1.静态链接:在程序运行之前先将各目标模块及它们所需的库函数连接成一个完整的可执行文件(装入模块),之后不再拆开。

2.装入时动态链接:将各目标模块装入内存时,边装入边链接的链接方式。

3.运行时动态链接:在程序执行中需要该目标模块时,才对它进行链接。其优点是便于修改和更新,便于实现对目标模块的共享。

静态库不需要加载,在测试程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中。当可执行程序被执行,静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题。由于静态库是将代码直接拷贝到C程序的代码区中,因此内存中的代码和数据可能会存在多份,造成内存空间浪费。

动态库fPIC形成位置无关码,采用相对编址的方式,在程序链接时将对应库中的偏移量添加到程序中,库函数在程序运行时加载进来,经过页表,把库映射到虚拟地址空间后(共享区),库就具有了起始地址。通过起始地址和偏移地址,就可以找到要调用的库函数。

  • 在程序编译的最后一个阶段也就是链接阶段:

    • 在gcc命令中虽然指定了库路径(使用参数 -L ), 但是这个路径并没有记录到可执行程序中,只是检查了这个路径下的库文件是否存在。
    • 同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字。
  • 可执行程序被执行起来之后:

    • 程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息

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

    • 当动态库中的函数在程序中被调用了, 这个时候动态库才加载到内存,如果不被调用就不加载

    • 动态库的检测和内存加载操作都是由动态连接器来完成的

​ 动态库相对于静态库更节省内存,静态库由多个程序使用相同的库函数,加载到内存中就会导致内存中有多份重复的库函数代码,而动态库则是多个程序共用一份动态库,不会导致出现重复的库函数代码,就节省了内存空间。

2.动态链接器

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

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

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

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

3.解决方案

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

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

    1. 找到相关的配置文件

      • 用户级别: ~/.bashrc —> 设置对当前用户有效
      • 系统级别: /etc/profile —> 设置对所有用户有效
    2. 使用 vim 打开配置文件, 在文件最后添加这样一句话

      # 自己把路径写进去就行了
      export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:动态库的绝对路径
      
    3. 让修改的配置文件生效

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

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

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

        # 修改的是哪一个就执行对应的那个命令
        # source 可以简写为一个 . , 作用是让文件内容被重新加载
        $ source ~/.bashrc          (. ~/.bashrc)
        $ source /etc/profile       (. /etc/profile)
        
  • 方案2: 更新 /etc/ld.so.cache 文件

    1. 找到动态库所在的绝对路径(不包括库的名字)比如:/home/robin/Library/

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

      # 1. 打开文件
      $ sudo vim /etc/ld.so.conf
      
      # 2. 添加动态库路径, 并保存退出
      
    3. 更新 /etc/ld.so.conf中的数据到 /etc/ld.so.cache

      # 必须使用管理员权限执行这个命令
      $ sudo ldconfig   
      
  • 方案3: 拷贝动态库文件到系统库目录 /lib/ 或者 /usr/lib 中 (或者将库的软链接文件放进去)

    # 库拷贝
    $ sudo cp /xxx/xxx/libxxx.so /usr/lib
    
    # 创建软连接
    $ sudo ln -s /xxx/xxx/libxxx.so /usr/lib/libxxx.so
    

四、静态库与动态库的优缺点

1.静态库

优点:

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

缺点:

  • 相同的库文件数据可能在内存中被加载多份, 消耗系统资源,浪费内存

  • 库文件更新需要重新编译项目文件, 生成新的可执行程序, 浪费时间

2.动态库

优点:

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

缺点:

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

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐