深入理解计算机系统书籍(2)

继续第二章剩下的内容

1.多字节对象的两个基本规则


对于跨多个字节的数据(比如 int、float),必须明确两个规则:

1.这个对象的地址是什么?
在几乎所有机器上,多字节对象都存为连续的字节序列。
对象的地址 = 它所使用的字节中最小的那个地址。

2.在内存中如何排列这些字节?

这就是字节顺序(Endianness)的问题。

两种字节顺序规则
小端法(Little Endian)
规则:最低有效字节 存储在最前面(低地址)。
即:先存低位字节,再存高位字节。
大端法(Big Endian)
规则:最高有效字节 存储在最前面(低地址)。
即:先存高位字节,再存低位字节。

假设变量 x 是 int 类型,地址 0x100,十六进制值为 0x01234567:
高位字节:0x01
低位字节:0x67
地址范围:0x100 ~ 0x103
大端法存储

地址0x1000x1010x1020x103
字节值01234567

特点:和我们平时写数字的顺序一致(从左到右:高位 → 低位)。
小端法存储

地址0x1000x1010x1020x103
字节值67452301

特点:和我们平时写数字的顺序相反(从左到右:低位 → 高位)。

除了字节顺序外,int 类型的字节内容在所有机器上完全一致。

float 类型的字节内容在所有机器上也一致,差异仅在于字节顺序。

指针值与机器 / 操作系统强相关,没有统一规律,只和当前运行环境的内存分配有关。

2.C 语言字符串的基本规则


存储形式:C 语言中字符串被编码为以 null 字符(值为 0)结尾的字符数组。
null 字符:即 '\0',十六进制表示为 0x00,作用是标记字符串结束。
每个字符都由标准编码表示,最常见的是 ASCII 字符码。
示例演示
字符串 "12345":包含 '1'、'2'、'3'、'4'、'5' 共 5 个可见字符,加上末尾的 '\0',总长度为 6 字节。

但是实际都要弄\0是不会打印的

31 32 33 34 35 00

'1' → 0x31

'2' → 0x32

'3' → 0x33

'4' → 0x34

'5' → 0x35
末尾的 00 就是 null 终止符 '\0'

在使用 ASCII 码的任何系统上,字符串的字节表示完全相同,与机器的字节顺序(大端 / 小端)、字长规则无关。
因此:文本数据(字符串)比二进制数据(int、float 等)具有更强的平台独立性。

我们来看下面这段代码

int sum(int x, int y) {
    return x + y;
}

当我们在示例机器上编译时,生成如下字节表示的机器代码

们发现指令编码是不同的。不同的机器类型使用不同的且不兼容的指令和编码方 式。

即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,

因此二进 制代码是不兼容的。

二进制代码很少能在不同机器和操作系统组合之间移植。

3.布尔代数

最简单的布尔代数定义在二元集合 {0, 1} 上,包含 4 种核心运算,符号和 C 语言位运算符完全匹配:

运算符号逻辑名称命题逻辑符号运算规则(二进制 0/1)
~非(NOT)¬P=0 时,~P=1;当 P=1 时,~P=0(取反)
&与(AND)只有 p=1q=1 时,p&q=1;否则为 0(全 1 才 1)
|或(OR)v只要 p=1或 q=1,否则为 0(有 1 就 1)
^异或(EXCLUSIVE-OR)pq 不同时(1 和 0 或 0 和 1),p^q=1;否则为 0(不同才 1)

书中扩展部分

整数算术里有乘法对加法的分配律:
a⋅(b+c)=(a⋅b)+(a⋅c)
布尔代数里也有类似的分配律:


& 对 | 的分配律:
a&(b∣c)=(a&b)∣(a&c)

| 对 & 的分配律:
a∣(b&c)=(a∣b)&(a∣c)

注意:整数算术不满足 
a+(b⋅c)=(a+b)⋅(a+c)
,但布尔代数的这两个分配律都成立。

