Skip to content

1. mips32和riscv32加载32位常数的策略

mips32和riscv32的指令长度只有32位, 因此它们不能像x86那样, 把C代码中的32位常数直接编码到一条指令中. 思考一下, mips32和riscv32应该如何解决这个问题?

MIPS32和RISC-V32都采用了多条指令组合的方式来加载32位常数。

  1. 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

  1. 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位多) - 提供了更灵活的伪指令支持 - 编译器会根据具体常数值选择最优的指令组合

  1. 两种架构的共同特点:
  2. 都采用了分段加载的策略
  3. 利用了指令级并行性
  4. 编译器会自动优化指令序列

  5. 实际应用中的优化:

  6. 如果常数较小,可能使用单条指令
  7. 对于频繁使用的常数,可能会保存在寄存器中
  8. 可能会重排指令顺序以提高性能

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;     // 设置当前PC
  • s->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 功能范围

  1. AM运行时环境
  2. 提供最基础的硬件抽象
  3. 仅包含必要的设备驱动和基本服务
  4. 接口简单,功能精简

  5. 操作系统运行时环境

  6. 提供完整的系统服务
  7. 包含进程管理、文件系统、网络等复杂功能
  8. 接口丰富,功能强大

3.1.2 抽象层次

  1. AM
  2. 直接面向硬件
  3. 提供底层硬件的简单抽象
  4. 主要关注基础功能实现

  5. 操作系统

  6. 面向应用程序
  7. 提供高层次的抽象
  8. 关注用户体验和系统效率

4. volatile关键字的作用

C 编译器为了优化性能,可能会对代码进行优化,例如将多次访问同一内存地址的操作合并或消除。

  • 设备寄存器的特殊性:
    设备寄存器与普通内存不同,每次读写都可能产生 副作用 (side effect),例如读取状态寄存器会返回设备当前状态,写入命令寄存器会触发设备动作.

  • volatile的作用:
    volatile 关键字告诉编译器 不要对被修饰的变量进行优化。编译器在编译访问 volatile 变量的代码时,会保证每次都 直接访问内存,而不是使用寄存器缓存的值.

5. 用户程序死循环检测

当用户程序陷入死循环时, 需要让用户程序暂停下来, 并输出相应的提示信息.

5.1 核心思路

  1. 指令计数:
    在 NEMU 的指令执行循环中,记录执行的指令数量.

  2. 设定阈值:
    设置一个指令计数阈值,例如 MAX_LOOP_INSTRUCTIONS.

  3. 循环检测:
    在每次指令执行后,检查指令计数是否超过阈值。如果超过,则认为程序可能陷入死循环.

  4. 暂停和提示:
    当检测到可能陷入死循环时,暂停 NEMU 的执行,并输出提示信息,例如 "User program seems to be in an infinite loop!".