Bootstrap

Linux 静态与动态编译、静态库与动态库

一、库的简介

什么是库文件呢?
所谓库文件,大家可以将其等价为压缩包文件,该文件内部通常包含不止一个目标文件(也就是二进制文件)。库文件中每个目标文件存储的代码,并非完整的程序,而是一个个实用的功能模块,以便提供给使用者一些可以直接拿来用的变量、函数或类

所以,其实库文件只是一个统称,代指的是一类压缩包,它们都包含有功能实用的目标文件。例如,C 语言库文件提供有大量的函数(如 scanf()printf()strlen() 等),C++ 库文件不仅提供有使用的函数,还有大量事先设计好的类(如 string 字符串类)。

那什么是又是库呢?为什么需要库?
库,即程序库,是一种特殊的程序,编写库的程序和编写一般的程序区别不大,只是库不能单独运行,必须作为其它执行程序的一部分来完成某些功能。程序库可分静态库(static library)和共享库(shared library)

库的好处

  1. 代码保密:将源文件打包成库分发给别人使用可以保护源码的实现不被公开,起到保密作用(详细可以看下文关于库文件与头文件的分析)。
    当然,有人可能会想到,可以使用反编译的工具反编译库文件,就可以获得源码,实际上,对于 Java 而言,反编译的还原度比较高,甚至可以达到95%以上,但对于 C/C++语言,反编译的还原度比较低,可以起到保护作用。
  2. 提高开发效率库的存在可以使得程序模块化,可以加快程序的再编译,可以实现代码重用,可以使得程序便于升级,极大的提高了程序员的开发效率,因为很多功能根本不需要从 0 开发,直接调取包含该功能的库文件即可
  3. 方便部署、分发和使用:我们将打包好的库文件方便、快速的分发给别人使用,并且库文件的调用方法也很简单,以 C 语言中的 printf() 输出函数为例,程序中只需引入 <stdio.h> 头文件,即可调用 printf() 函数。

库文件与头文件
我们在分发库文件给别人使用时,往往需要还需要提供相应的头文件。有人可能会问,调用库文件为什么还要牵扯到头文件呢?

实际上,头文件和库文件并不是一码事,它们最大的区别在于:头文件只存储变量、函数或者类等这些功能模块的声明部分,库文件才负责存储各模块具体的实现部分。大家可以这样理解:所有的库文件都提供有相应的头文件作为调用它的接口。也就是说,库文件是无法直接使用的,只能通过头文件间接调用

头文件和库文件相结合的访问机制,最大的好处在于,有时候我们只想让别人使用自己实现的功能,并不想公开实现功能的源码,就可以将其制作为库文件,这样用户获取到的是二进制文件,而头文件又只包含声明部分,这样就实现了“将源码隐藏起来”的目的,且不会影响用户使用。

二、静态链接和动态链接

事实上,库文件只是一个统称,代指的是一类压缩包,它们都包含有功能实用的目标文件。既然是目标文件,所以库文件用于程序的链接阶段,通常编译器提供有 2 种实现链接的方式,分别称为静态链接方式和动态链接方式

  • 采用静态链接方式实现链接操作的库文件,称为静态链接库、静态库
    静态库在程序的链接阶段被复制到了程序中。
  • 采用动态链接方式实现链接操作的库文件,称为动态链接库、动态库
    动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。

1、静态链接

静态链接库实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,GCC 编译器就会将该模板代码直接复制到程序文件的适当位置,最终生成可执行文件。

  • 优点:

    • 对运行环境的依赖性较小,即生成的可执行文件不再需要任何静态库文件的支持就可以独立运行(可移植性强)。
    • 静态库被打包到应用程序中,所以运行时加载速度快。
  • 缺点:

    • 运行前就加载程序文件中,所以生成的程序比较大,需要更多的系统资源,在装入内存时会消耗更多的时间。
    • 如果程序文件中多次调用库中的同一功能模块,则该模块代码势必就会被复制多次,生成的可执行文件中会包含多段完全相同的代码,造成代码的冗余。
    • 更新、部署、发布麻烦,即如果库有了更新,必须重新编译整个源程序文件。比如,静态库更新了,分发给别人后,需要把更新的库和头文件重新编译,使用者的应用程序需要重新编译部署。

