什么是库?
库(Library) 是一批提前编译完成、可被其他程序重复调用的通用二进制代码集合,它对外暴露固定的调用接口,使用者只需要知道「怎么调用」,不需要关心「内部如何实现」
在 Linux 系统中:
库本质是遵循 ELF 格式的二进制文件 ,内部封装了函数实现、全局变量、符号信息、重定位信息等。它不具备独立运行能力,必须被其他可执行程序「调用、链接」后,才能发挥作用
库的完整组成:头文件 + 库文件
第一部分:头文件(.h / .hpp)
作用:给「编译器」看 ------ 做接口声明
头文件中只存放:函数声明(函数名、参数列表、返回值);宏定义;结构体、枚举、类型别名;外部全局变量声明
第二部分:二进制库文件(.a / .so)
作用:给「链接器 / 动态链接器」看 ------ 做函数实现
库文件是真正存放函数二进制实现的载体
生命周期
- 静态库(
.a):编译链接阶段被整合进可执行文件,程序运行时不再依赖原库文件; - 动态库(
.so):编译阶段被记录依赖,运行阶段必须加载,程序运行全程依赖该库。
库的分类
静态库(Static Library)
- 文件后缀:
.a(Archive,归档文件) - 链接时机:编译链接阶段
- 核心逻辑:链接器会把库中被调用的代码,完整拷贝到最终可执行文件中。
- 特点:编译完成后,可执行文件和原静态库彻底解绑。
动态库(Dynamic Library / 共享库 Shared Library)
- 文件后缀:
.so(Shared Object,共享对象) - 链接时机:编译阶段仅记录依赖,运行阶段完成链接
- 核心逻辑:编译时不拷贝库代码,只在可执行文件中记录「我依赖哪个库、哪个函数」;程序运行时,由系统动态链接器加载库到内存。
- 特点:多个程序可以共享同一份库内存镜像,节省资源。
Windows 对应类比:静态库
.lib,动态库.dll
Linux 库的文件格式与强制命名规范
统一命名格式
所有 Linux 库文件都遵循:
bash
lib + 库名 + 后缀
示例:
- C 标准静态库:
libc.a - C 标准动态库:
libc.so
链接时的简写规则(-l 参数)
使用 gcc 链接库时,参数 -l 后面只写库名,省略 lib 和后缀
示例:
库文件 libmylib.a / libmylib.so
链接命令写:
bash
gcc main.c -o main -lmylib
库路径参数 -L
当库不在系统默认目录时,需要用 -L 指定库所在目录:
bash
gcc main.c -o main -L/xxx/yyy -lmylib
-L路径:告诉编译器「去这个目录下找库文件」
补充:
-L只在编译链接阶段生效,运行阶段动态库不会读取该路径(这也是后续「动态库找不到」问题的根源)
头文件搜索路径指定选项 -I
作用:告诉 gcc 编译器:除了默认路径外,去我指定的目录里查找头文件(.h)
bash
gcc [源文件] -o [输出文件] -I [头文件所在目录] ...
规则:
-I和 路径之间可以加空格,也可以不加- 路径支持 相对路径 和 绝对路径
- 可以写多个
-I,指定多个头文件目录
区分:
| 参数 | 作用 | 针对文件 | 场景 |
|---|---|---|---|
-I |
指定头文件搜索路径 | .h |
解决头文件找不到 |
-L |
指定静态库搜索路径 | .a |
解决库文件找不到 |
-l |
指定链接的库名 | libxxx.a |
省略lib和.a,直接写xxx |
静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库
静态库制作核心流程(底层逻辑)
- 把源码
.c编译成 目标文件.o - 用
ar归档工具,把 一个 / 多个.o文件打包 → 生成静态库.a
静态库生成
多文件源码制作静态库:
1. 准备多个库源码文件

补充 :mystdio.h 与 mystdio.c 是前文Linux基础I/O(2) 中 模拟glibc库 的文件
mystring.h:
c
#pragma once
int my_strlen(const char *s);
mystring.c:
c
#include "mystring.h"
int my_strlen(const char *s)
{
const char *start = s;
while(*s)
{
s++;
}
return s - start;
}
2. 批量编译 .c → .o

3. 批量打包 .o → 静态库

