欢迎光临
我们一直在努力

PA1 TRM 与 RTFSC 完全解析:从 NEMU 代码看懂最简单的计算机

本文结合 ysyx-workbench/nemu 实际代码,系统讲解 PA1《RTFSC》这一章。

重点回答三个问题:

  • TRM 到底对应 NEMU 里的哪些代码?
  • 一条客户机指令是如何被取指、译码、执行和更新 PC 的?
  • 如何用 SDB、GDB 和状态机视角真正“看懂” NEMU?

  • 摘要

    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 代码
    客户机 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 的核心。

    赞(0)
    未经允许不得转载:171主机测评 » PA1 TRM 与 RTFSC 完全解析:从 NEMU 代码看懂最简单的计算机
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址