【Linux】-库文件

  库是已经写好的,供复用的代码。大多数程序都不是从零开始,而是在各种库的基础上开发。库其实就是可执行代码的二进制形式,它可以被操作系统加载到内存中运行。库分为两种:静态库和动态库(亦称动态链接库、共享库)。

  本文从目标文件开始,逐步介绍静态库和动态库。


 

1. 目标文件

1.1 目标文件的概念

  目标文件是源文件经过编译产生的能被 CPU 直接识别的二进制代码。目标文件包含机器代码、代码运行时使用的数据(重定位信息等)、调试信息。

  目标文件常常按照特定格式来组织,在 Linux 下是 ELF(Executable Linkable Format,可执行可链接格式),在 Windows 下是 PE(Portable Executable,可以之可执行)。

1.2 目标文件的常见形式

1.2.1 可执行目标文件

  可执行目标文件,就是可以直接运行的二进制文件。

1.2.2 可重定位目标文件

  可重定位目标文件包含二进制的代码和数据,可以和其他可重定位目标文件合并,创建出一个可执行目标文件。

1.2.3 共享目标文件

  共享目标文件,是在加载或运行时进行链接的特殊可重定位目标文件。

1.3 示例

1.3.1 demo 代码

  接下来通过如下代码分析:

 1 /*
 2 * main.c
 3 */
 4 #include <stdio.h>
 5 
 6 int main( int argc, char *argv[] )
 7 {
 8     printf("%lf\n", exp(2));
 9 
10     return 0;
11 }

  以上代码中用到了 exp() 函数,所以要引入 libm.so(动态库)或 libm.a(静态库)。

1.3.2 生成可重定位目标文件

  通过以下命令编译源文件,生成可重定位目标文件 main.o:

1 $ gcc -c main.c

  接下来通过 readelf 命令查看生成的目标文件,可以看到编译生成的 .o 文件是可重定位目标文件:

 1 $ readelf -h main.o
 2 ELF Header:
 3   Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
 4   Class:                             ELF64
 5   Data:                              2's complement, little endian
 6   Version:                           1 (current)
 7   OS/ABI:                            UNIX - System V
 8   ABI Version:                       0
 9   Type:                              REL (Relocatable file)
10   Machine:                           Advanced Micro Devices X86-64
11   Version:                           0x1
12   Entry point address:               0x0
13   Start of program headers:          0 (bytes into file)
14   Start of section headers:          336 (bytes into file)
15   Flags:                             0x0
16   Size of this header:               64 (bytes)
17   Size of program headers:           0 (bytes)
18   Number of program headers:         0
19   Size of section headers:           64 (bytes)
20   Number of section headers:         13
21   Section header string table index: 10
22 $ 

1.3.3 查看动态库

  接下来看看前面提到的 libm.so,它是个共享目标文件:

 1 $ readelf -h /lib/x86_64-linux-gnu/libm.so.6 
 2 ELF Header:
 3   Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
 4   Class:                             ELF64
 5   Data:                              2's complement, little endian
 6   Version:                           1 (current)
 7   OS/ABI:                            UNIX - System V
 8   ABI Version:                       0
 9   Type:                              DYN (Shared object file)
10   Machine:                           Advanced Micro Devices X86-64
11   Version:                           0x1
12   Entry point address:               0x5610
13   Start of program headers:          64 (bytes into file)
14   Start of section headers:          1069632 (bytes into file)
15   Flags:                             0x0
16   Size of this header:               64 (bytes)
17   Size of program headers:           56 (bytes)
18   Number of program headers:         9
19   Size of section headers:           64 (bytes)
20   Number of section headers:         30
21   Section header string table index: 29
22 $ 

1.3.4 生成可执行目标文件

  通过以下命令,生成可执行目标文件:

1 $ gcc -o main main.c -lm

  注意:这里添加了 -lm 选项。-l(小写 L)用于指定要链接的库,m 表示 libm.so。如果我们使用的函数不在 libc 中,就要指定要链接的库。

  观察编译生成的 main 文件:

 1 $ readelf -h main
 2 ELF Header:
 3   Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
 4   Class:                             ELF64
 5   Data:                              2's complement, little endian
 6   Version:                           1 (current)
 7   OS/ABI:                            UNIX - System V
 8   ABI Version:                       0
 9   Type:                              EXEC (Executable file)
10   Machine:                           Advanced Micro Devices X86-64
11   Version:                           0x1
12   Entry point address:               0x400440
13   Start of program headers:          64 (bytes into file)
14   Start of section headers:          4472 (bytes into file)
15   Flags:                             0x0
16   Size of this header:               64 (bytes)
17   Size of program headers:           56 (bytes)
18   Number of program headers:         9
19   Size of section headers:           64 (bytes)
20   Number of section headers:         30
21   Section header string table index: 27
22 $ 

 

2. 静态库

2.1 静态库的工作原理

  上一节中的可重定位目标文件,可以用一种特殊方式打包成一个库,在链接阶段,将汇编生成的目标文件(.o)和引用到的库(不是把静态库整个打包进去,而是从静态库中把使用到的部分拷贝出来打包)打包到可执行文件中。这里所说的库就是静态库,这种连接方式就是静态链接。在 Windows 中,静态库文件后缀为 .lib。在 Linux 中,静态库文件后缀为 .a(archive)。

静态库的工作原理示意

2.2 静态库的利弊分析

