CSAPP 15-231 学习笔记一
Lecture 01 Course Overview
C/Cpp不会检查数组越界, 请保持良好的检查越界的好习惯
Lecture 02-03 Bits, Bytes, and Integer
左移/右移
-
右移分为逻辑右移和算术右移
- 逻辑右移: 左边填充0
- 算术右移: 右边填充原本数据的符号位
-
左移
X >> y == X >> (y mod w), w 是 X 类型占的位数, 以此保证左移的结果总是有效(非0)- 举个🌰:
int x = 1; int y = x << 32; printf("%d", y); // y = 1
Signed VS Unsigned
- 当出现符号整数(
int)和非符号整数(unsigned int)的混合运算时, C会将符号整数隐式转换为非符号整数- 举个🌰
printf("%d", -1 > 0U) // 1- 此处的
-1会被隐式转换为非符号整数看待, 而-1的二进制表示为32个1(即等于UINT_MAX的二进制表示), 所以会大于0(甚至大于所有32位表示的数字)
- 因而下面的表述也都是正确的
#include <limits.h> printf("%d",(unsigned)INT_MAX < INT_MIN);// 1 printf("%d", -1 > -2); // 1 printf("%d", INT_MAX - 1 < (unsigned )INT_MAX + 1); // 1
- 举个🌰
- 一个容易被忽视的案例
- 下🌰会无限循环,而非想象中的只打印5次
for (int i = 5; i - sizeof(char) >= 0; i--) printf("%d", i); // infinite loop - 由于
sizeof()返回的是无符号整数, 于是i - sizeof()也为无符号整数, 无符号整数永远不为负(0U-1U=UINT_MAX),于是无限循环
- 下🌰会无限循环,而非想象中的只打印5次
(无)有符号数的范围
abs(T_MAX) = abs(T_MIN) - 1- 即符号数中的最小数绝对值比最大数绝对值大1
- 符号数中根据最高位为0或1决定数字的正负, 首位为1的全为负数, 首位为0的包括正数和0, 由于这两部分总数相同(即对称), 所以正数数量要比负数少一个
U_MAX = 2 × T_MAX + 1- 此处将
×2视作左移一次number value(8-bit) T_MAX0111 11112×T_MAX1111 1110U_MAX1111 1111
- 此处将
符号扩展与截断
扩展无符号数
- 扩展的位数全置0即可
扩展有符号数
- 问题描述
- 给定
w-bit有符号数 x - 将其变为
w+b-bit有符号数 x'
- 给定
- 操作方法
- 将扩展的
b位全部置为x的符号位
- 将扩展的
- 分析
- x为正数, 最高位为0, 复制b位0, 不影响数的大小
- x为负数, 每增加一位1, 大小不变(过程如下, 第
w位权重为\(2^w\))
\[\begin{aligned}
x_w &= [1, x_{w-1}...] \\
&= -2^w + x_{w-1} \\
x_{w+1} &= [1, 1, x_{w-1}..] \\
&= -2^{w+1} + 2^w +x_{w-1} \\
&= -2^w + x_{w-1} \\
&= x_w
\end{aligned}
\]
- 举个🌰
- 5-bit数
11001B(-7D)延长为 7-bit数1111001B(-7D)
- 5-bit数
截断无符号数(一位)
- 问题描述: 去除无符号数 x 的最高位 n
- 操作方法:
- 做取模操作
- 分析
- 若最高位为0, 则去除后不改变大小
- 若最高位为1, 则等同于执行 \(x \mod 2^n\)
- 保留剩下的n-1位, 即等同于做取模操作
- 综合两种情况, 截断无符号数一位等同于做取2的幂次的模操作
- 举个🌰
11001U(25D)截取最高位为1001U(9D),25 mod 16 = 9
截断有符号数(一位)的🌰
- 截断
11011B(-5D)的最高位- 结果为
1011B(-5D), 大小不变, 类似于有符号数的拓展
- 结果为
- 截断
10011B(-13D)的最高位- 结果为
0011B(3D), 等同于对做了-13 + 16操作, 类似于取模操作-13 mod 16 = 3
- 结果为
- 截断符号数的操作可以由取模操作和不变构成
有符号数和无符号数的四则运算
加
- 均遵循一般的二进制加法, 只是最高位的进位的位会被舍弃(溢出现象), 其溢出结果表现为
- 无符号数的溢出等同于取模操作
- \(s = UAdd_w(u, v) = u + v \mod 2^w\)
- 有符号数的溢出会导致正数的相加结果为负数(正溢出
PosOver), 负数的相加结果为正数(负移除NegOver)
- 无符号数的溢出等同于取模操作
取负
- 有符号数的取负操作为:
- 每个位取反后,整体加一
- 举个🌰
- 符号数的相减可以看作加法和取负的结合
乘与左移
- 均遵循一般的二进制乘法, 同样最高位的进位会被舍弃
- 同样地, 有符号数的乘法溢出会导致符号位的变动
- 乘2的N次幂可以看作左移N位的操作
右移
- 除于2的N次幂可以看作右移N位的操作
- 其中无符号数的右移一定是逻辑右移
- 有符号数的右移, 至少在
C的标准中没有明确定义是何种右移方式lldb中, 对负数的右移是算术右移(即没有改变数的正负)
- 因而可以理解为什么
C中的奇数除于2会向下取整, 因为最低位的1被右移舍弃掉了
无符号数的使用
- 在
java中- 无符号数被抛弃, 一切数都用二进制补码表示
>>>表示逻辑右移>>表示算术右移
- 很多语言也保留了无符号数, 但是不允许做隐式转换(
C允许隐式转换) - 比起使用
unsigned(32-bit的无符号数), 更推荐使用size_t, 这是个64-bit 的无符号数 - 何时使用无符号数
- 当需要做模运算时(例如执行加密算法时)
- 当使用数字作为开关时(例如构建位掩码)
基于字节的内存组织模式
字长, 对齐
- 64-bit机器中, 地址由64位表示, 但最大的被允许使用的内存地址是47位, 约为128TB, 但实际上操作系统只允许使用该内存中的某些区域, 尝试访问其他区域则会发出段错误(
segmentation fault)的信号 - 字长(Word Size):一个整数数据的标准大小, 也是地址的长度
- 举个🌰:32位系统, 一个整数数据或地址使用32个二进制数来表示
- (默认:数据地址是数据首字节的地址)
- 32位系统中, 地址尾数往往是4的倍数(因为由32位的数据进行填充, 而一个数据又占四个字节);同样地, 64位系统中地址位数往往是8的倍数
- 这些固定的尾数往往也是cpu存取内存的粒度(即一次只能存取x个字节,
x=1,2,4...), 实际上并非所有的数据都是字长长度, 因此在设计数据格式时, 需要正好占满每个存取粒度, 这样的情况称作字节对齐 - 下面的代码就说明了这种情况
typedef struct { int x; char c; } S; printf("%d ", sizeof(int) + sizeof(char)); // 5 printf("%d ", sizeof(S)); // 8
字(Word)内的字节顺序
- 小端存储与大端存储
- 这里讨论的是一个数据内部的字节顺序
- 首先:我们写数字时, 最高位数在数字的最左边
- 小端:权重最小的字节(最低位字节)的字节排在最前, 越往后字节权重越大
- 大端:权重最大的字节(最高位字节)的字节排在最前, 越往后字节权重越小
x86,ARM架构的处理器都均采用小端存储, 小端存储的使用相当广泛- 现有的大端存储的应用: 通过
Internet发送32位字时,实际上是以大段顺序发送的 - 举个🌰:
- 现在有数据
0x01234567由四个word构成:01,23,45,67, 假设该数据地址为0x100, 则它们的排列顺序如下
- 现在有数据
| 字节顺序 | 0x100 | 0x101 | 0x102 | 0x103 |
|---|---|---|---|---|
| 小端 | 67 |
45 |
23 |
01 |
| 大段 | 01 |
23 |
45 |
67 |
字符串中的字节顺序
- 无论字节排序如何, 字符(
char)的排列顺序总是相同的(因为只占一个字节), 即从小到大排序(低位字节排在最前) - 因而C字符串是由一串字符表示(以字节值为0的数作为结束符
\0) - 因而字符串不存在不同机器的适配性问题, 是通用的
Lecture 04 Lecture 04 Floating Point
IEEE 标准
- 每个浮点数由
s,exp,frac三部分构成:s表示浮点数的正负exp表示浮点数的指数部分(视作无符号数)frac为浮点数的主体

