Skip to content

Commit

Permalink
update chapter 3
Browse files Browse the repository at this point in the history
  • Loading branch information
Iolop committed Mar 13, 2024
1 parent f259389 commit 05e0f3c
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 8 deletions.
46 changes: 38 additions & 8 deletions _posts/2024-03-08-cs-app读书笔记-2-信息的表示与处理.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ math: true

我们的操作系统能力那么大,它又是怎么来表示这些东西呢?每天打开电脑,我们要面对的文本,英文字母,数字,符号,中文和各种风马牛不相及的语言,他们各自的字符加起来成千上万,这些看起来完全不同的东西,如何在计算机内部进行统一的表达?在第一章,我们提到了ASCII这一种编码方式,但是它能表示的仅仅是很小一部分。整数,分数,实数我们又该怎么表示?在这一章,我们会主要学习三种数字的表达方式:无符号数、有符号数、浮点数。

> 在这里,我觉得有必要提及一下,我们常说的64位操作系统,这里面的64是什么意思。在第一章,我提到了CPU这一概念,CPU能够一次性处理的最长数据长度我们一般称为多少位的操作系统,而这个长度,通常也是寄存器的大小。如果有兴趣,可以去搜索寄存器是什么。
> 在这里,我觉得有必要提及一下,我们常说的64位操作系统,这里面的64是什么意思。在第一章,我提到了CPU这一概念,CPU能够一次性处理的最长数据长度我们一般称为多少位的操作系统,而这个长度,通常也是寄存器的大小。如果有兴趣,可以去搜索寄存器是什么。
同时,在这一章,也会适当的引入`溢出(overflow)`这一概念来更好的了解为什么数据的表示不是无限的。

Expand Down Expand Up @@ -51,13 +51,13 @@ gcc -m64 hello.c
| float | 4 |
| double | 8 |

> 有无符号对我们来说重要吗?很重要,因为这确保了他们的表示范围以及最后转化机器指令的时候不会出错。假如在使用int32_t的时候,却和uint32_t相比,无疑会出现纰漏。有一个很有趣的小实验,假如有以下两个数组,我们定义为:char a[2] = {0xfb, 0xf4};unsigned char b[2] = {0xfb, 0xf4};在使用printf打印他们的十六进制表示,会出现不同。
> 有无符号对我们来说重要吗?很重要,因为这确保了他们的表示范围以及最后转化机器指令的时候不会出错。假如在使用int32_t的时候,却和uint32_t相比,无疑会出现纰漏。有一个很有趣的小实验,假如有以下两个数组,我们定义为:char a[2] = {0xfb, 0xf4};unsigned char b[2] = {0xfb, 0xf4};在使用printf打印他们的十六进制表示,会出现不同。
### 寻址和字节顺序

在我们确定了类型的长度后,随之而来的一个问题就是顺序。这其实也是一个制造商们的习惯问题,比如我们有如下数据*int32_t data=0x12345678*,假定*data*是存放在地址*0x100*处,那我们该怎么排布这些数据呢?有两种方式供我们选择:

> 为什么一个地址能表示两个十六进制数?因为一个地址是8bit,但是十六进制最大的数是F(16)=1111(2)。
> 为什么一个地址能表示两个十六进制数?因为一个地址是8bit,但是十六进制最大的数是F(16)=1111(2)。
| | 0x100 | 0x101 | 0x102 | 0x103 |
| :----------: | :---: | :---: | :---: | :---: |
Expand All @@ -68,7 +68,7 @@ gcc -m64 hello.c

在网络上,我们的数据传输一般采用大端,而在目前的Intel上,一般采用小端。这就是我们在网络编程时,会用到的一个函数族`hton(l/s) ntoh(l/s)`的由来,如果不统一我们的数据排列形式,那么势必会引起误会。如果你想确定自己电脑的大小端,想一想该怎么编写C代码。

> 提示:强制类型转换来分别打印一个int的每一个字节数据。
> 提示:强制类型转换来分别打印一个int的每一个字节数据。
### 字符串的表示

