1. 动静态库的基本原理
在正式接触动静态库之前,需要理解从源文件形成可执行文件的过程。
- 预处理:完成头文件展开、去注释、宏替换、条件编译等,最终形成 xxx.i 文件。
本文讲解 Linux 下动静态库的基本原理、打包与使用方法。涵盖源文件到可执行文件的构建流程,静态库(.a)通过 ar 打包,动态库(.so)通过 gcc -shared 生成。介绍了使用 -I、-L、-l 选项链接库的方法,以及动态库运行时加载机制、编址方式和重定位过程。对比了动静态库在空间占用和依赖关系上的特征差异。

在正式接触动静态库之前,需要理解从源文件形成可执行文件的过程。

例如用 test1.c、test2.c、test3.c、test4.c 以及 main1.c 形成可执行文件,需要先得到各个文件的目标文件 test1.o、test2.o、test3.o、test4.o 以及 main1.o,然后再将这些目标文件链接起来,最终形成一个可执行程序。

如果我们在另一个项目当中也需要用到 test1.c、test2.c、test3.c、test4.c 和项目的 main2.c 或者 main3.c 分别形成可执行程序,那么可执行程序生成的步骤也是一样的。
对于可能频繁用到的源文件,例如这里的 test1.c、test2.c、test3.c、test4.c,我们可以将它们的目标文件 test1.o、test2.o、test3.o、test4.o 进行打包,之后需要用到这四个目标文件时就可以直接链接这个包当中的目标文件了,而这个包实际上就可以称之为一个库。
实际上,所有库本质都是一堆目标文件 (xxx.o) 的集合,库的文件当中并不包含主函数而只是包含了大量的方法以供调用,所以说动静态库本质是可执行程序的'半成品'。
在 Linux 下创建文件编写以下代码,并生成可执行程序。
#include <stdio.h>
#include <string.h>
int MyAdd(int x, int y) {
return x + y;
}
int MySub(int x, int y) {
return x - y;
}
int main() {
int a = 10;
int b = 20;
printf("%d + %d = %d\n", a, b, MyAdd(a, b));
return 0;
}
这是一个简单的示例代码,运行结果为 10 + 20 = 30。

在这份代码当中我们可以通过调用 printf 函数输出结果,原因是 gcc 编译器在生成可执行程序时,自动链接了 C 标准库。在 Linux 下,我们可以通过 ldd 文件名 来查看一个可执行程序所依赖的库文件。

其中的 libc.so.6 就是该可执行程序所依赖的库文件。
这里可执行程序所依赖的 libc.so.6 实际上就是 C 动态库,当我们去掉一个动静态库的前缀 lib,再去掉后缀.so 或者.a 及其后面的版本号,剩下的就是这个库的名字。
为便于理解,以下演示均基于三个文件:mymath.c、main.c 和 mymath.h。
#pragma once
int MyAdd(int, int);
int MySub(int, int);
int MyAdd(int x, int y) {
return x + y;
}
int MySub(int x, int y) {
return x - y;
}
#include <stdio.h>
#include <mymath.h>
int main() {
int a = 10;
int b = 20;
printf("%d + %d = %d\n", a, b, MyAdd(a, b));
return 0;
}
下面我们就利用这四个文件打包生成一个静态库。

第一步:让所有源文件生成对应的目标文件。

第二步:使用 ar 命令将所有目标文件打包为静态库。
ar 命令是 gnu 的归档工具,常用于将目标文件打包为静态库,接下来使用 ar 命令的 -r 选项和 -c 选项进行打包。

此外,可以用 ar 命令的 -t 选项和 -v 选项查看静态库当中的文件。

第三步:将头文件和生成的静态库组织起来。
分发库时,通常包含存放头文件的文件夹和存放库文件的文件夹。因此,在这里可以将 mymath.h 这个头文件放到一个名为 include 的目录下,将生成的静态库文件 libmy.a 放到一个名为 lib 的目录下,然后将这两个目录都放 mylib 下,此时就可以将 mylib 给别人使用了。

使用 gcc 编译 main.c 生成可执行程序时需携带三个选项:
gcc main.c -I ./mylib/include/ -L ./mylib/lib/ -lmy