- 其中
exp减去一个偏置项(32位为127)后得到的数E作为真正的指数部分 - 下面就
exp的取值来说明32位浮点数的计算exp为全零 \(F=(-1)^{s}*2^{1-127}*0.frac\) 此时得到的数是最接近0的一部分数exp介于全零和全一之间 \(F=(-1)^{s}*2^{exp-127}*1.frac\) 此时的frac部分实际上是浮点数主体的小数部分, 前面隐藏了1的表达exp为全一, 若frac为全0, 则代表无穷大, 否则代表NaN(Not a Number)

- 可以看出, 浮点数的分布越接近于0, 密度越高, 数字绝对值越大, 分布的越稀疏
- 实际上这样的设计相当巧妙, 下面给出ppt中的案例, 可以体会到
exp全零情况下为什么指数部分是1-127而非-127

浮点数的舍入
- 四种方法
- 舍去多余部分
- 向上舍入
- 向下舍入
Nearset Even(默认舍去规则)- 向最靠近的舍去值舍去, 若为正中间的值, 则向偶数舍去(二进制中以0为结尾视作偶数)
- 🌰:
| 数字 | 结果 | 备注 |
|---|---|---|
| 1.00100100 | 1.0010 | 小于一半 |
| 1.00101001 | 1.0011 | 大于一大 |
| 1.00101000 | 1.0010 | 等于一半, 舍向0 |
| 1.00111000 | 1.0100 | 等于一半, 舍向2 |
浮点数的加法和乘法
- 假设有无穷的位数, 计算出结果后, 舍入到当前格式下
- 浮点数的加法和乘法不存在结合律
- 因为连续两次运算之间会存在舍去现象
(lldb) print (1e20 - 1e20) + 3.14
(double) $0 = 3.1400000000000001
(lldb) print 1e20 + (-1e20 + 3.14)
(double) $1 = 0
浮点数和整数的相互转换
float/double->int- 舍去小数即可
- 若整数部分超出了
int能表示的范围(或NaN), 一般会被设置为TMIN
int->double- 无损转换, 因为
double可表示的整数范围要比int大
- 无损转换, 因为
int->float- 存在一定的舍入现象, 因为
float能表示的整数并不能坐落到int范围的每一个整数, 超出float表示范围的数会被按照缩进规则缩进到float范围内 - 随着
float表示的数越大, 数之间的空隙会变大, 导致出现表示范围外的整数 double就不存在这样的问题- 以下图的
6-bit类IEEE浮点数为例, 红框内的整数就超出了表示范围(需要按照舍入方式做舍入操作)
- 存在一定的舍入现象, 因为

Summary
- 这一部分是关于如何理解计算机内部数据表示的内容, 还蛮有趣的, 一旦理解了某些看似诡异设计后的巧妙之处,成就感还是挺大的

浙公网安备 33010602011771号