Skip to content

深入理解计算机系统

Computer Systems: A Programmer's Perspective

🧭 课程愿景

如果不理解底层,就无法写出最高效的代码。CSAPP的讲解基于以下核心原则,旨在帮助我们成为更优秀的程序员,并为后续的高级系统课程(如编译器、操作系统、网络)做好准备:

  1. 程序员的视角
    • 应该从“程序员”的角度而不是传统的“系统实现者”角度来接触计算机系统。
    • 这意味着什么? 我们关注的是:了解底层硬件、操作系统和编译器如何影响我们应用程序的正确性、性能和可靠性。
  2. 系统全貌
    • 应该获得对整个计算机系统的完整视野,涵盖硬件、操作系统、编译器和网络,而不是孤立地看待它们。
  3. 实战驱动
    • 学习的最佳方式是在真实的机器上开发和评估真实的程序

我们涵盖了数据表示、C 程序的机器级表示、处理器体系结构、程序优化、存储层次结构、链接、异常控制流(异常、中断、进程和 Unix 信号)、虚拟内存与内存管理、系统级 I/O、基础网络编程以及并发编程。

这些概念都由一系列有趣且动手能力极强的 Lab 作为支撑。


💥 核心实验

这是 CMU 15-213 的灵魂。所有实验代码都以自包含的 tar 包形式分发。 注:以下内容基于 CMU 最新发布的 64 位版本实验。

1. Data Lab

  • 核心任务:实现简单的逻辑函数、补码运算和浮点数函数。
  • 挑战:必须使用 高度受限的C语言子集(例如,只能使用位级操作符且不能使用循环或条件语句)。
  • 目的:例如,仅使用位运算和直线代码计算数字的绝对值。这能帮助我们深刻理解C语言数据类型的位级表示以及数据操作的底层行为。

2. Bomb Lab

  • 核心任务:一个“二进制炸弹”是一个作为目标代码文件提供的程序。运行时,它会要求用户输入 6 个不同的字符串。如果输入错误,炸弹就会“爆炸”(打印错误信息并扣分)。
  • 挑战:必须通过 反汇编逆向工程 来“拆除”属于他们自己的独特炸弹,找出那 6 个正确的字符串。
  • 目的:理解汇编语言,并强制学会使用 GDB 调试器。这是一个传奇般的实验,非常有趣。

3. Attack Lab

  • 说明:这是原版 32 位 Buffer Lab 的 64 位版本。
  • 核心任务:获得一对具有缓冲区溢出漏洞的自定义 x86-64 二进制可执行文件(称为 targets)。
  • 挑战
    1. 利用 代码注入 攻击第一个目标。
    2. 利用 面向返回编程 攻击第二个目标。
  • 目的:教授栈的规则,并展示编写易受缓冲区溢出攻击的代码的危险性。

4. Architecture Lab

  • 核心任务:获得一个基于 Y86-64 指令集的流水线处理器设计,以及一个数组复制函数。
  • 挑战:修改 Y86-64 的处理器设计(HCL 硬件描述语言)和汇编代码,以最小化每个数组元素的时钟周期数 (CPE)。
  • 目的:对硬件和软件之间的交互有深刻的理解。

5. Cache Lab

  • 核心任务
    1. 编写一个通用的 Cache 模拟器。
    2. 优化一个矩阵转置核心函数。
  • 挑战:利用 Valgrind 生成地址追踪,并最大限度地减少模拟 Cache 上的未命中数。
  • 目的:展示 Cache 存储器的特性,并积累底层程序优化的经验。

6. Shell Lab

  • 核心任务:实现自己的简易 Unix Shell 程序。
  • 挑战:必须支持作业控制 (Job Control),包括 ctrl-cctrl-z 按键处理,以及 fg, bg, jobs 命令。
  • 目的:第一次接触 应用级并发,并让他们对 Unix 进程控制、信号和信号处理有清晰的认识。

7. Malloc Lab

  • 核心任务:实现自己的 malloc, freerealloc 函数库。
  • 挑战:要求评估 空间利用率时间效率(吞吐量)之间的权衡。
  • 目的:真正理解指针、内存布局和碎片处理!

8. Proxy Lab

  • 核心任务:实现一个并发的缓存 Web 代理服务器,它位于浏览器和万维网之间。
  • 挑战:处理 HTTP 请求解析、转发和缓存。
  • 目的:将课程中的许多概念联系起来:字节序、缓存、进程控制、信号处理、文件 I/O、并发和同步。

⚖️ 编码规范

"Just as important as the functionality of your code is your code's readability."

1. 良好的文档 (Documentation)

代码应该是自文档化的。注释不应描述代码“做什么”,而应描述“为什么”。

  • 文件头 (File Header):每个文件必须包含注释,描述文件的用途以及它如何适应整个项目。这也是放置你的姓名和邮箱的地方。
  • 函数头 (Function Header):每个函数前应有注释,描述函数的功能、参数、返回值、相关的错误情况、副作用以及函数所做的任何假设。
  • 复杂代码块:对于长代码块或晦涩的指针运算,必须添加解释性注释。

2. 空白与格式 (Whitespace)

  • 缩进:每次打开代码块(函数、if、loop)时,缩进一个层级。
  • 一致性:你可以自由选择缩进风格(建议使用空格而非 Tab),但必须在整个项目中保持一致。
  • 行宽:代码行不得超过 80 个字符
    • Tip: 运行 wc -L file.c 来检查最大行宽。

3. 变量命名 (Variable Names)

  • 变量名应描述其中存储的值。
  • 局部变量:如果是自解释的(如循环计数器 i),可以使用单字母。
  • 参数:使用一个精心挑选的单词。
  • 全局变量:应包含两个或更多单词,且格式统一(例如统一使用 hashtable_array_size,不要混用驼峰和下划线)。

4. 拒绝魔术数字 (Magic Numbers)

  • 不要直接在代码中使用像 256 这样的数字。
  • 规范:使用 #define BUFLEN 256,然后在代码中使用 BUFLEN

5. 模块化与健壮性 (Modularity & Robustness)

  • 无死代码:提交的代码中不应包含未运行的代码(如注释掉的 printf)。
  • 错误检查必须考虑失败情况
    • 如果 malloc 返回 NULL 怎么办?
    • 如果用户输入的文件不存在怎么办?
    • 网络服务器必须特别健壮,不能因为一个客户端的错误而崩溃。
  • 内存管理:分配的内存必须释放(无内存泄漏),打开的文件必须关闭(防止缓冲区数据丢失)。

🛠️ 参考硬件环境

我们在 Lab 0 环境配置中,将尽可能模拟 CMU "Shark Machines" 的核心参数:

🦈 CMU Shark Machine Specs (Reference)

  • OS: 64-bit Enterprise Linux (kernel 2.6.18+)
  • CPU: Intel Xeon (Nehalem microarchitecture or newer)
  • Cache Structure:
    • L1 Cache: 32 KB (Instruction) + 32 KB (Data), 8-way associative.
    • L2 Cache: 256 KB Unified, 8-way associative.
    • L3 Cache: 8 MB Unified, 16-way associative (Shared).
    • Block Size: 64 bytes for all levels. :::

🚀 Ready to start?

建议按照侧边栏顺序,从 第 0 章:环境配置 开始,搭建属于你的 Linux 实验台。

版权所有 © 2025-至今 赵熠楷(Yikai Zhao)