【Linux系统】静态链接库与动态链接库

链接库

    • [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. 为什么要用链接库?

  1. 代码复用:一次编写,多处使用。
  2. 模块化:程序可以拆分成多个库,便于维护和升级。
  3. 加快编译:链接库已经是编译好的二进制文件,链接比重新编译快很多。
  4. 节省内存/磁盘(动态库):多个程序可以共享同一份库代码。

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 个文件,在正常编译、链接为可执行程序时,有以下几步:

  1. 编译所依赖的的源文件,生成目标文件(.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
  1. 链接,链接所有目标文件

链接器(ld,由 gcc 间接调用)将 add.osub.omain.o 合并

bash 复制代码
# 2. 链接所有目标文件,生成名字为 a.out 的可执行程序
gcc add.o sub.o main.o -o a.out

4.2 创建静态库

在创建自定义静态链接库的时候,就是先把 add.csub.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 是一个动态链接的可执行文件 ,它在运行时需要依赖三个动态库(实际上两个是真正的库,一个是虚拟的):
    1. linux-vdso.so.1 (0x00007ffefdff3000)
      • 这是一个虚拟动态共享对象 ,由内核在进程启动时"伪造"注入,不是磁盘上的文件
      • 它提供一些快速系统调用的优化实现(如 gettimeofdayclock_gettime),避免陷入内核。
      • 任何动态链接(甚至静态链接)的程序都可能出现这一行,它不影响程序部署。
    2. 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 程序都需要它(提供 printfmallocmemcpy 等函数)。
      • => 右侧的地址是运行时加载的内存基址。
    3. /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.csub.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 内容:

    text 复制代码
    include /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 决定,先后顺序如下:

  1. 可执行文件内嵌的 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)

可以看出,当前的 自定义动态库 的路径,就是我们指定的当前路径。

  1. 环境变量 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)
  1. /etc/ld.so.cache 缓存(由 /etc/ld.so.conf 生成)。

这个就是我们前面使用的方法,也是最常用的。

  1. 系统默认路径 /lib,随后是 /usr/lib

当前面的方式,都没有设置时,链接器会在 系统默认路径下,查找动态库。

6.3 库对应的头文件

在 Linux 中,与静态库(.a)和动态库(.so)配套使用的头文件.h)通常存放在以下目录中,其组织逻辑与库文件路径相对应:

头文件目录 定位 存放内容 特点与用途
/usr/include 系统级 操作系统和核心工具(如 libclibpthread)的头文件。 包管理器(apt)安装的库的头文件默认位置 。例如 stdio.hstdlib.h
/usr/local/include 本地管理员 用户从源码手动编译安装的软件的头文件。 /usr/local/lib 对应,优先级高于 /usr/include,用于覆盖系统默认头文件。
/usr/include/<subdir> 特定库的子目录 大型库(如 glibboostopenssl)的头文件通常放在以库名命名的子目录下。 避免头文件名冲突,例如 #include <openssl/ssl.h> 对应 /usr/include/openssl/ssl.h
/opt/<package>/include 大型第三方软件 独立安装的软件(如 Oracle 数据库、某些商业软件)的私有头文件。 /opt/<package>/lib 配对,隔离性强。
~/.local/include 单用户 用户通过 pipconan 或手动安装的库的头文件。 无 Root 权限时安装库的默认用户目录。

6.4 头文件搜索路径的优先级

GCC/G++ 在编译时搜索头文件的默认顺序(通过 #include <> 查找):

  1. -I 指定的路径 (优先级最高,如 -I/opt/myapp/include)。
  2. /usr/local/include(多数发行版中,该路径在标准系统路径之前)。
  3. GCC 自身的系统头文件目录 (如 /usr/lib/gcc/x86_64-linux-gnu/13/include,存放编译器内置头文件)。
  4. /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

查找顺序,从上到下依次搜索:

  1. /usr/lib/gcc/x86_64-linux-gnu/9/include:GCC 编译器自带内置头文件(如stdarg.hlimits.h等)
  2. /usr/local/include:本地手动安装的库 / 头文件
  3. /usr/include/x86_64-linux-gnu:系统架构相关标准头文件
  4. /usr/include:Linux 系统全局标准 C 库头文件(stdio.hstdlib.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)和引用的外部符号(如 printfadd)。
    • 重定位信息:告诉链接器哪些地址需要修正。

8.2 链接阶段(从目标文件到可执行文件)

链接阶段由链接器ld,通过 gcc 间接调用)完成。输入 :一个或多个 .o 文件、静态库(.a)、动态库(.so)。输出:可执行文件(或动态库)。

