跳转至

调试思路与测试用例

进阶功能

本章节属于拓展功能,其主要目的是给大家一个做一个简单的进阶参考。进阶实验的目的是锻炼大家的自学能力。因此对本章节内容的答疑可能不会被接受。特此声明。

相信大家在之前的个人实验中已经体会到了调试中的一些困难,在大实验中,大家需要在更复杂的 CPU 上运行更加复杂的程序,调试难度会更大。本节将介绍测试中的一些基本概念,帮助大家寻找可能的调试思路,并提供一些测试用例。

测试基本概念

我们以一段 C 程序为例,为大家介绍测试中常用的一组概念:Fault,Error,Failure。

int a[1000];
for (int i = 0; i < 2000; ++i) {
    a[i] = i;
}

在这段代码中,程序员误将循环条件误写成了i < 2000,这种编码上的错误被称为 Fault;这将导致程序在运行过程中访问的数组下标超过 1000,这种内部非预期的状态被称为 Error;最终,程序将在运行时发生 Segmentation Fault,这种可被外部观测到的不符合预期的行为被称为 Failure。

在对 CPU 的测试中,我们常常只能观测到 Failure,如上板运行监控程序时输出不符合预期。这时候就需要通过仿真的方式观测 CPU 内部的状态,回溯 Error 出现的时刻,提升调试效率的关键也在这里。而在成功发现 Error 后,修复代码中的 Fault 往往并不困难。

程序的状态机视角

我们可以把正在 CPU 上运行的程序视为一个大状态机,PC,寄存器和内存共同构成程序的状态,而每执行一条指令都会进行状态的转移(改变 PC,写寄存器,写内存)。因此,通过仿真寻找 Error 时,我们可以主要关心 CPU 中 PC 和寄存器的值是否正确,发现 PC 或寄存器的值首次不正确的时刻后,再在这一时刻附近逐一排查 CPU 中其它信号的问题。

一个自然的想法是,将你实现的 CPU 与一个标准实现的 CPU 或模拟器(称为 Golden Model)的状态进行对比。对比的粒度取决于实现,可以只对比状态转移(如 PC 跳转/写寄存器/写内存),也可以进行全状态的对比。

思考

如果考虑时钟中断或串口输入输出,CPU 和 Golden Model 的行为将可能不一致,如何解决这一问题?

可能的调试思路

自动化测试

事实上已经有一些调试框架,可以自动实现 CPU 和 Golden Model 的运行时状态比较。不过这些框架的学习成本较高,我们暂时没有将它们引入到课程中。如果你对 CPU 的自动化测试感兴趣,欢迎报名参加龙芯杯。

以下是一些需要人工协助的调试方法,它们的核心都是尽可能减少从 Failure 回溯 Error 的成本,若你难以在仿真的波形中看出端倪,可以试试它们。你也可以从这一点出发,思考其它的高效调试策略,我们同样鼓励大家在这方面进行创新。

  • 从 QEMU 等 Golden Model 获得程序执行的指令流(称为 trace),在tb.sv中从文件读取 trace,和你的 CPU 的指令流进行对比,发现不同时进行相应输出。
  • 使用 gdb + QEMU,在某个 PC 处设置断点,在仿真中找到 CPU 第一次运行至该 PC 的时刻,对比 CPU 和 QEMU 的状态。

测试用例

在运行监控程序乃至更加复杂的系统软件之前,我们十分推荐你先编写一些小测例进行测试。这些测例能够帮助你更快地从 Failure 寻找 Error。

riscv-tests 是 RISC-V 官方提供的测试用例,大家可以参考并使用。不过,原仓库中的测试针对相对比较完整的 RISC-V CPU,可能需要自行修改一些内容才能用于测试你的 CPU。我们也为大家提供了已经编译好的最简版本,包含每条指令的小测试,将基础版本监控程序的 19 条指令的测试合并到一起的test19,以及将所有小测试合并到一起的testall

测例运行结束后,若测试通过,则寄存器 t1 以及内存地址 0x80300000 的值均为0x666

我们还提供了testalltest19的 trace(每一行的含义为:PC、是否写寄存器、写寄存器的编号、写寄存器的数据,均为十六进制),感兴趣的同学可以自行修改tb.sv实现 trace 的比对。

testall

testall包含了 RV32I 中除ecallebreak之外的所有指令,在你实现所有相应指令之前将无法正确运行它。这超出实验的基础要求,它的作用是帮助你在运行更复杂的程序之前确保指令实现正确。

对于fence.i,若你没有实现指令缓存,可以直接将其实现为nop

下载链接


最后更新: 2023年10月18日
作者:Jiajie Chen (1.61%), Shaofeng Ding (98.39%)