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),于是无限循环

(无)有符号数的范围

  • 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_MAX 0111 1111
      2×T_MAX 1111 1110
      U_MAX 1111 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)

截断无符号数(一位)

  • 问题描述: 去除无符号数 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三部分构成:
    1. s表示浮点数的正负
    2. exp表示浮点数的指数部分(视作无符号数)
    3. frac为浮点数的主体

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

  • 这一部分是关于如何理解计算机内部数据表示的内容, 还蛮有趣的, 一旦理解了某些看似诡异设计后的巧妙之处,成就感还是挺大的
posted @ 2020-11-22 13:09  Muxv  阅读(197)  评论(0)    收藏  举报