《深入理解计算机系统》读书笔记
解密计算机底层原理的学习之旅。
持续更新中...
从最底层的数据在内存中表示,到流水线指令的构成,到虚拟存储器,到编译系统,到动态加载库,到最后的用户态应用。贯穿本书的一条主线是使程序员在设计程序时,能充分意识到计算机系统的重要性,建立起所写程序可能被执行的数据或指令流图,明白执行程序时到底发生了什么事,从而能设计出高效、可移植、健壮的程序,并能够更快的对程序排错,改进程序性能等。
对于底层的东西,一直都很感兴趣,总想明白一个程序是怎么从我们写的一行行代码,成为了五彩缤纷的世界。这本书买来已经有两个月了,阅读过几天,前段时间总是会被一些其它的俗事影响,导致连续一个多月都没怎么翻这本书,最近想把一些没必要的事情抛弃掉,加上这本书的内容确实很吸引自己,现在终于可以沉浸在这本书里面了,这篇博文只是做为自己理解的笔记,很多人都说值得看几遍,于是我开始了这个长久的计划...
第1章 计算机系统漫游
1 一个C程序的生命周期
#include < stdio.h>
int main()
{
printf("Hello,Word!\n");
return 0;
}
一个简单的Hello,Word程序,每条C语句都必须被其他程序打包成一系列的低级机器语言指令,然后这些指令按照一种称为可执行目标程序的格式打包,并以二进制磁盘文件的形式存放起来,目标程序也可以称之为可执行目标文件。
gcc -o hello hello.c
在Unix操作系统中,使用GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行的目标文件:hello,这个编译过程可以分为四段,如上图所示,是使用的命令分别是:
gcc -E Hello.c -o hello.i
1.1 预编译(Preprocessing)
会对各种预处理指令(#include #define #ifdef 等#开始的代码行)进行处理,删除注释和多余的空白字符,生成一份新的代码,得到hello.i文件
gcc -S hello.i -o hello.s
1.2 编译(Compilation)
对代码进行语法、语义分析和错误判断,生成汇编代码文件,得到hello.s文件
gcc -c hello.s -o hello.o
1.3 汇编(Assembly)
把汇编代码转换与计算机可认识的二进制文件,得到hello.o文件
gcc -o hello.o hello
1.4 链接(Linking/Build)
通俗的讲就是把多个*.o文件合并成一个可执行文件,二进制指令文件,得到hello可执行文件
1.5 执行(exec)
此刻hello.c源程序已经被编译系统翻译成了可执行的目标文件hello,并存放在磁盘上,想要在Unix系统上原型该可执行文件,我们将它的文件名输入到称为外壳(shell)的应用程序中
./hello
2 了解编译系统如何工作是大有益处的
- 优化程序性能。
- 理解链接时出现的错误
- 避免安全漏洞
3 处理器读并解释存储在存储器中的指令
3.1 外壳
外壳是一个命令行解释器,它输出一个提示符,等待你输入一个命令行,然后执行这个命令,如果该命令的第一个单词不是一个内置的命令的话,那么外壳就会假设这是一个可执行文件的名字,它将加载并运行这个文件。
3.2 系统的硬件组成
3.2.1 总线
贯穿整个系统的一组电子管道,负责各个部件之间的信息传递。
3.2.2 I/O设备
输入/输出设备,是系统与外部世界的联系通道,每个I/O设备都与总线相连(鼠标,键盘,显示器,磁盘等)
3.2.3 主存
主存是一个临时的存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理上来说,主存是一组动态随机存取存储器(DRAM)芯片组成。从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(即数组索引),这些地址都是从0开始的
3.2.4 处理器
解释执行存储在主存中的指令的引擎,处理器的核心是一个字节长的存储设备(或寄存器),称为程序计数器(PC),在任何时候,PC都指向主存中的某条机器语言指令(即含有该条指令的地址)
- 加载:
把一个字节或一个字从主存复制到存储器,以覆盖这个位置上原来的内容。 - 存储:
把一个字节从存储器复制到主存中的某个位置,已覆盖这个位置上原来的内容。 - 操作:
把两个寄存器中的内容复制到ALU(算术逻辑单元),ALU对这两个字做算术运算,并将结果放到一个存储器中,以覆盖存储器中原来的内容。 - 跳转:
从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中原来的值。
3.3 运行Hello程序
- 外壳程序执行它的指令,等待我们输入一个命令
- 当我们在键盘上输入
./hello
后,外壳程序将字符逐一读入寄存器,再把它放入存储器中 - 当我们在键盘上输入回车键时,外壳程序就知道我们已经结束了命令的输入,然后外壳执行了一些了的指令来加载可执行的hello文件
- 外壳程序将hello目标文件中的代码和数据从磁盘复制到主存,数据包最终会变成输出字符串“Hello,word\n”
- 利用直接存储器存取(DMA)的技术,数据可以不通过处理器而直接从磁盘到达主存。(入下图所示)
- 一旦目标文件hello中的代码被加载到主存,处理器就开始执行hello程序的main程序中的机器语言指令,这些指令将“Hello,word\n”字符串中的字节从主存复制到寄存器文件,在从寄存器文件中复制到显示设备,最终显示在屏幕上。
4 高速缓存的至关重要
数据的搬迁将耗费大量的时间和开销,为了解决处理器寄存器与主存读取数据的巨大差异,系统设计者采用了更小,更快的存储设备,即高速缓存,作为暂时的集结区域,用来存放处理器近期可能需要的信息
高速缓存存储器
5 存储设备形成层次机构(L0~L6)
存储器层次结构主要思想是一层上的存储器作为低一层存储器的高速缓存。
6 操作系统管理硬件
操作系统可以比喻为应用程序和硬件中插入的一层软件,所有的应用对硬件的操作尝试必须通过操作系统。
**操作系统的两个基本功能:**1)防止硬件被失控的应用程序滥用。2)向应用程序提供简单一致的机制来控制复杂而又通常大相径庭的低级硬件设备,操作系统通过几个基本的抽象概览来实现这两个功能:
- 进程:对处理器、主存和I/O设备的抽象表示
- 虚拟存储器:对主存和磁盘I/O设备的抽象表示
- 文件:对I/O设备的抽象表示
6.1 进程
进程是计算机科学中最重要和最成功的概念之一,是操作系统对一个正在运行的程序的一种抽象,并发执行只是处理器在多个进程中来回切换实现的,操作系统实现这种交错执行的机制称之为上下文切换,而上下文就是保持跟踪进程运行时所需的所有状态信息,在任何时候,单处理器系统都只能执行一个进程的代码。
6.2 线程
进程中的控制流,执行单元,运行在进程的上下文中,共享同样全局代码和数据。
6.3 虚拟存储器
虚拟存储器是一个抽象概念,它为每个进行提供了一个假象,即每个进程都在独占使用主存,每个进程看到的是一直的存储器,称为虚拟地址空间
6.3.1 程序代码和数据
对于所有的进程来说,代码是从同一个固定地址开始,紧接着的是C全局变量和相对应的数据位置。
6.3.2 堆
代码和数据区后紧随着的是运行时堆,代码和数据区是在进程一开始运行时就被规定了大小,创建和释放对象是会改变运行时堆的大小。
6.3.3 共享库
位于虚拟存储器的中间部分,放着想C标准库,数学函数库之类的东西。
6.3.4 栈
位于虚拟存储器的顶部,编译器用它来实现函数的调用,也可以动态的收缩,在执行函数时会变大,在return时会收缩。
6.3.5 内核虚拟存储器
内核总是驻留在内存中,是操作系统的一部分。这部分区域不能被程序调用修改
6.4 文件
文件就是字节序列,仅此而已。(每个I/O设备,包括磁盘,键盘,显示器,甚至网络,都可以视为文件)
第2章 信息的表示和处理
1、信息存储
1.1 地址
大多数的计算器使用8位的块,或者字节(byte)来作为最小的可寻址的存储器单位,而不是在存储器中访问单独的位;机器级程序将存储器视为一个非常大的字节数组,称为“虚拟存储器
”,存储器的每个字节都由一个唯一的数字来标识,称为它的地址,所有可能地址的集合称为虚拟地址空间。
十六进制表示法:0~9 A-F的16个字符用来表示地址,简写hex
列:0xABCDEF7B
,以0x
为开头,以16进一位。为什么要使用这种方式来表示地址,因为二进制或者十进制的模式来表示非常不方便。
0~15的二、十、十六进制
二进制 | 十进制 | 十六进制 |
---|---|---|
0000 | 0 | 0 |
0001 | 1 | 1 |
0010 | 2 | 2 |
0011 | 3 | 3 |
0100 | 4 | 4 |
0101 | 5 | 5 |
0110 | 6 | 6 |
0111 | 7 | 7 |
1000 | 8 | 8 |
1001 | 9 | 9 |
1010 | 10 | A |
1011 | 11 | B |
1100 | 12 | C |
1101 | 13 | D |
1110 | 14 | E |
1111 | 15 | F |
小端法:从最低有效字节到最高有效字节的存储方式的称之为小端法;
例:int a = 1;
&a的4字节空间为{0x100,0x101,0x102,0x103}
大端法:从最高有效字节到最低有效字节的存储方式称之为大端法;
例:int a = 1;
&a的4字节空间为{0x103,0x102,0x101,0x100}