CSAPP 计算机系统漫游

2018-12-24

1.1 信息就是二进制位+上下文

例如 C 语言源码:

hello.c
#include <stdio.h>

int main() {
    printf("hello, world\n");
    return 0;
}

用 vim 以二进制方式打开 vim -b hello.c,由于是纯 ASCII 码写成的,所以和以文本方式打开没有区别,然后使用 :%!xxd 转换为 16 进制 ASCII 码:

00000000: 2369 6e63 6c75 6465 203c 7374 6469 6f2e  #include <stdio.
00000010: 683e 0a0a 696e 7420 6d61 696e 2829 207b  h>..int main() {
00000020: 0a20 2020 2070 7269 6e74 6628 2268 656c  .    printf("hel
00000030: 6c6f 2c20 776f 726c 645c 6e22 293b 0a20  lo, world\n");. 
00000040: 2020 2072 6574 7572 6e20 303b 0a7d 0a       return 0;.}.

1.2 程序被其他程序翻译成不同的格式

以下运行环境是 Linux debian 4.19.0-9-amd64 + GCC 8.3.0。

GCC 编译保留中间文件:

$ gcc -save-temps -o hello hello.c

翻译过程:

  • 预处理。

    预处理器(cpp)根据以 # 开头的命令修改原始 C 程序,例如根据 hello.c 中的 #include <stdio.h> 读取系统头文件 stdio.h 中的内容,并将其直接插入程序文本,得到另一个 C 程序 hello.i

    hello.i
    # 1 "hello.c"
      # 1 "<built-in>"
      # 1 "<command-line>"
      # 31 "<command-line>"
      # 1 "/usr/include/stdc-predef.h" 1 3 4
      # 32 "<command-line>" 2
      # 1 "hello.c"
      # 1 "/usr/include/stdio.h" 1 3 4
      # 27 "/usr/include/stdio.h" 3 4
      # 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4
  • 编译。

    编译器(ccl)将 hello.i 翻译成文本文件 hello.s(和书上有区别),它包含一个汇编语言程序,该程序包含函数 main 的定义:

    hello.s
    	.file	"hello.c"
      	.text
      	.section	.rodata
      .LC0:
      	.string	"hello, world"
      	.text
      	.globl	main
      	.type	main, @function
      main:
      .LFB0:
      	.cfi_startproc
      	pushq	%rbp
      	.cfi_def_cfa_offset 16
      	.cfi_offset 6, -16
      	movq	%rsp, %rbp
      	.cfi_def_cfa_register 6
      	leaq	.LC0(%rip), %rdi
      	call	puts@PLT
      	movl	$0, %eax
      	popq	%rbp
      	.cfi_def_cfa 7, 8
      	ret
      	.cfi_endproc
      .LFE0:
      	.size	main, .-main
      	.ident	"GCC: (Debian 8.3.0-6) 8.3.0"
      	.section	.note.GNU-stack,"",@progbits
      
  • 汇编。

    汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成可重定位目标程序,并将结果保存到二进制文件 hello.o 中。

  • 链接。

    hello 程序调用了 printf 函数,它存在于一个名为 printf.o 的单独的预编译好的目标文件中,链接器(ld)将它合并到 hello.o 程序中,最终得到可执行文件 hello

1.7 操作系统管理硬件

1.7.3 虚拟内存

虚拟内存是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存,每个进程看到的内存都是一致的,称为虚拟地址空间。基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作为磁盘的高速缓存。

Linux 中,虚拟地址空间从低到高有:

  • 程序代码和数据。代码和数据区直接按照可执行目标文件的内容初始化。

  • 堆。代码和数据区在开始运行时就被指定了大小,而调用 mallocfree 这样的 C 标准函数时,堆可以在运行时动态扩展和收缩。

  • 共享库。存放像 C 标准库和数学库这样的共享库的代码和数据的区域。

  • 栈。位于用户虚拟地址空间顶部,编译器用它来实现函数调用,在程序执行期间可以动态地扩展和收缩。

  • 内核虚拟内存。地址空间顶部区域是为内核保留的,不允许应用程序读写这个区域的内容或直接调用内核代码定义的函数,而必须调用内核来执行这些操作。

1.7.4 文件

文件就是字节序列,每个 I/O 设备包括磁盘、键盘、显示器甚至网络,均可看作文件,系统中所有输入输出都是通过称为 Unix I/O 的系统函数调用读写文件来实现的。

1.9 重要主题

1.9.1 Amdahl 定律

若某程序需要时间为 $T_{old}$,某部分所需执行时间占比为 $\alpha$,该部分性能提升比例为 $k$,则总的执行时间变为

$$T_{new} = (1 - \alpha)T_{old} + (\alpha T_{old}) / k = T_{old}[(1 - \alpha) + \alpha / k]$$

加速比 $S = T_{old} / T_{new}$ 为

$$S = \frac{1}{(1 - \alpha) + \alpha / k}$$

Amdahl 定律说明,要想显著加速整个系统,必须提升全系统中相当大的部分。假设我们可以将系统某一部分加速到时间可以忽略不计,即 $k$ 趋向于 $\infty$,则

$$S_\infty = \frac{1}{1 - \alpha}$$

我们获得的加速比仍然是很有限的。

1.9.2 并发和并行

  1. 线程级并发

    多核处理器是将多个 CPU(称为“核”)集成到一个集成电路芯片上。

    超线程是允许一个 CPU 执行多个控制流的技术,涉及 CPU 某些硬件有多个备份,比如程序计数器和寄存器文件(寄存器堆),而其他硬件只有一份,比如执行浮点算数运算的单元。

  2. 指令级并行

    现代处理器可以同时执行多条指令的属性称为指令级并行。

    采用流水线技术。

    如果处理器执行速率可以达到比每个时钟周期一条指令更快的执行速率,就称之为超标量处理器。

  3. 单指令、多数据并行

    SIMD 并行,现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,例如并行地对 8 对单精度浮点数做加法。

CSAPP读书笔记

本作品根据 署名-非商业性使用-相同方式共享 4.0 国际许可 进行授权。

CSAPP 信息的表示和处理

幂运算 pow(x, n) 的一个迭代实现