深入理解计算机系统(第三版)笔记

第1章 计算机系统漫游

GitHub笔记地址:https://github.com/loulan-ling/CSAPP3e-Notes

1.1 程序编译流程

编译命令

1
2
3
4
gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s -o hello.o
gcc -o hello hello.o -Wl -t

1、源程序文本
hello.c

    1. 源代码文本
1
2
3
4
5
6
7
8
#include <stdio.h>

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

}

经过预处理器(cpp)后,会将stdio.h的代码添加到hello.c中,这时候程序后缀是.i

    1. 编译阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 "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

......

extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;


extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 857 "/usr/include/stdio.h" 3 4
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 874 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2


# 3 "hello.c"
int main()
{
printf("hello, world\n");
return 0;

预处理结束后到了编译阶段,编译器(ccl)会将修改了的源程序代码编译为汇编语言,版本不一样的汇编时的代码也不一致

    1. 汇编阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
       .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
movl $.LC0, %edi
call puts
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 10.2.1 20200825 (Alibaba 10.2.1-3.8 2.32)"
.section .note.GNU-stack,"",@progbits

现在到了汇编阶段,汇编器(as)会将hello.s翻译为机器语言

    1. 汇编阶段
      使用vim查看hello.o只能看到乱码

使用objdump可以看到十六进制的机器语言

1
2
3
4
5
6
7
8
9
10
11
12
hello.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 callq e <main+0xe>
e: b8 00 00 00 00 mov $0x0,%eax
13: 5d pop
  • 5 链接阶段
    hello程序调用了printf函数,该函数是每个C编译器都提供的标准C库中的一个函数。printf函数存在一个名为printf.o的单独预编译好了的目的文件中,而这个文件会在链接的过程中合并到我们的hello.o程序中,连接器(ld)就负责处理这种合并。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gcc -o hello hello.o -Wl,-t
/usr/lib/gcc/x86_64-redhat-linux/10/../../../../lib64/crt1.o
/usr/lib/gcc/x86_64-redhat-linux/10/../../../../lib64/crti.o
/usr/lib/gcc/x86_64-redhat-linux/10/crtbegin.o
hello.o
/usr/lib/gcc/x86_64-redhat-linux/10/libgcc.a
/usr/lib/gcc/x86_64-redhat-linux/10/libgcc_s.so
/lib64/libgcc_s.so.1
/usr/lib/gcc/x86_64-redhat-linux/10/libgcc.a
/usr/lib/gcc/x86_64-redhat-linux/10/../../../../lib64/libc.so
/lib64/libc.so.6
/usr/lib64/libc_nonshared.a
/lib64/ld-linux-x86-64.so.2
/usr/lib64/libc_nonshared.a
/lib64/ld-linux-x86-64.so.2
/usr/lib/gcc/x86_64-redhat-linux/10/libgcc.a
/usr/lib/gcc/x86_64-redhat-linux/10/libgcc_s.so
/lib64/libgcc_s.so.1
/usr/lib/gcc/x86_64-redhat-linux/10/libgcc.a
/usr/lib/gcc/x86_64-redhat-linux/10/crtend.o
/usr/lib/gcc/x86_64-redhat-linux/10/../../../../lib64/crtn.o

1.2 可重定位目标程序(relocatable object program)格式

可重新定位目标程序格式是一种中间代码格式,通常由汇编器(as)生成。这种格式的主要作用是支持程序的动态加载和重定向,从而使得程序能够在不同的内存位置上执行。

  1. 格式结构:
    • 机器指令的二进制编码:
      • 可重定位目标程序包含了编译后的机器指令,这些指令由汇编器生成,是计算机能够直接执行的代码。
    • 位置信息:
      • 此格式还包含重定位信息,用于指示在程序最终链接时需要调整的地址。这些信息用户管理不同模式之间的地址依赖,确保程序在加载时可以正确引用。
    • 符号表:
      • 可重定位目标程序通常会包含符号表,列出所有的函数和全局变量符号,以供后续连接器解析。这使得程序在链接阶段能够顺利处理不同模块之间的符号引用。
  2. 生成过程:
    • 预处理: 将源代码进行处理,添加标准库代码,生成预处理文件
    • 编译: 将预处理文件翻译位汇编语言
    • 汇编: 汇编器将汇编代码转换为机器代码,同时生成可重定位(如hello.o),其中包括机器指令和相关

1.4 处理器读并解释储存在内存中的指令

shell是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。如果该命令行的第一个单词不是一个内置的shell命令,那么shell会认为这个命令是一个可执行的文件的名字,它将会加载并运行文件。

1.4.1 系统的硬件组成

系统硬件组成思维导图

1.4.1.1 总线

贯穿整个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部件间传递。

1.4.1.2 I/O设备

I/O(输出/输入)设备是系统与外部世界的联系通道。
示例中展示的I/O设备:

  • 键盘
  • 鼠标
  • 显示器
  • 磁盘

1.4.1.3 主存(内存条)

主存是处理器执行程序时用于临时存储程序及其数据的设备。物理上,主存由动态随机存取存储器(DRAM)芯片组成;逻辑上,它是一个线性的字节数组,每个字节都有唯一的地址。程序指令由不同数量的字节构成,C程序变量的大小取决于其数据类型(例如,在x86-64 Linux系统上,short类型占2字节,int和float字节占4字节,long和double字节占8字节)。这里只是说明程序指令和C程序变量会根据情况变化

1.4.1.4 处理器(CPU)

CPU是计算机系统的核心处理引擎,负责解释和执行存储在主存中的指令
CPU主要组成部件

  1. 程序计算器(PC)
    • 存储当前执行指令的地址
    • 决定程序执行的指令流
  2. 寄存器文件
    • 小型存储设备,由单字长寄存器(寄存器是CPU中用于存储数据和指令的小型存储设备)组成
    • 每个寄存器都有唯一名称
    • 可存储临时数据和指针
  3. 算术/逻辑单元(ALU)
    • 执行算术和逻辑单元
    • 计算新的数据和地址值
  4. 高速缓存(Cache)
    • 多级缓存: L1 L2 L3
    • 分为指令高速缓存(i-cache)和数据高速缓存(d-cache)
    • 提高数据访问速度
  5. 控制单元(CU)
    • 控制指令的解码和执行流程
    • 协调各个组件的操作

CPU的基本功能

  • 加载: 从主存复制数据到寄存器
  • 存储: 从寄存器复制数据到主存
  • 操作: 执行算术和逻辑运算
  • 跳转: 更新程序计算器

1.4.2 运行hello程序

指令到达寄存器后,CPU就会执行指令。
image-20250819181441239

1.5 高速缓存只关重要

根据上面示例可以看到,运行程序就是将信息从一个地方挪到另一个地方,最后到达处理器。

信息运行速度表格

存储设备类型 容量 访问时间相对(从处理器访问) 备注
寄存器文件 几百字节 ≈主存的1000倍快 用于存储最常用的数据,速度非常快
主存(DRAM) 几十亿字节 假设1 容量大,但访问速度相对较慢
磁盘驱动器 大约主存的1000倍 ≈ 主存的10000000(1000万倍) 存储容量极大,但访问速度极慢

针对处理器和主存之间的差异,系统设计者采用了更小更快的存储设备,成为高速缓存存储器。

  • L1高速缓存位于处理器芯片上,容量是数万字节,速度仅比寄存器慢
  • L2高速缓存通过一条特殊的总线连接处理器,容量为数十万到数百万字节,速度比L1慢5倍,但是依然比主存快5~10倍

1.6 存储设备形成层次结构

看图

1.7 操作系统管理硬件

操作系统是介于应用程序和硬件之间的软件

操作系统有两个基本功能:

  1. 防止硬件被失控应用程序滥用
  2. 向应用程序提供简单一致的机制控制不同的硬件设备
    操作系统通过几个基本的抽象概念(进程、虚拟内存和文件)来实现这两个功能。

文件只是对I/O设备的抽象表示
虚拟内存是对主存和磁盘I/O设备的抽象表示
进程则是对处理器、主存和I/O设备的抽象表示

1.7.1 进程

一个处理器可以运行多个进程,每个进程在一个线性上来回运行不同的程序,操作系统内核管理进程时,会为每个进程提供一个假象,就是每个进程都能独自占有整个处理器,处理器也只有占有的那么大。
关键点:

  • 并发执行: 多个进程可以在同一个系统上并发运行,这意味着一个进程的指令和另一个进程的指令交错执行。
  • 多任务: 操作系统使用多任务处理(或时间片)技术,将处理器时间划分时间片,给每个进程安排一个时间片,让进程在时间片内运行(如果时间片时间到了,进程还没结束将会保留上下文切换),通过快速切换进程,操作系统可以实现多进程同时运行的效果。
  • 上下文切换: 当操作系统决定切换到另一个进程时,它会保存当前上下文信息(例如, 程序、计数器、寄存器内容),然后恢复上下文。

1.7.2 线程

每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。
线程切换上下文的速度比进程切换上下文速度快很多,因为线程之间会共享大部分的信息。线程切换上下文只需保存线程独有的信息,例如:栈、寄存器等。
多线程一般来说是比多进程更高效。

1.7.3 虚拟内存

虚拟内存是一个抽象概念,它为每个进程提供一个假象,即每个进程都在独占使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。

  • 程序代码和数据。对所有的进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置。
  • 堆。代码和数据区后紧随着的是运行时堆。堆可以在运行时动态的扩展和收缩。
  • 共享库。地址空间的中间部分有一个块存放像C标准库和数学库这样的共享库的代码和数据的区域。
  • 栈。每次调用函数时,栈就会增长,从一个函数返回时,栈就会收缩。
  • 内核虚拟内存。地址空间顶部的区域是为内核保留。

1.7.4 文件

文件就是字节序列。每个I/O设备,包括磁盘、键盘、显示器,甚至是网络,都可以看成是文件。Linux系统上的所有东西都是文件,因此Linux可以称为文件操作系统。

1.8 系统之间利用网络通信

跟上面的hello运行差不多,只是输入地方和输出地方不同

![](C:/Users/lan/Documents/笔记/img/深入理解计算机系统(第三版)笔记/Pasted image 20250724181127.png)

1.9 重要主题

系统不仅仅只是硬件。系统是硬件和系统软件互相交织的集合体,它们必须共同协作以达到运行应用程序的最终目的。

1.9.1 Amdahl定理

Amdahl定律的主要观点–想要显著加速整个系统,必须提升全系统中相当大部分的速度.
练习题1:

行程中固定消耗10个小时也就是1000公里,因此2500-1000=1500公里,我们只能在这1500公里作文章.
25小时/1.67=15小时,15-10=5小时
现在通过蒙大拿州的时间只有5小时,1500/5=300,因此我们通过蒙大拿的时速为300公里

练习题2:
Amdahl公式:
$$
S_\infty = \frac{1}{(1 - \alpha)+\alpha/k}
$$
已知:性能加速是2X,可以加速部份为80%,那么公式表达如下:
$$
2_\infty = \frac{1}{(1 - 0.8)+0.8/k}
$$

  1. 两边取倒数(互换分子和分母):1/2 = (1−0.8) + 0.8/k
  2. 代入 (1−0.8)=0.2:
    • 1/2 = 0.2 + 0.8/k
  3. 两边同时减去 0.2:
    • 1/2 − 0.2 = 0.8/k
    • 0.5 − 0.2 = 0.8/k
    • 0.3 = 0.8/k
  4. 交叉相乘或两边取倒数解 k:
    • k = 0.8 / 0.3 ≈ 2.67

1.9.2 并发和并行

  1. 线性级并发
    • 传统意义上,并发执行只是模拟出来,是通过使用一台计算机在它正在执行的进程间快速切换来实现的。
  2. 指令级并行
    • 在较低层次上,现代处理器可以同时执行多条指令的属性称为指令级并行。
  3. 单指令、多数据并行
    • 在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据。即SIMD并行。

1.9.3 计算机系统中抽象的重要性

抽象的使用是计算机科学中最为重要的概念之一。

  • 文件是对I/O设备的抽象
  • 虚拟内存是对程序存储器的抽象
  • 进程是对一个正在运行程序的抽象
  • 虚拟机是对整个计算机的抽象,包括操作系统、处理器和程序。

1.10 小结

  • 计算机系统是由硬件和系统软件组成的,它们共同协作以运作应用程序。计算机内部的信息被表示为一组组的位,它们根据上下文有不同的解释方式。程序被其他程序翻译成不同的形式,开始时是ASCII文本,然后被编译器和链接器翻译成二进制可执行文件。
  • 处理器读取并解释存放在主存里的二进制文件。因为计算机花费大量的时间在内存、I/O设备和CPU寄存器之间复制数据,所以将系统中的存储设备划分层层级结构——CPU寄存器在顶级,接着是多层硬件高速缓存存储器、DRAM主存和磁盘存储器。在层级模型中,位于更高层的存储设备比低层的存储设备更快,单位比特造价也更高。层次结构中较高层次的存储设备可以作为较低层次设备的高速缓存。通过理解和运用这种存储层次结构的知识,程序员可以优化C程序的性能。
  • 操作系统内核是应用程序和硬件之间的媒介。它提供三个基本的抽象:
    1. 文件是I/O的抽象
    2. 虚拟内存是对主存和磁盘的抽象
    3. 进程是处理器、主存和I/O设备的抽象
  • 网络也是一种I/O设备

2.1 信息存储

大多数计算机使用8位的字节(byte),字节作为可通过虚拟内存地址找到的内存单位,而不是访问内存中单独的位。
机器级程序(对计算机发出指令的程序)将内存视为一个非常大的字节数组,称为虚拟内存(virtual memory)。虚拟内存中的每个字节都由一个唯一的数字标识,这个数字被称为地址(address),所有可能地址的集合就称为虚拟地址空间(virtual address space)。
虚拟地址空间是展现机器级程序的抽象概念,实际的实现是将动态随机访问存储器(DRAM)、闪存、磁盘存储器、特殊硬件和操作系统结合起来,给程序提供一个字节数组。
每个程序对象都可以简单地视为一个字节块,而程序本身就是一个字节序列。

2.1.1 十六进制表达法

一个字节有8位组成。
在C语言中,以0x或0X开头的数字常量被认为是十六进制的值。
十六进制转二进制时,如果总数不是4的倍数,在最左边的一组可以少于4位,前面用0补足。

练习题 2.1 完成下面的转换:

1
2
3
4
A. 将 Ox39A7F8 转换为二进制。
B. 将二进制 1100100101111011 转换为十六进制。
C. 将 OxD5E4C 转换为二进制。
D. 将二进制 1001101110011110110101 转换为十六进制。

A.

1
2
3
4
5
6
7
8
9
10
分解
39A7F8
3=0011
9=1001
A=1010
7=0111
F=1111
8=1000

001110011010011111111000

B.

1
2
1100 1001 0111 1011
C97B

C.

1
2
3
4
5
6
7
8
分解
DSE4C
D=1101
5=0101
E=1110
4=0100
C=1100
11010101111001001100

D.

1
2
3
前面不足4位,补位加到4位
0010 0110 1110 0111 1011 0101
26E7B5

练习题 2.2

N 2N(十进制) ) 2N(十六进制)
9 512 0x200
19 524288 0x80000
14 16384 0x4000
16 65536 0x10000
17 131072 0x20000
5 32 0x20
7 128 0x80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
解:
N=19
2x2x2x2...x2=524288