2、动态链接

动态链接库,又称为共享链接库。和静态链接库不同,采用动态链接库实现链接操作时,程序文件中哪里需要库文件的功能模块,GCC 编译器不会直接将该功能模块的代码拷贝到文件中,而是将功能模块的位置信息记录到文件中,直接生成可执行文件。

显然,这样生成的可执行文件是无法独立运行的。采用动态链接库生成的可执行文件运行时,GCC 编译器会将对应的动态链接库一同加载在内存中,由于可执行文件中事先记录了所需功能模块的位置信息,所以在现有动态链接库的支持下,也可以成功运行。

所以:动态链接时,链接器在链接时仅仅建立与所需库函数的之间的链接关系,在程序运行时才将所需资源调入可执行程序

  • 优点:

    • 由于可执行文件中只记录的是功能模块的地址,真正的实现代码并没有放入程序文件中,所以相对于静态编译,有着较小的程序体积
    • 实现进程之间的资源共享(避免重复拷贝):由于可执行文件中记录的是功能模块的地址,真正的实现代码会在程序运行时被载入内存,这意味着,即便功能模块被调用多次,使用的都是同一份实现代码(这也是将动态链接库称为共享链接库的原因)。
    • 更新、部署、发布简单,简化了程序的升级:比如,动态库更新了,分发给别人后,只需要把更新的库重新编译即可,使用者的应用程序不需要重新编译部署。
  • 缺点:

    • 动态库没有被打包到应用程序中,所以运行是加载速度慢。
    • 依赖动态库,不能独立运行:发布程序时需要提供依赖的动态库。
    • 动态库依赖版本问题严重

3、GCC 下动态库与静态库

GCC 编译器生成可执行文件时,默认情况下会优先使用动态链接库实现链接操作,除非当前系统环境中没有程序文件所需要的动态链接库,GCC 编译器才会选择相应的静态链接库。如果两种都没有(或者 GCC 编译器未找到),则链接失败。(如果同一目录下同时存在同名的动态库和静态库,比如 libmax.so 和 libmax.a 都在当前路径下, 则 gcc 会优先链接动态库。)

什么时候使用静态库,什么时候使用动态库?
建议如果库比较少,编译建议使用静态库,如果库比较大,编译建议使用动态库。

三、静态库制作和使用

(1)静态库命名规则

  • 在 Linux 发行版系统中,静态链接库文件的后缀名通常用 .a 表示,libxxx.a
    • lib : 前缀(固定)
    • xxx : 库的名字,自己起
    • .a : 后缀(固定)
  • 在 Windows 系统中,静态链接库文件的后缀名为 .lib,libxxx.lib

(2) 静态库制作
步骤1:将 c 源文件生成对应的 .o 文件,只进行汇编不进行链接。
【注意】头文件和 main.c 不需要要汇编

yxm@192:~/calc$ ls
add.c  div.c  head.h  main.c  mult.c  sub.c
yxm@192:~/calc$ gcc -c add.c  -o add.o
yxm@192:~/calc$ gcc -c sub.c -o sub.o
yxm@192:~/calc$ gcc -c mult.c -o mult.o
yxm@192:~/calc$ gcc -c div.c -o div.o
yxm@192:~/calc$ ls
add.c  add.o  div.c  div.o  head.h  main.c  mult.c  mult.o  sub.c  sub.o

步骤2:使用打包工具 ar 将准备好的 .o 文件打包为 .a 文件 libxxx.a。

ar rcs libxxx.a xxx.o xxx.o

  • r – 将文件插入备存文件中
  • c – 建立备存文件
  • s – 索引
