南京大学/NJU 人工智能/AI 计算机系统基础/ICS PA2 记录

PA2.3

从这里开始记录。

已经能通过大部分测试用例——除了 hello-str 和 string。

image

观察这两个测试用例究竟是为什么挂了。首先定位到错误的位置,发现都是 putch 函数中发生了内存越界。

但是这两个代码本身并没有这个函数,因此先寻找代码与 putch 函数的关系。不断寻找,最终可以发现这个 putch 是某个 trm.c 中的函数。

根据经验,我们可以判断是 nemu/trm.c,毕竟这是加载在 nemu 上的程序。而想要验证也很简单,只需要在其 putch 函数前加入一条 printf("Hello") 即可,如果 nemu 的报错是识别到了没被植入的指令,就说明是这个 trm.c 了。

而使用 putch 的原因是进入了 panic,因此,只需要把对应的环境函数实现,即可避免进入 panic,从而避免错误。

字符串相关的函数都很好实现,难点在于 sprintf 等。

第一步,参数处理

va_list args;
va_start(args, fmt);
//TODO
va_end(args);

这样可以得到一个 args 的 va_list。

然后每次使用

va_arg(ap, int)
va_arg(ap, char*)

来获取参数列表中的对应类型。

第二部,实现逻辑

按照对应函数逻辑实现即可。

难点,printf

其实实现并不困难,只需要这样实现即可:

int printf(const char *fmt, ...) {
  // panic("Not Implemented");
  char out[4096];
  va_list args;
  va_start(args, fmt);
  int ret = vsnprintf(out, sizeof(out), fmt, args);
  va_end(args);
  putstr(out);
  return ret;
}

但是问题在于 putstr 这个函数并不能发挥作用!因为这个函数本身就是会访问一个非内存的地址!

实际上,原因在于需要在 make menuconfig 中打开 device 设置,打开之后,就会有一个 mmio(memory map input/output) 的代码被实现,也就是把映射到 nemu 中外设的地址,映射到 nemu 中外设的地址,再有 nemu 中模拟设备的代码,映射到真实的地址。

理论上,把这些东西实现之后,就可以通过了。

在中间的时候,有某个版本突然无法通过 cpu-test 了。本来想用 git bisect 查错,但是对于中间有 git merge 的提交历史,bisect 难以使用。最后没招,只好人眼识别两个版本哪里有差别,结果是因为 addi, xori, ori 的逻辑错了。

这三个指令,本来想着既然立即数只有低 12 位,那我就应该 &0xfff 来截断立即数,结果正确的逻辑就是需要符号扩展到 32 位。

实现完 printf 后,在实现 malloc 的时候卡住了——根本不知道要干啥;这就是前面 PA 反复提及的,不认真阅读框架代码,不理解项目的运行原理,只求通过测试,我认为还有一个原因是理论基础不扎实,这导致对于项目没什么理解,更无法正确找到合适的接口了。

于是我花了两天,重新认真阅读了 PA2.2 的内容,受益匪浅,彻底理解了框架代码是如何实现取指令-译码-执行-更新的过程,也让我有了迎接接下来挑战的信心。

认真阅读了 PA2.3 的内容,大体明白了这部分项目的设计思考。设计 AM 的出发点是,想要运行一个程序,我们需要对应的环境(比如 helloworld 需要 printf)。我们可以轻松提供这样的环境,比如让这个程序直接连接到计算机的内存区,然后写到即将打印到屏幕上的内存。但是这有一个致命的缺点,就是如果这个程序想换到别的平台上运行,输出这部分的代码就要大幅度改动。

解决这个问题有个很简单的方法,ducktyping,或者说 API。这里面的哲学就是,我不关心你是怎么提供这个环境的,我只知道我写下 printf 你就能给我输出。而 AM 就是所有运行时环境 API 的一个总和。

VGA 实现

前面的时钟,键盘都比较简单,甚至不需要看代码就能写。但是 VGA 不一样了,因为这要利用一些 ioe.c 的代码,所以需要理解实现原理是什么。

看到,在 AM 中有一些设备的起始地址。对这个流程的理解:首先,AM 中根据设备的地址,这些是虚拟地址,调用 MMIO 的接口,注册这些设备,并给出这些设备虚拟起始地址和大小;然后,当写入这些地址时,NEMU 中就看看是否是 MMIO 映射的,再在 NEMU 中真实的计算机中地址这些位置,进行对应的读入或者写出。

可以看到,在 AM 的 nemu.h 和 NEMU 的 autoconf.h 中,都有设备注册的地址,这些地址必须保持一致。而 autoconf.h 就是根据前者编译生成的。

然后需要理解一下 ioe.c 中的 ioe_read 和 ioe_write,以及 amdev.h 中的 AM_DEVREG,和 klib-macros.h 中的 io_read/io_write。

理解完这些,只需要在 AM 中的 gpu.c 和 NEMU 中的 vga.c 中修改几个函数逻辑即可。

分别是 gpu.c 中的 __am_gpu_init, __am_gpu_config, __am_gpu_fbdraw 和 vga.c 中的 vga_update_screen。

声卡实现

写完 VGA 之后我以为自己已经领悟一切了,结果过 5 个小时回来写声卡发现又啥都不会了。

最后重新梳理了一遍实现原理。对于 AM-kernels 来说,它看到的是声卡的最高层——声卡提供五个“寄存器”(其实我感觉这个名字很有误导性),对应着五个功能:初始化,配置,播放控制,获取播放状态,加入播放音频,它只需要调用这五个 API 即可。对于 AM 来说,它看到的是中间层,要做的工作是“传递”,比如在 AM-kernels 调用配置或者加入播放音频的时候,将对应的数据通过 inl/outl 传递到 NEMU 的模拟硬件中,要做的全部就是传递控制信号和数据。对于 NEMU 来说,它看到的是最底层,它正在实现一个硬件——只是它本质上也是软件,所以它需要调用 SDL 库来实现这个功能,是真正实现硬件功能的部分——虽然实现的手段也是通过调用另一个 API。

带着这些思想,做起来就轻松许多了。

posted @ 2025-11-04 21:09  哼唧昂叽  阅读(91)  评论(0)    收藏  举报