南京大学计算机系统基础实验 ICSPA(2023)PA 2 阶段 2

AM 给我的感觉就是在硬件和软件之间的一个兼容层。有点像 rCore 当中使用的 Rust-SBI,提供了一组用于直接和硬件交互的 API。相比于操作系统,AM 感觉更像驱动,它并且没有操作系统那么多复杂的功能,不会进行进程管理,只是给它上层的软件提供和硬件交互的 API 而已。所以可能操作系统下面也可以有一个 AM?

这样做的好处,当然是把软件和硬件解耦和了。每次有一个新的架构发布的时候,或着新的操作系统发布,都只需要多维护一份代码就可以了。

至于第一个必做题,从 main() 开始找,稍微找一点就可以找到,在 sdb_mainloop() 里面一开始就判断了是否是批处理模式。找到这个变量被修改的地方,可以发现是通过传递 -b 参数来开启 NEMU 的批处理模式。于是,只需要在 Makefile 文件中 找到 NEMU 启动时候传入的参数 NEMUFLAGS,加上 -b 参数就好了。

这其实没什么好说的,就是基础的语法题。完成这个任务的前提是需要实现大部分的指令。

sprintf() 的实现相比上一个任务来说就有趣的多了。这里第一次接触到了可变参数。

可变参数是由 stdarg.h 库提供的。其关键的几个宏和使用方法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdarg.h>
#include <stdio.h>

// 功能:求可变个数个 int 的和
int sum(int num_args, ...) {
    int total = 0;
    va_list args;

    // 初始化 args 以便访问可变参数列表
    va_start(args, num_args);

    // 遍历所有的参数并累加它们
    for (int i = 0; i < num_args; i++) {
        total += va_arg(args, int);  // 这里的第二个参数的值是当前 arg 的类型
    }

    // 清理 args
    va_end(args);

    return total;
}

在我们需要实现的代码中可以看到,vsprintf() 的参数里有一个 va_list ap 变量,所以我们在 sprintf() 中只需要使用宏 va_start 来初始化可变参数,然后调用 vsprintf() 将其传入进行处理即可。具体的字符串处理部分是在 svprintf() 里面实现的。

这里也运用了讲义上面讲到的「抽象」的思想,将不同用途的 printf 抽象出来一个 vsprintf(),然后其他地方只需要将参数进行一些处理,然后调用 vsprint() 就可以了。大大减少了代码的重复率。

需要注意的是,sprintf() 等函数也会在输出最后添加 '\0',我因为这个被卡了半小时……

PA 里反复强调了一个概念:程序是一个状态机。这个概念也在 jyy 老师的另一门课《操作系统:设计与实现》当中反复强调。当有了这个概念之后,再去理解计算机到底是怎么实现执行我们编写的指令,就会豁然开朗:其实计算机就是不停根据当前寄存器、内存的状态,按照指令指引的方向,不停进入到下一个状态。

而对于程序来说,计算机其实给我们提供了跟硬件交流的接口,通过这些接口,我们可以在不需要知道底层硬件实现原理和运行方式的情况下,实现对硬件的操控。

环形缓冲区其实就是一个循环数组,循环将每次执行的指令保存下来,满了就直接覆盖就好了。

我新建了 include/cpu/iringbuf.h 文件,在里面实现了 iringbuf 的相关内容,然后在 NEMU 进行记录和退出的地方调用函数进行记录和输出。Kconfig 文件中也需要新建一个新的选项 IRINGBUF,这样就可以通过判断宏标记 CONFIG_IRINGUF 来动态编译代码。

实现效果:

/nju-icspa-pa2-2/image.png

mtrace 的实现就没什么好说的了。按照讲义来写就好了。另外,我觉得在终端中输出 mtrace 没有太大的意义,所以我只将其写入了 log 文件中。

Bug

我在实现的时候发现了一个问题,如果按照讲义在 paddr_read() 中实现 mtrace 的话,会发现每次指令指令都会输出。这是因为 NEMU 的取指调用的 vaddr_ifetch() 函数,其实里面调用了 paddr_read()

所以我最后是在 vaddr_read()vaddr_write() 中实现的 mtrace。如果后面遇到了问题再改吧。

实现效果如下,与内存访问有关的指令在 log 文件中都输出了 mtrace:

/nju-icspa-pa2-2/image-1.png

有关符号表的内容我正好最近在实习的时候有接触过,所以实现起来还是比较简单的。其实本质上符号表就是一个「名称-地址」映射表,记录着每一个符号对应的地址。而对于函数来说,就是函数名对应其第一行指令的地址。

消失的符号
其实也很好理解,因为函数的参数是通过寄存器传递的,而局部变量是在进入函数后在栈上创建的。这些是在编译的时候就把指令确定好了。并不需要符号来链接。