CSAPP 阅读笔记 (1)
CSAPP 阅读笔记 (1)
第 1 章:计算机系统漫游。这一章通过研究 “hello, world” 这个简单程序的生命周期,介绍计算机系统的主要概念和主题。
本章通过讲述如下的一个简单的源代码从编译到运行过程中发生的事情阐述了计算机以及操作系统工作的原理以及其中的一些重要的特性。
1 | |
编译系统
我们通常说的编译(也就是输入 gcc hello.c -o hello 再回车)实际上发生了四件事情:预处理,编译,汇编,链接。相应的编译系统也包括预处理器、编译器、汇编器和链接器。
预处理主要是文本的替换,比如 C 中以 # 开头的宏命令。
编译是从高级语言转换为汇编语言。笔者之前做编译原理的课设在这里花了很大功夫,虽然大部分都是机械性的工作。
汇编总是让人忽略,以至于我总是习惯性地说预处理、编译、链接。实际上汇编这一步才是真正完成了从文本文件到二进制文件的跨越。这个阶段的产物可重定位目标程序 (relocatable object program) 算是我经常听说但是没有深究的名词。但实际上,这个概念还是很复杂且重要的。我们将在链接阶段讲述。
链接阶段是将多个可重定位目标程序合并的过程。这里的链接本质就是 “重定位”。可重定位目标程序之所以被称为 “可重定位” (Relocatable),是因为它内部的代码和数据没有固定的内存绝对地址,链接器可以根据需要把它们重新放置(Relocate)到内存的任何合法位置。
如果你尝试用 objdump -d hello.o 命令查看,会发现以下内容:
1 | |
这里 e8 00 00 00 00 call e <main+0xe> 实际上是想要调用 puts 库函数,但是此时并不知道 puts 的位置,所以只能用全 0 来占位。
链接器根据可重定位目标程序中的重定位条目表来给这些占位填入真实的地址。但是请注意,这里虽然称作真实的地址,但是实际上还是虚拟地址,而不是物理地址。
如果你尝试用 objdump -r hello.o 命令,会发现以下内容:
1 | |
这里的 000000000000000a 正是上文占位的地址的偏移量,.text 的重定位条目表告诉链接器这里实际要填入 puts 的偏移量减 4(PC 相对寻址)。不过这里是动态链接,并不是真正的地址,这部分我也还没有完全弄懂,后面的章节应该会涉及。
此外还要说的是,我的环境下直接按照书上的命令运行会由于编译器版本以及优化等级的问题导致结果不同,需要使用 gcc -Og -fno-pie -fcf-protection=none 参数。
计算机硬件系统
计算机系统的硬件由我们熟知的总线、I/O 设备、主存和处理器四个部分组成。各个部分的功能就不多赘述。
对于 hello 程序:当我们在 shell 中输入 ./hello 并回车之后,从硬件的视角看大致发生了三件事:shell 先把键入的字符串逐一读入寄存器,再存入主存;回车后 shell 通过一系列指令把磁盘上 hello 目标文件中的代码和数据复制到主存,这一步利用的是直接存储器存取 (DMA) 技术,数据直接从磁盘到达主存而不经过处理器;最后处理器开始执行 hello 中的机器指令,把 “hello, world\n” 字符串的字节从主存复制到寄存器,再从寄存器复制到显示设备,我们就在屏幕上看到了输出。
整个过程不过是在各个部件之间来回搬运字节,但计算机系统的其他所有内容都建立在这个朴素的搬运之上。
显而易见,这个过程其实并没有太多的计算,大部分是系统把数据从一个硬件搬到另一个硬件。根据后文提到的 Amdahl 定律,优化数据传输的过程是很有必要的。使用缓存技术可以有效解决这一问题。其中高速缓存(cache)是为了弥补处理器与主存之间的速度差异。相较于主存使用 DRAM,cache 使用的是 SRAM,访存速度更快。这也提示我们写缓存友好的代码。比如经典的矩阵乘法的优化,只需要交换循环顺序就能很高效地提升运行速度。
操作系统
操作系统是夹在应用程序和硬件之间的一层软件,应用程序对硬件的任何操作都必须经过它。它的主要功能有二:一是防止硬件被失控的应用程序滥用,二是向应用程序提供简单一致的机制来操纵复杂且千差万别的低级硬件设备。这两个功能通过进程、虚拟内存和文件三个抽象来实现。
进程是操作系统对正在运行的程序的一种抽象。多个进程可以并发执行,不过这里的并发并不是真正的同时执行,而是进程交替执行:操作系统通过上下文切换 (context switching) 让处理器在进程间来回轮转,给每个进程造成独占处理器的假象。上下文 (context) 就是内核为了恢复一个被挂起进程所需的全部状态信息,包括通用寄存器、PC、用户栈以及页表、进程表、文件表等内核数据结构。执行切换动作的是内核 (kernel) 代码,但内核本身并不是一个独立的进程,而是系统管理全部进程所用的代码和数据结构的集合,这一点笔者以前一直没搞清楚。上下文切换是有代价的,除了保存恢复现场的开销,它还会打乱缓存,影响前面提到的缓存友好性。
回到 hello 程序。shell 运行它时实际发生了两个系统调用:fork 创建一个新进程,execve 把 hello 加载到这个新进程中执行,上一节所说的 DMA 搬运就发生在这里。此后 hello 进程和系统中其他进程交替运行,我们在终端里看到的“同时”运行的各种程序,本质上都是上下文切换的结果。
虚拟内存是操作系统对主存和磁盘 I/O 设备的抽象,它为每个进程提供了一个假象:每个进程独占整个主存。每个进程看到的地址空间结构都相同,从低地址到高地址依次是程序代码和数据、堆 (heap)、共享库、用户栈 (stack),最顶端是内核保留的内核虚拟内存区。堆向上增长,是 malloc/free 操作的对象;栈向下增长,用于实现函数调用。回忆编译一节提到的“所谓的真实地址其实是虚拟地址”,背后正是这个抽象:不同进程看到的相同虚拟地址会被映射到不同的物理位置,这同时也防止了进程之间互相干扰。
文件是字节的序列,仅此而已。每个 I/O 设备,包括磁盘、键盘、显示器甚至网络,都可以被看作文件,系统中所有的输入输出都是通过读写文件完成的,这也就是 Unix “一切皆文件” 思想的由来。hello 程序的 printf 归根结底是向 stdout 这个文件写入字节,操作系统再把这些字节交给显示器。从这个角度看,键盘、显示器和磁盘并没有本质区别,都只是字节流的来源或去向。
本章其余内容
最后的一些内容更多是概念,这里简单罗列一下:Amdahl 定律指出,加速系统某一部分时,整体加速比受限于这一部分占总时间的比例,所以要优先优化那些时间占比大的部分;并发指同时有多个活动的系统,并行指用并发使系统更快,可分为线程级并发、指令级并行和单指令多数据 (SIMD) 并行;抽象是计算机系统中最重要的概念,指令集架构是对处理器的抽象,进程、虚拟内存、文件是对操作系统的抽象,虚拟机是对整个计算机的抽象。全书的其余部分,就是不断深入这些抽象、将其一一拆开的过程。