二进制与位运算

硬件寄存器常把好几个开关挤在一个数里,每个开关占一位或几位。内核要做的,经常是“只改其中两位,其他位不动”。这一页教的就是这个固定写法。

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 才是 11100 & 1010 = 1000
a | b按位或:有一边是 1 就是 11100 | 1010 = 1110
~a取反:0 变 1,1 变 0~00001111 = 11110000
a << n左移 n 位,右边补 01 << 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 就是把这几个写法写成函数。