本页目录

CSAPP III · 链接与异常控制流

对标:CS:APP 第 7–8 章 | 前置:csapp-01/02 两个常被跳过、却是"程序如何真正运行起来"的关键机制:链接(多个源文件、库怎么拼成一个可执行文件,为什么会有那些诡异的 "undefined reference" 和 "multiple definition")与异常控制流(程序的执行怎么被硬件中断、系统调用、信号打断和切换——一切并发与操作系统的物理起点)。

1. 链接:符号的解析与重定位

多个 .o → 符号解析 + 重定位 → 可执行文件的拼装。

图 csapp-03.3多个 .o → 符号解析 + 重定位 → 可执行文件的拼装。

你的程序由多个 .c 编译成多个 .o,还要连上 libc——链接器负责把它们拼成一个可执行文件,干两件事:

强弱符号规则(C 的隐藏坑):函数和已初始化全局变量是强符号,未初始化全局变量是弱符号;同名强弱并存时选强的、多个弱的任选一个——这条规则会让"两个文件各有一个同名全局变量"静默共享同一块内存,酿成极难查的 bug。理解它你才敢用 static 把符号限制在文件内(内部链接)。

2. 静态库 vs 动态库:链接的时机

为什么你要懂LD_LIBRARY_PATH 找不到 .so、容器里缺库、Python 的 C 扩展 ABI 不兼容——这些日常报错全是动态链接机制的表象(🔗 cloud-01 容器为什么要打包依赖、web 线的部署问题都源于此)。

3. 异常控制流:程序执行被打断的四种方式

异常控制流四类(中断/陷阱/故障/终止):触发者、同异步、是否返回。

图 csapp-03.2异常控制流四类(中断/陷阱/故障/终止):触发者、同异步、是否返回。

到目前为止程序是"一条指令接一条"的顺流。但真实系统里,执行随时被打断——这叫异常控制流(ECF),是并发与操作系统的物理基础。四类(按谁触发、是否返回):

类型 触发者 例子 返回
中断 interrupt 外部硬件(异步) 网卡收包、时钟滴答 返回下一条
陷阱 trap 程序主动(同步) 系统调用 syscall 返回下一条
故障 fault 错误(可能可恢复) 缺页、除零 重试或终止
终止 abort 不可恢复 硬件校验错 不返回

关键洞察"系统调用"就是一次受控的陷阱——用户程序想读文件/开进程,无权直接碰硬件,于是执行 syscall 主动陷入内核(切到特权态),内核代劳后返回。用户态/内核态的边界、以及跨越它的唯一合法通道(系统调用),是操作系统安全模型的地基(🔗 os-01)。缺页故障则是虚拟内存(csapp-04)的引擎——访问未映射页触发 fault,内核悄悄把页调进来再重试,程序毫无察觉。

4. 进程与信号:ECF 在用户层的两个抽象

操作系统把 ECF 包装成两个程序员能用的抽象:

fork()(复制出子进程)、exec()(换上新程序)、wait()(回收子进程)——这三个系统调用是 Unix 进程模型的全部核心,也是 [实验 L02 手写 shell] 的主角:shell 就是一个 fork + exec + wait 的循环。

5. 练习与要点

fork();fork(); 产生 4 进程的进程树。

图 csapp-03.1fork();fork(); 产生 4 进程的进程树。

例 1(读懂链接错误) 给定 "undefined reference to foo",列出三种可能原因(没链接含 foo 的库 / 声明了没定义 / C++ 名字修饰不匹配)。把报错映射到符号解析的哪一步失败——这是真实调试力

例 2(fork 的数感) fork() 后父子进程都从 fork 返回处继续,父得子 PID、子得 0——写四行代码预测 fork(); fork(); 产生几个进程(答:4)。理解"复制整个执行状态"这个 Unix 精髓,是 L02 的入场券。

例 3(系统调用可见)strace ls(Linux)/ dtruss(Mac)看 ls 到底发了哪些系统调用——openat/read/write/close"程序与内核的所有对话都在这张系统调用清单里"亲眼看一次,操作系统就不再抽象。\(\blacksquare\)

▶ 实验 L02(手写 shell)labs/L02-shell/ —— 实现 fork/exec/pipe/重定向/后台作业,把本页的进程控制系统调用全部用一遍。对标 CS:APP shell lab。


下一页:CSAPP IV——虚拟内存与动态内存:每个进程独占内存的幻觉如何造出来,malloc 底下是什么。