《深入理解计算机系统》读书笔记
## 第一章 计算机系统漫游
通过以下C语言代码执行的生命周期来了解计算机的主要概念。
```c
#include
int main()
{
printf("hello, world\n");
}
```
### 编译系统

了解编译系统可优化程序性能,理解链接出现时的错误,避免安全漏洞。
### 执行过程
在黑窗口输入./hello 命令的执行过程。
从键盘接受 `./hello` 通过I/O读取到CPU,CPU处理后加载到主存中。

从硬盘读取hello可执行文件到主存中。

通过CPU处理主存中hello的数据,在显示器展示最终的执行结果。

### 存储器分层结果
主要思想就是一个层次的的存储器作为下一个层次的存储器的高速缓存,合理安排节约成本。

我们在开发过程中可以运用L1和L2的知识来提高程序性能。
## 第二章 信息的表示和处理
### 2.1 信息存储
#### 字节
大多数计算机使用 **8位的块**,或叫做 **字节(byte)** ,来作为最小的可寻址的存储器单位,而不是访问存储器中单独的位。
机器级程序将存储器视为一个非常大的字节数组, **称为虚拟存储器(virtual memory)** 。存储器的每个字节都由一个惟一的数字来标识,称为它的 **地址(address)** ,所有可能地址的集合就称为 **虚拟地址空间(virtual address space)** 。正如它的名字表明的,这个虚拟地址空间只是一个展现给机器级程序的概念性映像(image)。
一个字节的取值范围
- 二进制 00000000 ~ 11111111
- 十进制 0 ~ 255
- 十六进制 00 ~ FF
#### 字
每台计算机都有一个**字长(word size)**,指明整数和指针数据的标称大小(nominal size)。因为虚拟地址是以这样的字来编码的,所以**字长决定**的最重要的系统参数就是**虚拟地址空间的最大大小**。也就是说,对于一个字长为 n 位的机器而言,虚拟地址的范围为 **0~$2^n$-1**,程序**最多访问$2^n$字节**。
32位的电脑的虚拟地址空间最大为 $2^{32}$ 字节,换算为 G 则为 4G。
64位的电脑的虚拟地址空间最大为 $2^{64}$ 字节,换算为 G 则为 17179869184G。
#### 字节顺序
计算机硬件有两种储存数据的字节顺序:
大端法(big endian):最高有效字节在最前面的方式。
小端法(little endian):最低有效字节在最前面的方式。
例如:字0x01234567在地址范围 0x101~0x103 中存储,下图为该字在不同顺序下的存储。

