Linux 基础 IO 收官:库的构建与使用、进程地址空间及核心知识点全解
大家好!这是我们“Linux 基础 IO”系列的收官篇。前面我们聊了文件描述符、标准 IO、系统调用 IO、缓冲区、mmap、文件孔洞、du vs ls 差异等内容,今天把两个“压轴”主题彻底讲透:
- 静态库 & 动态库 的构建、使用、区别(链接视角)
- 进程地址空间(虚拟内存布局)的完整图解与核心知识点
目标:让你既能手写库,也能看懂 /proc/<pid>/maps 和 valgrind 的输出。
一、库的本质:为什么要有库?
库 = 已编译好的、可复用的目标代码集合
目的:代码复用、模块化、减小二进制体积、便于升级维护
Linux 下两种主流库:
| 特性 | 静态库 (.a) | 动态库 / 共享库 (.so) |
|---|---|---|
| 文件后缀 | .a | .so |
| 链接时机 | 编译链接阶段(ld 阶段) | 运行时加载(动态链接器 ld.so) |
| 是否包含在可执行文件中 | 是(完整拷贝进 exe) | 否(只记录依赖关系) |
| 可执行文件大小 | 较大 | 较小 |
| 内存占用(多进程) | 每个进程独立一份 | 所有进程共享一份(物理内存只一份) |
| 更新方式 | 必须重新编译链接所有使用它的程序 | 替换 .so 文件即可(主程序无需重编) |
| 启动速度 | 稍快(无运行时解析) | 稍慢(需要 PLT/GOT 重定位) |
| 典型命名 | libxxx.a | libxxx.so(或 libxxx.so.版本号) |
| 链接选项 | -L -lxxx | -L -lxxx(运行时需 -rpath 或 LD_LIBRARY_PATH) |
一句话总结:
静态库 → “复制粘贴进每个程序”
动态库 → “大家共享一个 dll,大家改一个文件全生效”
二、实战:手写 & 构建 & 使用静态库与动态库
准备代码(calc 简单计算库)
// calc.h
#ifndef CALC_H
#define CALC_H
int add(int a, int b);
int sub(int a, int b);
#endif
// calc.c
#include "calc.h"
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
// main.c
#include <stdio.h>
#include "calc.h"
int main() {
printf("3 + 4 = %d\n", add(3, 4));
printf("10 - 7 = %d\n", sub(10, 7));
return 0;
}
1. 构建 & 使用静态库(.a)
# 1. 编译成位置无关的目标文件(现代推荐)
gcc -c -fPIC calc.c -o calc.o
# 2. 打包成静态库
ar rcs libcalc.a calc.o
# 或一步:ar cr libcalc.a calc.o(旧写法)
# 3. 查看静态库内容
nm -g libcalc.a # 或 ar -t libcalc.a
# 4. 编译 main(链接静态库)
gcc main.c -L. -lcalc -o main_static
# 5. 运行
./main_static
注意:-L. 表示当前目录找库,-lcalc 表示找 libcalc.a(去掉 lib 和 .a)
2. 构建 & 使用动态库(.so)
# 1. 编译成位置无关的目标文件(必须 -fPIC)
gcc -c -fPIC calc.c -o calc.o
# 2. 链接成共享库
gcc -shared calc.o -o libcalc.so
# 推荐带版本:
# gcc -shared -Wl,-soname,libcalc.so.1 -o libcalc.so.1.0.0 calc.o
# ln -s libcalc.so.1.0.0 libcalc.so.1
# ln -s libcalc.so.1 libcalc.so
# 3. 编译 main(链接动态库)
gcc main.c -L. -lcalc -o main_dynamic
# 4. 运行(最简单方式:当前目录)
LD_LIBRARY_PATH=. ./main_dynamic
# 更好方式:使用 rpath
gcc main.c -L. -lcalc -Wl,-rpath,. -o main_dynamic_rpath
./main_dynamic_rpath
查看动态依赖(非常实用):
ldd ./main_dynamic
readelf -d ./main_dynamic | grep NEEDED
三、进程地址空间(虚拟内存布局)完整解析
现代 Linux(x86_64 64位系统)一个进程的虚拟地址空间 ≈ 256TB(实际使用远小于此),典型布局如下:
0x0000000000000000
↓
+-----------------------------------+ ← 0x0000_0000_0000_0000
| 保留区(通常不可访问,catch null指针) |
+-----------------------------------+ ← ~0x0000_7fff_ffff_ffff
| 用户空间 (User Space) |
| 代码段 (.text) |
| 只读数据 (.rodata) |
| 已初始化数据 (.data) |
| 未初始化数据 (.bss) |
| 堆 (Heap) ↑ | ← brk / sbrk / malloc 增长方向
| mmap 区域(共享库、文件映射、匿名映射) | ← mmap 默认从这里分配
| 栈 (Stack) ↓ | ← 线程栈、main栈 向下增长
+-----------------------------------+ ← ~0x0000_7fff_ffff_ffff ~ 0xffff_ffff_ffff_ffff
| 内核空间 (Kernel Space) |
| 直接映射区、vmalloc、模块、内核代码等 |
+-----------------------------------+ ← 0xffff_ffff_ffff_ffff
32位经典布局(对比参考)(3:1 分割,用户3GB,内核1GB):
0x00000000 ────────────────
用户空间 (0 ~ 3GB)
.text → .rodata → .data → .bss → heap ↑
← mmap 区(共享库通常放这里)
stack ↓(从 0xc0000000 往下)
0xc0000000 ────────────────
内核空间 (3GB ~ 4GB)
0xffffffff
64位系统中,用户空间最高地址通常在 0x00007fffffffffff 附近,栈从高地址向下增长,mmap 从中间偏高位置开始分配。
关键区域详解
| 区域 | 增长方向 | 管理方式 | 典型内容 | /proc//maps 特征 |
|---|---|---|---|---|
| 代码段 | — | execve 加载 | .text、.rodata | r-xp … libxxx.so 或 exe |
| 数据段 | — | execve + 初始化 | .data、.bss | rw-p … |
| 堆 | ↑ | brk/sbrk/malloc | new、malloc、calloc、realloc | rw-p … [heap] |
| mmap 区 | ↓ 或随机 | mmap/munmap | 共享库(.so)、文件映射、匿名大块内存 | r-xp 或 rw-p … libxxx.so |
| 栈 | ↓ | 自动增长 | 局部变量、函数调用帧、参数 | rw-p … [stack] |
| vvar/vdso | — | 内核注入 | vsyscall 加速(如 gettimeofday) | r-xp … [vvar] / [vdso] |
常用查看命令
cat /proc/$$/maps # 当前 shell 的地址空间
pmap -x <pid> # 更人性化显示
size a.out # 查看各段理论大小
ldd a.out # 动态库依赖
readelf -l a.out # Program Headers(段)
四、面试/生产高频问题速查
- 静态库和动态库链接区别?什么时候选哪个?
- 动态库如何实现“改一个地方全生效”?
- 为什么共享库必须编译成 -fPIC?
- 进程栈和线程栈地址空间区别?
- malloc 很大时为什么会触发 mmap 而不是 brk?
- /proc//maps 里 [heap] 后面为什么还有一大堆 rw-p 没有标签?
- ASLR(地址随机化)随机的是哪些区域?
- 共享库的 GOT/PLT 表在哪个段?作用是什么?
总结一句话
静态库让程序“自带行李”,体积大但独立;动态库让程序“按需借衣服”,体积小、易升级但有运行时依赖。
进程地址空间是每个进程的“私人4GB/256TB沙盒”,内核通过页表 + mmap 把物理内存“切片出租”。
Linux 基础 IO 系列到此完结!希望你从“会用”升级到“理解底层原理”。
后续想深入哪个方向(文件系统、page cache、epoll、零拷贝、网络栈等),留言告诉我,我们继续拆!🚀