第四章:寄存器与指令
涉及内核源码: ,
楔子
林小源终于看到了 CPU 的心跳。
他开始观察数据在寄存器之间流动——a0 存着返回值和第一个参数,a1 存着第二个参数,sp 指向栈顶,ra 记住函数返回的位置。每一次函数调用,寄存器都会被保存和恢复;每一次算术运算,结果都会被写入某个寄存器。
他原以为内存是一切的中心。但 CPU 真正计算时,数据必须先进入寄存器。
在 RISC-V 64 中,通用寄存器有 32 个。x0 永远是 0,sp 负责栈,s0 到 由被调用者保存,a0 到 a7 负责参数和返回值。
但他最感兴趣的,是一个叫 的寄存器。
那里记录着特权态的状态:中断是否打开,之前来自哪个特权级,是否允许内核访问用户页。说到底,CPU 不只是执行指令——它还时刻记着自己身处何处、能做什么、不能做什么。
一、RISC-V 64 通用寄存器
林小源走进了一间巨大的工坊。
工坊的中央是一张工作台,台上整整齐齐地排列着 32 个格子。每个格子都发着微弱的光,里面存放着一个 64 位的值。有些格子的光是稳定的,有些在不停地闪烁——数据正在被读取和写入。
一个声音从工作台后传来——干脆利落,像连续敲击的鼓点:"别乱碰。这 32 个格子是 RISC-V 的通用寄存器。CPU 做任何计算之前,数据都得先放在这里。"
林小源看到一个矮壮的身影从工作台后走出来。它没有名字,但林小源心里把它叫作"寄存器守卫"。守卫的身体由 32 块金属板拼成,每块板上刻着一个寄存器名。
守卫指着最左边的格子——里面空空如也,却散发着一种绝对的虚无感:"x0,也叫 。永远是 0。你往里写什么都没用,读出来永远是 0。它是常量 0 的硬件实现。"
然后它指向紧挨着的几个格子:"x1 是 ra——返回地址。每次函数调用时,CPU 把返回地址存在这里。x2 是 sp——栈指针,指向栈顶。x3 是 gp——全局指针。x4 是 tp——线程指针。"
"参数怎么传?"
守卫的手指向另一排格子:"a0 到 a7——八个参数寄存器。函数的前八个参数依次放在这里。返回值放在 a0,放不下的时候 a1 也用。"
它又指着另一排:"s0 到 ——保存寄存器。被调用者负责保存和恢复。如果你的函数要用这些寄存器,你得先把它们的值压栈,用完再弹出来。"
"还有 t0 到 t6——临时寄存器。调用者负责保存。用了就可能被覆盖,不保证安全。"
林小源看着那 32 个格子。它们的分工如此明确——谁传参数,谁存返回值,谁是临时的,谁是永久的。这就是 ABI——应用程序二进制接口——在硬件层面的体现。
"Linux RISC-V 的系统调用约定呢?"林小源问。
守卫的声音变得严肃:"a7 存系统调用号。a0 到 a5 存参数。执行 指令进入内核。返回值放在 a0——如果是负值,表示错误码取反。"
二、状态寄存器 sstatus
守卫带着林小源来到工坊的一面墙前。墙上挂着一块特殊的面板——比其他面板都大,上面布满了指示灯。
"这是 ,"守卫的声音低了下来,"S 态状态寄存器。它不属于那 32 个通用寄存器——它是控制寄存器,也叫 CSR。用户态不能直接读取它。"
林小源看着面板上的指示灯。有些亮着,有些灭着。
守卫指着一盏灯:"SIE——S 态中断使能。亮着表示中断打开,灭着表示中断关闭。内核在执行关键操作时会关中断,操作完再打开。"
又指着另一盏:"SPIE——trap 前的 SIE。记录进入内核之前中断是什么状态。返回用户态时,SPIE 的值会恢复到 SIE。"
"SPP——trap 前特权级。记录进入内核之前你在哪个特权级——U 态还是 S 态。返回时根据这个位决定回到哪个态。"
"SUM——S 态允许访问用户页。如果这个位被置位,内核可以直接读写用户态的内存。否则——访问用户页会触发异常。"
"MXR——可执行页也可读。置位后,标记为'可执行'的内存页也可以被读取。这在某些场景下有用。"
林小源盯着那块面板。这些位看似简单——0 或 1,亮或灭——但每一个都关系到系统的安全和稳定。中断的开关、特权级的切换、内存的访问控制——CPU 不只是执行指令,它还时刻记着自己身处何处、能做什么、不能做什么。
"这块面板就是 CPU 的'自我意识',"守卫说,"它让 CPU 知道:我现在是什么身份?我能做什么?我不能做什么?没有它,内核和用户态之间就没有边界。"
三、常用指令
守卫走到工作台前,开始演示基本的操作。
"RISC-V 的指令集很精简,"它一边说,一边在工作台上操作,"数据传送——mv 移动,li 加载立即数,la 加载地址,ld 从内存读 8 字节,sd 写 8 字节到内存。"
它拿起两个格子里的值——10 和 3——开始运算:"算术—— 加, 减, 乘, 除。位移—— 左移, 右移。逻辑—— 与,or 或, 异或。"
"控制流呢?"
守卫放下手里的值:"条件分支—— 相等则跳, 不等则跳, 小于则跳, 大于等于则跳。函数调用——jal ra, func,把返回地址存到 ra,跳到 。返回——,从 ra 跳回去。"
"还有 ,"守卫的声音变得郑重,"这是用户态进入内核的唯一合法通道。执行 后,CPU 从 U 态陷入 S 态,跳转到内核的异常入口。"
四、栈帧
工作台的旁边,有一根向下生长的柱子。柱子上刻着一层又一层的标记——每一层都是一个函数的栈帧。
"栈向低地址增长,"守卫指着柱子的顶部,"每次函数调用,都会在栈上分配一个新的栈帧。函数返回时,栈帧被释放。"
林小源看着柱子上的标记。最顶层是 ,下面是 outer_function,再下面是 inner_function。每一层都记录着:返回地址、保存的寄存器、局部变量。
"每次调用都会在栈上留下痕迹,"守卫说,"参数、返回地址、保存的 s 寄存器和局部变量。调试器就是通过遍历这些栈帧来回溯调用链的。"
林小源注意到,柱子很窄——只有 8KB 到 16KB 的空间。"这么小?"
"内核栈,"守卫的声音带着警告,"不能递归、不能在栈上分配大数组、不能做深度函数嵌套。每一字节都得精打细算。内核开发者有个不成文的规矩——单个函数的栈帧不要超过几百字节。"
五、系统调用
柱子的底部,有一扇紧闭的门。门上刻着 三个字母,门框两侧站着两个守卫——一个标着 U,一个标着 S。
"这扇门是用户态和内核态之间的边界,"寄存器守卫说,"用户态的程序不能直接访问硬件、不能直接操作内存页表、不能直接读写设备寄存器。它需要什么,都得通过这扇门——系统调用。"
林小源看着门上的流程。用户态的程序先把系统调用号放到 a7,参数放到 a0 到 a5,然后执行 。CPU 从 U 态陷入 S 态,跳转到内核的异常入口。内核保存用户寄存器到 ,根据 a7 查找系统调用表,执行对应的处理函数,把返回值写回 a0,最后恢复寄存器,返回用户态。
"整个过程,"守卫说,"用户态的程序什么都不用管——它只看到 之前和 a0 返回之后。中间发生了什么,是内核的事。"
"如果用户态伪造了系统调用号呢?"
守卫笑了——如果一块金属板能笑的话:"内核会检查 a7 的范围。超出范围的调用号直接返回错误。而且——用户态能做的只有 。它不能跳过这扇门,不能直接执行 S 态的指令。硬件强制的边界。"
门后面传来低沉的声音——那是异常入口的代码在运行。每一次 ,都是一次穿越。从用户态到内核态,从无权到有权,从受限到自由。
但只有内核允许你穿过这扇门。
道藏笔记
内核启示
寄存器是 CPU 的工作台。 C 代码看起来在操作变量,汇编层面却是在寄存器、栈和内存之间搬运数据。
RISC-V 的简洁之处在于:寄存器命名、函数调用、异常入口都非常规整。a7 决定系统调用号,a0 传入第一个参数并接收返回值,sp 维持栈,ra 决定返回位置。
而 中的中断位、特权级位和访问控制位,决定了内核能不能被打断、能不能访问用户空间、能不能安全返回。日后看到调度器、陷入处理、内存访问检查时,这些看似抽象的位都会变成具体的代码路径。
代码典籍
/*
* RISC-V 64 通用寄存器:
*
* x0/zero 永远为 0
* x1/ra 返回地址
* x2/sp 栈指针
* x3/gp 全局指针
* x4/tp 线程指针
* x5-x7 t0-t2 临时寄存器
* x8/s0/fp 保存寄存器 / 帧指针
* x9/s1 保存寄存器
* x10-x17 a0-a7 参数 / 返回值
* x18-x27 s2-s11 保存寄存器
* x28-x31 t3-t6 临时寄存器
*/
printf("=== RISC-V 64 通用寄存器 ===\n\n");
printf("寄存器 大小 用途\n");
printf("zero/x0 8B 常量 0\n");
printf("ra/x1 8B 返回地址\n");
printf("sp/x2 8B 栈指针\n");
printf("gp/x3 8B 全局指针\n");
printf("tp/x4 8B 线程指针\n");
printf("t0-t6 8B 临时寄存器,调用者保存\n");
printf("s0/fp-s11 8B 保存寄存器,被调用者保存\n");
printf("a0-a7 8B 参数 / 返回值\n\n");
printf("函数调用约定:\n");
printf(" 参数依次放入: a0, a1, a2, a3, a4, a5, a6, a7\n");
printf(" 返回值放在: a0 (必要时 a1)\n");
printf(" ra 保存返回地址,sp 指向当前栈顶\n\n");
printf("Linux RISC-V 系统调用约定:\n");
printf(" 系统调用号放在 a7\n");
printf(" 参数放在 a0-a5\n");
printf(" ecall 进入内核\n");
printf(" 返回值放在 a0\n");#include <stdio.h>
/*
* RISC-V 64 通用寄存器:
*
* x0/zero 永远为 0
* x1/ra 返回地址
* x2/sp 栈指针
* x3/gp 全局指针
* x4/tp 线程指针
* x5-x7 t0-t2 临时寄存器
* x8/s0/fp 保存寄存器 / 帧指针
* x9/s1 保存寄存器
* x10-x17 a0-a7 参数 / 返回值
* x18-x27 s2-s11 保存寄存器
* x28-x31 t3-t6 临时寄存器
*/
int main() {
printf("=== RISC-V 64 通用寄存器 ===\n\n");
printf("寄存器 大小 用途\n");
printf("zero/x0 8B 常量 0\n");
printf("ra/x1 8B 返回地址\n");
printf("sp/x2 8B 栈指针\n");
printf("gp/x3 8B 全局指针\n");
printf("tp/x4 8B 线程指针\n");
printf("t0-t6 8B 临时寄存器,调用者保存\n");
printf("s0/fp-s11 8B 保存寄存器,被调用者保存\n");
printf("a0-a7 8B 参数 / 返回值\n\n");
printf("函数调用约定:\n");
printf(" 参数依次放入: a0, a1, a2, a3, a4, a5, a6, a7\n");
printf(" 返回值放在: a0 (必要时 a1)\n");
printf(" ra 保存返回地址,sp 指向当前栈顶\n\n");
printf("Linux RISC-V 系统调用约定:\n");
printf(" 系统调用号放在 a7\n");
printf(" 参数放在 a0-a5\n");
printf(" ecall 进入内核\n");
printf(" 返回值放在 a0\n");
return 0;
}/*
* sstatus 是 RISC-V S 态状态寄存器。
* 用户态不能直接读取它;这里用普通整数模拟关键位。
*/
#define SSTATUS_SIE (1UL << 1) /* S 态中断使能 */
#define SSTATUS_SPIE (1UL << 5) /* trap 前的 SIE */
#define SSTATUS_SPP (1UL << 8) /* trap 前特权级 */
#define SSTATUS_SUM (1UL << 18) /* S 态允许访问用户页 */
#define SSTATUS_MXR (1UL << 19) /* 可执行页也可读 */
static void show_bit(const char *name, unsigned long value, unsigned long bit) {
printf("%-10s: %s\n", name, (value & bit) ? "置位" : "清零");
}
unsigned long sstatus = SSTATUS_SPIE | SSTATUS_SUM;
printf("=== sstatus 状态位 ===\n\n");
show_bit("SIE", sstatus, SSTATUS_SIE);
show_bit("SPIE", sstatus, SSTATUS_SPIE);
show_bit("SPP", sstatus, SSTATUS_SPP);
show_bit("SUM", sstatus, SSTATUS_SUM);
show_bit("MXR", sstatus, SSTATUS_MXR);
printf("\n内核含义:\n");
printf(" SIE 控制 S 态中断是否响应\n");
printf(" SPP 记录 trap 前来自 U 态还是 S 态\n");
printf(" SUM 控制内核是否能访问用户地址\n");
printf(" MXR 控制可执行页是否也可读\n");#include <stdio.h>
#include <stdint.h>
/*
* sstatus 是 RISC-V S 态状态寄存器。
* 用户态不能直接读取它;这里用普通整数模拟关键位。
*/
#define SSTATUS_SIE (1UL << 1) /* S 态中断使能 */
#define SSTATUS_SPIE (1UL << 5) /* trap 前的 SIE */
#define SSTATUS_SPP (1UL << 8) /* trap 前特权级 */
#define SSTATUS_SUM (1UL << 18) /* S 态允许访问用户页 */
#define SSTATUS_MXR (1UL << 19) /* 可执行页也可读 */
static void show_bit(const char *name, unsigned long value, unsigned long bit) {
printf("%-10s: %s\n", name, (value & bit) ? "置位" : "清零");
}
int main() {
unsigned long sstatus = SSTATUS_SPIE | SSTATUS_SUM;
printf("=== sstatus 状态位 ===\n\n");
show_bit("SIE", sstatus, SSTATUS_SIE);
show_bit("SPIE", sstatus, SSTATUS_SPIE);
show_bit("SPP", sstatus, SSTATUS_SPP);
show_bit("SUM", sstatus, SSTATUS_SUM);
show_bit("MXR", sstatus, SSTATUS_MXR);
printf("\n内核含义:\n");
printf(" SIE 控制 S 态中断是否响应\n");
printf(" SPP 记录 trap 前来自 U 态还是 S 态\n");
printf(" SUM 控制内核是否能访问用户地址\n");
printf(" MXR 控制可执行页是否也可读\n");
return 0;
}long a = 10, b = 3;
printf("=== RISC-V 常用指令 ===\n\n");
printf("数据传送:\n");
printf(" mv a0, a1 -> a0 = a1\n");
printf(" li a0, 42 -> a0 = 42\n");
printf(" la a0, symbol -> a0 = &symbol\n");
printf(" ld a0, 0(sp) -> 从栈读取 8 字节\n");
printf(" sd a0, 0(sp) -> 写入 8 字节到栈\n\n");
printf("算术:\n");
printf(" add : %ld + %ld = %ld\n", a, b, a + b);
printf(" sub : %ld - %ld = %ld\n", a, b, a - b);
printf(" mul : %ld * %ld = %ld\n", a, b, a * b);
printf(" div : %ld / %ld = %ld\n\n", a, b, a / b);
printf("位移与逻辑:\n");
printf(" slli : %ld << 2 = %ld\n", a, a << 2);
printf(" srli : %ld >> 1 = %ld\n", a, (unsigned long)a >> 1);
printf(" and : %ld & %ld = %ld\n", a, b, a & b);
printf(" or : %ld | %ld = %ld\n", a, b, a | b);
printf(" xor : %ld ^ %ld = %ld\n\n", a, b, a ^ b);
printf("控制流:\n");
printf(" beq/bne/blt/bge -> 条件分支\n");
printf(" jal ra, func -> 调用函数\n");
printf(" ret -> 返回到 ra\n");
printf(" ecall -> 触发系统调用或异常\n");#include <stdio.h>
int main() {
long a = 10, b = 3;
printf("=== RISC-V 常用指令 ===\n\n");
printf("数据传送:\n");
printf(" mv a0, a1 -> a0 = a1\n");
printf(" li a0, 42 -> a0 = 42\n");
printf(" la a0, symbol -> a0 = &symbol\n");
printf(" ld a0, 0(sp) -> 从栈读取 8 字节\n");
printf(" sd a0, 0(sp) -> 写入 8 字节到栈\n\n");
printf("算术:\n");
printf(" add : %ld + %ld = %ld\n", a, b, a + b);
printf(" sub : %ld - %ld = %ld\n", a, b, a - b);
printf(" mul : %ld * %ld = %ld\n", a, b, a * b);
printf(" div : %ld / %ld = %ld\n\n", a, b, a / b);
printf("位移与逻辑:\n");
printf(" slli : %ld << 2 = %ld\n", a, a << 2);
printf(" srli : %ld >> 1 = %ld\n", a, (unsigned long)a >> 1);
printf(" and : %ld & %ld = %ld\n", a, b, a & b);
printf(" or : %ld | %ld = %ld\n", a, b, a | b);
printf(" xor : %ld ^ %ld = %ld\n\n", a, b, a ^ b);
printf("控制流:\n");
printf(" beq/bne/blt/bge -> 条件分支\n");
printf(" jal ra, func -> 调用函数\n");
printf(" ret -> 返回到 ra\n");
printf(" ecall -> 触发系统调用或异常\n");
return 0;
}static void inner_function(int x) {
int local = x * 2;
printf("inner_function:\n");
printf(" 参数 x = %d\n", x);
printf(" 局部变量 local = %d\n", local);
printf(" local 地址: %p\n", (void *)&local);
}
static void outer_function(void) {
int outer_local = 42;
printf("outer_function:\n");
printf(" outer_local 地址: %p\n\n", (void *)&outer_local);
inner_function(outer_local);
}
int main_local = 100;
printf("=== RISC-V 栈帧 ===\n\n");
printf("main:\n");
printf(" main_local 地址: %p\n\n", (void *)&main_local);
outer_function();
printf("\nRISC-V 函数调用通常会:\n");
printf(" 1. 调整 sp,为栈帧分配空间\n");
printf(" 2. 保存 ra 和需要保留的 s 寄存器\n");
printf(" 3. 用 s0/fp 作为帧指针(如果启用帧指针)\n");
printf(" 4. ret 从 ra 返回\n");#include <stdio.h>
#include <stdint.h>
static void inner_function(int x) {
int local = x * 2;
printf("inner_function:\n");
printf(" 参数 x = %d\n", x);
printf(" 局部变量 local = %d\n", local);
printf(" local 地址: %p\n", (void *)&local);
}
static void outer_function(void) {
int outer_local = 42;
printf("outer_function:\n");
printf(" outer_local 地址: %p\n\n", (void *)&outer_local);
inner_function(outer_local);
}
int main() {
int main_local = 100;
printf("=== RISC-V 栈帧 ===\n\n");
printf("main:\n");
printf(" main_local 地址: %p\n\n", (void *)&main_local);
outer_function();
printf("\nRISC-V 函数调用通常会:\n");
printf(" 1. 调整 sp,为栈帧分配空间\n");
printf(" 2. 保存 ra 和需要保留的 s 寄存器\n");
printf(" 3. 用 s0/fp 作为帧指针(如果启用帧指针)\n");
printf(" 4. ret 从 ra 返回\n");
return 0;
}#define RV_SYS_read 63
#define RV_SYS_write 64
#define RV_SYS_openat 56
#define RV_SYS_close 57
#define RV_SYS_mmap 222
#define RV_SYS_ioctl 29
#define RV_SYS_exit 93
printf("=== RISC-V 系统调用 ===\n\n");
printf("1. 用户态准备寄存器:\n");
printf(" a7 = %d (SYS_write)\n", RV_SYS_write);
printf(" a0 = 1 (stdout)\n");
printf(" a1 = 缓冲区地址\n");
printf(" a2 = 长度\n");
printf(" -> ecall 指令\n\n");
printf("2. 内核处理:\n");
printf(" - 从 U 态陷入 S 态\n");
printf(" - 保存用户寄存器到 pt_regs\n");
printf(" - 根据 a7 查找系统调用表\n");
printf(" - 返回值写回 a0\n\n");
printf("3. 常用系统调用号 (RISC-V Linux):\n");
printf(" SYS_read = %d\n", RV_SYS_read);
printf(" SYS_write = %d\n", RV_SYS_write);
printf(" SYS_openat = %d\n", RV_SYS_openat);
printf(" SYS_close = %d\n", RV_SYS_close);
printf(" SYS_mmap = %d\n", RV_SYS_mmap);
printf(" SYS_ioctl = %d\n", RV_SYS_ioctl);
printf(" SYS_exit = %d\n\n", RV_SYS_exit);
pid_t pid = getpid();
printf("getpid() = %d\n", pid);#include <stdio.h>
#include <unistd.h>
#define RV_SYS_read 63
#define RV_SYS_write 64
#define RV_SYS_openat 56
#define RV_SYS_close 57
#define RV_SYS_mmap 222
#define RV_SYS_ioctl 29
#define RV_SYS_exit 93
int main() {
printf("=== RISC-V 系统调用 ===\n\n");
printf("1. 用户态准备寄存器:\n");
printf(" a7 = %d (SYS_write)\n", RV_SYS_write);
printf(" a0 = 1 (stdout)\n");
printf(" a1 = 缓冲区地址\n");
printf(" a2 = 长度\n");
printf(" -> ecall 指令\n\n");
printf("2. 内核处理:\n");
printf(" - 从 U 态陷入 S 态\n");
printf(" - 保存用户寄存器到 pt_regs\n");
printf(" - 根据 a7 查找系统调用表\n");
printf(" - 返回值写回 a0\n\n");
printf("3. 常用系统调用号 (RISC-V Linux):\n");
printf(" SYS_read = %d\n", RV_SYS_read);
printf(" SYS_write = %d\n", RV_SYS_write);
printf(" SYS_openat = %d\n", RV_SYS_openat);
printf(" SYS_close = %d\n", RV_SYS_close);
printf(" SYS_mmap = %d\n", RV_SYS_mmap);
printf(" SYS_ioctl = %d\n", RV_SYS_ioctl);
printf(" SYS_exit = %d\n\n", RV_SYS_exit);
pid_t pid = getpid();
printf("getpid() = %d\n", pid);
return 0;
}寄存器试炼
RISC-V Linux 系统调用中,用来放置系统调用号的寄存器是哪一个?