二进制与位运算
硬件寄存器常把好几个开关挤在一个数里,每个开关占一位或几位。内核要做的,经常是“只改其中两位,其他位不动”。这一页教的就是这个固定写法。
1. 二进制和位编号
计算机里的数都是二进制存的。一个二进制位(bit)只能是 0 或 1。位从右往左编号,最右边是第 0 位:
位编号: 7 6 5 4 3 2 1 0 二进制: 0 0 0 0 1 0 1 1 = 8 + 2 + 1 = 11
第 i 位是 1,就贡献 2i。和十六进制互换时,每 4 位一组(见十六进制):0x1234 = 0001 0010 0011 0100。
2. 五个位运算符
| 写法 | 含义 | 例子(8 位) |
|---|---|---|
a & b | 按位与:两边都是 1 才是 1 | 1100 & 1010 = 1000 |
a | b | 按位或:有一边是 1 就是 1 | 1100 | 1010 = 1110 |
~a | 取反:0 变 1,1 变 0 | ~00001111 = 11110000 |
a << n | 左移 n 位,右边补 0 | 1 << 3 = 1000(= 8) |
a >> n | 右移 n 位,右边的位丢掉 | 1100 >> 2 = 0011 |
别和 &&、||、! 搞混:那三个是逻辑运算,只关心“是不是 0”,结果只有 0 或 1。
3. 四个固定写法
1 << i 是“只有第 i 位是 1”的数。3 << 11 是“第 11、12 位是 1”(3 的二进制是 11)。这种用来框住某几位的数叫掩码(mask)。
x | (1 << i) // 置位:第 i 位变 1,其他不变(和 0 或不变,和 1 或变 1) x & ~(1 << i) // 清位:第 i 位变 0,其他不变(~ 之后只有第 i 位是 0) x & mask // 取出:只留下掩码框住的位,其他全变 0 (x >> 11) & 3 // 读字段:把第 11-12 位移到最右边,读出它的值(0-3)
改一个多位字段,就是“先清掉,再放进新值”:
x = (x & ~mask) | new_value;
4. 在内核里的样子
RISC-V 的 mstatus 寄存器里,第 11–12 位叫 MPP,记录“进入 M-mode 之前是什么模式”(含义见特权级与异常)。框架在 src/kernel/arch/type.h 里定义了:
#define MSTATUS_MPP_MASK (3L << 11) // 第 11-12 位的掩码。L 表示这是 long,64 位 #define MSTATUS_MPP_S (1L << 11) // MPP = 01,代表 S-mode
start.c 里的这三行就是上面的“先清掉,再放进新值”:
uint64 status = r_mstatus(); // 读出寄存器 status &= ~MSTATUS_MPP_MASK; // 清掉第 11-12 位 status |= MSTATUS_MPP_S; // 放进 01 w_mstatus(status); // 写回寄存器
a &= b 是 a = a & b 的简写,|= 同理。
动手:看每一步变了哪些位
cd prereq/examples make bits
观察:每一行和第一行 x 比,只有目标位变了。最后一行把第 11–12 位从 10 改成了 01。自己改 x 的初值再跑,验证你的预测。Lab1 的练习 1 就是把这几个写法写成函数。