1000 0000 0000 0000 0000
8 0 0 0 0
0x80000

这样算出2^n(十进制)为524288
2^n(十六进制)


16 384推出十六进制和N次方
log(16384)/0.3010≈14
n=14

0100 0000 0000 0000
4 0 0 0
0x4000

Ox1OOOO 推出十进制和N次方
0x10000=1x16^4+0x16^3+0x16^2+0x16^1+0x16^0
=1x65536+0+0+0+0
=65535

log(65535)/0.3010≈16


n=17 推出十进制和16进制
2^17=131072
0010 0000 0000 0000 0000
2 0 0 0 0 0
0x200000

32推出十六进制和N次方
log(32)/0.3010≈5
0010 0000
2 0
0x20

Ox80 推出十进制和N次方
n=8x16^1+0+16^0
n=128


练习题 2.3

十进制 二进制 十六进制
0 0000 0000 0x00
167 1010 01111 0xA7
62 0011 1110 0X3E
188 1011 1100 0XBC
55 0011 0111 0x37
136 1000 1000 0x88
243 1111 0011 0xf3
82 0101 0010 0x52
172 1010 1100 0xAC
231 1110 0111 0xE7
解:
十进制转二进制、十六进制
1
2
3
167=10x16...7
10 = 十六进制(A) 二进制 1010
7 = 十六进制(7) 二进制 0111

二进制转十六进制、十进制

1
2
3
4
5
6
7
0011 0111
0011 = 十进制3 十六进制 3
0111 = 十进制7 十六进制 7

十六进制0X37
十进制=3x16+7

十六进制转二进制、十进制

1
2
3
4
5
6
7
Ox52

5=二进制 0101 十进制 5
2=二进制 0010 十进制 2
0101 0010
5x16+2

练习题 2.4

A.0x503c+0x8=0x5044
B.0x503c-0x40=0x4ffc
C. 0x503c+64=0x507c
D.0x50ea-0x503c=0xAE
使用小学教的加减法,不过进位数是16即可。