【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语言头文件这些标准库,只需要在虚拟机中编译好 可执行文件,把可执行文件放到开发板环境中,运行时,只要 相应的动态库 存在即可正常执行。

相关推荐
syc78901232 小时前
中文语境下AI编码工具实战对比:从迭代体验看日常开发选择
linux·人工智能·ubuntu
凡人叶枫2 小时前
Effective C++ 条款22:将成员变量声明为 private
linux·开发语言·c++
努力小周3 小时前
STM32智能安防系统
c语言·stm32·单片机·嵌入式硬件·物联网·计算机网络·pcb工艺
vsropy4 小时前
Ubuntu网络图标消失问题/有网络问号
linux·运维·ubuntu
coderwu4 小时前
Ubuntu 24.04 终端输入 openclaw config 提示未找到命令解决办法
linux·运维·ubuntu
华科大胡子5 小时前
在STM32上跑通TinyML
stm32·单片机·嵌入式硬件
凡人叶枫6 小时前
Effective C++ 条款28:避免使用 handles 指向对象内部
linux·服务器·开发语言·c++·嵌入式开发
AI帮小忙6 小时前
Debian系linux操作系统里安装OpenClaw
linux·运维·debian
极创信息6 小时前
Linux挖矿病毒深度清理实战教程,从进程隐藏、Rootkit驻留到彻底根除
java·大数据·linux·运维·安全·tomcat·健康医疗