深入理解CC++的编译与链接技术3:如何制作和使用静态库
在上一篇博客中,笔者就简单的提及了一下关于静态库和动态库的基本导论,笔者将链接放在这里:
所以在之前,我们就简单的讲述了静态库的本质是什么。尽管,在今天,使用动态库作为代码的共享是一种更加基本的策略。但是处于完整,和笔者自己也喜欢用静态库打包一个只依赖于C/C++最基本运行时的人(其实笔者的确没有什么技术原因选择,纯粹是不太喜欢将一大坨可重定位文件直接塞给Linker)
如何制作静态库?
ar工具
所以一个很自然的问题出现了,在之前我们学习了静态库的最基本的原理(若干可重定位文件的有机组合),那又如何制作它呢?答案是利用一个小巧而强大的工具——ar(Archiver)
简单的介绍一下ar吧!它是一个用于创建、修改和提取归档文件(Archive Files)的工具。这些归档文件通常以 .a (a是archive,归档的缩写)扩展名结尾,最常见的用途就是打包目标文件(.o 文件)来创建 静态链接库(Static Libraries)。对于Linux而言,我们喜欢给一个静态库(至少是静态库),假设库的名字我们决定叫Charlie,那么一般我们生成的库会是libCharlie.a
可能会有朋友困惑,为什么一定是lib起头,生成Charlie.a显然不是更加的直观吗?是这样的,最核心的原因是:这是后续我们拿来做链接的时候,链接器存在的工作约定要求导致的。最经常的,我们gcc/g++编译准备链接对象的时候,会发派发ld来链接目标的库和重定位文件,一般而言,上层构建工具会习惯采用-L文件夹检索路径配合-l(这是小写的L)找库。比如说,当我们试图给main.c提供知名路径下的math静态库,比如说咱们这样写:
gcc main.c -lmath
链接器并不会直接去寻找名为 math 的文件。相反,它会根据约定,尝试寻找名为 libmath.a(静态库)或 libmath.so(动态库)的文件。简单的说就是:
-l参数后面的名字(本例中的math)被称为“库名”。- 链接器会自动在这个名字前加上前缀
lib。 - 然后根据情况(和优先级)加上
.a(静态库)或.so(动态库)等后缀,从而构成完整的文件名。
因此,将库文件命名为 lib<name>.a 的格式,是为了主动迎合链接器的自动查找机制。如果库文件不以此格式命名,链接器就无法通过简便的 -l 选项找到它,你只能通过直接指定库文件完整路径的笨拙方式来链接,这非常不方便,而且出现一个很严重的问题,这个问题将会在动态库的时候我们重新拿出来说(静态库无所谓,它会被打包进去目标的文件)
ar的一些常用命令格式
ar 的基本语法相对简单,它需要一个操作码(类似于一个主命令)和一些修饰符(Modifier)来指定具体行为。
ar [操作码][修饰符] <归档文件名> <文件...>
| 操作码 | 描述 | 常用修饰符 | 示例命令 |
|---|---|---|---|
r | 插入/替换:将文件添加到归档中。如果归档中已存在同名文件,则替换它。 | v (显示详细信息) | ar rv libmy.a file1.o file2.o |
t | 列表:显示归档中包含的文件列表。 | v (显示详细信息) | ar t libmy.a |
x | 提取:从归档中提取(解包)文件。 | v (显示详细信息) | ar xv libmy.a |
查看Man手册总是好的:ar(1) - Linux man page
Windows呢?
这个事情其实是MSVC工具链搞定它,不过,鲜有人自己手动做这些事情,在Windows上,大家基本都是委托巨无霸IDE: Visual Studio,或者是笔者喜欢用轻量的Visual Studio Code委托CMake处理这个事情。具体的细节可以去看看CMake编译的详细日志,这里笔者处于篇幅不计划展开
我们在什么地方会使用静态库呢?
笔者仔细想了想,稍微结合了一下自己粗浅的工程经验(可以说几乎没有)和自己看过的一点资料,其实在今天,静态库几乎可以被动态库所替代了,但是在这些场景下,使用静态库显然更加的合适,当然,笔者在嵌入式中使用静态库显然多一些,所以就打算这样说:
- 分发简单化: 只需要分发一个可执行文件,无需携带一堆
.dll(Windows) 或.so/.dylib(Linux/macOS) 文件。 - 版本锁死: 你需要绝对保证你的程序使用的是特定版本的库,不会被用户系统上的其他版本干扰。
- 小型工具或嵌入式系统: 在对文件数量或动态链接支持有严格限制的环境中。
那反过来,不使用静态库的理由呢
回顾上一个博客,我们已经说明了静态库的工作原理。所以,很容易想到第一个不使用静态库的理由就是:
可执行文件尺寸剧增 (Executable Bloat)
在注重复用接口的情况下,显然使用静态库会导致所有依赖于此的库和可执行文件的尺寸剧增 (Executable Bloat),所以,任何那些目的是给其他依赖提供功能接口,自身绝对独立的模块,请使用动态库。这个时候,我们让代码依赖只存在一份,让操作系统和加载器自动协调所有的映射符号关系,显然更好
更新需要重新编译和发布(Hot Reloading Request)
在注重热更新的场景下,显然采用静态库不合理,比如说,一些包我们不方便直接替换掉可执行文件,而是只用更新其中的一个子依赖的时候(比如说,我们采用的一个库被一个热心的开源主义程序员发现了漏洞并且及时向您反馈),即我们发现了库中一个安全漏洞或需要修复一个Bug,采用静态库我们必须重新编译并重新分发整个应用程序(静态链接让这些代码成为了本体的一部分而不是需要的依赖)
潜在的符号冲突和版本管理问题 (Symbol Collisions)
如果我们将多个版本或同名符号的静态库链接到同一个可执行文件中,编译器/链接器会尝试解决,但风险很高(笔者没有记错的话,是按照符号强弱和等同下随机丢弃),这真的很危险,谁也不喜欢自己的程序猜猜乐。