1. 符号解析
  • 作用 :将每个目标文件中引用的符号(如 addprintf)与定义该符号的代码关联起来。
    • 本地符号(如本文件的静态函数)已解析。
    • 外部符号:查找其他 .o 文件或库。
  • 库的搜索顺序
    • 从左到右扫描命令行中的 .o.a
    • 静态库:链接器从中抽取需要的 目标文件(整个 .o 提取,不是单个函数)。
    • 动态库(.so):此时仅记录依赖关系,不提取代码(留到运行时)。
  • 示例main.o 引用了 addprintf
    • addadd.o(或 libmymath.a)中找到。
    • printf 在 C 标准库 libc.so 中(默认链接)找到。
2. 重定位
  • 作用 :合并所有输入目标文件的节(.text.data 等),为符号分配运行时内存地址(在进程的虚拟地址空间中),并修改指令中的地址占位符。
  • 步骤
    1. 合并相同属性的节,形成可执行文件的节。
    2. 分配虚拟地址:例如代码段从 0x400000 开始。
    3. 根据重定位表,修正指令中对符号的引用(如 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)。
    • 按照搜索顺序查找每个库:
      1. 环境变量 LD_LIBRARY_PATH(可覆盖)。
      2. 可执行文件中的 RPATH / RUNPATH
      3. /etc/ld.so.cache(由 ldconfig 生成)。
      4. 默认系统路径 /lib/usr/lib
    • 找到库文件后,将其代码/数据段映射到进程的地址空间(通常使用 mmap)。
  • 符号解析与重定位 (类似编译时链接,但针对动态库):
    • 解析可执行文件和动态库之间的符号引用(如 main 调用 printfprintflibc.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_PATHldconfigld-linux.so

总结

  • 编译 :源文件 → 汇编 → 机器码,生成可重定位的目标文件,地址未定。
  • 链接:合并目标文件,解析符号,分配地址,生成可执行文件(动态链接的只留占位符)。
  • 运行:操作系统加载 + 动态链接器完成最终地址绑定,加载共享库,执行程序。

理解这个过程,就能明白为什么头文件只在编译时需要,而库文件在链接和运行时都需要,也能明白静态链接和动态链接的本质差异。

在我们开发板 Linux 应用时,为了裁剪固件体积大小,可以完全去掉 C语言头文件这些标准库,只需要在虚拟机中编译好 可执行文件,把可执行文件放到开发板环境中,运行时,只要 相应的动态库 存在即可正常执行。

相关推荐
盐焗鹌鹑蛋1 天前
【Linux】缓冲区
linux·运维·服务器
hsjiasb1 天前
FreeRTOS学习(二十三)——消息缓冲区(Message Buffer)
stm32·单片机·嵌入式硬件·学习·freertos
ltl1 天前
向量库与图 RAG:HNSW、DiskANN 到 GraphRAG
linux
学代码的CJY1 天前
Linux 库制作与原理:静态库、动态库、ELF 与动态链接完全指南
linux·运维·服务器
沸速存储1 天前
CPU 和 GPU 核心差别在哪?为什么 AI 训练离不开 GPU
服务器·人工智能·科技·嵌入式硬件·电脑
呱呱巨基1 天前
Linux 查找命令
linux·笔记·学习·查找命令
basketball6161 天前
软考中级网络工程师 第4章 无线通信网 备考总结
linux·服务器·网络·网络工程师·软考
我不会JAVA!1 天前
【无标题】
嵌入式硬件·h.264
芯岭技术郦1 天前
OM6625A芯片 Timer 定时器计数模式使用说明
stm32·单片机·嵌入式硬件
好评1241 天前
【Linux】进程信号
linux·运维·服务器