yxm@192:~/calc$ ar -rcs libcalc.a add.o mult.o sub.c div.o
yxm@192:~/calc$ ls
add.c  add.o  div.c  div.o  head.h  libcalc.a  main.c  mult.c  mult.o  sub.c  sub.o
yxm@192:~/calc$ rm add.o div.o mult.o sub.o add.c div.c mult.c sub.c 
yxm@192:~/calc$ ls
app  head.h  libcalc.a  main.c

(3)静态库使用
静态库制作完成之后,需要将 .a 文件和头文件一起发布给用户(具体原因请参看本文前面部分),用户再引用静态库编译成可执行文件 。

假设用户的测试文件为 main.c,静态库文件为 libcalc.a,编译命令如下:

yxm@192:~/calc$ ls
head.h  libcalc.a  main.c
yxm@192:~/calc$ gcc main.c  -I ./ -L ./ -lcalc -o app
yxm@192:~/calc$ ls
app  head.h  libcalc.a  main.c
yxm@192:~/calc$ ./app 
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667

参数说明(详细参考Linux 下 GCC 编译常用总结):

  • -L:表示要链接的库所在目录,即 libxxx.a 所在的目录。
  • -I(大写 i): 表示库文件对应的头文件所在的目录。
  • -l(小写L):指定链接时需要的库名,即 libxxx.a 去掉前缀和后缀之后的部分。
    【注意】-l 与后面的库名可以有空格,也可以没有空格,两个方式都对。

四、动态库制作和使用

(1)动态库命名规则

  • 在 Linux 发行版系统中,动态链接库的后缀名通常用 .so 表示,libxxx.so

    • lib : 前缀(固定)
    • xxx : 库的名字,自己起
    • .a : 后缀(固定)
  • 在 Windows 系统中,动态链接库文件的后缀名为 .dll,libxxx.dll

(2)动态库制作
步骤一:gcc 生成 .o 目标文件,此时要加编译选项:-fPIC(fpic也可)。

gcc -c –fpic/-fPIC a.c b.c
参数:

  • fPIC 创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享。
yxm@192:~/calc$ ls
add.c  div.c  head.h  main.c  mult.c  sub.c
yxm@192:~/calc$ gcc -c add.c  -o add.o -fPIC
yxm@192:~/calc$ gcc -c sub.c -o sub.o -fPIC
yxm@192:~/calc$ gcc -c mult.c -o mult.o -fPIC
yxm@192:~/calc$ gcc -c div.c -o div.o -fPIC
yxm@192:~/calc$ ls
add.c  add.o  div.c  div.o  head.h  main.c  mult.c  mult.o  sub.c  sub.o

步骤二:生成共享库,此时要加链接器选项: -shared(指定生成动态链接库)

gcc -shared a.o b.o -o libcalc.so

yxm@192:~/calc$ gcc -shared add.o div.o mult.o sub.o -o libcalc.so
yxm@192:~/calc$ ls
add.c  add.o  div.c  div.o  head.h  libcalc.so  main.c  mult.c  mult.o  sub.c  sub.o

(3)动态库使用测试

动态库制作完成之后,需要将 .so 文件和头文件一起发布给用户(具体原因请参看本文前面部分),用户再引用动态库编译成可执行文件(编译方法跟静态库方式一样) 。

假设用户拿到 .so 文件和头文件后,测试文件目录如下:

yxm@192:~/library$ tree
.
├── include
│   └── head.h
├── lib
│   └── libcalc.so
├── main.c
└── src
    ├── add.c
    ├── div.c
    ├── mult.c
    └── sub.c

3 directories, 7 files

编译命令(编译方法跟静态库方式一样)如下:

yxm@192:~/library$ gcc main.c  -I include/ -L lib/ -lcalc -o main
yxm@192:~/library$ ls
include  lib  main  main.c  src
yxm@192:~/library$ ./main
./main: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory

然后运行:./main,发现竟然报错了!!!

  • 当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道动态库的绝对路径。此时就需要系统动态载入器(dynamic linker/loader),即 ldd。通过 ldd 可以查看可执行文件的依赖的动态库。
    yxm@192:~/library$ ldd main
    linux-vdso.so.1 (0x00007ffdb9fc1000)
    libcalc.so => not found		# 没有找到可执行文件的依赖的动态库
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4c15eb4000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f4c162a5000)
    
  • 既然需要动态库绝对路径,那如何定位共享库文件呢?
    对于 elf 格式的可执行程序,是由 ld-linux.so 来完成定位的,它先后搜索elf文件的 DT_RPATH段 ——> 环境变量LD_LIBRARY_PATH ——> /etc/ld.so.cache文件列表 ——> /lib/,/usr/lib目录找到库文件后将其载入内存。详细方式参考下文:如何让系统找到动态库。

(4)如何让系统找到动态库

DT_RPATH段 由操作系统定义,不能改变,所以只能同通过修改环境变量 LD_LIBRARY_PATH、/etc/ld.so.cache文件列表和、/lib/和/usr/lib目录的方法来让系统找到动态库。

方法一:临时设置,修改环境变量 LD_LIBRARY_PATH 的值:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径,$LD_LIBRARY_PATH 表示旧的 LD_LIBRARY_PATH 值,: 表示追加新值。

yxm@192:~/library$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/yxm/library/lib
yxm@192:~/library$ ldd main
        linux-vdso.so.1 (0x00007fffad786000)
        libcalc.so => /home/yxm/library/lib/libcalc.so (0x00007efdad23e000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007efdace4d000)
        /lib64/ld-linux-x86-64.so.2 (0x00007efdad440000)
yxm@192:~/library$ ./main
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667

【注意】这种方法运行成功后,如果关闭终端,再重新打开一个新的终端,会发现系统依旧无法找到动态库,因为是在终端中配置 LD_LIBRARY_PATH 值,终端一旦关闭,配置也将清除,所以无法找到动态库。

方法二:永久设置,把 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径,设置到 ~/.bashrc 或者 /etc/profile 文件中

# 用户级设置环境变量
yxm@192:~/library$ vim ~/.bashrc

# 在 .bashrc 文件的最后一行添加如下内容:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/yxm/library/lib

yxm@192:~/library$ ./bashrc		# 设置环境变量生效,相当于 source /bashrc
yxm@192:~/library$ ./main
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
# 系统级设置环境变量
yxm@192:~/library$ vim /etc/profile

# 在 profile 文件的最后一行添加如下内容:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/yxm/library/lib

yxm@192:~/library$ source /etc/profile   # 设置环境变量生效
yxm@192:~/library$ ./main
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667

方法三:修改 /etc/ld.so.cache 文件列表,但是 ld.so.cache 是一个二进制文件,所以不能直接修改,但是可以间接修改,即将动态库文件所在目录的路径添加到 /etc/ld.so.conf 文件中。

yxm@192:~/library$ sudo vim /etc/ld.so.conf

# 在 ld.so.conf 文件的最后一行添加如下内容:
/home/yxm/library/lib

yxm@192:~/library$ sudo ldconfigs  # 设置生效,该命令会重建/etc/ld.so.cache文件
yxm@192:~/library$ ./main
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667

方法四:拷贝自己制作的共享库到 /lib 或者 /usr/lib 下(不是/lib64目录),不过这种方法不推荐使用,因为这两个目录本身就自带了一些系统的库文件,如果把自定义的动态库文件放到这两个文件夹下,容易导致命名冲突,可能会替换掉系统自带的文件,导致系统的程序的运行可能出现问题。

总结

学了本文,你可以充分了解静态库与动态库的区别,学会制作并使用静态库与动态库。其实静态库与动态库的制作与使用是 GCC 使用的进一步扩展(如果需要了解 GCC 基础使用请移步 Linux 下 GCC 编译常用总结),由于比较重要所以单独用一篇文章展现,与大家一起共勉,不足之处请指正。

;