补充:ar 指令
用途:打包 .o 目标文件 → 生成 .a 静态库
基础语法:ar [选项参数] 生成的静态库名 目标文件1.o 目标文件2.o ...
核心参数
| 参数 | 英文全称 | 核心作用 |
|---|---|---|
| r | replace | 向库中插入 / 替换目标文件(不存在则添加,存在则覆盖) |
| c | create | 自动创建新的库文件(库不存在时不报错) |
| s | index | 生成符号索引表 |
静态库使用
1. 编写测试主程序
如:main.c(调用静态库的函数)
2. 编译链接静态库
命令格式:
bash
gcc 主程序.c -o 可执行文件 -I头文件路径 -L库文件路径 -l库名
3. 运行程序
如:./main
示例:

高阶版:Makefile 制作静态库
Makefile文件:
bash
libmyc.a:mystdio.o mystring.o
ar -rc $@ $^
mystdio.o:mystdio.c
gcc -c $<
mystring.o:mystring.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 libmyc.a lib lib.tgz

使用:

动态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码
动态库专属核心参数
和静态库制作完全不同,动态库必须用两个 gcc 专属参数:
| 参数 | 作用 | 必要性 |
|---|---|---|
-fPIC |
生成位置无关代码(让库能加载到任意内存地址,实现多进程共享) | 强制必须 |
-shared |
告诉 gcc 生成动态共享库 | 强制必须 |
动态库 vs 静态库
| 维度 | 静态库(.a) | 动态库(.so) |
|---|---|---|
| 制作工具 | gcc -c + ar rc |
gcc -c -fPIC + gcc -shared |
| 链接时机 | 编译时拷贝代码 | 编译时仅记录依赖 |
| 运行依赖 | 不依赖原库 | 必须依赖原 .so 文件 |
| 可执行文件体积 | 大 | 小 |
| 内存占用 | 高(多进程多副本) | 低(多进程共享) |
动态库生成
沿用静态库的代码,源码 / 头文件完全不用改 ,仅修改编译命令

- 编译生成 位置无关目标文件(.o)
必须加-fPIC
bash
gcc -fPIC -c *.c
- 链接生成 动态库(.so)
必须加-shared,生成共享库:
bash
gcc -shared -o libmyc.so *.o
Makefile 自动化制作动态库
Makefile:
bash
libmyc.so:mystdio.o mystring.o
gcc -shared -o $@ $^
mystdio.o:mystdio.c
gcc -fPIC -c $<
mystring.o:mystring.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 libmyc.so lib lib.tgz

动态库使用
1. 编写测试主程序(和静态库完全一样)
2. 编译链接动态库(命令和静态库完全相同)
3. 运行程序(必现错误 )
示例:

为什么动态库运行会找不到,静态库永远不会有这个问题?
- 动态库的核心机制:编译时不拷贝代码,只记录「我需要这个库」
- 编译路径
-L不生效 :因为它只告诉gcc 库在哪,没告诉运行时系统。运行时,系统根本不知道你当前目录有库,只会去系统默认路径搜索 - 静态库不报错:因为编译链接时,直接把库代码复制到可执行文件里,运行时不需要原始库文件
库运行搜索路径
问题:动态库运行找不到库
Linux动态库默认搜索顺序:
- 编译时指定的
RPATH路径 - 环境变量
LD_LIBRARY_PATH指定路径 - 系统默认库配置路径
/etc/ld.so.conf - 默认系统路径:
/lib、/usr/lib(64 位系统还有/lib64、/usr/lib64)
解决方案:
- 方法一:把
.so动态库拷贝到系统默认共享库路径 ,如:/usr/local/lib、/usr/lib64、/lib64
示例:

- 方法二:向系统共享库路径建立同名软链接
示例:

- 方法三:更改临时环境变量
LD_LIBRARY_PATH
示例:

- 方法四:配置
/etc/ld.so.conf.d/,ldconfig更新
示例:

