链接库
-
- [1. 为什么要用链接库?](#1. 为什么要用链接库?)
- [2. 链接库的两种主要形式](#2. 链接库的两种主要形式)
- [3. 静态库和动态库的区别对比](#3. 静态库和动态库的区别对比)
- [4. 静态库使用演示: Linux 环境下](#4. 静态库使用演示: Linux 环境下)
-
- [4.1 正常编译、链接过程](#4.1 正常编译、链接过程)
- [4.2 创建静态库](#4.2 创建静态库)
- [4.3 使用静态库](#4.3 使用静态库)
- [4.4 检查可执行文件是否依赖动态库:](#4.4 检查可执行文件是否依赖动态库:)
- [5. 动态链接库](#5. 动态链接库)
-
- [5.1 创建动态库](#5.1 创建动态库)
- [5.2 使用动态库](#5.2 使用动态库)
- [5.3 链接器的扫描路径设置](#5.3 链接器的扫描路径设置)
-
- [1. 扫描配置文件中指定的目录](#1. 扫描配置文件中指定的目录)
- [2. 扫描这些目录下的所有共享库](#2. 扫描这些目录下的所有共享库)
- [3. 更新缓存文件 `/etc/ld.so.cache`](#3. 更新缓存文件
/etc/ld.so.cache) - [4. 指定任意路径](#4. 指定任意路径)
- [6. 链接库和头文件](#6. 链接库和头文件)
-
- [6.1 库文件的主要路径](#6.1 库文件的主要路径)
- [6.2 系统如何定位动态库](#6.2 系统如何定位动态库)
- [6.3 库对应的头文件](#6.3 库对应的头文件)
- [6.4 头文件搜索路径的优先级](#6.4 头文件搜索路径的优先级)
- [7. 用户库使用的典型目录结构](#7. 用户库使用的典型目录结构)
- [8. 再谈动态链接和静态链接的区别](#8. 再谈动态链接和静态链接的区别)
-
- [8.1 编译阶段(从源码到目标文件)](#8.1 编译阶段(从源码到目标文件))
-
- [1. 预处理](#1. 预处理)
- [2. 编译](#2. 编译)
- [3. 汇编](#3. 汇编)
- [8.2 链接阶段(从目标文件到可执行文件)](#8.2 链接阶段(从目标文件到可执行文件))
-
- [1. 符号解析](#1. 符号解析)
- [2. 重定位](#2. 重定位)
- [3. 动态链接与静态链接的区别](#3. 动态链接与静态链接的区别)
- [8.3 运行阶段(从可执行文件到进程)](#8.3 运行阶段(从可执行文件到进程))
-
- [1. 加载可执行文件](#1. 加载可执行文件)
- [2. 动态链接器的工作(运行时链接)](#2. 动态链接器的工作(运行时链接))
- [3. 执行过程中的动态库调用](#3. 执行过程中的动态库调用)
- [4. 进程终止](#4. 进程终止)
- [8.4 程序到运行整个过程总结](#8.4 程序到运行整个过程总结)
链接库 是一种将可复用代码预先编译 、打包 而成的二进制文件。它让程序在链接阶段能够使用这些代码,而不是每次都要重新编写或编译相同的功能。
1. 为什么要用链接库?
- 代码复用:一次编写,多处使用。
- 模块化:程序可以拆分成多个库,便于维护和升级。
- 加快编译:链接库已经是编译好的二进制文件,链接比重新编译快很多。
- 节省内存/磁盘(动态库):多个程序可以共享同一份库代码。
2. 链接库的两种主要形式
静态链接库在 编译时 将所有需要的代码直接嵌入到可执行文件中,而动态链接库则在 程序运行时 加载所需的代码。
根据链接的时机,分为:
| 类型 | 链接时机 | 后缀(Linux) | 特点 |
|---|---|---|---|
| 静态库 | 编译链接阶段 | .a |
库代码被复制到最终的可执行文件中。可执行文件大,独立运行,更新库需重新编译、链接程序。 |
| 动态库(共享库) | 运行(加载)时 | .so |
可执行文件中仅存引用,运行时再去加载库文件。多个程序可共享内存中的同一份库,更新库只需替换文件(接口不变时)。 |
Windows 上静态库和动态库的区分:静态库
.lib,动态库.dll。macOS 动态库.dylib。
3. 静态库和动态库的区别对比
下面从几个关键维度进行详细对比:
| 维度 | 静态库 | 动态库 |
|---|---|---|
| 链接时机 | 发生在编译/链接阶段,是最终可执行文件的一部分。 | 发生在程序加载或运行时,需要时才加载。 |
| 文件大小 | 生成的可执行文件较大,因为包含了库代码。 | 生成的可执行文件较小,只存储了引用信息。 |
| 内存占用 | 多进程运行时,每个进程都有库的独立副本,内存占用高。 | 多进程运行时,可以共享内存中的一份库代码,节省内存。 |
| 部署与更新 | 库更新后,必须重新链接并重新发布整个应用。 | 库更新后,只需替换库文件,无需重新发布应用(前提是接口不变)。 |
| 依赖问题 | 无运行时依赖,编译好的程序可独立运行。 | 运行时必须能找 到依赖的库文件,否则会报错(如 error while loading shared libraries)。 |
| 加载速度 | 快,因为代码已在程序中,无需额外查找和加载。 | 稍慢,因为启动时需要查找、加载和解析动态库。 |
| 典型文件后缀 | .a (Linux/macOS),.lib (Windows) |
.so (Linux),.dylib (macOS),.dll (Windows) |
4. 静态库使用演示: Linux 环境下
自定义一个mymath 库,这个库中实现 加减函数,通过 mymath.h 头文件可以调用 mymath 库。
add.c 内容如下:
c
#include "mymath.h"
int add(int a, int b)
{
return a+b;
}
sub.c 内容如下:
c
#include "mymath.h"
int sub(int a, int b)
{
return (a-b);
}
mymath.h 头文件声明如下:
c
#ifndef MY_MATH_H
#define MY_MATH_H
int add(int a, int b);
int sub(int a, int b);
#endif
其他,main.c 函数如下:
c
#include <stdio.h>
#include "mymath.h"
int main()
{
int a = 10, b = 5;
printf("static lib test\n");
printf("add(%d,%d) = %d\n", a,b, add(a,b));
printf("sub(%d,%d) = %d\n", a,b, sub(a,b));
return 0;
}
以上四个文件都在同一个路径下。
4.1 正常编译、链接过程
在当前文件夹中,有上面 4 个文件,在正常编译、链接为可执行程序时,有以下几步:
- 编译所依赖的的源文件,生成目标文件(.o 结尾):add.c 、sub.c 、main.c 这些文件会被编译为目标文件;
bash
gcc -c add.c -o add.o # -c 表示只编译不链接
gcc -c sub.c -o sub.o
gcc -c main.c -o main.o
- 链接,链接所有目标文件
链接器(ld,由 gcc 间接调用)将 add.o、sub.o、main.o 合并
bash
# 2. 链接所有目标文件,生成名字为 a.out 的可执行程序
gcc add.o sub.o main.o -o a.out
4.2 创建静态库
在创建自定义静态链接库的时候,就是先把 add.c 和 sub.c 编译为 一个 库,当其他程序需要用到这个库中代码的时候,在编译链接时,会从这个库中找到相应的实现代码并替换,最终生成一个可执行文件。
使用 ar 打包静态库:
ar(archive)是 GNU 的归档工具,通常配合 rcs 选项:
r:将文件插入归档(替换已有同名文件)。c:创建归档(不显示警告)。s:生成索引(相当于运行ranlib),便于链接器快速查找符号。
bash
ar rcs libmymath.a add.o sub.o
库名必须遵循 lib 前缀 + 库名 + .a 的命名规则,例如 libmymath.a。
之后的链接器(如 gcc -lmymath)会自动搜索 libmymath.a。
先创建一个静态库,名字为 mymath,并使用命令如下:
bash
# 打包静态库,名字为 mymath
ar rcs libmymath.a add.o sub.o
查看静态库内容:
用 ar t 列出包含的目标文件:
bash
static_lib_demo$ ar t libmymath.a
add.o
sub.o
用 nm 查看符号表:
bash
static_lib_demo$ nm libmymath.a
add.o:
0000000000000000 T add
sub.o:
0000000000000000 T sub
至此我们创建了一个名为 mymath 的静态库。
4.3 使用静态库
使用静态库(.a 文件)需要 确保头文件可用,应用程序在调用静态库时,需要使用静态库提供的头文件才知道能够使用哪些API。
根据上面的操作,我们得到了一个静态库libmymath.a , 在当前项目中,我们使用这个静态库,目录结构如下:
text
已经有 libmymath.a 和对应的头文件 mymath.h。
project/
├── libmymath.a # 静态库
├── mymath.h # 头文件
└── main.c # 你的程序
在 main.c 中 #include 静态库提供的头文件,这样编译器就能知道函数声明。
c
#include <stdio.h>
#include "mymath.h"
int main()
{
int a = 10, b = 5;
printf("static lib test\n");
printf("add(%d,%d) = %d\n", a,b, add(a,b));
printf("sub(%d,%d) = %d\n", a,b, sub(a,b));
return 0;
}
编译链接时,指定对应的静态库:
使用 GCC,编译命令格式:
bash
gcc main.c -L<库所在目录> -l<库名> -I<头文件目录> -o <可执行文件名>
-L:指定库的搜索路径(-L.表示当前目录)-l:链接库,忽略lib前缀和.a后缀 (-lmymath会找libmymath.a)-I:指定头文件搜索路径(如果头文件不在标准路径,如/usr/include)
编译链接时,需要指定库的搜索路径(-L)和库名(-l,省略 lib 前缀和 .a 后缀):
由于我们把静态库也放在当前目录中,因此命令如下:
bash
gcc main.c -L. -lmymath -o a.out
-L.表示在当前目录查找库。-lmath会展开为libmath.a。- 省略了
-I表示头文件和main.c在同一个路径下。
完成的命令如下:
bash
# 1. 编译目标文件
gcc -c add.c sub.c
# 2. 打包静态库,名字为 mymath
ar rcs libmymath.a add.o sub.o
# 3. 清理临时 .o(可选,保留也可以)
rm add.o sub.o
# 4. 编译,使用当前文件目录下的静态库,生成可执行程序
gcc main.c -L. -lmymath -o a.out
# 5. 运行程序,正常
./a.out
4.4 检查可执行文件是否依赖动态库:
ldd 命令用于在 Linux 中 查看可执行文件或共享库依赖了哪些动态库 (即运行时需要加载的 .so 文件)
使用方式: ldd your_program
我们使用这个方式验证一下当前创建的静态库,有没有调用其他的动态库:
bash
static_lib_demo$ ldd a.out
linux-vdso.so.1 (0x00007ffefdff3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007800f0200000)
/lib64/ld-linux-x86-64.so.2 (0x00007800f0553000)
- 这个输出说明
a.out是一个动态链接的可执行文件 ,它在运行时需要依赖三个动态库(实际上两个是真正的库,一个是虚拟的):linux-vdso.so.1 (0x00007ffefdff3000)- 这是一个虚拟动态共享对象 ,由内核在进程启动时"伪造"注入,不是磁盘上的文件。
- 它提供一些快速系统调用的优化实现(如
gettimeofday、clock_gettime),避免陷入内核。 - 任何动态链接(甚至静态链接)的程序都可能出现这一行,它不影响程序部署。
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007800f0200000)- 表示程序依赖 GNU C 标准库的动态版本 (
libc.so.6),实际路径是/lib/x86_64-linux-gnu/libc.so.6。 - 这是最核心的依赖:几乎所有 C 程序都需要它(提供
printf、malloc、memcpy等函数)。 =>右侧的地址是运行时加载的内存基址。
- 表示程序依赖 GNU C 标准库的动态版本 (
/lib64/ld-linux-x86-64.so.2 (0x00007800f0553000)- 这是 动态链接器 (也叫程序解释器),负责在程序启动时加载
libc.so.6等其他动态库,并完成符号重定位。 - 它本身也是一个共享库,但在 ELF 文件中被标记为解释器。
- 这是 动态链接器 (也叫程序解释器),负责在程序启动时加载
a.out 没有 采用完全静态链接(否则 ldd 会输出 statically linked),它在运行时必须能找到 libc.so.6 和动态链接器(通常系统自带,路径固定)。
如果要生成一个完全采用静态链接的可执行文件,只需要在编译时加 -static 选项,就可以得到一个采用静态链接,不依赖动态库的可执行文件。
要完全消除动态库依赖,编译时执行:
bash
gcc -static main.c -L. -lmymath -o a.out_static
然后用 ldd a.out_static 检查。
bash
static_lib_demo$ ldd a.out_static
不是动态可执行文件
对比之前的 a.out 和 a.out_static 这两个文件大小:
bash
static_lib_demo$ ls -lh a.out
-rwxrwxr-x 1 ant ant 16K a.out
static_lib_demo$ ls -lh a.out_static
-rwxrwxr-x 1 ant ant 880K a.out_static
可以看到,采用静态链接的文件,体积会大很多。在链接的时候,链接器会优先使用动态库( .so 版本),如果同时存在 .a的静态库,除非用 -static ,否则默认优先动态库)
5. 动态链接库
在 Linux 下制作动态链接库(.so,Shared Object)需要两个关键编译选项:-fPIC (生成位置无关代码)和 -shared(生成共享库)。
下面以 上文中的 add.c、sub.c 为例,演示完整流程。
5.1 创建动态库
编译为目标文件(需位置无关代码)
bash
gcc -c -fPIC add.c -o add.o
gcc -c -fPIC sub.c -o sub.o
-fPIC表示生成位置无关代码(Position Independent Code),这样动态库加载到内存的任何地址都能正确执行,实现多进程共享。
链接为动态库
bash
gcc -shared add.o sub.o -o libmymath.so
-shared表示生成共享库。- 库命名规则:
lib+ 库名 +.so(如libmymath.so)。
一步到位(直接生成 .so):
bash
gcc -shared -fPIC add.c sub.c -o libmymath.so
5.2 使用动态库
编译链接时,与静态库类似,但运行时需要确保能找到 .so:
bash
gcc main.c -L. -lmymath -o a.out
-L.表示在当前目录查找库。-lmymath链接libmymath.so(链接器会优先使用.so版本,如果同时存在.a则优先动态库,除非用-static)。- 省略了
-I表示头文件和main.c在同一个路径下。
这里我们使用 ldd 先查看 a.out 使用到的动态链接库有哪些:
bash
shared_lib_demo$ ldd a.out
linux-vdso.so.1 (0x00007ffda1dfe000)
libmymath.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000072da69a00000)
/lib64/ld-linux-x86-64.so.2 (0x000072da69c82000)
其中,我们自己创建的动态库 libmymath.so 提示找不到,这是因为 动态链接器默认搜索路径(/lib、/usr/lib 等)不包含当前目录。
需要把 用户动态库,添加到 链接器的默认搜索路径下,
将库安装到系统路径下,这里是用户级
bash
sudo cp libmymath.so /usr/local/lib
sudo ldconfig # 更新缓存
之后,再来验证先前的可执行文件 a.out ,即可正确找到对应的动态库位置:
bash
shared_lib_demo$ ldd a.out
linux-vdso.so.1 (0x00007ffeea7f7000)
libmymath.so => /usr/local/lib/libmymath.so (0x00007c2411c3e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007c2411a00000)
/lib64/ld-linux-x86-64.so.2 (0x00007c2411c5e000)
再运行可执行程序a.out ,输出也是正确的。
5.3 链接器的扫描路径设置
前面我们把 自定义的动态库,存放到 /usr/local/lib 之后,使用 sudo ldconfig 命令之后,链接器缓存更新之后,就能正确找到 libmymath.so 路径了,这期间发生了什么?
ldconfig 命令执行了以下关键步骤:
1. 扫描配置文件中指定的目录

-
读取
/etc/ld.so.conf文件及其包含的所有子配置文件(如/etc/ld.so.conf.d/*.conf)。 -
这些配置文件里列出了系统需要索引的库目录,通常情况下
/usr/local/lib就在其中(如果没有,你需要手动添加,但多数 Linux 发行版默认已包含)。 -
例如,典型的
/etc/ld.so.conf内容:textinclude /etc/ld.so.conf.d/*.conf而
/etc/ld.so.conf.d/libc.conf文件中包含了/usr/local/lib路径,因此刚好可以搜索到我们添加的自定义动态库。
2. 扫描这些目录下的所有共享库
- 对每个配置的目录,
ldconfig递归查找所有动态库文件(命名模式:lib*.so*)。 - 它会解析每个库的 SONAME (库的正式名称,例如
libmymath.so.1),并创建必要的符号链接(如libmymath.so->libmymath.so.1.0.0)。
3. 更新缓存文件 /etc/ld.so.cache
- 将所有找到的库的路径 和SONAME 信息写入一个二进制缓存文件
/etc/ld.so.cache。 - 这个缓存是一个哈希表,能够帮助动态链接器快速查找库的位置,而不需要每次去磁盘扫描目录。
4. 指定任意路径
因此,如果我们需要指定到一个新的路径,只需要在 /etc/ld.so.conf.d/ 路径下,创建一个新的配置文件 *.conf 文件,并填上绝对路径,之后,再更新缓存文件就行。
把 自定义的 libmymath.so 移动到新的文件夹中,路径为 /home/ant/mylib。
示例:
在 /etc/ld.so.conf.d/ 路径下,创建一个新的配置文件 my-lib.conf 文件

并在 my-lib.conf 中填入当前 自定义动态库的 绝对路径。

再使用命令:sudo ldconfig 重新更新链接器的缓存文件。
再次验证:
bash
shared_lib_demo$ ldd a.out
linux-vdso.so.1 (0x00007ffd3bddc000)
libmymath.so => /home/ant/mylib/libmymath.so (0x000078bf2d6da000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000078bf2d400000)
/lib64/ld-linux-x86-64.so.2 (0x000078bf2d6fa000)
可以看到,这次链接器就能够找到自定义动态库所在位置。
6. 链接库和头文件
6.1 库文件的主要路径
在 不同的在 Linux 系统中,库文件的存放和管理遵循一套清晰的规则。
通常,库文件会根据其作用范围和来源,被安排在不同的目录下。
下表为最主要的路径
| 路径 | 定位 | 存放内容 | 特点与用途 |
|---|---|---|---|
/lib |
系统启动与核心命令依赖 | 系统启动和 /bin、/sbin 下核心命令所需的最基本的共享库 |
最核心,不可缺 。例如,libc.so.*(C 标准库)和 ld-linux.so.*(动态链接器)就位于此。 |
/usr/lib |
操作系统级程序 | 绝大多数用户程序(如/usr/bin下的)所需的共享库 和静态库 |
包管理器 (如 APT)安装的程序库,默认驻留在此。 |
/usr/local/lib |
本地管理员手动安装的程序 | 用户从源码手动编译安装的软件的库文件 | 用户自己管理 ,优先级通常高于 /usr/lib,方便覆盖系统默认版本。 |
/opt/lib |
大型第三方软件包 | 独立安装的大型第三方软件(如 Oracle 数据库)的库文件 | 隔离性强,软件及其依赖集中存放,便于安装、升级和卸载。 |
~/.local/lib |
单个用户 | 用户通过 Pip 或 Conan 等工具为特定用户空间安装的库 | 无 Root 权限安装库的默认路径,完全归属于特定用户。 |
注意 :对于静态库(
.a)和动态库(.so),路径规则是相同的。上述所有路径,都是同时存放这两种库文件的。x86_64 架构特殊性:在 64 位系统上,为了同时支持 32 位库,会有类似
lib/x86_64-linux-gnu这样的多架构目录。当直接查看/lib或/usr/lib时,你可能会看到链接到此类目录的软链接。
库的命名规则
- 静态库 :
lib<库名>.a,例如libffi.a。 - 动态库 :
lib<库名>.so.<主版本号>.<次版本号>.<修订版>,例如libbd_loop.so.2.0.0
6.2 系统如何定位动态库
动态库的搜索路径由 动态链接器(ld-linux.so) 决定,先后顺序如下:
- 可执行文件内嵌的
RPATH/RUNPATH(编译时由-Wl,-rpath指定)。
首先如果我们在链接的时候,强制指定了 动态链接库的位置,那么在 可执行文件运行时,就会从该路径下找到对应的源文件;
以前面的例子,我们可以指定动态库的所在路径,使用命令:
bash
# 可执行文件中会嵌入库的搜索路径,运行时不需要设置 LD_LIBRARY_PATH 也无需修改系统配置。
hared_lib_demo$ gcc main.c -L. -lmymath -Wl,-rpath=. -o b.out
其中 . 表示当前路径下。
bash
shared_lib_demo$ ldd b.out
linux-vdso.so.1 (0x00007ffc44b4c000)
libmymath.so => ./libmymath.so (0x0000797a96983000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000797a96600000)
/lib64/ld-linux-x86-64.so.2 (0x0000797a9698f000)
可以看出,当前的 自定义动态库 的路径,就是我们指定的当前路径。
- 环境变量
LD_LIBRARY_PATH。
临时使用环境变量指定链接路径,使用命令如下:
bash
shared_lib_demo$ export LD_LIBRARY_PATH=./
shared_lib_demo$ gcc main.c -lmymath -o c.out
shared_lib_demo$ ldd c.out
linux-vdso.so.1 (0x00007ffd0b7bc000)
libmymath.so => ./libmymath.so (0x00007f87031e8000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8702e00000)
/lib64/ld-linux-x86-64.so.2 (0x00007f87031f4000)
/etc/ld.so.cache缓存(由/etc/ld.so.conf生成)。
这个就是我们前面使用的方法,也是最常用的。
- 系统默认路径
/lib,随后是/usr/lib。
当前面的方式,都没有设置时,链接器会在 系统默认路径下,查找动态库。
6.3 库对应的头文件
在 Linux 中,与静态库(.a)和动态库(.so)配套使用的头文件 (.h)通常存放在以下目录中,其组织逻辑与库文件路径相对应:
| 头文件目录 | 定位 | 存放内容 | 特点与用途 |
|---|---|---|---|
/usr/include |
系统级 | 操作系统和核心工具(如 libc、libpthread)的头文件。 |
包管理器(apt)安装的库的头文件默认位置 。例如 stdio.h、stdlib.h。 |
/usr/local/include |
本地管理员 | 用户从源码手动编译安装的软件的头文件。 | 与 /usr/local/lib 对应,优先级高于 /usr/include,用于覆盖系统默认头文件。 |
/usr/include/<subdir> |
特定库的子目录 | 大型库(如 glib、boost、openssl)的头文件通常放在以库名命名的子目录下。 |
避免头文件名冲突,例如 #include <openssl/ssl.h> 对应 /usr/include/openssl/ssl.h。 |
/opt/<package>/include |
大型第三方软件 | 独立安装的软件(如 Oracle 数据库、某些商业软件)的私有头文件。 | 与 /opt/<package>/lib 配对,隔离性强。 |
~/.local/include |
单用户 | 用户通过 pip、conan 或手动安装的库的头文件。 |
无 Root 权限时安装库的默认用户目录。 |
6.4 头文件搜索路径的优先级
GCC/G++ 在编译时搜索头文件的默认顺序(通过 #include <> 查找):
-I指定的路径 (优先级最高,如-I/opt/myapp/include)。/usr/local/include(多数发行版中,该路径在标准系统路径之前)。- GCC 自身的系统头文件目录 (如
/usr/lib/gcc/x86_64-linux-gnu/13/include,存放编译器内置头文件)。 /usr/include,C 标准库头文件路径。
验证当前编译器的默认头文件搜索顺序:
bash
$ gcc -v -E -xc - < /dev/null 2>&1 | grep -A 10 '#include <...> search starts here:'
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/9/include
/usr/local/include
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.
# 1 "<stdin>"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
查找顺序,从上到下依次搜索:
/usr/lib/gcc/x86_64-linux-gnu/9/include:GCC 编译器自带内置头文件(如stdarg.h、limits.h等)/usr/local/include:本地手动安装的库 / 头文件/usr/include/x86_64-linux-gnu:系统架构相关标准头文件/usr/include:Linux 系统全局标准 C 库头文件(stdio.h、stdlib.h都在这里)
7. 用户库使用的典型目录结构
根据前面提到的,把我们的链接库和头文件放在标准路径下,这样我们在 main.c 程序中,就可以直接使用 #include <mymath.h> 来导入头文件,且在链接时,不需要手动指定链接库的路径。
自定义的安装的库(如 libmymath)通常会放在 usr/local 路径下,按如下方式组织:
text
/usr/local/
├── lib/
│ └── libmymath.a # 静态库
│ └── libmymath.so # 动态库
└── include/
└── mymath.h # 头文件
在编译、链接时不需要指定路径:
bash
shared_lib_demo$ gcc main.c -lmymath -o a.out
8. 再谈动态链接和静态链接的区别
在使用静态库,进行编译链接后,生成可执行文件中已经融入了静态库中的代码,在运行的时候,就不需要查找静态库的路径进行链接整合了。而动态库就不同,如果我们把 usr/local/lib 下的动态库移除掉,那么程序就不能正确执行。
bash
shared_lib_demo$ ./a.out
./a.out: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory
运行 :执行 ./a.out 时,动态链接器加载 libmymath.so 失败。
C 程序的整个生命周期
我们以一个简单的 C 程序为例,贯穿 编译、链接、运行 的全过程,结合前面讨论的头文件搜索、静态/动态库、链接器、动态链接器等知识点,详细说明每个阶段做了什么事。
8.1 编译阶段(从源码到目标文件)
编译阶段由 编译器 (如 gcc)完成,包含三个子步骤:预处理 → 编译 → 汇编 。最终输出可重定位目标文件 (.o 或 .obj)。
1. 预处理
- 做什么 :处理以
#开头的指令。#include:将头文件内容插入到源文件中(递归展开)。#define:宏替换。#ifdef/#if:条件编译。
- 对
main.c的影响 :- 找到
stdio.h:使用系统头文件搜索路径 (如/usr/include)找到并展开。 - 找到
mymath.h:使用当前目录 (因为"mymath.h")或-I指定的路径。
- 找到
- 输出 :一个没有预处理指令的纯 C 代码(可保存为
.i文件)。
2. 编译
- 做什么 :将预处理后的 C 代码翻译成汇编语言 (与 CPU 架构相关,如 x86-64 的
.s文件)。 - 关键动作:语法分析、语义分析、生成中间代码、优化等。
- 输出:汇编代码文件。
3. 汇编
- 做什么 :将汇编代码翻译成机器指令 ,生成可重定位目标文件 (
.o)。 - 特点 :机器指令中的地址尚未固定 ,使用相对地址 或占位符 。文件中包含:
- 代码段 (
.text):机器指令。 - 数据段 (
.data/.rodata):已初始化的全局/静态变量。 - 符号表 :记录本文件定义的符号(如
add)和引用的外部符号(如printf、add)。 - 重定位信息:告诉链接器哪些地址需要修正。
- 代码段 (
8.2 链接阶段(从目标文件到可执行文件)
链接阶段由链接器 (ld,通过 gcc 间接调用)完成。输入 :一个或多个 .o 文件、静态库(.a)、动态库(.so)。输出:可执行文件(或动态库)。
1. 符号解析
- 作用 :将每个目标文件中引用的符号(如
add、printf)与定义该符号的代码关联起来。- 本地符号(如本文件的静态函数)已解析。
- 外部符号:查找其他
.o文件或库。
- 库的搜索顺序 :
- 从左到右扫描命令行中的
.o和.a。 - 静态库:链接器从中抽取需要的 目标文件(整个
.o提取,不是单个函数)。 - 动态库(
.so):此时仅记录依赖关系,不提取代码(留到运行时)。
- 从左到右扫描命令行中的
- 示例 :
main.o引用了add和printf。add在add.o(或libmymath.a)中找到。printf在 C 标准库libc.so中(默认链接)找到。
2. 重定位
- 作用 :合并所有输入目标文件的节(
.text、.data等),为符号分配运行时内存地址(在进程的虚拟地址空间中),并修改指令中的地址占位符。 - 步骤 :
- 合并相同属性的节,形成可执行文件的节。
- 分配虚拟地址:例如代码段从
0x400000开始。 - 根据重定位表,修正指令中对符号的引用(如
call add中的偏移量改为add的实际地址)。
- 输出:可执行文件(如 Linux 的 ELF 格式),其中大部分地址已固定(动态库除外)。
3. 动态链接与静态链接的区别
- 静态链接 (使用
-static或链接.a):- 库代码(如
libc.a中的printf代码)被复制到最终可执行文件的代码段。 - 可执行文件独立,体积大,更新库需重新链接。
- 库代码(如
- 动态链接 (默认):
- 只记录动态库的名称 和需要的符号 (如
printf)。 - 在可执行文件中生成
.interp节 ,指定动态链接器的路径(如/lib64/ld-linux-x86-64.so.2)。 - 可执行文件不包含 库代码,体积小,运行时需找到
.so文件。
- 只记录动态库的名称 和需要的符号 (如
8.3 运行阶段(从可执行文件到进程)
运行过程由操作系统加载器 和动态链接器配合完成。
1. 加载可执行文件
- 用户在 shell 中输入
./a.out,shell 调用fork()创建新进程,再调用execve()加载a.out。 - 内核读取 ELF 文件头,创建进程的虚拟地址空间,映射代码段、数据段等。
- 对于动态链接的可执行文件,内核会解释
.interp节,将动态链接器 (如ld-linux.so)的代码映射到进程地址空间,并将控制权交给它(而非直接交给a.out的入口)。
2. 动态链接器的工作(运行时链接)
动态链接器(也叫程序解释器)负责完成最终链接:
- 加载动态库 :
- 读取可执行文件的
.dynamic节和DT_NEEDED条目,得到依赖的库名(如libc.so.6)。 - 按照搜索顺序查找每个库:
- 环境变量
LD_LIBRARY_PATH(可覆盖)。 - 可执行文件中的
RPATH/RUNPATH。 /etc/ld.so.cache(由ldconfig生成)。- 默认系统路径
/lib、/usr/lib。
- 环境变量
- 找到库文件后,将其代码/数据段映射到进程的地址空间(通常使用
mmap)。
- 读取可执行文件的
- 符号解析与重定位 (类似编译时链接,但针对动态库):
- 解析可执行文件和动态库之间的符号引用(如
main调用printf,printf在libc.so中)。 - 执行全局偏移表(GOT) 和过程链接表(PLT) 的重定位,实现位置无关代码。
- 解析可执行文件和动态库之间的符号引用(如
- 初始化 :调用动态库的初始化函数(例如 C++ 全局对象构造、
__attribute__((constructor))函数)。 - 转交控制 :最后跳转到可执行文件的入口(默认
_start),最终调用main()。
3. 执行过程中的动态库调用
对于动态库中的函数(如 printf),第一次调用时通过 PLT/GOT 懒绑定(默认):
- 第一次调用会跳转到 PLT 中的桩代码,桩代码调用动态链接器的
_dl_runtime_resolve来查找printf的真实地址,并填充 GOT。 - 后续调用直接通过 GOT 跳转,不再需要链接器介入。
4. 进程终止
main返回后,运行库的 exit 处理(如刷新stdio缓冲区),最后调用_exit系统调用结束进程。
8.4 程序到运行整个过程总结
| 阶段 | 头文件 | 静态库 | 动态库 | 相关工具/环境 |
|---|---|---|---|---|
| 预处理 | 搜索路径:-I、系统默认 |
无关 | 无关 | #include 指令 |
| 编译+汇编 | 仅需声明 | 无关 | 无关 | gcc -c |
| 链接 | 不需要 | 提取代码,合并进可执行文件 | 记录依赖,不提取 | ld、-L、-l、-static |
| 运行 | 不需要 | 已在可执行文件中 | 运行时查找并加载 | LD_LIBRARY_PATH、ldconfig、ld-linux.so |
总结
- 编译 :源文件 → 汇编 → 机器码,生成可重定位的目标文件,地址未定。
- 链接:合并目标文件,解析符号,分配地址,生成可执行文件(动态链接的只留占位符)。
- 运行:操作系统加载 + 动态链接器完成最终地址绑定,加载共享库,执行程序。
理解这个过程,就能明白为什么头文件只在编译时需要,而库文件在链接和运行时都需要,也能明白静态链接和动态链接的本质差异。
在我们开发板 Linux 应用时,为了裁剪固件体积大小,可以完全去掉 C语言头文件这些标准库,只需要在虚拟机中编译好 可执行文件,把可执行文件放到开发板环境中,运行时,只要 相应的动态库 存在即可正常执行。