布尔环(Boolean ring)
当考虑位向量上的 ^(异或)、&(与)、~(非)运算时,会得到另一种数学结构 ——布尔环。
布尔环与整数运算的相似性:加法逆元
整数运算:每个值 x 都有加法逆元 −x,满足 x+(−x)=0。

 

布尔环:这里的 “加法” 是 ^(异或),每个元素的加法逆元是它自己。
即:对任意位向量 a,都有a∧a=0(这里的 0表示全 0 的位向量)
验证:对单个位成立
0∧0=0
1∧1=0
扩展到位向量:无论怎么排列组合顺序,
a∧a=0
 都成立。这个性质会带来很多有趣的结果和技巧(后续练习题 2.10 会探讨)。

 

位向量表示有限集合

核心规则:位的位置 = 集合里的元素
我们约定:
位向量长度 = w,对应元素范围是 {0, 1, ..., w-1}
位向量里第 i 位是 1 ⇔ 元素 i 在集合里
位向量里第 i 位是 0 ⇔ 元素 i 不在集合里
注意:位向量最右边是第 0 位,往左依次是第 1 位、第 2 位……

按位与 a & b → 集合交集 A∩B

按位或 a | b → 集合并集 A∪B

按位取反 ~a → 集合补集 A

4.位移运算

左移运算 x << k

规则:x 向左移动 k 位
丢弃最高的 k 位
在右端补 k 个 0

例子(8 位)
x = [01100011],x << 4:
丢弃最高 4 位 0110
右端补 4 个 0
结果:[00110000]

移运算 x >> k

右移行为分两种,这是和左移最大的区别:
1. 逻辑右移
规则:在左端补 k 个 0

2.算术右移

规则:在左端补 k 个 “最高有效位” 的值(即符号位)
如果原数最高位是 1,就补 1
如果原数最高位是 0,就补 0

用途:对有符号整数运算非常有用

左移:右端补 0
逻辑右移:左端补 0
算术右移:
[01100011] 最高位是 0 → 补 0 → [00000110]
[10010101] 最高位是 1 → 补 1 → [11111001]

C 语言标准的模糊性
有符号数:C 标准没有明确定义是用逻辑右移还是算术右移


风险:假设某一种右移的代码,可能在不同编译器 / 机器上有不同行为(可移植性问题)


现实:几乎所有编译器 / 机器组合,对有符号数都使用算术右移,程序员也普遍默认这一点

无符号数:C 标准明确要求必须使用逻辑右移

优先级陷阱
很多人会写类似 1 << 2 + 3 << 4 的表达式,本意是:
(1<<2)+(3<<4)
但在 C 语言中,加法(+、-)的优先级比移位运算(<<、>>)更高,所以实际等价于:
1<<(2+3)<<4
再按移位运算从左到右结合,变成:
((1<<5)<<4)
计算结果:
1 << 5 = 32
32 << 4 = 512

而不是期望的 (1<<2)+(3<<4) = 4 + 48 = 52。
核心结论
优先级:+、- > <<、>>
结合性:移位运算从左到右结合

风险:搞错优先级是 C 语言中非常常见且隐蔽的 bug 来源

✅ 最佳实践
当你拿不准优先级时,一定要加括号!

特性无符号数编码 B2Uw​补码编码 B2Tw​
符号只能表示非负数可正可负
最高位权重+2^w−1−2^w−1(符号位)
最小值0−2^w−1
最大值2^w−1[2^(w−1) ]−1
范围对称性对称(0 到最大值)不对称(负数范围比正数多 1)
唯一性双射(一一对应)双射(一一对应)

这张表展示了不同字长 w(8/16/32/64 位)下,几个核心整数的十六进制位模式和十进制数值:

数值w=8 位w=16 位w=32 位w=64 位
UMaxw​0xFF / 2550xFFFF / 655350xFFFFFFFF / 42949672950xFFFFFFFFFFFFFFFF / 18446744073709551615
TMinw​0x80 / -1280x8000 / -327680x80000000 / -21474836480x8000000000000000 / -9223372036854775808
TMaxw​0x7F / 1270x7FFF / 327670x7FFFFFFF / 21474836470x7FFFFFFFFFFFFFFF / 9223372036854775807
−10xFF0xFFFF0xFFFFFFFF0xFFFFFFFFFFFFFFFF
00x000x00000x000000000x0000000000000000