因为编译器无法自动找到头文件 mymath.h,因此需要指定头文件搜索路径。因为头文件 mymath.h 只有 MyAdd 函数声明,没有该函数的定义,所以还需要指定所要链接库文件的搜索路径。实际中,在库文件的 lib 目录下可能会有大量的库文件,因此我们需要指明需要链接库文件路径下的哪一个库。库文件名去掉前缀 lib,再去掉后缀.so 或者.a 及其后面的版本号,剩下的就是这个库的名字。-I、-L、-l 这三个选项后面可以加空格,也可以不加空格。
若编译器无法找到头文件和库文件,可将其拷贝至系统路径下。
sudo cp mylib/include/*.h /usr/include
sudo cp mylib/lib/*.a /lib64
已将库安装到系统路径,为何编译仍失败?gcc/g++默认识别系统库,对于第三方库 libmy.a 仍需手动指定。

因此当使用 gcc 编译 main.c 生成可执行程序时,还是需要指明链接库文件路径下的哪一个库。

注:将文件拷贝到系统路径即安装库的过程,但不建议修改系统路径以免污染系统文件。
#pragma once
int MyAdd(int, int);
int MySub(int, int);
int MyAdd(int x, int y) {
return x + y;
}
int MySub(int x, int y) {
return x - y;
}
#include <stdio.h>
#include <mymath.h>
int main() {
int a = 10;
int b = 20;
printf("%d + %d = %d\n", a, b, MyAdd(a, b));
return 0;
}
动态库的打包相对于静态库来说有一点点差别,但大致相同,我们还是利用这三个文件进行打包演示。
第一步:让除了 main.c 以外的源文件生成对应的目标文件。

此时用源文件生成目标文件时需要携带 -fPIC 选项:
简要说明-fPIC 作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确地执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。如果不加-fPIC 选项,则加载.so 文件的代码段时,代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个.so 文件代码的进程在内核里都会生成这个.so 文件代码段的拷贝,并且每个拷贝都不一样,取决于这个.so 文件代码段和数据段内存映射的位置。不加-fPIC 编译出来的.so 是要在加载时根据加载到的位置再次重定位的,因为它里面的代码不是位置无关代码。如果该.so 文件被多个应用程序共同使用,那么它们必须每个程序维护一份.so 的代码副本 (因为.so 被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然也就不能共享)。我们总是用-fPIC 来生成.so,但从来不用-fPIC 来生成.a。但是.so 一样可以不用-fPIC 选项进行编译,只是这样的.so 必须要在加载到用户程序的地址空间时重定向所有表目。
第二步:使用-shared 选项将所有目标文件打包为动态库。
与生成静态库不同的是,生成动态库时我们不必使用 ar 命令,我们只需使用 gcc 的-shared 选项即可。
gcc -shared mymath.o -o libmyc.so.

第三步:将头文件和生成的动态库组织起来。

我们还是用刚才使用过的 main.c 来演示动态库的使用。

简单说明一下,使用该动态库的方法与刚才我们使用静态库的方法一样,既可以使用-I、-L、-l 这三个选项来生成可执行程序,也可以先将头文件和库文件拷贝到系统目录下,然后仅使用-l 选项指明需要链接的库名字来生成可执行程序,下面博主以第一种方法为例进行演示。此时使用 gcc 编译 main.c 生成可执行程序时,需要用-I 选项指定头文件搜索路径,用-L 选项指定库文件搜索路径,最后用-l 选项指明需要链接库文件路径下的哪一个库。
gcc main.c -I mylib/include/ -L mylib/lib/ -lmyc
与静态库的使用不同的是,此时我们生成的可执行程序并不能直接运行。
注意:-I、-L、-l 选项仅在编译期告知编译器库的位置。程序运行后,若操作系统找不到依赖的动态库,可使用 ldd 命令检查。

编译成功但无法运行,是因为动态库需在运行时查找。编译时指定的路径未告知操作系统。静态库为什么没有这个问题,因为在编译期间,静态库已经被拷贝到我们的可执行程序的内部了,那么此时加载就跟库没有关系了。
解决该问题的方法如下:
方法一:拷贝.so 文件到系统共享库路径下。
sudo cp mylib/lib/libmyc.so /lib/x86_64-linux-gnu/


方法二:建立软链接。

方法三:通过导入环境变量 (LD_LIBRARY_PATH----->加载库路径)。

方法四:修改.bashrc 配置文件,让环境变量永久生效。

第五种方式:在/etc/ld.so.conf.d 目录下新增动态库配置文件 (.conf 结尾),然后使用 ldconfig 加载。

静态库
优点:使用静态库生成可执行程序后,该可执行程序就可以独自运行,不再需要库了。 缺点:使用静态库生成可执行程序会占用大量空间,特别是当有多个静态程序同时加载而这些静态程序使用的都是相同的库,这时在内存当中就会存在大量的重复代码。
动态库

优点:节省磁盘空间,且多个用到相同动态库的程序同时运行时,库文件会通过进程地址空间进行共享,内存当中不会存在重复代码。 缺点:必须依赖动态库,否则无法运行。
与静态库不同,静态库在程序编译时会被完全复制到可执行程序中,而共享库则在程序运行时被加载到内存中,如果多个程序使用同一个共享库,OS 会让这些进程共享内存中的同一份库代码和数据,即:动态库的代码和数据在内存中只存在一份。
第一次加载:如果动态库尚未被加载到内存中,动态链接器会将该库加载到内存中,并映射到进程地址空间的共享区中。后续加载:如果其他进程也需要共享这个库,动态链接器会检查内存中是否已存在该库;如果已经存在,只需修改进程地址空间中的映射关系,指向已存在的库副本;如果不存在则重复第一次加载的过程。
优点:节省内存、易于更新、提高了程序的性能和安全性。
我们举一个简单的例子,以下面这张图为例。

磁盘中有 test.exe 这个可执行程序和 libmyc.so 这个动态库。当 test.exe 这个可执行程序编译执行后加载到了物理内存中,由于 test.exe 这个进程在加载的时候,使用了 libmyc.so 这个动态库,所以就需要将 libmyc.so 通过页表,映射到地址空间的共享区里面。不一定只有一个进程会使用 libmyc.so 这个动态库,那么当有多个进程使用 libmyc.so 这个动态库的时候,是不是每一个进程都要将 libmyc.so 这个动态库都加载一遍呢?答案是不需要,因为在后面进程的使用 libmyc.so 这个动态库之后,它已经被加载到了内存里面,那么如果需要使用动态库的话,首先将自己的代码和数据加载到内存里面,然后将已经加载到内存里面的动态库通过页表映射到自己的地址空间里面。
我们的可执行程序,编译成功,没有加载运行,那么所对应的二进制代码中有"地址"吗?答案是包含了地址。还有一点,在 Linux 当中形成的可执行程序叫做 ELF 的可执行程序。不要简单认为编译完成即为二进制。ELF 可执行程序有固定格式,包含头部及多种属性。可执行程序在编译之后,会变成很多的汇编语句,每条汇编语句都有对应的地址。
那么有的 uu 有些疑问,程序都没有加载,那么编译出来的地址是什么地址呢,那么又是如何对多条汇编语句进行编址的呢?在编址时,就一定要有个地址范围,当前我们的编址范围就是从 0000 0000(全零,有多少位就有多少 0)~~~FFFF.FFFF ------>不一定从全 0 开始,也不一定从全 F 开始。在系统当中,把一个可执行程序从全 0 到全 F 去进行绝对编址的做法叫做平坦模式。
可编址的范围:32 位平台,[0,2^32]----->[0,4GB],64 位平台:[0,2^64]---->[0,16GB].
PS: 绝对编址中的地址 == 实际的物理内存地址。
符号地址 = 基地址 + 偏移量。基地址在编译链接阶段是未知的,通常是由 OS 在程序加载时分配的虚拟地址,是在地址空间内的一个起始地址,如:0x800000。
那么地址空间、页表中的数据来自哪里。
每个可执行程序大小不同,说明了每个程序中各个区域虚拟地址范围也会不同。相应地,当这些程序被加载到内存变为进程时,则每个进程地址空间中各个区域的虚拟地址的范围也是不同的。
一般程序加载的过程。
地址空间的构建和管理,需要由 CPU、编译器、OS 三者共同配合完成。
CPU 执行程序的过程

OS 将可执行程序加载到内存,然后拿每行代码的物理地址构建页表的右侧,拿虚拟地址构建页表的左侧,然后就能对代码进行范围性的映射。OS 读取可执行程序表头中的入口地址 (main),将其交给 CPU,CPU 的程序计数器指向 main 函数的虚拟地址 117b,CPU 从这个地方开始执行指令:取指令 (虚拟地址)--->地址转换---->分析指令---->执行指令----->更新 PC 指针 (虚拟地址)。PC 指针会保存正在执行的指令的下一条指令的地址----->pc 指针指向哪里,CPU 就去执行哪里的代码。
对于库的数据和方法的访问,都是可以通过库在地址空间的起始地址 + 程序内部的偏移量来实现。


微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online