你所不知道的 C 语言: 数值系统篇
尽管数值系统并非 C 语言所持有,但在 Linux 核心大量存在 u8/u16/u32/u64 这样通过 typedef 所定义的类型,伴随着各种 alignment 存取,如果对数值系统的认知不够充分,可能立即就被阻拦在探索 Linux 核心之外——毕竟你完全搞不清楚,为何 Linux 核心存取特定资料需要绕一大圈。
Balanced ternary
balanced ternary 三进制中 -, 0, + 在数学上具备对称性质。它相对于二进制编码的优势在于,其本身就可以表示正负数 (通过 +-, 0, +),而二进制需要考虑 unsigned 和 signed 的情况,从而决定最高位所表示的数值。
相关的运算规则:
|
|
以上运算规则都比较直观,这也决定了 balanced ternary 在编码上的对称性 (减法等价于加上逆元,逆元非常容易获得)。但是需要注意,上面的运算规则并没有涉及到相同位运算的规则,例如 $+\ (add)\ +$,这种运算也是 balanced ternary 相对于二进制编码的劣势,可以自行推导一下这种运算的规则。
数值编码与阿贝尔群
阿贝尔群也用于指示为什么使用二补数编码来表示整数:
- 存在唯一的单位元 (二补数中单位元 0 的编码是唯一的)
- 每个元素都有逆元 (在二补数中几乎每个数都有逆元)
浮点数 IEEE 754:
An example of a layout for 32-bit floating point is
单精度浮点数相对于整数 在某些情況下不满足結合律和交换律,所以不构成 阿贝尔群,在编写程序时需要注意这一点。即使编写程序时谨慎处理了单精度浮点数运算,但是编译器优化可能会将我们的处理破划掉。所以涉及到单精度浮点数,都需要注意其运算。
- 你所不知道的 C 语言: 浮点数运算
- 你所不知道的 C 语言: 编译器和最佳化原理篇
Integer Overflow
2002 年 FreeBSD [53]
|
|
假设将“负”的数值带入 maxlen
,那么在上述的程式码第 4 行时 len
会被赋值为 maxlen
,在第 5 行中,根据
memcpy
的原型声明
|
|
会将 len (=maxlen)
解释为 size_t
类型,关于 size_t
类型
- C99 [7.17 Common definitions <stddef.h>]
size_t which is the unsigned integer type of the result of the sizeof operator;
所以在 5 行中 memcpy
会将 len
这个“负“的数值按照无符号数的编码进行解释,这会导致将 len
解释为一个超级大的无符号数,可能远比 KSIZE
这个限制大。copy_from_kernel
这个函数是运行在 kernel 中的,这样可能会造成潜在的 kernel 信息数据泄露问题。
2002 年 External data representation (XDR) [62]
|
|
假设将 ele_cnt = $2^{20}+1$, ele_size=$2^{12}$ 代入,显然在第 2 行的 ele_cnt * ele_size
会超出 32 位整数表示的最大值,导致 overflow。又因为
malloc
的原型声明
|
|
malloc
会将 ele_cnt * ele_size
溢出后保留的值解释为 size_t
,这会导致 malloc
分配的内存空间远小于 ele_cnt * ele_size
Bytes (这是 malloc
成功的情况,malloc
也有可能会失败,返回 NULL)。
因为 malloc
成功分配空间,所以会通过第 3 行的测试。在第 5~8 行的 for 循环,根据 ele_cnt
和 ele_size
的值进行 memcpy
,但是因为分配的空间远远小于 ele_cnt * ele_size
,所以这样会覆写被分配空间外的内存区域,可能会造成 kernel 的信息数据被覆盖。
Bitwise
- 3Blue1Brown: How to count to 1000 on two hands [YouTube]
本质上是使用无符号数的二进制编码来进行计数,将手指/脚趾视为数值的 bit
Power of two
通过以下程式码可以判断 x 是否为 2 的次方
|
|
通过值为 1 的最低位来进行归纳法证明,例如,对 0b00000001
, 0b00000010
, 0b00000100
, … 来进行归纳证明 (还需要证明 x 中只能有一个 bit 为值 1,不过这个比较简单)。另一种思路,通过 LSBO 以及 $X$ 和 $-X$ 的特性来证明。
- LSBO: Least Significant bit of value One
- $-X = ~(X - 1)$
- $-X$ 的编码等价于 $X$ 的编码中比 LSBO 更高的 bits 进行反转,LSBO 及更低的 bits 保持不变
ASCII table
通过 ASCII table 中对 ASCII 编码的分布规律,可以实现大小写转换的 constant-time function
也可以通过命令
man ascii
来输出精美的 ASCII table
|
|
Each lowercase letter is 32 + uppercase equivalent. This means simply flipping the bit at position 5 (counting from least significant bit at position 0) inverts the case of a letter.
XOR swap
通过 xor 运算符可以实现无需临时变量的,交换两个数值的程式码
|
|
- 第 3 行的
*y ^= *x
的结果等价于*y ^ *x ^ *y
,整数满足交换律和结合律,所以结果为*x
- 第 4 行的
*x ^= *y
的结果等价于*x ^ *y ^ *x
,整数满足交换律和结合律,所以结果为*y
这个实作方法常用于没有额外空间的情形,例如 Bootloader
避免 overflow
整数运算 (x + y) / 2
可能会导致 overflow (如果 x, y 数值都接近 UINT32_MAX),可以改写为以下不会导致 overflow 的程式码
|
|
使用加法器来思考: 对于 x + y
,x & y
表示进位,x ^ y
表示位元和,所以 x + y
等价于
|
|
这个运算不会导致 overflow (因为使用了 bitwise 运算)。因此 (x + y) / 2
等价于
|
|
整数满足交换律和结合律
macro DIRECT
|
|
DIRECT 宏的作用是侦测 32bit/64bit 中是否存在一个 Byte 为 NULL。我们以最简单的情况 1 个 Byte 时来思考这个实作的本质:
|
|
~((X) - 0x01)
是 X
的取负值编码,即 -X
,根据二补数编码中 -X
和 X
的特性,可得 (~((X) - 0x01) | X)
为: X
二补数编码中值为 1 的最低位 (后续称之为 LSBO) 及更低位保持不变,LSBO 更高位均为 1。则 ~(~((X) - 0x01) | X)
为: X
二补数编码中值为 1 的最低位 (后续称之为 LSBO) 的更低位翻转,LSBO 及更高位均为 0。
LSBO: Least Significant Bit with value of One
|
|
可以自行归纳推导出: 对于任意不为 0 的数值,上述流程推导的最终值都为 0,但对于值为 0 的数值,最终值为 0x80。由此可以推导出最开始的实作 DIRECT
宏。
这个 DIRECT
宏相当实用,常用于加速字符串操作,将原先的以 1-byte 为单元的操作加速为以 32bit/64bit 为单位的操作。可以阅读相关实作并寻找其中的逻辑:
Count Leading Zero
计算 $\log_2N$ 可以通过计算数值对应的编码,高位有多少连续的 0’bits,再用 31 减去即可。可以通过 0x0001, 0x0010, 0x0002, … 等编码来进行归纳推导出该结论。
- iteration version
|
|
- binary search technique
|
|
- byte-shift version
|
|
在这些实作中,循环是比较直观的,但是比较低效;可以利用编码的特性,使用二分法或位运算来加速实作。
避免循环
|
|
这段程式码的作用是,对一个 32bit 的数值进行逐位元反转。这个逐位元反转功能非常实用,常实作于加密算法,例如 DES、AES。
但是与上面的 Count Leading Zero 类似,上面程式码使用了循环,非常低效,可以通过位运算来加速。
|
|
- Reverse integer bitwise without using loop [Stack Overflow]
加解密的应用
假設有一張黑白的相片是由很多個0 ~255 的 pixel 組成 (0 是黑色,255 是白色),這時候可以用任意的 KEY (000000002 - 111111112) 跟原本的每個 pixel 做運算,如果使用 AND (每個 bit 有 75% 機率會變成 0),所以圖會變暗。如果使用 OR (每個 bit 有 75% 機率會變 1),圖就會變亮。這兩種幾乎都還是看的出原本的圖片,但若是用 XOR 的話,每個 bit 變成 0 或 1 的機率都是 50%,所以圖片就會變成看不出東西的雜訊。
上圖左 1 是原圖,左 2 是用 AND 做運算之後,右 2 是用 OR 做運算之後,右 1 是用 XOR,可見使用 XOR 的加密效果最好。
这就是在密码学领域偏爱 XOR 的原因之一。除此之外,XOR 在概率统计上的优异特性也是另一个原因,具体证明推导请查看原文的说明。