1. mips32和riscv32加载32位常数的策略
mips32和riscv32的指令长度只有32位, 因此它们不能像x86那样, 把C代码中的32位常数直接编码到一条指令中. 思考一下, mips32和riscv32应该如何解决这个问题?
MIPS32和RISC-V32都采用了多条指令组合的方式来加载32位常数。
- MIPS32的解决方案:
MIPS32主要使用以下两条指令的组合来加载32位常数:
lui $reg, upper16 # Load Upper Immediate:加载高16位
ori $reg, $reg, lower16 # 通过OR立即数指令设置低16位
具体工作原理:
- lui
指令将16位立即数加载到寄存器的高16位,低16位填充0
- ori
指令通过位或操作将低16位设置到正确的值
- 编译器会自动将32位常数拆分并生成适当的指令序列
例如,要加载常数 0x12345678:
lui $t0, 0x1234 # 加载 0x12340000
ori $t0, $t0, 0x5678 # 或运算得到 0x12345678
- RISC-V32的解决方案:
RISC-V32提供了几种方式:
a) 基本方法(使用lui+addi):
lui rd, imm20 # 加载高20位
addi rd, rd, imm12 # 添加低12位
b) 伪指令li(Load Immediate):
li rd, immediate # 编译器会自动展开成合适的指令序列
RISC-V的优势:
- lui
可以加载高20位(比MIPS的16位多)
- 提供了更灵活的伪指令支持
- 编译器会根据具体常数值选择最优的指令组合
- 两种架构的共同特点:
- 都采用了分段加载的策略
- 利用了指令级并行性
-
编译器会自动优化指令序列
-
实际应用中的优化:
- 如果常数较小,可能使用单条指令
- 对于频繁使用的常数,可能会保存在寄存器中
- 可能会重排指令顺序以提高性能
2. 单条指令的执行过程是什么
2.1 指令执行入口
整个执行流程从cpu_exec()
函数开始,用户可以指定执行的指令数量n
。主要功能包括:
- 设置执行环境与状态
- 记录开始时间戳
- 调用实际执行函数
- 处理执行后的状态
2.2 指令执行循环
在execute(n)
函数中实现主循环,对每条指令:
for (; n > 0; n--) {
exec_once(&s, cpu.pc); // 执行单条指令
g_nr_guest_inst++; // 指令计数增加
trace_and_difftest(&s, cpu.pc); // 跟踪和差分测试
if (nemu_state.state != NEMU_RUNNING) break;
IFDEF(CONFIG_DEVICE, device_update()); // 更新模拟设备状态
}
2.3 单条指令执行过程
在exec_once()
函数中完成单条指令执行的核心流程:
2.3.1 准备阶段
s->pc = pc;
// 设置当前PCs->snpc = pc;
// 设置静态下一PC(static next PC)
2.3.2 指令执行
调用isa_exec_once(s);
来执行ISA相关的指令,该函数在各个ISA实现中不同,但大致包括:
- 取指令: 从内存中获取指令编码
- 解码: 将指令编码解析为操作类型和操作数
- 执行: 根据指令类型执行相应操作(如计算、访存等)
- 设置下一指令地址: 将动态下一PC(
dnpc
)设为下一条指令的地址
2.3.3 更新PC
cpu.pc = s->dnpc; // 更新CPU程序计数器为动态下一PC
2.3.4 生成指令日志
- 记录指令地址、机器码,例如:
p += snprintf(p, sizeof(s->logbuf), FMT_WORD ":", s->pc);
- 记录指令的二进制表示:
for (i = ilen - 1; i >= 0; i--) {
p += snprintf(p, 4, " %02x", inst[i]);
}
- 添加反汇编结果:
disassemble(p, s->logbuf + sizeof(s->logbuf) - p, ...);
2.4 指令执行后处理
在trace_and_difftest()
函数中完成指令执行后的处理:
2.4.1 指令跟踪
- 将指令日志保存到环形缓冲区中
- 如果启用了指令跟踪,输出指令日志
2.4.2 差分测试
- 如果启用了差分测试,与参考模型比较执行结果
2.4.3 监视点检查
- 检查所有监视点表达式的值是否发生变化
- 如果发生变化,将模拟器状态设为NEMU_STOP并打印相关信息
2.5 状态更新与统计
执行完成后,会更新模拟器状态、统计执行时间和指令数;在异常情况下打印指令缓冲区内容以辅助调试
3. AM与操作系统运行时环境的区别
操作系统也有自己的运行时环境. AM和操作系统提供的运行时环境有什么不同呢? 为什么会有这些不同?
- 分层抽象的需求
- AM作为硬件和操作系统之间的中间层
- 提供了一个统一的抽象接口,隐藏了底层硬件的具体细节
- 使得上层软件开发更加简单和可移植
3.1 AM vs 操作系统运行时环境的区别
3.1.1 功能范围
- AM运行时环境
- 提供最基础的硬件抽象
- 仅包含必要的设备驱动和基本服务
-
接口简单,功能精简
-
操作系统运行时环境
- 提供完整的系统服务
- 包含进程管理、文件系统、网络等复杂功能
- 接口丰富,功能强大
3.1.2 抽象层次
- AM
- 直接面向硬件
- 提供底层硬件的简单抽象
-
主要关注基础功能实现
-
操作系统
- 面向应用程序
- 提供高层次的抽象
- 关注用户体验和系统效率
4. volatile关键字的作用
C 编译器为了优化性能,可能会对代码进行优化,例如将多次访问同一内存地址的操作合并或消除。
-
设备寄存器的特殊性:
设备寄存器与普通内存不同,每次读写都可能产生 副作用 (side effect),例如读取状态寄存器会返回设备当前状态,写入命令寄存器会触发设备动作. -
volatile的作用:
volatile
关键字告诉编译器 不要对被修饰的变量进行优化。编译器在编译访问volatile
变量的代码时,会保证每次都 直接访问内存,而不是使用寄存器缓存的值.
5. 用户程序死循环检测
当用户程序陷入死循环时, 需要让用户程序暂停下来, 并输出相应的提示信息.
5.1 核心思路
-
指令计数:
在 NEMU 的指令执行循环中,记录执行的指令数量. -
设定阈值:
设置一个指令计数阈值,例如MAX_LOOP_INSTRUCTIONS
. -
循环检测:
在每次指令执行后,检查指令计数是否超过阈值。如果超过,则认为程序可能陷入死循环. -
暂停和提示:
当检测到可能陷入死循环时,暂停 NEMU 的执行,并输出提示信息,例如 "User program seems to be in an infinite loop!".