Re:Linux系统篇(三十四)文件篇·七:Linux环境下的动静态库制作、打包交付与运行加载全方案指南


◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录

  • 概要&序論
  • [一、 基础概念:什么是库?](#一、 基础概念:什么是库?)
    • [1.1 库的本质](#1.1 库的本质)
    • [1.2 静态库与动态库的命名规范](#1.2 静态库与动态库的命名规范)
  • [二、 静态库的制作与使用](#二、 静态库的制作与使用)
    • [2.1 静态库的打包制作](#2.1 静态库的打包制作)
    • [2.2 静态库的发布与交付](#2.2 静态库的发布与交付)
    • [2.3 静态库的常规链接使用](#2.3 静态库的常规链接使用)
    • [2.4 实战案例:从生产者到用户](#2.4 实战案例:从生产者到用户)
      • [2.4.1 开发者的目录视角(Creater_Mr.Li)](#2.4.1 开发者的目录视角(Creater_Mr.Li))
      • [2.4.2 使用者的目录视角(User_Mr.Zc)](#2.4.2 使用者的目录视角(User_Mr.Zc))
  • [三、 动态库的制作与使用](#三、 动态库的制作与使用)
    • [3.1 动态库的特殊编译与打包](#3.1 动态库的特殊编译与打包)
    • [3.2 动态库的运行报错问题](#3.2 动态库的运行报错问题)
      • [3.2.1 为什么静态库没有这个问题?](#3.2.1 为什么静态库没有这个问题?)
      • [3.2.2 核心根源:编译器 ≠ 操作系统](#3.2.2 核心根源:编译器 ≠ 操作系统)
  • [四、 解决动态库无法加载的四种方案](#四、 解决动态库无法加载的四种方案)
    • [4.1 方案一:拷贝至系统标准路径(所谓的"安装")](#4.1 方案一:拷贝至系统标准路径(所谓的“安装”))
    • [4.2 方案二:在系统标准路径下建立软链接](#4.2 方案二:在系统标准路径下建立软链接)
    • [4.3 方案三:配置环境变量 LD_LIBRARY_PATH](#4.3 方案三:配置环境变量 LD_LIBRARY_PATH)
    • [4.4 方案四:在 /etc/ld.so.conf.d/ 下添加配置文件](#4.4 方案四:在 /etc/ld.so.conf.d/ 下添加配置文件)
  • [五、 动静态库混合链接的优先级](#五、 动静态库混合链接的优先级)
    • [5.1 gcc 的默认行为](#5.1 gcc 的默认行为)
    • [5.2 强制静态链接](#5.2 强制静态链接)

概要&序論

  Hello大家好我是此方,本文聚焦 Linux 环境下动静态库的制作、交付与运行加载机制。

  • 库文件作为 .o 目标文件集合的底层归档原理;
  • ar 命令打包、构建发布目录到 gcc -I -L -l 选项链接的全流程;
  • -fPIC 位置无关码的编译要求与 -shared 共享对象的打包策略;
  • 分析"编译器与操作系统分离"导致的加载报错,并提供系统路径拷贝、软链接、LD_LIBRARY_PATH 变量及 /etc/ld.so.conf.d/ 配置四种永久或临时解决方案;
  • gcc 默认动态链接的行为以及 -static 强制静态链接的触发条件与约束。
      我们开始吧。

一、 基础概念:什么是库?

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 静态库。

bash 复制代码
# 1. 将所有源文件编译成对应的目标文件 .o
gcc -c mystdio.c mystring.c

# 2. 使用 ar 命令将 .o 文件打包为静态库
# -r (replace): 若库中已有同名文件则替换
# -c (create): 创建新的归档文件
ar -rc libmyc.a mystdio.o mystring.o

静态库中要不要包含 main 函数?

绝对不能包含。库是提供给他人调用的功能集合,如果库中包含了 main 函数,会与使用者项目中的 main 函数发生符号冲突,导致链接失败。

2.2 静态库的发布与交付

  为了方便使用者安装和使用,我们通常会将头文件和库文件组织到一个特定的目录结构中,并打包压缩:

bash 复制代码
# 构建发布目录
lib/
├── include/
│   ├── mystdio.h
│   └── mystring.h
└── mylib/
    └── libmyc.a

  我们可以使用 tar czf lib.tgz lib 将其打包为压缩包,这就是一个简易的库"安装包"。

  还可以写一个安装脚本和卸载脚本放里面,如果你愿意的话。

2.3 静态库的常规链接使用

  当使用者拿到我们的库之后,由于 gcc 默认不会去当前工作目录下寻找非标准库的头文件 and 库文件,因此在编译用户的源文件(如 usercode.c)时,必须通过显式选项指明路径:

bash 复制代码
gcc -o usercode usercode.c -I lib/include/ -L lib/mylib/ -lmyc

  选项含义解析:

  • -I :指明头文件的搜索路径(告诉编译器去哪里找头文件)。
  • -L :指明库文件的搜索路径(告诉编译器去哪里找库文件)。
  • -l :指明需要链接的库名称(去掉 lib 前缀和 .a/.so 后缀)。

为什么 C/C++ 的标准库不需要这些 -I、-L、-l 选项?

因为 gcc/g++ 编译器默认就认识系统的标准库,它们存放在特定的系统标准路径下(如 /usr/include、/lib64),编译器会自动去这些路径检索并完成链接。若我们要链接任何非 C/C++ 标准库(包括第三方库或我们自己写的库),都需要用这些选项明确指出。

2.4 实战案例:从生产者到用户

  为了更直观地理解静态库的制作。在这个案例中,项目被划分为两个核心角色:开发者(Creater_Mr.Li)使用者(User_Mr.Zc)

2.4.1 开发者的目录视角(Creater_Mr.Li)

  作为库的创作者,Mr.Li 负责编写核心源代码并完成打包交付。其工作目录如下:

bash 复制代码
Creater_Mr.Li
├── libmyc.h            # 核心功能头文件
├── libmyc.cpp          # 核心功能实现源文件
├── MyString.h          # 字符串工具头文件
├── MyString.cpp        # 字符串工具源文件
├── ObjectOfStatic/     # 静态库专用目标文件目录
│   ├── libmyc.o
│   └── MyString.o
├── lib/                # 准备交付的规范目录
│   ├── include/        # 包含所有的对外公开头文件 (libmyc.h, MyString.h)
│   └── mylib/          # 存放打包生成的静态库文件 (libmyc.a)
└── lib.tgz             # 最终交付给用户的压缩包

  步骤复现:

  1. Mr.LiObjectOfStatic 目录下执行 g++ -c ../libmyc.cpp ../MyString.cpp 生成普通的目标文件。
  2. 随后利用 ar -rc libmyc.a *.o 将它们归档为静态库。
  3. 将生成的库放入 lib/mylib/,将对应的头文件放入 lib/include/,最后通过 tar czf lib.tgz lib 打包发布。

2.4.2 使用者的目录视角(User_Mr.Zc)

  作为库的引入和使用者,Mr.Zc 拿到了创作者发布的 lib.tgz,并在自己的工作空间内解压、编写业务逻辑。其工作目录如下:

text 复制代码
User_Mr.Zc
├── lib.tgz             # 从 Mr.Li 处获取的静态库压缩包
├── lib/                # 解压出来的库资源
│   ├── include/        # 引入的头文件 (libmyc.h, MyString.h)
│   └── mylib/          # 引入的静态库 (libmyc.a)
├── usercode.cpp        # Mr.Zc 自己的业务代码 (包含 #include "libmyc.h")
├── Makefile            # 自动化编译脚本
└── proc                # 最终链接生成的独立可执行程序

  编译运行实操:

  Mr.ZcMakefile 中封装了链接规则,执行编译时实际调用的底层指令为:

bash 复制代码
g++ -o proc usercode.cpp -I lib/include -L lib/mylib -lmyc

  由于是静态链接,编译成功后生成的 proc 可执行程序已经将库中 libmyc.oMyString.o 的二进制代码拷贝到了自身内部。此时,哪怕 Mr.Zc 彻底删除解压出来的整个 lib/ 目录,proc 依然可以独立运行。

三、 动态库的制作与使用

3.1 动态库的特殊编译与打包

  动态库(共享对象)与位置无关,这意味着它在内存中可以被多个进程共享。因此,编译动态库时需要引入特殊的编译选项。

bash 复制代码
# 1. 产生位置无关码 (Position Independent Code)
# 必须使用 -fPIC 选项
gcc -fPIC -c mystdio.c mystring.c

# 2. 使用 -shared 选项进行动态库打包
gcc -shared -o libmyc.so mystdio.o mystring.o

  使用 file libmyc.so 命令查看该文件,可以清晰地看到其属性为 shared object 且属于 dynamically linked(动态链接)。

bash 复制代码
[zbc@VM-0-9-opencloudos blog_Linux_file_4]$ file User_Mr.Zc/lib/mylib/libmyc.so
User_Mr.Zc/lib/mylib/libmyc.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=fb30dcc0b85625aa7f5b8999530a56954efff93a, not stripped
[zbc@VM-0-9-opencloudos blog_Linux_file_4]$ 

3.2 动态库的运行报错问题

  在使用上,动态库的编译链接指令和静态库一模一样:

bash 复制代码
gcc -o usercode usercode.c -I lib/include/ -L lib/mylib/ -lmyc

  然而,当编译成功并尝试执行 ./usercode 时,系统却会抛出以下经典错误:

text 复制代码
./usercode: error while loading shared libraries: libmyc.so: cannot open shared object file: No such file or directory

3.2.1 为什么静态库没有这个问题?

  静态链接时,编译器将库中的代码直接拷贝到了最终的可执行程序中 。程序一旦生成,就不再依赖原有的静态库文件,因此可以独立运行。

  而动态链接在编译时只是在可执行程序中记录了符号表等信息。当程序真正运行时,操作系统需要将动态库加载到内存中。

3.2.2 核心根源:编译器 ≠ 操作系统

  使用 ldd usercode 查看程序依赖的动态库,会发现 libmyc.so => not found

  通过 -L 选项,我们仅仅是告诉了 gcc 编译器 去哪里找库,但是操作系统(OS)在运行程序时并不知道这个路径

  Gemini的这个讲法不错,大家可以看看

四、 解决动态库无法加载的四种方案

  为了让操作系统在运行时能够成功定位并加载我们的动态库,可以采用以下四种主流方案:

4.1 方案一:拷贝至系统标准路径(所谓的"安装")

  Linux 系统的动态链接器默认会自动前往系统标准路径下搜寻动态库。因此,将我们自己的头文件和库文件拷贝进去,在本质上就是完成了库的"安装"。

bash 复制代码
# 将头文件拷贝到系统标准的头文件搜索路径
sudo cp lib/include/* /usr/include/

# 将动态库文件拷贝到系统标准的库文件搜索路径
sudo cp lib/mylib/libmyc.so /lib64/

注意 :相比于直接放入系统的 /lib64,对于用户自己编译、安装的第三方库,更推荐的标准推荐做法是放置在 /usr/local/include/usr/local/lib (或 /usr/local/lib64) 下,这样能避免与系统自带的、由包管理器(如 yum/apt)维护的系统基础库发生冲突。

4.2 方案二:在系统标准路径下建立软链接

  如果不希望破坏系统原有目录或直接复制文件,可以在系统标准库路径下为我们的动态库建立一个快捷方式(软链接):

bash 复制代码
sudo ln -s /home/whb/code/lib/mylib/libmyc.so /lib64/libmyc.so

  当我们在这些标准路径下建立一个指向自定义动态库的软链接时,操作系统在扫描标准目录并尝试加载该库时,会顺着该软链接的路径,追踪到我们在用户目录下维护的真实 .so 文件。

4.3 方案三:配置环境变量 LD_LIBRARY_PATH

  操作系统在运行动态链接的程序时,会专门去检查名为 LD_LIBRARY_PATH 的环境变量。我们可以将动态库所在的绝对路径追加到该环境变量中:

bash 复制代码
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/whb/code/lib/mylib/

  这种在命令行中通过 export 配置的环境变量是内存级别的。一旦当前的终端会话被关闭或重新打开,该配置就会失效。

4.4 方案四:在 /etc/ld.so.conf.d/ 下添加配置文件

  这是系统级别的永久生效方案。Linux 系统允许我们在 /etc/ld.so.conf.d/ 目录下任意创建一个专属的配置文件,并在其中记录动态库的搜索路径。

bash 复制代码
# 1. 在配置目录下创建一个自定义的配置文件
sudo vim /etc/ld.so.conf.d/my_test_lib.conf

# 2. 将动态库所在的绝对路径直接粘贴进文件中并保存退出
/home/whb/code/lib/mylib/

# 3. 运行 ldconfig 命令,使系统的缓存配置重新加载生效
sudo ldconfig

  此时再次使用 ldd usercode 查看,即可看到库路径已成功匹配,程序可以永久正常运行。

复制代码
	libmyc.so => /home/zbc/Code/blog_Linux_file_4/User_Mr.Zc/lib/mylib/libmyc.so (0x00007f857122f000)
	libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f8570e00000)
	libm.so.6 => /lib64/libm.so.6 (0x00007f857114b000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f857112b000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f8570c0c000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f857123c000)

五、 动静态库混合链接的优先级

  在 Linux 系统下,默认情况下安装的大部分库都优先安装和提供动态库。

5.1 gcc 的默认行为

  gcc/g++ 默认情况下倾向于使用动态库进行链接。   如果我们在同一个路径下,同时存在同名的动态库和静态库(例如同时存在 libmyc.solibmyc.a),并且编译时不加特殊限制:

bash 复制代码
gcc -o usercode usercode.c -I lib/include/ -L lib/mylib/ -lmyc

  使用 file usercode 查看生成的可执行程序,会发现它优先选择动态库进行链接(dynamically linked)。

5.2 强制静态链接

  如果我们非要进行静态链接,必须在编译命令的末尾显式加上 -static 选项:

bash 复制代码
gcc -o usercode usercode.c -I lib/include/ -L lib/mylib/ -lmyc -static

  局限性约束

  • 一旦使用了 -static 选项,就意味着程序中所有的库都必须存在对应的静态库版本。如果系统中缺失了某一个依赖库(如 C 标准库)的静态版本,整个编译过程将会直接报错。
  • 只有当系统只存在静态库,完全没有动态库时,gcc 别无选择,才会默认对该库采取静态链接。

好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!