一、基础概念:什么是库?
1.1 库的本质
在 Linux 环境下,无论是静态库还是动态库,其本质都是源文件对应的.o(目标文件)的集合。简单来说,库的制作就是把一堆.o文件打包压缩,并配上相应的头文件(.h),以便于他人复用代码,同时又能保护源代码不被泄露。
1.2 静态库与动态库的命名规范
在 Linux 中,库的命名有着严格的规范,编译器也是通过特定的前后缀来识别库文件的:
- 静态库:前缀为
lib,后缀为.a。例如:libmyc.a。 - 动态库:前缀为
lib,后缀为.so。例如:libmyc.so。
Windows 环境下的对应:
- 静态库:通常以后缀
.lib结尾。 - 动态库:通常以后缀
.dll(Dynamic Link Library)结尾。
二、静态库的制作与使用
2.1 静态库的打包制作
静态库本质上是一种归档文件。我们可以使用ar(archive)命令将编译生成的.o文件打包成.a静态库。
# 1. 将所有源文件编译成对应的目标文件 .ogcc-cmy_stdio.c calc.c# 2. 使用 ar 命令将 .o 文件打包为静态库# -r (replace):若库中已有同名文件则替换# -c (create):创建新的归档文件ar-rclibmyc.a my_stdio.o calc.o- Makefile 示例
libmyc.a: calc.o my_stdio.o ar -rc $@ $^ calc.o: calc.c gcc -c $^ my_stdio.o: my_stdio.c gcc -c $^ .PHONY: clean clean: rm -rf *.o *.a- 静态库中要不要包含
main函数?
绝对不能包含。库是提供给他人调用的功能集合,如果库中包含了
main函数,会与使用者项目中的main函数发生符号冲突,导致链接失败。
使用file libmyc.a命令查看该文件,可以清晰地看到其属性为ar archive:
[ljm@iZ0jlgycwuno4vikja7kneZ lib_a]$file./lib/mylib/libmyc.a ./lib/mylib/libmyc.a: current ar archive2.2 静态库的发布与交付
为了方便使用者安装和使用,我们通常会将头文件和库文件组织到一个特定的目录结构中,并打包压缩:
lib/ ├── include/ │ ├── my_stdio.h │ └── calc.h └── mylib/ └── libmyc.acalc.o: calc.c gcc -c $^ my_stdio.o: my_stdio.c gcc -c $^ .PHONY: output output: mkdir -p lib/include mkdir -p lib/mylib cp -f *.h lib/include cp -f *.a lib/mylib tar czf lib.tgz lib .PHONY: clean clean: rm -rf *.o *.a我们可以使用
tar czf lib.tgz lib将其打包为压缩包,这就是一个简易的库"安装包"。还可以写一个安装脚本和卸载脚本放里面,如果你愿意的话。
2.3 静态库的常规链接与使用
当使用者拿到我们的库之后,由于gcc默认不会去当前工作目录下寻找非标准库的头文件和库文件,因此在编译用户的源文件(如code.c)时,必须通过显式选项指明路径:
gcc code.c-ocode-Ilib/include/-Llib/mylib/-lmyc选项含义解析:
-I:指明头文件的搜索路径(告诉编译器去哪里找头文件)。-L:指明库文件的搜索路径(告诉编译器去哪里找库文件)。-l:指明需要链接的库名称(去掉lib前缀和.a/.so后缀)。
以上包含头文件都可以因为我们在编译时带了-I选项。但是如果我们不想写这么长,只能用绝对路径或者相对路径:
gcc code.c-ocode-Llib/mylib/-lmyc那我们又能不能把-L也去掉、像正常编译一样呢?这就要先回答这个问题了:
为什么 C/C++ 的标准库不需要这些
-I、-L、-l选项?
因为gcc/g++编译器默认就认识系统的标准库,它们存放在特定的系统标准路径下(如/usr/include、/lib64),编译器会自动去这些路径检索并完成链接。若我们要链接任何非 C/C++ 标准库(包括第三方库或我们自己写的库),都需要用这些选项明确指出。
2.4 从生产者到用户
为了更直观地理解静态库的制作。在这个案例中,项目被划分为两个核心角色:开发者与使用者。
1. 开发者的目录视角
作为库的创作者,开发者负责编写核心源代码并完成打包交付。其工作目录如下:
code ├── calc.h # 核心功能头文件 ├── calc.c # 核心功能实现源文件 ├── my_stdio.h # 字符串工具头文件 ├── my_stdio.c # 字符串工具源文件 ├── calc.o # 静态库专用目标文件 ├── my_stdio.o # 静态库专用目标文件 ├── lib/ # 准备交付的规范目录 │ ├── include/ # 包含所有的对外公开头文件 (calc.h, my_stdio.h) │ └── mylib/ # 存放打包生成的静态库文件 (libmyc.a) └── lib.tgz # 最终交付给用户的压缩包- 开发者在
code目录下执行gcc -c calc.c my_stdio.c生成普通的目标文件。 - 随后利用
ar -rc libmyc.a *.o将它们归档为静态库。 - 将生成的库放入
lib/mylib/,将对应的头文件放入lib/include/,最后通过tar czf lib.tgz lib打包发布。
2. 使用者的目录视角
作为库的引入和使用者,拿到了创作者发布的lib.tgz,并在自己的工作空间内解压、编写业务逻辑。其工作目录如下:
code ├── lib.tgz # 从 Mr.Li 处获取的静态库压缩包 ├── lib/ # 解压出来的库资源 │ ├── include/ # 引入的头文件 (calc.h, my_stdio.h) │ └── mylib/ # 引入的静态库 (libmyc.a) ├── code.c # 自己的业务代码 ├── Makefile # 自动化编译脚本 └── code # 最终链接生成的独立可执行程序编译运行实操:使用者在 Makefile 中封装了链接规则,执行编译时实际调用的底层指令为:
gcc code.c-ocode-Ilib/include/-Llib/mylib-lmyc由于是静态链接,编译成功后生成的
code可执行程序已经将库中my_stdio.o和calc.o的二进制代码拷贝到了自身内部。此时,哪怕彻底删除解压出来的整个lib/目录,code依然可以独立运行。
三、动态库的制作与使用
3.1 动态库的特殊编译与打包
动态库(共享对象)与位置无关,这意味着它在内存中可以被多个进程共享。因此,编译动态库时需要引入特殊的编译选项:
# 1. 产生位置无关码(Position Independent Code)# 必须使用 -fPIC 选项gcc-fPIC-cmy_stdio.c calc.c# 2. 使用 -shared 选项进行动态库打包gcc-shared-olibmyc.so calc.o my_stdio.o- Makefile 示例
libmyc.so: calc.o my_stdio.o gcc -shared -o libmyc.so $^ calc.o: calc.c gcc -fPIC -c $^ my_stdio.o: my_stdio.c gcc -fPIC -c $^ .PHONY: output output: mkdir -p lib/include mkdir -p lib/mylib cp -f *.h lib/include cp -f *.so lib/mylib tar czf lib.tgz lib .PHONY: clean clean: rm -rf *.o *.so lib *.tgz使用file libmyc.so命令查看该文件,可以清晰地看到其属性为shared object且属于dynamically linked:
[ljm@iZ0jlgycwuno4vikja7kneZ lib_shared]$file./lib/mylib/libmyc.so ./lib/mylib/libmyc.so: ELF64-bit LSB shared object, x86-64, version1(SYSV), dynamically linked, BuildID[sha1]=9f99535aaf01594fba368359548c3c7c7c06c13d, not stripped3.2 动态库的运行报错问题
在使用上,动态库的编译链接指令和静态库一模一样:
gcc code.c-ocode-Ilib/include-Llib/mylib-lmyc然而,当编译成功并尝试执行时,系统却会抛出错误:
[ljm@iZ0jlgycwuno4vikja7kneZ user]$ ./code ./code: errorwhileloading shared libraries: libmyc.so: cannotopenshared object file: No suchfileor directory1. 为什么静态库没有这个问题?
静态链接时,编译器将库中的代码直接拷贝到了最终的可执行程序中。程序一旦生成,就不再依赖原有的静态库文件,因此可以独立运行。
而动态链接在编译时只是在可执行程序中记录了符号表等信息。当程序真正运行时,操作系统需要将动态库加载到内存中。
2. 核心根源:编译器 ≠ 操作系统
使用ldd code查看程序依赖的动态库,会发现libmyc.so => not found。
通过-L选项和-l仅仅是告诉了gcc编译器去哪里找什么名字的库,但是操作系统(OS)在运行程序时并不知道这个路径。
根本原因在于:编译阶段和运行阶段是由两个完全不同的角色负责的。
- gcc 只管编译阶段:你在编译时用
-L选项,是告诉gcc编译器:“嘿,你去这个路径下帮我把关一下,看有没有这个动态库,有的话就允许编译通过。”gcc听话地去看了,发现库在,于是给你的可执行程序盖了个戳,记录下"这个程序运行时需要链接libmyc.so",然后gcc的工作就彻底结束了。- 操作系统只管运行阶段:当你敲下
./code运行程序时,负责把程序加载到内存并跑起来的是操作系统(内核与动态链接器ld-linux.so)。这时候gcc早已下班了。操作系统要帮你去找libmyc.so加载到内存里,但它根本不知道gcc编译时用的-L路径是什么!你告诉了
gcc库在哪里,并不等于告诉了操作系统库在哪里。操作系统有它自己固定寻找动态库的路径(类似于 PATH,比如/lib64、LD_LIBRARY_PATH等)。如果你的自定义路径不在它的"朋友圈"里,操作系统就会抛出not found的报错。
四、解决动态库无法加载的四种方案
为了让操作系统在运行时能够成功定位并加载我们的动态库,可以采用以下四种主流方案:
4.1 方案一:拷贝至系统标准路径(所谓的"安装")
Linux 系统的动态链接器默认会自动前往系统标准路径下搜寻动态库。因此,将我们自己的头文件和库文件拷贝进去,在本质上就是完成了库的"安装"。
# 将头文件拷贝到系统标准的头文件搜索路径sudocplib/include/* /usr/include/# 将动态库文件拷贝到系统标准的库文件搜索路径sudocplib/mylib/libmyc.so /lib64/相比于直接放入系统的
/lib64,对于用户自己编译、安装的第三方库,更推荐的标准做法是放置在/usr/local/include与/usr/local/lib(或/usr/local/lib64)下,这样能避免与系统自带的、由包管理器(如yum)维护的系统基础库发生冲突。
自己安装一个第三方库:
sudoyuminstall-yncurses-devel4.2 方案二:在系统标准路径下建立软链接
如果不希望破坏系统原有目录或直接复制文件,可以在系统标准库路径下为我们的动态库建立一个快捷方式(软链接):
sudoln-s/home/ljm/day15/lib_shared/user/lib/mylib/libmyc.so /lib64/libmyc.so当我们在这些标准路径下建立一个指向自定义动态库的软链接时,操作系统在扫描标准目录并尝试加载该库时,会顺着该软链接的路径,追踪到我们在用户目录下维护的真实.so文件。
4.3 方案三:配置环境变量 LD_LIBRARY_PATH
操作系统在运行动态链接的程序时,会专门去检查名为LD_LIBRARY_PATH的环境变量。我们可以将动态库所在的绝对路径追加到该环境变量中:
exportLD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/ljm/day15/lib_shared/user/lib/mylib/这种在命令行中通过export配置的环境变量是内存级别的。一旦当前的终端会话被关闭或重新打开,该配置就会失效。
4.4 方案四:在/etc/ld.so.conf.d/下添加配置文件
这是系统级别的永久生效方案。Linux 系统允许我们在/etc/ld.so.conf.d/目录下任意创建一个专属的配置文件,并在其中记录动态库的搜索路径:
# 1. 在配置目录下创建一个自定义的配置文件sudovim/etc/ld.so.conf.d/my_test_lib.conf# 2. 将动态库所在的绝对路径直接粘贴进文件中并保存退出/home/ljm/day15/lib_shared/user/lib/mylib/# 3. 运行 ldconfig 命令,使系统的缓存配置重新加载生效sudoldconfig此时使用ldd code查看,即可看到库路径已成功匹配,程序可以永久正常运行。
五、动静态库混合链接的优先级
在 Linux 系统下,默认情况下安装的大部分库都优先安装和提供动态库。
5.1 gcc 的默认行为
gcc/g++默认情况下倾向于使用动态库进行链接。如果我们在同一个路径下,同时存在同名的动态库和静态库(例如同时存在libmyc.so和libmyc.a),并且编译时不加特殊限制:
gcc-ocode code.c-Ilib/include/-Llib/mylib/-lmyc使用file code查看生成的可执行程序,会发现它优先选择动态库进行链接(dynamically linked)。
5.2 强制静态链接
如果我们非要进行静态链接,必须在编译命令的末尾显式加上-static选项:
gcc code.c-ocode-Ilib/include-Llib/mylib-lmyc-static这个时候会报错,因为我们没有安装 C 的静态库,要安装一下:
[ljm@iZ0jlgycwuno4vikja7kneZ user]$ gcc code.c-ocode-Ilib/include-Llib/mylib-lmyc-static/usr/bin/ld: cannotfind-lccollect2: error: ld returned1exitstatus[ljm@iZ0jlgycwuno4vikja7kneZ user]$sudoyuminstall-yglibc-static局限性约束:
- 一旦使用了
-static选项,就意味着程序中所有的库都必须存在对应的静态库版本。如果系统中缺失了某一个依赖库(如 C 标准库)的静态版本,整个编译过程将会直接报错。 - 只有当系统只存在静态库、完全没有动态库时,
gcc别无选择,才会默认对该库采取静态链接。