在字0x01234567中,高位值为:**0x01**,低位值为:**0x67**。
### 整数表示
- 无符号编码:基于传统的二进制表示法,表示大于或等于0的数字。
- 浮点数编码:表示有符号整数的最常见的方式,可为0、正或负。
#### 有符号数和无符号数之间的转换
C语言允许在各种不同的数字数据类型之间做强制转换。
强制类型转换的结果保持位值不变,只是改变了解释这些位的方式。
执行一个运算的时候,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式的将有符号数强制类型转换为无符号数,并假设两个数都是非负的,来执行运算。
### 整数运算
- 无符号加法
- 二进制补码加法
- 二进制补码的非
- 无符号乘法
- 二进制补码乘法
- 乘以二的幂
- 除以二的幂
### 浮点数
#### 二进制小数
$b = \displaystyle \sum^{m}_{i=-n}2^i \times b_i$
符号“.”现在变为了二进制的点,点左边的位的权是2的正幂,点右边的位是2的负幂。例如:
$101.101_2$ 表示数字:
$1\times2^2 + 0\times2^1 + 1\times2^0 + 1\times2^{-1} + 0\times2^{-2} + 1\times2^{-3}$
$=4+0+1+{1\over2}+0+{1\over8} = 5{5\over8} = 5.625_{10}$
#### IEEE浮点表示
$V = (-1)^s \times M \times 2^E$
- 符号(sign):**s** 决定数是负数(s=1)还是正数(s=O),而对于数值 0 的符号位解释作为特殊情况处理。
- 有效数(significand):**M** 是一个二进制小数,它的范围在1~2-ε之间,或者在 0~1-ε之间。
- 指数(exponent):**E** 是2的幂(可能是负数),它的作用是对浮点数加权,也就是决定小数点的位置。
浮点数的位表示被划分为三个域,以编码这些值:
- 一个单独的符号位s直接编码符号s。
- k位的指数域$exp=e^{k-l}…e_1e_0$ 编码指数E。
- n位小数域 $frac =f_{n-1}…f_1f_0$ 编码有效数M,但是被编码的值也依赖于指数域的值是否等于零。
在单精度浮点格式(C语言中的float)中,s、exp和frac域分别为1位、k=8位和n=23位,产生一个32位的表示。在双精度浮点格式(C语言中的double)中,s、exp和frac域分别为1位、k=11位和n=52 位,产生一个64位的表示。
给定位表示,根据 exp 的值,被编码的值可以分成三种不同的情况。
- 规格化的值:
普遍情况,当exp不全为0,也不全为1,此情况解码以偏置形式表示有符号整数,即E = e – Bias,e是无符号数,e表示为阶码位的编码 $e_{k-1}...e_0$,而Bias是一个等于(单精度k=8,双精度k=11)的偏置值,由此产生指数的取值范围即:(单精度-126~+127,双精度-1022~+1023),对于frac解释为描述小数值f,期中0≤f<1,其二进制表示为$0.f_{n-1}...f_0$,当尾数定义为M = 1+f时,叫做隐含以1开头的表示,所以$M = 1.f_{n-1}...f_0$,因为二进制的基数是2,且我们总能在不溢出的范围内调整阶码,使得尾数M的范围总在1≤M<2中,符合IEEE定义M的范围,从而表示小数。
- 非规格化的值:
当阶码exp域全0,阶码值为E = 1-Bias,而尾数值为M = f,不包含隐含开头都 1(为什么),非规格化的值用来表示0和非常接近0的小数,因为规格化要求M≥1,所以无法表示0,实际上+0.0的浮点表示位模式全0,而当符号位为1时候得到-0.0,逐渐溢出的属性使得数值均匀的接近0.0。
- 特殊值:
当阶码exp域全1,当frac全0表示无穷,根据符号位的表示的正负来确定正负无穷,无穷能表示溢出的结果,当小数域非零时,NaN被称为不是一个数的缩写,一些运算结果为虚数,无法表示。
## 第三章 程序的机器级表示
- 3.1 历史观点
- 3.2 程序编码
- 3.3 数据格式
- 3.4 访问信息
- 3.5 算术和逻辑操作
- 3.6 控制
- 3.7 过程
- 3.8 数组分配和访问
- 3.9 异类的数据结构
- 3.10 对齐(alignment)
- 3.11 综合:理解指针
- 3.12 现实生活:使用 GDB 调试器
- 3.13 存储器的越界引用和缓冲区溢出
- 3.14 *浮点代码
- 3.15 *在C程序中嵌入汇编代码
- 3.16 小结
### 程序编码
```c
unix> gcc -O2 -S code.c
```
这会使编译器产生一个汇编文件 `code.s`,但是不做其他工作。
`-S`:使用-S选项,就能看到C编译器产生的汇编代码。
`-O2`:编译选项-O2告诉编译器使用第二级优化。通常,提高优化级别会使最终程序运行得更快,但是编译时间可能会变长,对代码进行调试会更困难。第二级优化是性能优化和使用方便之间的一种很好的妥协。
```c
unix> gcc -O2 -c code.c
```
这会产生目标代码文件code.o,它是二进制格式的,所以无法直接读。
`-c`:使用-c命令行选项,GCC会编译并汇编该代码。
```c
unix> objdump -d code.o
```
通过反汇编器(disassembler)根据.o文件生成一种类似汇编代码的格式。反汇编器只基于机器代码文件中的字节序列来确定汇编代码,不需要访问程序的源码。
```c
unix> gcc -O2 -o prog code.o main.c
```
生成可执行代码文件p,其中不仅包含两个过程的代码,还包含了用来启动和终止程序的信息,以及用来与操作系统交互的信息。
### 数据格式

大多数GCC生成的汇编代码指令都有一个字符后缀,表明操作数的大小。
例如,mov(传送数据)指令有三种形式:movb(传送字节)、movw(传送字)和movl(传送双字)。
### 访问信息
64位的CPU包含一组16个存储64位值的通用寄存器。%r8~%r15为64位CPU新加入的寄存器。

#### 操作数指示符
- 立即数(immediate):也就是常数值。在 GAS 中,采用标准 C 的表示方法,立即数的书写方式是“ $ ”后面跟一个整数,比如,$-577 或$0x1F。任何 32 位的字都可以用做立即数,不过汇编器在可能时会使用一个或两个字节的编码。
- 寄存器(register):它表示某个寄存器的内容,对双字操作来说,可以是八个32位寄存器中的一个(如%eax),对字节操作来说,可以是八个单字节寄存器元素中的一个(如%a1)。在我们的图中,我们用符号 $E_a$ 来表示任意寄存器a,用引用$R[E_a]$来表示它的值,这是将寄存器集合看成一个数组R,用寄存器标识符作为索引。
- 存储器引用:它会根据计算出来的地址(通常称为有效地址)访问某个存储器位置。因为将存储器看成一个很大的字节数组,我们用符号 $M_b[Addr]$ 表示对存储在存储器中从地址Addr开始的 b 字节值的引用。为了简便,我们通常省去写在下方的 b。