- 结论1 :同一个目录下,同时存在同名的静态库和动态库时,gcc/g++ 链接器会默认优先选择 动态库(.so)进行链接
如何强制 gcc 链接 静态库?
方法:-static 参数
让所有库都强制静态链接:
bash
gcc -o code usercode.c -I lib/include/ -L lib/mylib/ -lmyc -static
- 结论2:在Linux系统下,默认情况安装的大部分库,默认都优先安装的是动态库
使用外部库
我们现在没接触过太多的库,唯一接触过的就是C、C++标准库,这里我们可以推荐一个好玩的图形库:ncurses
安装 ncurses:
bash
# CentOS
sudo yum install -y ncurses-devel
# Ubuntu
sudo apt install -y libncurses-dev
示例:Hello ncurses + 颜色版(AI生成的测试代码)
c
#include <ncurses.h>
#include <signal.h>
// 信号处理函数:确保程序退出时恢复终端
void cleanup(int sig) {
endwin();
exit(0);
}
int main() {
signal(SIGINT, cleanup); // 捕获 Ctrl+C 信号
// 1. 初始化
initscr();
cbreak();
noecho();
keypad(stdscr, TRUE);
curs_set(0); // 隐藏光标(0=隐藏,1=正常,2=高亮)
// 2. 颜色设置(需先检查终端是否支持颜色)
if (has_colors()) {
start_color();
init_pair(1, COLOR_RED, COLOR_YELLOW); // 颜色对1:红文字+黄背景
init_pair(2, COLOR_GREEN, COLOR_BLACK); // 颜色对2:绿文字+黑背景
}
// 3. 绘制内容
clear();
box(stdscr, 0, 0); // 用默认字符绘制边框
// 移动到屏幕中心并输出彩色文字
move(LINES/2, (COLS-16)/2); // LINES/COLS 是终端尺寸常量
attron(COLOR_PAIR(1)); // 启用颜色对1
addstr("Hello ncurses!");
attroff(COLOR_PAIR(1)); // 禁用颜色对
// 底部提示信息
move(LINES-2, 2);
attron(COLOR_PAIR(2));
addstr("Press any key to exit...");
attroff(COLOR_PAIR(2));
refresh(); // 刷新显示
getch(); // 等待用户输入
// 4. 清理
endwin();
return 0;
}
编译与运行
bash
gcc test.c -lncurses
./a.out
目标文件
通过 gcc -c 编译生成的 .o 文件即为目标文件 ,是源码编译后的中间二进制文件,未完成链接、地址重定位,无法直接运行。
所有.o目标文件、.a静态库、.so动态库、可执行文件,本质都是ELF格式文件

如何生成目标文件?
使用 gcc -c 命令:只编译,不链接
示例:
c
// hello.c
#include<stdio.h>
void run();
int main() {
printf("hello world!\n");
run();
return 0;
}
// code.c
#include<stdio.h>
void run() {
printf("running...\n");
}
// 编译两个源⽂件
$ gcc -c hello.c
$ gcc -c code.c
$ ls
code.c code.o hello.c hello.o
为什么需要目标文件?
1. 模块化编译
如果项目有 100 个 .c 文件:
- 不用每次全部重新编译
- 只修改 1 个文件 → 只重新生成 1 个
.o - 链接器直接合并所有
.o→ 极大节省编译时间
2. 制作动静态库
- 静态库(.a)= 多个目标文件的打包归档
- 动态库(.so)= 位置无关的目标文件 + 共享链接
- 链接生成可执行文件
多个目标文件 + 系统库 → 链接成最终程序
查看目标文件
示例:

ELF文件
ELF = Executable and Linkable Format
可执行与可链接格式
它是 Linux 系统的标准二进制文件格式,统一管理系统中所有需要编译、链接、加载、运行的二进制文件
ELF 文件的 4 种标准类型
| 类型 | 英文全称 | 对应文件 | 作用 |
|---|---|---|---|
| 1. 可重定位文件 | Relocatable File | .o 目标文件 |
存放编译后的机器码,供链接器使用 |
| 2. 可执行文件 | Executable File | 最终程序 main |
链接完成,可直接被系统加载运行 |
| 3. 共享目标文件 | Shared Object File | .so 动态库 |
位置无关代码,运行时动态加载 |
| 4. 核心转储文件 | Core Dump File | core |
程序崩溃时生成,用于调试 |
ELF 文件的核心结构

1. ELF Header(ELF 文件头)
定位
位于 ELF 文件最开头 ,是文件的第一个字节,所有 ELF 必须拥有
核心作用
告诉系统:这个文件是什么、能干嘛、其他结构在哪里
关键存储信息
- 魔数(Magic) :
0x7F 45 4C 46→ 标记这是 ELF 文件 - 位数:32 位 / 64 位
- 文件类型:
REL:可重定位文件(.o)EXEC:可执行文件DYN:动态库(.so)
- 机器架构:x86_64 / ARM
- 程序入口地址(可执行文件才有)
- 程序头表 / 节头表的位置和大小
实操查看
bash
readelf -h main.o # 查看目标文件头
readelf -h main # 查看可执行文件头

