本文结合 ysyx-workbench/nemu 实际代码,系统讲解 PA1《RTFSC》这一章。
重点回答三个问题:
摘要
TRM(Turing Machine)是 AbstractMachine 提供的最简抽象计算机。
在 NEMU 中,TRM 并不是一个单独的文件,而是由以下模块共同实现:
monitor -> 初始化、加载程序、提供调试器
CPU -> PC、通用寄存器、CSR
memory -> pmem、paddr_read/write、vaddr_read/write
ISA -> inst_fetch、decode_exec、riscv32 指令实现
核心执行逻辑可以概括为:
while (1) {
从 PC 指向的内存取出指令;
译码并执行指令;
更新 PC;
}
在 NEMU 中,这个循环对应:
cpu_exec()
-> execute()
-> exec_once()
-> isa_exec_once()
-> inst_fetch()
-> decode_exec()
-> cpu.pc = s->dnpc
本文会逐项把这些代码和 TRM 的硬件概念对应起来。
关键词
NEMU、TRM、AbstractMachine、riscv32、RTFSC、PA1、ysyx、SDB、GDB、状态机
一、TRM 是什么
TRM 是 Turing Machine 的缩写,是 AM 中最简单的抽象模型。
TRM 提供:
内存
PC
通用寄存器
指令执行
putch
halt
它不包含:
中断
异常
虚拟内存
多处理器
设备
TRM 的运行模型就是:
while (1) {
从 PC 指示的存储器位置取出指令;
执行指令;
更新 PC;
}
对应到 NEMU:
存储器 -> pmem
CPU -> CPU_state
寄存器 -> cpu.gpr[32]
PC -> cpu.pc
取指 -> inst_fetch()
译码执行 -> decode_exec()
循环 -> cpu_exec()/exec_once()
二、NEMU 的四个模块
讲义把 NEMU 分为四部分:
monitor
CPU
memory
device
对应代码目录:
nemu/src/monitor/
nemu/src/cpu/
nemu/src/memory/
nemu/src/device/
PA1 阶段只需要关心:
monitor
CPU
memory
设备在 PA2 才介绍。
三、Monitor 模块
Monitor 不是客户计算机的一部分,而是 NEMU 在 Linux 主机上的调试和运行基础设施。
它负责:
- 读取客户程序;
- 初始化内存;
- 初始化 CPU 状态;
- 初始化 SDB;
- 监控客户机状态;
- 加载镜像到内存。
相关文件:
nemu/src/nemu-main.c
nemu/src/monitor/monitor.c
nemu/src/monitor/sdb/sdb.c
nemu/src/monitor/sdb/expr.c
nemu/src/monitor/sdb/watchpoint.c
3.1 NEMU 入口
代码:
int main(int argc, char *argv[]) {
#ifdef CONFIG_TARGET_AM
am_init_monitor();
#else
init_monitor(argc, argv);
#endif
engine_start();
return is_exit_status_bad();
}
入口流程:
main
-> init_monitor
-> engine_start
3.2 init_monitor
代码:
void init_monitor(int argc, char *argv[]) {
parse_args(argc, argv);
init_rand();
init_log(log_file);
init_mem();
IFDEF(CONFIG_DEVICE, init_device());
init_isa();
long img_size = load_img();
#ifdef CONFIG_DIFFTEST
init_difftest(diff_so_file, img_size, difftest_port);
#endif
init_sdb();
IFDEF(CONFIG_ITRACE, init_disasm());
welcome();
}
初始化顺序及其原因:
解析参数 -> 得到 log_file、img_file、diff_so_file
初始化随机数 -> 后续 memset 随机内存需要 rand()
初始化日志 -> 后续初始化过程要写日志
初始化内存 -> 创建 pmem
初始化设备 -> 注册 MMIO
初始化 ISA -> 设置 cpu.pc 和 gpr[0]
加载镜像 -> 把客户程序写入 pmem
初始化 SDB -> 准备调试器
打印欢迎信息 -> 显示配置信息
3.3 SDB
SDB 是 NEMU 的简易调试器。
代码:
static struct {
const char *name;
const char *description;
int (*handler)(char *);
} cmd_table[] = {
{ "help", "Display information about all supported commands", cmd_help },
{ "c", "Continue the execution of the program", cmd_c },
{ "q", "Exit NEMU", cmd_q },
{ "si", "Execute N instruction(s)", cmd_si },
{ "info", "Show registers / watchpoints", cmd_info },
{ "x", "Scan memory", cmd_x },
{ "p", "Calculate expression", cmd_p },
{ "w", "Set watchpoint", cmd_w },
{ "d", "Delete watchpoint/breakpoint", cmd_d },
{ "b", "Set breakpoint", cmd_b },
};
常见命令:
| si | 执行 N 条指令 |
| info r | 查看寄存器 |
| x | 查看内存 |
| p | 计算表达式 |
| w | 设置观察点 |
| b | 设置断点 |
| c | 继续执行 |
| q | 退出 NEMU |
SDB 是 RTFSC 的重要工具,可以让状态机一次只前进一步。
四、Memory 模块
讲义说:
存储器是一个大数组。
你的 NEMU 中,存储器定义在:
nemu/src/memory/paddr.c
代码:
#if defined(CONFIG_PMEM_MALLOC)
static uint8_t *pmem = NULL;
#else
static uint8_t pmem[CONFIG_MSIZE] PG_ALIGN = {};
#endif
uint8_t* guest_to_host(paddr_t paddr) {
return pmem + paddr – CONFIG_MBASE;
}
当前配置:
CONFIG_MBASE=0x80000000
CONFIG_MSIZE=0x8000000
于是:
客户机地址 0x80000000 -> pmem[0]
客户机地址 0x80000004 -> pmem[4]
客户程序访问内存:
word_t paddr_read(paddr_t addr, int len) {
if (likely(in_pmem(addr))) {
return pmem_read(addr, len);
}
IFDEF(CONFIG_DEVICE, return mmio_read(addr, len));
out_of_bound(addr);
}
void paddr_write(paddr_t addr, int len, word_t data) {
if (likely(in_pmem(addr))) {
pmem_write(addr, len, data);
return;
}
IFDEF(CONFIG_DEVICE, mmio_write(addr, len, data); return);
out_of_bound(addr);
}
抽象接口是:
vaddr_read()
vaddr_write()
它们最终会调用:
vaddr_read -> paddr_read
vaddr_write -> paddr_write
当前还没有 MMU,因此虚拟地址和物理地址的转换可以看作是直接的。
因此:
讲义中的存储器 = pmem + paddr_read/write + vaddr_read/write
五、CPU 模块
CPU 状态定义在:
nemu/src/isa/riscv32/include/isa-def.h
代码:
typedef struct {
word_t gpr[32];
vaddr_t pc;
word_t mstatus;
word_t mtvec;
word_t mepc;
word_t mcause;
} riscv32_CPU_state;
在 cpu-exec.c 中:
CPU_state cpu = {};
这个 cpu 就是客户机 CPU 当前状态。
对应关系:
cpu.gpr[32] -> 通用寄存器
cpu.pc -> 程序计数器
cpu.mstatus -> 机器状态寄存器
cpu.mtvec -> trap 入口
cpu.mepc -> 异常返回地址
cpu.mcause -> 异常原因
寄存器访问:
#define R(i) gpr(i)
gpr 定义在:
nemu/src/isa/riscv32/local-include/reg.h
#define gpr(idx) (cpu.gpr[check_reg_idx(idx)])
x0 必须始终为 0:
R(0) = 0;
六、指令执行
6.1 取指
代码:
int isa_exec_once(Decode *s) {
s->isa.inst = inst_fetch(&s->snpc, 4);
return decode_exec(s);
}
inst_fetch() 从当前 PC 指向的内存读取 4 字节,得到一条 riscv32 指令。
例如 ebreak:
0x00100073
内存中的小端字节序:
73 00 10 00
6.2 译码
译码机制在:
nemu/include/cpu/decode.h
代码:
#define INSTPAT(pattern, …) do { \\
uint64_t key, mask, shift; \\
pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift); \\
if ((((uint64_t)INSTPAT_INST(s) >> shift) & mask) == key) { \\
INSTPAT_MATCH(s, ##__VA_ARGS__); \\
goto *(__instpat_end); \\
} \\
} while (0)
思想是:
指令二进制位 & mask == key
则匹配成功。
6.3 部分 riscv32 指令实现
inst.c 中的 addi:
INSTPAT("??????? ????? ????? 000 ????? 00100 11",
addi, I, R(rd) = src1 + imm);
add:
INSTPAT("0000000 ????? ????? 000 ????? 01100 11",
add, R, R(rd) = src1 + src2);
sub:
INSTPAT("0100000 ????? ????? 000 ????? 01100 11",
sub, R, R(rd) = src1 – src2);
ebreak:
INSTPAT("000000000001 00000 000 00000 11100 11",
ebreak, I, {
s->dnpc = isa_raise_intr(3, s->pc);
});
这些代码就是 TRM 中的“指令”和“运算器”。
七、TRM 的执行循环
7.1 exec_once
static void exec_once(Decode *s, vaddr_t pc) {
s->pc = pc;
s->snpc = pc;
isa_exec_once(s);
cpu.pc = s->dnpc;
}
对应:
从 PC 取指
执行指令
更新 PC
7.2 execute
static void execute(uint64_t n) {
Decode s;
for (; n > 0; n–) {
if (nemu_state.state != NEMU_RUNNING) {
break;
}
exec_once(&s, cpu.pc);
g_nr_guest_inst++;
trace_and_difftest(&s, cpu.pc);
IFDEF(CONFIG_DEVICE, device_update());
}
}
7.3 cpu_exec
void cpu_exec(uint64_t n) {
g_print_step = (n < MAX_INST_TO_PRINT);
switch (nemu_state.state) {
case NEMU_END:
case NEMU_ABORT:
case NEMU_QUIT:
printf("Program execution has ended…\\n");
return;
default:
nemu_state.state = NEMU_RUNNING;
}
execute(n);
…
}
因此:
TRM 工作方式 = cpu_exec() + execute() + exec_once()
八、程序是状态机
讲义强调:
程序运行就是计算机状态不断发生确定性的转移。
在你的 NEMU 中,一次状态转移就是:
exec_once()
状态由:
cpu.gpr[32]
cpu.pc
pmem
组成。
例如:
旧状态:
pc = 0x80000000
gpr[1] = 0
gpr[2] = 0
执行一条指令后:
新状态:
pc = 0x80000004
gpr[1] = 新值
gpr[2] = 新值
整个程序就是:
S0 -> S1 -> S2 -> S3 -> …
因此:
程序 = 状态转移序列
NEMU = 模拟状态转移序列的软件
九、调试宏:Log、Assert、panic
文件:
nemu/include/debug.h
代码:
#define Log(format, …) \\
_Log(ANSI_FMT("[%s:%d %s] " format, ANSI_FG_BLUE) "\\n", \\
__FILE__, __LINE__, __func__, ## __VA_ARGS__)
#define Assert(cond, format, …) \\
do { \\
if (!(cond)) { \\
… \\
assert_fail_msg(); \\
assert(cond); \\
} \\
} while (0)
#define panic(format, …) Assert(0, format, ## __VA_ARGS__)
#define TODO() panic("please implement me")
作用:
| Log() | 带文件名、行号、函数名的调试输出 |
| Assert() | 条件失败时输出信息并断言 |
| panic() | 无条件失败并终止 |
| TODO() | 标记待实现代码 |
十、RTFSC:不要只用眼睛看代码
讲义说:
不要只是盯着代码看,要用工具观察程序运行状态。
10.1 SDB
SDB 命令:
si
info r
x
p
w
b
si:
static int cmd_si(char *args) {
int step = 1;
if (args != NULL) {
sscanf(args, "%d", &step);
}
cpu_exec(step);
return 0;
}
info r:
void isa_reg_display() {
int i;
for (i = 0; i < 32; i++) {
printf("%s\\t0x%08x\\t%u\\n",
regs[i], cpu.gpr[i], cpu.gpr[i]);
}
printf("pc\\t0x%08x\\t%u\\n", cpu.pc, cpu.pc);
}
x:
word_t data = vaddr_read(addr + i * 4, 4);
printf("0x%08x: 0x%08x\\n", addr + i * 4, data);
p:
word_t res = expr(args, &succ);
if (succ) {
printf("%s = 0x%08x (%u)\\n", args, res, res);
}
10.2 GDB
为了使用 GDB,需要打开:
Build Options
[*] Enable debug information
常用命令:
layout split
break
watch
step
next
finish
print
x
bt
SDB 和 GDB 的目标是一样的:
让状态机一次前进一步,观察状态变化
十一、讲义中的“优美地退出”问题
直接输入:
q
时,终端会报错。
原因在:
nemu/src/monitor/sdb/sdb.c
当前代码:
static int cmd_q(char *args) {
return -1;
}
它只是退出 SDB,但没有设置 NEMU 状态。
状态定义:
nemu/src/utils/state.c
NEMUState nemu_state = { .state = NEMU_STOP };
int is_exit_status_bad() {
int good = (nemu_state.state == NEMU_END && nemu_state.halt_ret == 0) ||
(nemu_state.state == NEMU_QUIT);
return !good;
}
因此直接 q 时:
nemu_state.state == NEMU_STOP
good == false
return 1
main() 返回 1,外层 make 就会认为程序异常退出。
修复方法:
static int cmd_q(char *args) {
nemu_state.state = NEMU_QUIT;
return -1;
}
这样:
q
-> NEMU_QUIT
-> main 返回 0
十二、TRM 与代码对应总表
| 客户机 | NEMU 模拟的 riscv32 |
| 客户程序 | AM 生成的 .bin |
| Monitor | nemu/src/monitor/ |
| 存储器 | pmem |
| 内存访问 | paddr_read/write、vaddr_read/write |
| CPU | CPU_state cpu |
| 通用寄存器 | cpu.gpr[32] |
| PC | cpu.pc |
| 取指 | inst_fetch() |
| 译码 | decode_exec() + INSTPAT |
| 运算器 | inst.c 中每条指令的 C 表达式 |
| 执行循环 | cpu_exec() → execute() → exec_once() |
| 状态机 | cpu + pmem + PC 的连续状态 |
| 调试 | Log/Assert/panic + SDB + GDB |
| 退出 | NEMU_QUIT + is_exit_status_bad() |
十三、你代码中需要特别注意的点
1. g_print_step 不要强制为 true
错误写法:
g_print_step = true;
正确写法:
g_print_step = (n < MAX_INST_TO_PRINT);
作用:
si 少量指令 -> 打印
c 继续执行 -> 不打印
-b -> 不打印
2. ebreak 要和 nemu_trap 配套
AM 的 halt():
void halt(int code) {
nemu_trap(code);
while (1);
}
nemu_trap() 展开为:
mv a0, code
ebreak
NEMU 必须在 inst.c 中正确识别 ebreak,并设置:
nemu_state.state = NEMU_END
3. DiffTest 下不要重复加载镜像
当前 init_monitor() 中可能有:
load_img();
#ifdef CONFIG_DIFFTEST
long img_size = load_img();
init_difftest(…);
#endif
更好的写法是:
long img_size = load_img();
#ifdef CONFIG_DIFFTEST
init_difftest(diff_so_file, img_size, difftest_port);
#endif
十四、总结
这份 TRM/RTFSC 讲义真正想让你掌握的是:
TRM 不是要你从零发明,而是已经存在于 NEMU 框架中。
你要做的,是在代码中找到它:
存储器 -> pmem
CPU -> CPU_state
寄存器 -> cpu.gpr[32]
PC -> cpu.pc
取指 -> inst_fetch()
译码执行 -> decode_exec()
循环 -> cpu_exec() / exec_once()
然后用工具观察状态机:
SDB:si / info r / x / p / w / b
GDB:layout split / break / watch / print / x / bt
调试宏:Log / Assert / panic
最终理解:
程序运行 = 状态机不断进行确定性的状态转移
NEMU = 模拟这个状态转移过程的软件
TRM = NEMU 中最简单的那台抽象计算机
这就是 PA1 TRM 与 RTFSC 的核心。