Expand Down Expand Up @@ -185,20 +185,20 @@ int str_longer(char *s1, char *s2){
三者顺序排布,在常见的单精度(32位)下,s=1、k=8、n=23。双精度(64位)时,s=1、k=11、n=52。下面介绍三种情况来如何编码。
#### 规格化
### 规格化
英文原文是*Normalized*,直译过来叫归一。搞机器学习或者深度学习的同学看文档没少看这个词,经常数据要归一化,平整到0-1。这里代表最普遍的情况:exp不全为0或者1。这时候,exp字段被解释成*偏置(biased)*形式的有符号整数。(老演员了,和上面一样。)这时候有$$E=e-Bias$$。此时$$e=e_{k-1}e_{k-2}...e_2e_1e_0$$(无符号数e的位表示),$$Bias=2^{k-1}-1$$。
小数字段frac被解释为$$0\le f < 1$$,且被表示为$$0.f_{n-1}...f_1f_0$$。此时,$$M=f+1$$。我们就此得到了浮点数V的表示。
#### 非规格化
### 非规格化
exp位全部为0。此时$$E=1-Bias,M=f$$。书中提到了非规格化的两种好处,虽然没理解,还是列出来
1. 能够表示数值0。因为普通情况下$$M\ge 1$$。
2. 对于非常接近0的数,其可能的数值均匀分布地接近0。
#### 特殊值
### 特殊值
exp位全部为1。frac字段为全0时,表示无穷$$\infty$$。frac不为0,表示“NaN”(not a number)。
Expand All @@ -216,6 +216,36 @@ exp位全部为1。frac字段为全0时,表示无穷$$\infty$$。frac不为0
由于非规格化使用了$$E=1-Bias,M=f$$,我们在最大地非规格化和最小地规格化之间平滑过渡。
### 舍入问题
由于表示方法的问题,所以我们会有一个无法绕开的问题,那就是表示的精度和范围。我们无法像在现实世界那样进行实数运算。在C里面,我们经常使用的是——向偶数舍入(向最接近的数进行舍入)[^round-to-nearest-even]。也许你见过向上舍入或者向下舍入,但是这两种方式都会无法避免的引入一个统计问题,前者会导致偏大而后者偏小。采用向偶数舍入能够在平均值上避免这类问题。这里有个例子,我们可以参考下
|方式|1.4|1.6|1.5|-1.5|
| :---:| :---:| :---:| :---:| :---:|
|向偶数舍入|1|2|2|-2|
同样的,对于二进制小数我们有如下舍入方式,以保留2位为例。
1. 111.1011 = 111.11
2. 101.1110 = 110.00
由于精度和舍入的问题,所以浮点数运算时,我们要特别注意下我们熟知的加法结合律可能不再适用了。考虑如下代码
```c
float a=1e10;
float b=3.14;
printf("%f\n", a+b-a);
printf("%f\n", a-a+b);
```

实际运算结果显示为`0.000000``3.140000`。浮点数自己的特性,导致在这种运算时结果往往出乎意料,而且由于精度问题,往往减法的值也不会如同我们预料中那样稳定。

> 有兴趣的可以尝试一下,在float的单精度下,我们如果做差,精度能够到达多少。看一下上面的非规格化最小值,尝试计算一次。
至此,我们完成了第二章的内容浏览。本书的第二章内容比较详实,如果深入探讨需要手动计算和理解才能深刻。有兴趣的朋友可以试试书后的练习题,加强了解。

## 参考链接

[^ABI-for-what]: ABI的简单说明 <https://zhuanlan.zhihu.com/p/386106883>
[^ABI-for-what]: ABI的简单说明 <https://zhuanlan.zhihu.com/p/386106883>

[^round-to-nearest-even]: 可以参考这个链接 <https://blog.leodots.me/post/45-ieee754-rounding-rules.html>
39 changes: 39 additions & 0 deletions _posts/2024-03-13-cs-app读书笔记-3-程序的机器级表示.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
layout: post
title: CS:APP读书笔记-3-程序的机器级表示
date: 2024-03-13 21:31 +0800
categories: [CS:APP]
tags: [读书笔记]
math: true
---

# 第三章:程序的机器级表示

面对现在的可执行文件,我们千头万绪不知道该怎么去理解它。在Windows上,我们知道双击去运行它;Linux上,我们可以在命令行中通过`./file`的形式去执行。这一章,我们将会走进程序的机器级表示,来理解代码时如何被转变成CPU可以理解的模式的,我们又是如何来表示我们在源代码中的各种数据,而CPU又是如何访问它们。除此外,我们还将学习到我们常用的流程控制又是如何实现。以上这些内容采用C语言进行描述以及通过gcc编译出的各类中间文件进行解释,其他的类型比如虚拟机、JIT、解释器类型语言不在此列。

## CPU的演化

要理解可执行文件是怎么运行的,我们有必要先回头去看看CPU的演化过程。再最早时期,也许你曾看到书上提到一种打孔程序机[^punched-card]。这种形式后来被汇编代码所取代,因为汇编代码更容易被人们记忆。再后来,随着CPU的发展,我们拥有了更强的性能和更广阔的可用空间,随之演变的是更抽象的语言——我们现在常用的高级语言。简单的来看,我们把这三种演化的关系:机器码——汇编代码——高级语言,可以映射到如今的源代码到可执行代码的翻译之路上:编译——汇编——链接。

以前常说的x86,其实是Intel的32位处理器,可惜Intel没把握住64这个风口,让AMD先一步迈了出去,先有了amd64(注意和arm64不是同一个东西),然后Intel跟进,出现了x86-64(强行更名)。

## 程序编码

通过如下命令,我们可以编译源代码到可执行文件

```shell
gcc -Og -o hello hello.c
```

gcc是我们之前说的一整套工具,这是Linux上的默认编译器。通过上述命令,gcc会自动地帮助你执行一系列地命令来完成预处理、编译、汇编、链接过程。

1. 预处理:完成对源文件的拓展,插入头文件。
2. 编译:通过编译器,转化位汇编代码,产生`.s`文件。
3. 汇编:将汇编代码转化成二进制目标代码,产生`.o`文件。
4. 链接:将上述文件和库进行链接,包括重定位等一系列操作,生成可执行文件。

> 除了gcc,或许你还听说过clangd。
## 参考链接

[^punched-card]: 打孔卡 <https://zh.wikipedia.org/wiki/%E6%89%93%E5%AD%94%E5%8D%A1>

0 comments on commit 05e0f3c

Please sign in to comment.