2.2.1 静态库的劣势

  静态库的链接是在编译时期完成的,程序在运行时不再和函数库发生联系。由于每个需要使用静态库的可执行文件中都有一份静态库,所以这种方式比较浪费空间和资源。

  此外,在程序编译过程中链接静态库的做法不利于更新。一旦静态库更新,所有使用它的应用程序都要重新编译、部署。

2.2.2 静态库的优势

  静态库的优势在于,移植时只要移动这一个库文件即可。

静态库示意

2.3 创建静态库

2.3.1 Linux 静态库命名规则

  Linux 的静态库命名规范,必须是“lib[name].a”。即 lib 作为前缀,后面跟着静态库名,扩展名为 .a。

2.3.2 创建静态库的方法

  创建静态库,首先将代码文件编译成 .o,然后使用 ar 工具将目标文件 .o 打包成 .a 静态库文件。

  我们用以下代码为例说明:

 1 /*
 2 * max.c
 3 */
 4 #include <stdio.h>
 5 #include "max.h"
 6 
 7 int max(int a, int b, int c)
 8 {
 9     return a > b ? (a > c ? a : c) : (b > c ? b : c);
10 }

  执行以下命令,生成静态库文件 libmax.a:

1 $ gcc -c max.c
2 $ ar -crv libmax.a max.o

2.4 静态库使用示例

  编写如下 main.c,调用 max():

 1 /*
 2 * main.c
 3 */
 4 #include <stdio.h>
 5 
 6 int main( int argc, char *argv[] )
 7 {
 8     printf("The max value is: %d\n", max(7, 5, 9));
 9 
10     return 0;
11 }

  执行以下命令,生成 main.o:

1 $ gcc -c main.c

  链接静态库 libmax.a,注意选项 -L.,将当前目录添加到库文件搜索路径。在我们使用 -l 参数时,默认使用动态库。如果要使用静态库,则需添加 -static 选项显式声明。

1 $ gcc -static -o main main.o -L. -lmax

  注意:-lmax 参数必须放在最后。放在最后时,它的解析过程如下:

  (1)链接器从左到右扫描可重定位目标文件和静态库;

  (2)扫描 main.o 时,发现未解析的符号 max;

  (3)扫描 libmax.a,找到 max,提取相关代码;

  (4)最终没有任何未解析符号,编译链接完成。

  如果将 -lmax 参数放在前面,其解析过程如下:

  (1)链接器从左到右扫描可重定位目标文件和静态库;

  (2)扫描 libmax.a,由于前面没有发现未解析符号,因此不会提取任何代码;

  (3)扫描 main.o 时,发现未解析的符号 max;

  (4)扫描结束,还有未解析符号,报错。

  由于静态链接的方式会从静态库中将相关代码拷贝并打包到可执行文件中,可执行文件已经包含了 max 相关的二进制代码,因此这个可执行文件在没有 libmax.a 的系统中也能正常运行。

  为了后续和动态库方式对比,我们先看看使用静态链接编译出的可执行文件大小:

1 $ ls -lh main
2 -rwxr-xr-x 1 albert albert 857K  1月 20 16:23 main

 

3 动态库

3.1 动态库工作原理

  动态库和静态库最大的差异在于,并没有在链接时把需要的二进制代码都拷贝到可执行文件中,仅仅拷贝了一些重定位信息和符号表信息。在程序运行时,依靠这些信息动态载入,完成真正的链接过程。

3.2 动态库的利弊

3.2.1 动态库的优点

  鉴于动态载入的机制,应用程序中并没有打包这份库,只要内存中有一份动态库的实例,就可以实现多个进程共享一个库。这就解决了空间浪费的问题。此外,这种动态载入的方式便于更新,动态库更新时,只要替换库文件即可,应用无需重新编译、部署。

3.2.2 动态库的缺点

  对于使用动态库的应用,在移植时不仅要移动对应程序,还要注意处理相关的动态库,否则会因为找不到动态库而报错。

动态库示意

3.3 创建动态库

  接下来将 2.3 小节中的 max.c 生成动态库。

  执行以下指令,生成动态库。-fPIC 是编译选项,PIC(Position Independent Code,位置无关的代码)。-shared 则是链接选项,指定生成动态库。

1 $ gcc -fPIC -shared -o libmax.so max.c

  为了让用户知道动态库中有哪些接口可用,还要提供对应的头文件:

1 #ifndef _MAX_H_
2 #define _MAX_H_
3 
4 int max(int a, int b, int c);
5 
6 #endif

3.4 动态库使用示例

  在 main.c 中包含 max.h,通过以下命令即可编译生成可执行文件:

1 $ gcc -o main main.c -L./ -lmax

  但在运行时,会提示以下错误:

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

  这是因为,Linux 通过 /etc/ld.so.cache 文件来搜索要链接的动态库,而 /etc/ld.so.cache 又是 ldconfig 程序读取 /etc/ld.so.conf 文件生成的。本次使用的动态库并不在这个目录下,所以无法找到。

  如果只是在本地使用,可以通过以下方式指定搜索动态库的目录,从而运行可执行文件:

1 $ LD_LIBRARY_PATH=. ./main

  如果要在系统层面共享这个库,可以把动态库所在的路径添加到 /etc/ld.so.conf,然后再执行以下命令,更新 /etc/ld.so.cache:

1 $ sudo ldconfig

 

posted @ 2021-01-21 08:42  Albert-陌尘  阅读(497)  评论(0编辑  收藏  举报