原因:
一半位模式(符号位 = 1)表示负数
另一半位模式(符号位 = 0)表示非负数(包含 0)
0 占用了一个非负数编码,导致可表示的负数比正数多 1 个
影响:补码运算会有特殊属性,容易引发细微程序错误

无符号最大值与补码最大值的关系

标准的模糊性
C 语言标准没有强制要求用补码表示有符号整数
但几乎所有现代机器都使用补码表示有符号数
可移植性建议:
不要假设具体数值范围(除了 C 标准保证的最小范围)
不要假设有符号数的具体表示方式
想获得最大可移植性,应依赖 <limits.h> 中定义的常量

C 库头文件 <limits.h>

特性补码(Two's complement)反码(Ones' complement)原码(Sign-Magnitude)
0 的编码唯一 [00…0]两种:+0/-0两种:+0/-0
负数范围比正数多 1和正数对称和正数对称
现代机器几乎全部使用几乎淘汰几乎淘汰
负数计算2w−x[11…1]−x只改符号位

补码为什么比原码行,why_补码范围为什么比原码大-CSDN博客

C 语言中,同字长的有符号数 ↔ 无符号数强制转换时,底层二进制位模式完全不变,只是改变了 “如何解读这些位” 的规则:
有符号 → 无符号:用无符号规则(B2U)去解释原来的补码位模式
无符号 → 有符号:用补码规则(B2T)去解释原来的无符号位模式
数值可能变,但位模式本身没有修改

 运算中的隐式升级规则
当表达式同时包含有符号数和无符号数时:
C 语言会隐式将有符号数转换为无符号数,并假设两者都是非负数
这会导致非直观的比较结果

5.截断数字

short sx = -12345;        // 补码:0xCFC7(16位)
unsigned short usx = sx;  // 无符号:53191(0xCFC7)
int x = sx;               // 符号扩展 → 0xFFFFCFC7(值仍-12345)
unsigned ux = usx;        // 零扩展 → 0x0000CFC7(值仍53191)

输出:
sx = -12345 → 位模式 cf c7(16 位)→ ff ff cf c7(32 位,符号位补 1)
usx = 53191 → 位模式 cf c7(16 位)→ 00 00 cf c7(32 位,补 0)

.符号扩展的数学证明

转换顺序的影响
C 语言规定:先扩展位长度,再转换符号。

short sx = -12345;
unsigned uy = sx;  // 等价于 (unsigned)(int)sx,先符号扩展到32位,再转无符号

结果:uy = 4294954951(0xFFFFCFC7),而非 53191。
若先转 unsigned short 再扩展:(unsigned)(unsigned short)sx 结果才是 53191。

截断数字(从大类型 → 小类型)
核心规则
将 w 位截断为 k 位:丢弃高位 w−k 位,数值可能改变(溢出)。

int x = 53191;
short sx = (short)x;  // 截断为16位 → -12345
int y = sx;           // 符号扩展回32位 → -12345

关于有符号数与无符号数的建议
1. 隐式转换的风险
有符号数 ↔ 无符号数的隐式转换会导致非直观行为,极易引发程序错误(甚至安全漏洞)。
例子:-1 < 0U 被解释为 4294967295 < 0,结果为假(逻辑错误)。

实安全漏洞案例(FreeBSD)

int copy_from_kernel(void *user_dest, int maxlen) {
  int len = KSIZE < maxlen ? KSIZE : maxlen;
  memcpy(user_dest, kbuf, len);  // maxlen 为负数时,len 被转为无符号大整数
}

风险:若 maxlen 为负数,len 被隐式转为无符号数,导致 memcpy 复制大量内核数据到用户缓冲区,泄露敏感信息。
修复:将 maxlen、len 声明为 size_t(无符号类型,与 memcpy 参数一致)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值