书 · 2003 · 兰德尔·布莱恩特(Randal E. Bryant)、大卫·奥哈拉伦(David R. O'Hallaron)

《深入理解计算机系统》

计算机与 AI书单第 6 名

一句话结论

计算机系统的抽象不是魔法:只有看清程序在机器里怎么被表示、翻译、存取和调度,才写得出正确、快速又安全的代码。

值不值得读完整本?

值得读完,但要当教材练着读,不能当读物扫一遍。这是少数能把“写代码的人”和“机器实际在干什么”接起来的书,价值大部分在配套实验里。如果你只想要概念地图,读第 1、2、3、6 章加虚拟内存一章就够了。

  • 先读第 1 章(系统总览)和第 2 章(整数与浮点的表示),这两章篇幅不大,却是后面所有章节的前提。
  • 第 3 章(机器级代码)是全书的骨架,一定要细读,最好边读边用 gcc -S 和 objdump 对照。读完后,缓冲区溢出等问题才会有具体画面。
  • 第 6 章(存储层次)和虚拟内存一章是回报最高的部分,对写应用和做性能优化都直接有用。
  • 第 4 章(用 Y86 讲处理器设计)和第 5 章(性能优化)可以按需取舍:不关心硬件流水线的话,第 4 章读思想即可;第 5 章先读结论和最后的小结。
  • 第 7 章(链接)、第 8 章(异常控制流)、网络与并发几章,写服务端或系统软件的人要细读,其余读者可以略读。
  • 一定要配合习题和实验(数据实验、二进制炸弹、缓冲区攻击、malloc 等)。不动手,这本书的收益会掉一半。
  • 注意版本:2003 年初版以 32 位 x86 为主,且有一章讲程序计时(后来被删)。现在学习更推荐第 3 版,核心思想不变,但汇编部分基于 x86-64。

核心观点

1. 抽象会漏,懂底层才能诊断问题

作者认为,高级语言、操作系统给程序员的是一层层抽象。但程序出错、变慢或被攻击时,原因往往在抽象之下。

为什么:抽象只在“正常情况”下保持幻象:整数其实会溢出,内存访问速度其实参差不齐,程序其实被翻译成了机器指令。这些细节平时被隐藏,一旦出 bug、变慢或有人刻意攻击,问题的根源就在被隐藏的那一层。如果程序员不知道下面发生了什么,面对症状就只能乱猜。所以作者把读者定位成“应用程序员”,而不是系统开发者:目标是让你在出事时知道往哪一层去查。

2. 数在机器里是有限的,整数和浮点都不是数学里的数

计算机用固定位数表示数,所以整数运算是模运算,浮点数是有限精度的近似,常见的数学恒等式因此会失效。

为什么:位宽是有限的(比如 32 位),超出范围的结果被截断,等价于对 2 的 w 次方取模。同一串比特按无符号和有符号解读,值不同,但运算的位级操作完全一样,所以混用有符号和无符号时,比较和转换会悄悄给出反直觉的结果。浮点数用有限位的尾数和指数,只能近似表示实数,结合律、分配律等性质因舍入而不再成立。理解了这些机制,你就能预判哪些表达式会出问题,而不是靠测试碰运气。

3. 机器级代码才是真正在运行的程序

编译器把 C 翻译成汇编和机器码,读懂这层翻译,才能看清程序真实的行为,也才能理解安全漏洞。

为什么:源码只是意图,CPU 执行的是指令。局部变量、函数参数和返回地址是怎样在栈上排布的,由编译器和调用约定决定,源码里看不见。一旦知道返回地址就存放在局部数组的“上方”,就能明白越界写入为什么能覆盖它,攻击者又为何能借此劫持控制流。同样,读汇编也能看出编译器有没有做你期望的优化,以及某段代码为什么慢。这样“程序到底做了什么”就有了可检验的答案。

4. 性能取决于存储层次与局部性,而不只是算法复杂度

程序快慢很大程度上取决于它如何访问存储层次,所以要有意识地写出有局部性的代码。

为什么:存储设备有一条普遍规律:越快越小越贵,越大越慢。系统用多级缓存把常用数据留在靠近 CPU 的小而快的层级,前提是程序反复访问相邻或最近用过的数据,也就是有时间局部性和空间局部性。如果程序的访问模式跳来跳去,缓存就频繁不命中,要回到慢的下一层取数据。因此同样复杂度的两段代码,因为遍历顺序不同,速度可以差几倍甚至更多。在处理器内部,指令级并行和流水线也有类似的限制,所以要消除不必要的依赖和冗余操作。

5. 进程与虚拟内存给每个程序造出“独占机器”的假象

操作系统通过异常控制流和虚拟内存,让每个程序以为自己独占 CPU 和独占一整块内存。

为什么:CPU 同一时刻只能运行一个控制流,所以系统靠异常(中断、陷阱、故障)把控制权交给内核,再通过上下文切换在多个进程间快速轮换,给人并行的感觉。内存方面,每个进程使用自己的虚拟地址,由硬件配合页表翻译到物理地址,同一块物理内存可以被安全共享,也可以按需换进换出。这套机制同时提供了隔离(一个进程越界不会碰到别人)、简化编程(链接和加载只需面对统一的地址空间)和更高的内存利用率。理解它,你才明白段错误、缺页和 fork 之类的行为从何而来。

6. 系统是多个层次的组合,I/O、网络和并发的难点来自“共享”

到了文件、网络和多线程这一层,系统的复杂性大多来自多个执行流共享资源和状态,需要严格的规则才能保证正确。

为什么:Unix 把大量设备抽象成“文件”,这样一套读写接口就能同时对付磁盘、终端和套接字,网络程序因此能建立在同样的 I/O 抽象上。并发则不同:多个线程共享数据时,指令交错的顺序不可预测,同一个程序可能跑几百次才出一次错,也就是竞争条件。死锁则来自循环等待资源。作者的思路是不能靠“多测几次”,而要用同步原语(比如信号量)加推理来保证安全,并先搞清哪些变量是共享的。

反对意见

“C 与汇编就是机器”只是个历史遗留的幻觉(大卫·奇斯诺尔,《C 不是一门低级语言》)

奇斯诺尔(David Chisnall)认为,C 的抽象机器是 PDP-11 时代的顺序执行模型,现代处理器靠乱序执行、推测、缓存层次等复杂机制,才勉强维持住这个假象。因此把“读懂 C 和汇编”当成理解机器的捷径,会给人一种过于简单的画像。他的文章并非专门针对本书,但这个批评直接指向本书的“程序员视角”:书里的 ISA 模型比真实硬件简单得多。

理解计算机应该自底向上亲手造一遍(诺姆·尼桑、肖恩·肖肯,《计算机系统要素》)

尼桑(Noam Nisan)和肖肯(Shimon Schocken)的教学思路是:从与非门出发,一路构造出计算机、汇编器、编译器、操作系统和应用程序,通过亲手搭建来理解系统。与本书“站在程序员视角自上而下看”相对,这种看法认为不亲手造过各层,理解会停留在表面。两本书其实可以互补。

过早优化是万恶之源(高德纳,《带 goto 语句的结构化程序设计》)

高德纳(Donald Knuth)在这篇 1974 年的论文里提醒:程序员常把时间花在非关键部分的小效率上,应当先测量,再优化真正的热点。这篇文章不是针对本书写的,但可以拿来提醒读者:学完第 5、6 章后,别在没有测量的情况下就动手做手工调优。本书自己也强调要先找瓶颈。

程序员眼中的 ISA 不足以解释现代处理器的安全问题(科赫等,《幽灵攻击》)

科赫(Paul Kocher)等人的论文显示,推测执行会在微架构状态(比如缓存)里留下痕迹,让攻击者读出本不该读的数据,而这在指令集层面的顺序语义里完全看不到。这说明只靠“程序员视角”的机器模型,会漏掉一整类安全问题。这是我对该论文意义的归纳,作者们并没有直接批评这本书。

相关推荐

导读由 AI 根据公开资料整理,没有读取原书全文,一切以原著为准。导读 Primer 首页