在 gem5 中只运行 U-Boot(不加载 Linux 内核)实际上是一个非常灵活且强大的裸机开发/调试平台。我们可以做很多有意思的事情,从简单的命令行操作到深入的固件级开发研究。
以下是几个不同层次的有趣工作:
第一层:U-Boot 命令行直接操作(无需改代码)
一旦 U-Boot 在 gem5 中启动并显示命令提示符 =>,我们可以直接与它交互:
1. 内存探索与测试
bash
=> bdinfo # 查看板级信息(内存布局、时钟等)
=> printenv # 查看所有环境变量
=> md 0x80000000 64 # 查看物理内存内容
=> mw 0x80100000 0xDEADBEEF # 向内存写入数据
=> mtest 0x80000000 0x81000000 # 内存压力测试
=> crc32 0x80000000 0x100000 # 计算 CRC 校验
2. 研究设备树 (DTB)
bash
=> fdt addr 0x87E00000 # 告诉 U-Boot DTB 在哪里(gem5 生成的 device.dtb)
=> fdt print /cpus # 查看 CPU 节点
=> fdt print /memory # 查看内存节点
=> fdt print /soc # 查看 SoC 设备
这能让我们直观理解 gem5 模拟的 HiFive Unmatched 板子的硬件拓扑。
3. 利用 U-Boot 作为调试跳板
U-Boot 可以加载并执行我们自己的裸机程序:
bash
# 在 gem5 运行后,通过串口将裸机程序加载到内存
=> loadb 0x80200000 # 通过串口加载二进制文件到指定地址
# 然后用终端工具发送文件
=> go 0x80200000 # 跳转到该地址执行
第二层:修改 U-Boot 源码做开发
这是最实用的部分。我们可以直接在 U-Boot 源码中添加功能,然后重新编译在 gem5 中测试。
示例:添加一个自定义命令
编辑 u-boot/common/cmd_custom.c(新建文件):
c
#include <common.h>
#include <command.h>
static int do_hello(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
printf("Hello from U-Boot on gem5 RISC-V!\n");
// 读取 mhartid CSR 显示当前核心 ID
unsigned long hartid;
__asm__ volatile ("csrr %0, mhartid" : "=r"(hartid));
printf("Current hart ID: %lu\n", hartid);
// 读取 misa CSR 显示 ISA 信息
unsigned long misa;
__asm__ volatile ("csrr %0, misa" : "=r"(misa));
printf("MISA: 0x%lx (RV64%s)\n", misa, (misa & (1L << 8)) ? "I" : "?");
return CMD_RET_SUCCESS;
}
U_BOOT_CMD(
hello, 1, 0, do_hello,
"Print hello message and CPU info",
""
);
然后在 u-boot/common/Makefile 中加上:
make
obj-y += cmd_custom.o
重新编译后在 gem5 中运行:
bash
=> hello
Hello from U-Boot on gem5 RISC-V!
Current hart ID: 0
MISA: 0x8000000000101105 (RV64I)
示例:开发 RISC-V 汇编测试代码
在 U-Boot 中运行内联汇编来研究 RISC-V 特权架构:
c
static int do_csrtest(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
unsigned long mstatus, mepc, mscratch, mtvec;
__asm__ volatile ("csrr %0, mstatus" : "=r"(mstatus));
__asm__ volatile ("csrr %0, mepc" : "=r"(mepc));
__asm__ volatile ("csrr %0, mscratch": "=r"(mscratch));
__asm__ volatile ("csrr %0, mtvec" : "=r"(mtvec));
printf("mstatus = 0x%016lx\n", mstatus);
printf("mepc = 0x%016lx\n", mepc);
printf("mscratch= 0x%016lx\n", mscratch);
printf("mtvec = 0x%016lx\n", mtvec);
// 解析 mstatus 的 MPP 字段 (Machine Previous Privilege)
int mpp = (mstatus >> 11) & 0x3;
printf("MPP (previous privilege) = %d (%s)\n", mpp,
mpp == 3 ? "M-mode" : mpp == 1 ? "S-mode" : "U-mode");
return CMD_RET_SUCCESS;
}
U_BOOT_CMD(csrtest, 1, 0, do_csrtest, "Read RISC-V CSRs", "");
第三层:结合 gem5 的强大功能做研究
这才是 gem5 + U-Boot 组合的真正威力所在:
1. 指令级跟踪分析
bash
./build/RISCV/gem5.opt --debug-flags=Exec,RiscvMisc \
--debug-start=0 --debug-file=trace.txt \
run_uboot_riscv.py --kernel=./u-boot/u-boot
# 查看执行轨迹
head -100 m5out/trace.txt
我们会看到每一条 RISC-V 指令的执行细节(PC、指令编码、寄存器变化)。
2. 启动性能分析
bash
# 加上统计输出
./build/RISCV/gem5.opt --stats-file=stats.txt \
run_uboot_riscv.py --kernel=./u-boot/u-boot
cat m5out/stats.txt | head -50
可以看到:
- 模拟的指令总数
- 各类指令的比例
- 缓存命中/未命中统计
- 分支预测准确率
- 每条指令的平均 CPI
3. 研究 U-Boot → Linux 的交接过程
如果我们让 U-Boot 加载 Linux 内核,可以完整观察:
- U-Boot 如何解压内核镜像
- 如何设置设备树(FDT)传递给内核
mstatus.MPP如何从 M-mode 切换到 S-modesret指令的执行瞬间
4. 内存一致性研究
利用 gem5 的缓存模型,研究 U-Boot 中的 DMA 操作、缓存刷新指令的效果。
第四层:完整实战场景
场景 A:裸机 RISC-V 教学实验
编写一个极简的 "Hello World" 裸机程序,通过 U-Boot 加载执行:
bash
# 编写 baremetal.S
.section .text
.globl _start
_start:
li t0, 0x10000000 # UART 基地址 (gem5 HiFive 的 UART0)
la t1, msg
loop:
lb t2, 0(t1)
beqz t2, done
sb t2, 0(t0) # 写入 UART
addi t1, t1, 1
j loop
done:
j done # 死循环
.section .rodata
msg:
.string "Hello baremetal RISC-V!\n"
编译成裸机二进制,通过 U-Boot 的 go 命令执行------完全不依赖操作系统。
场景 B:安全启动研究
研究 U-Boot 的:
- SPL (Secondary Program Loader) 阶段
- FIT (Flattened Image Tree) 镜像验证
- Secure Boot 签名链验证
- PMP (Physical Memory Protection) 配置
场景 C:网络协议栈极简测试
U-Boot 内置了极简的 TCP/IP 栈(如果编译时启用了网络支持),可以在 gem5 中测试:
bash
=> setenv ipaddr 10.0.0.2
=> setenv serverip 10.0.0.1
=> ping ${serverip}
=> tftpboot 0x80200000 baremetal.bin
推荐起步路线
如果这是第一次尝试,建议按这个顺序:
| 步骤 | 操作 | 收获 |
|---|---|---|
| 1 | 运行 U-Boot,用 bdinfo、printenv、md 探索 |
熟悉裸机环境 |
| 2 | 用 fdt print 查看设备树 |
理解 gem5 模拟的硬件 |
| 3 | 添加一个 hello 命令,打印 CSR 信息 |
学会修改/编译/测试循环 |
| 4 | 写一个简单的裸机汇编程序,用 go 执行 |
理解 RISC-V 裸机编程 |
| 5 | 加 --debug-flags=Exec 跟踪指令执行 |
深入理解硬件行为 |