#### 数据传送指令

最常用的是传送双字的 movl 指令。**源操作数**指定一个值,它可以是立即数,可以存放在寄存器中,也可以存放在存储器中。**目的操作数**指定一个位置,它可以是寄存器,也可以是存储器地址。
movsbl 和 movsbl 指令负责拷贝一个字节,并设置目的操作数中其余的位。
pushl 和 popl 指令都只有一个操作数用于压入的源数据和用于弹出的目的数据。
### 算术和逻辑操作

- 第一类:加载有效地址(Load Effective Address)指令 leal 实际上是 movl 指令的变形。它的指令形式是从存储器读数据到寄存器。
- 第二类:一元操作,只有一个操作数,既作源,也作目的。这个操作数可以是一个寄存器,也可以是一个存储器位置。类似于`D++`、`D--`。
- 第三类:二元操作,第二个操作数既是源又是目的。第一个操作数可以是立即数、寄存器或是存储器位置。第二个操作数可以是寄存器或是存储器位置。不过,同 `movl` 指令一样,两个操作数不能同时都是存储器位置。类似于`D+=S`、`D-=S`。
- 第四类:移位操作,先给出移位量,然后是待移位的值。可以进行算术和逻辑右移。移位量用单个字节编码,因为只允许进行0到31位的移位。
### 控制
#### 条件码
除了整数寄存器,CPU还包含一组单个位的条件码(condition code)寄存器,它们描述了最近的算术或逻辑操作的属性。对这些寄存器的检测,将有助于执行条件分支指令。最有用的条件码是:
- CF:进位标志。最近的操作使最高位产生了进位,它可用来检查无符号操作数的溢出。
- ZF:零标志。最近的操作得出的结果为0。
- SF:符号标志。最近的操作得到的结果为负数。
- OF:溢出标志。最近的操作导致一个二进制补码溢出——正溢出或负溢出。

`cmpb`、`cmpw` 和`cmpl`指令根据它们的两个操作数之差来设置条件码。
`testb`、`testw` 和 `testl` 指令会根据它们的两个操作数的与(AND)来设置零标志和负数标志。
#### 访问条件码

#### 跳转指令

#### 条件分支
C中的if-else语句的通用形式是这样的:
```C
if (test-expr)
then-statement
else
else-statement
```
对于这种通用形式,汇编实现通常会使用下面这种形式,这里,我们用C语法来描述控制流:
```C
t = test-expr;
if (t)
goto true;
else-statement
goto done;
true:
then-statement
done:
```
> 实例
原始C语言代码
```C
int absdiff(int x, int y)
{
if (x < y)
return y - x;
else
return x - y;
}
```
与上面意思一样的goto代码
```C
int absdiff(int x, int y)
{
int rval;
if (x < y)
goto less;
rval = x - y;
goto done;
less:
rval = y - x;
done:
}
```
编译产生的汇编代码

#### 循环
do-while循环
```C
do
body-statement
while (test-expr);
```
翻译为
```C
loop:
body-statement
t = test-expr;
if (t)
goto loop;
```
while循环
```C
while (test-expr)
body-statement
```
翻译为
```C
loop:
t = test-expr;
if (!t)
goto done;
body-statement
goto loop;
done:
```
for 循环
```C
for (init-expr; test-expr; update-expr)
body-statement
```
翻译为
```C
init-expr
t = test-expr;
if (!t)
goto done;
loop:
body-statement
update-expr
t = test-expr;
if (t)
goto loop;
done:
```
#### switch语句
switch(开关)语句通过使用一种称为跳转表(jump table)的数据结构使得实现更加高效。跳转表是一个数组,表项ⅰ是一个代码段的地址,这个代码段实现的是当开关索引值等于ⅰ时程序应该采取的动作。

### 过程
有些看不下去了,让人头大。。。
[参考](https://blog.csdn.net/weixin_30673611/article/details/96172374?ops_request_misc=&request_id=&biz_id=102&utm_term=%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E7%AC%AC%E4%B8%89%E7%AB%A0%E7%AC%94%E8%AE%B0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-4-96172374.nonecase&spm=1018.2226.3001.4187)