2. Program Header Table(程序头表)
定位
在 ELF 头之后,只有可执行文件 / 动态库(.so)拥有
核心作用
给操作系统内核看 → 描述「如何把文件加载到内存运行」
核心存储信息
按 Segment(程序段) 分组,描述:
- 段的权限(只读 / 可写 / 可执行)
- 段在文件中的位置
- 段要加载到内存的哪个地址
- 段的大小
实操查看
bash
readelf -l main # 查看可执行文件的程序头表
readelf -l libmylib.so # 查看动态库的程序头表
3. Sections(节区)
定位
ELF 文件的实际数据存储区,所有代码、数据、符号都存在这里
核心作用
存放二进制真实内容,是链接器操作的最小单位
6 个核心节
| 节名 | 类型 | 存储内容 | 作用 |
|---|---|---|---|
.text |
代码节 | 函数机器指令(如 add、printf 调用) |
程序执行的代码 |
.data |
数据节 | 已初始化的全局 / 静态变量 | 存放有初始值的数据 |
.bss |
零数据节 | 未初始化的全局 / 静态变量 | 不占磁盘空间,仅占位 |
.symtab |
符号表 | 函数名、变量名、外部符号 | 链接的核心!解析函数用 |
.rel.text |
重定位节 | 未确定的地址标记 | 告诉链接器需要修改地址 |
.dynsym |
动态符号节 | 动态库的符号表 | 动态链接专用 |
适用文件
所有 ELF 都有
实操查看
bash
readelf -S main.o
4. Section Header Table(节头表)
定位
在 ELF 文件末尾,所有 ELF 必须拥有
核心作用
给链接器(ld)看 → 节区的「目录索引」
它记录了:
- 每个节叫什么名字
- 每个节在文件的哪个位置
- 每个节多大
- 每个节是什么类型
链接器通过它快速找到 .text/.symtab 等节,完成链接工作
实操查看
bash
readelf -S main
ELF从形成到加载轮廓
ELF形成可执行
- step-1:将多份
C/C++源代码,翻译成为目标.o文件 + 动静态库(ELF) - step-2:将多份
.o文件section进行合并

注意:实际合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库合并,此处不做过多追究
ELF可执行文件加载
- 一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
- 合并规则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等
- 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起
- 很显然,这个合并工作也已经在形成
ELF的时候,合并方式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table) 中
为什么要将Section 合并成 Segment?
- Section合并的主要原因是为了减少页面碎片,提高内存使用效率。
- 内存权限管理
Linux 内存有严格权限:
- 代码:必须
只读 + 可执行(防止被篡改,安全) - 数据:必须
可读 + 可写(允许修改变量) - 其他 Section(符号表、节头表):不需要加载到内存
链接器把权限相同的 Section 合并成一个 Segment,统一设置权限
核心定论
- Section(节) :是链接器 用的细粒度拆分(代码、数据、符号分开存)
- Segment(段) :是操作系统 用的粗粒度内存块
- 多个 Section(节) → 合并 → 一个 Segment(段)
实操验证
- 查看所有 Section
bash
[user1@VM-0-17-centos lesson22]$ readelf -S main.exe
There are 30 section headers, starting at offset 0x1960:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
0000000000000060 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400318 00000318
000000000000003d 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400356 00000356
0000000000000008 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400360 00000360
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400380 00000380
0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400398 00000398
0000000000000048 0000000000000018 AI 5 23 8
[11] .init PROGBITS 00000000004003e0 000003e0
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400400 00000400
0000000000000040 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400440 00000440
0000000000000192 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004005d4 000005d4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004005e0 000005e0
0000000000000028 0000000000000000 A 0 0 8
...
- 查看所有 Segment

链接视图和执行视图
- 链接视图 = 节 + 节头表 → 给链接器拼文件用 ,.o 专属
◦ 文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解 ELF 文件中包含的各个部分的信息 - 执行视图 = 段 + 程序头表 → 给操作系统跑程序用 ,可执行 / 动态库专属
◦ 告诉操作系统,如何加载可执行文件,完成进程内存的初始化。⼀个可执行程序的格式中,一定有 program header table - 说白了就是:一个在链接时作用,一个在运行加载时作用
| 维度 | 链接视图 (Linking View) | 执行视图 (Execution View) |
|---|---|---|
| 使用者 | 链接器 (ld) | 操作系统、动态链接器 |
| 核心单位 | Section(节) | Segment(程序段) |
| 核心表格 | 节头表 (Section Header) | 程序头表 (Program Header) |
| 核心作用 | 编译、链接、制作库 | 加载程序、运行代码 |
| 地址类型 | 相对地址(未确定) | 绝对地址(已确定) |
| 必备结构 | 节头表 + 节区 | 程序头表 + 程序段 |
