NJU PA1

未分类
5.4k 词

fceux

按照readme把内容下载下来之后运行可能会出现这个问题:

1
2
3
4
5
6
7
8
9
/home/suzume/proj/ics2022/fceux-am/src/emufile.cpp: In member function ‘void EMUFILE_FILE::open(const char*, const char*)’:
/home/suzume/proj/ics2022/fceux-am/src/emufile.cpp:33:22: error: array subscript 1 is above array bounds of ‘rom [1]’ [-Werror=array-bounds=]
33 | if (strcmp(roms[i].name, fname) == 0) {
| ~~~~~~^
In file included from /home/suzume/proj/ics2022/fceux-am/src/emufile.cpp:27:
./nes/gen/roms.h:10:12: note: while referencing ‘roms’
10 | struct rom roms[] = {
| ^~~~
cc1plus: all warnings being treated as errors

然后看了一下makefile,他应该是先执行了一个小脚本来生成了一个roms.h,当然这个roms.h不在这个目录下。之后这里的程序会用到nroms这个变量,虽然有这个条件限制但是编译器还是给了一个warning,猜测:如果他能看到这个nroms是一个常量可能就不会抛出这个warning。

我的解决方案:想办法直接压制这个warning,stfw找到压制特定某个warning的方式然后加进编译选项就可以玩了。

RTFSC

计算机可以没有寄存器吗

可以没有?如果站在寄存器只是内存的缓存的角度来看?

尝试理解计算机如何计算

先放置两个值到寄存器里一个是结果r1一个是当前迭代的数r2,然后每次让迭代值先+1再加到结果r1里,之后跳转到开始的位置继续执行直到满足条件r2 >= 100。这个用例的意义在于我们只用跳转和基础指令理论上可以表达出任意的效果。

计算机是个状态机

当读上面那段文档的时候我想到一个问题:怎么把这些单元区分开了,下面的格子就解释了:一部分是时序相关的单元,一部分是和时序无关的,他们只是在那里执行任务而已。

他有什么好处?现实中的程序状态数量可能非常多,因此可以通过提取抽象状态来刻画你的程序的进行路径来分析出错的位置。这种做法应该每个程序员都会随着经验的增长而自然学会,但是这是一个更准确具体的描述,一些对你来说习以为常的事情。

其次这个视角会更方便描述递归这一行为。

程序的起点?

解决了pa0的时候可能会觉得这个问题理所当然:)

好像要到pa4的阶段才会对这个问题有解释

为什么全部都是函数?

从实现上来说可以分离执行函数,减少局部变量引入。从设计上来说,如果有类似于与isa有关的行为,那么只需要定义相同的api,具体实现由条件编译来给出就可以了。

参数的处理过程

如果是说参数在内存中的具体位置的话,这是一个比较远的话题,由操作系统放在制定的位置,程序读取环境数据,在之后pa4的阶段会实现。

如果是说nemu运行时给定的额外参数的话,就需要理解到make做了什么:基本上他只是帮你执行了一些额外的步骤来构造可执行程序,然后启动了nemu程序,然后在合适的地方加入参数就可以了。思考一下make启动nemu和手动编译然后启动nemu的方式的不同就可以理解这个蓝框题目。

这一点还是很重要的,后面的实验里有要求对这一点的理解。

初探操作系统启动

看了一下dmesg | wc -l在我的机器上有1163行,还是挺长的。这个东西主要是可以给你这个”用户”一种直觉:我可以知道我的操作系统上发生的任何事情,计算机世界里不存在黑盒。

物理内存的起始位置

这里注意一点,说的不是宿主机的内存而是用户内存。现在运行的是用户程序,用户程序想要访问地址0x8000000他最终走到的宿主机上为他模拟的内存pmem[0]。是的,模拟的内存只是一个非常大的数组:)

注意这里的物理内存和模拟内存是不同的概念,并不是说调用paddr_read(x)就相当于访问pmem[x]。物理内存是相对虚拟内存来说的,对于用户程序他并不知道模拟内存的概念(如果我们活在模拟器之内,怎么样知道我们在模拟器里)

这里有一张图来描述此时用户程序加载到哪里了,然后我们约定他固定会加载到RESET_VECTOR。之所以要这样做是因为用户程序的cpu需要知道指令从哪里开始执行。可以看到图上还画了pc也指向这个位置,具体地代码里是直接让cpu.pc=RESET_VECTOR

在这个简化的模型中,很简单,上下约定好的固定位置,在实际的世界中可能需要了解BIOS启动和操作系统的cpu初始化的过程,在操作系统的课程中可能会提到。

:question: RESET_HEADER在哪里

打印会发现一开始的位置=0x80000000,是不是有点意外?这张图只是起了一个象征意义,没有留空。

这里有个比较蠢的事情,我的lsp给我跳到了其他的isa的init.c里导致我没有输出成功,似乎删掉其他没有用的isa也不会有什么问题。

究竟要执行多久?

-1为永远执行。

注意这里的说明,menu会不断的执行指令,这和cpu的基本假设是一致的。指示结束的人是nemu_trap这条虚拟的指令。但是除此之外还有一种情况:达到了要求的循环次数,这一点应该是用来防止无止尽的执行的。

潜在的威胁

  • TODO

谁来指示程序的结束?

[ ] TODO

我感觉如何证明这个问题是一个不trivial的问题,之后补充

有始有终

实际程序是怎样中止的?存疑。

这是一个比较关键的问题,因为在之前的模型假设中cpu是一个无情的执行指令的设备,他只会不停的执行指令,对他来说应该只有启动和断电两种状态,他没有中止这个概念。

gdb

  • 我个人是没有用gdb调试,但是这里有个遗留的问题:当打开gdb选项的时候,内部发生了什么变化。

优美地退出

NEMU有自己的运行状态,看结束时的代码是怎么输出有关程序状态的信息的就可以了。

基础设施

有代码的地方,就有基础设施 :)

在做之前重新梳理一下结构:在宿主机上模拟了硬件设备(例如上一节的内存只是一个巨大的数组),然后在模拟的硬件上运行了模拟的程序NEMU,这个NEMU还会加载用户程序,所以对于NEMU来说,用户程序对他来说是透明的,他可以随时了解用户程序的信息。但是这些信息对于外部的程序例如gdb是不好理解的,因为gdb在的位置是宿主机调试NEMU这个程序。

因此这里的sdb是为了调试模拟的程序NEMU上加载的用户程序,他打印展示的也是我们模拟的硬件信息,例如打印模拟的用户内存,模拟的cpu的寄存器值等等。

单步执行

单步执行的时候有个小细节:如果是直接c执行完整程序他不会打印执行的指令,但是单步会。这一点由框架代码给出了,主要是用来验证单步执行的正确性(虽然单步执行这个任务只有一行代码),演示了正确性的验证这个话题。

打印寄存器

这里有一个抽象,因为具体实现和isa有关,所以在$isa/reg.c里,但是方法的签名是通用的,具体在include/isa.h里。

:question: 寄存器的数量

  • cpu只有32个寄存器吗?如果有更多的寄存器,应该怎么定义?

先别急,在printf输出的时候占位符也是与isa有关的,所以如果要写出有可移植性的代码不能直接假定输出的字长是多少,那printf应该怎么写占位符呢?之前文档中煞有介事的提了一下如果你没有用过printf应该RTSC一下,现在就是践行这件事的时刻了:)

扫描内存

扫描内存,扫描的是谁的内存?至少首先他不是宿主机的内存,因为现在是在NEMU上开发的调试器。而下面的文档里也提到如果要测试可以扫描0x80000000附近的内存,因为这是riscv32上的物理内存的起始位置。之前提到在客户程序运行的过程中,总是用vaddr_read/vaddr_write来访问模拟的内存。

:question: 虚拟内存

这里为什么要用vaddr_read而不是paddr_read。如果你在这个阶段去看vaddr_read的实现,会看到他只是转调了paddr_read,等价于直接调用,所以这里不必纠结。至少在现阶段,他们两个是一样的。

  • 但是这里用vaddr_read是有理由的,之后这里补充。

文档中说还可以扫描0x1000000这个地址,我没太理解,因为这个位置显然不在模拟的内存里,会直接爆掉。

表达式求值

为什么printf的输出要换行?

需要刷新缓冲区,否则crash的时候可能看不到输出的结果。

实现带有负数的求值

单独对待符号和减号即可。

实现完成之后就可以拓展原有的扫描内存功能了。

监视点

监视一个表达式的变化,一个比较强力的工具,尤其是你的场景比较底层的时候,例如直接对一个内存地址监视。

拓展表达式求值的功能

这里的实现方式已经很明确了,另外负数的处理方式也是一样的。

不过在拓展了表达式求值的功能之后原有的随机测试就无法直接描述了,因为行为被拓展了。当然他没有修改旧有的行为,所以你的测试仍然可以运行只是此时测试的是你的功能的子集。当然,测试也不是100%覆盖到所有情况的:)

:question: 为什么要用无符号类型?

  • 也许是因为符号拓展的效果?

实现监视点

这里有一个比较烦的事情是,WP这个结构体是直接定义在.c里的,因此在sdb里是没有办法看到WP这个结构体的定义的,所以我把new_wp的定义改成了返回创建的编号,同时free_wp按编号来释放。

然后因为监视点需要在每次执行指令之后来扫描所有监视的表达式,所以如果任何时候都把这个功能启用的话损耗的性能比较多,所以可以类似的在Kconfig里写一个可配置项来控制是否期望有这个行为,当然加入这个配置之后不要忘了make menuconfig以及在命令执行的部分加上,因为如果你确实需要这个功能但是他没有启用的话,命令应该是不允许执行的。否则你只能看着你能加监视点但是却不生效,这很迷惑。

:warning: 设置为bool的配置项消失了

我是这么设置WATCHPOINT的:

1
2
3
config WATCHPOINT
bool "Enable watchpoint"
default y

当配置真的时候会有一个CONFIG_WATCHPOINT存在,但是为假时这个宏会直接删除,我本来期望的是他有一个为假的宏而不是直接删除。这导致我本来期望写的是if (CONFIG_WATCHPOINT),而实际上我只能写 IFDEF(CONFIG_WATCHPOINT, run_watchpoint());

同时为了防止命令的错误调用,我得这么添加两处相同的代码:

1
2
3
4
#ifndef CONFIG_WATCHPOINT
printf("watchpoint is not enabled\n");
return 0;
#endif /* ifdef CONFIG_WATCHPOINT */

此时我才反应过来之前为什么写成那样:(,之前RTFSC环节没有太研究makefile踩坑了。

  • 有没有在假的时候也继续存在的配置?这样的话可以简单的写if结构而不是使用宏ifndef来做。
  • 在实现完了监视点之后,之前实现的基础设施中的info w也可以补齐了。

调试工具与原理

  • 机器永远是对的
  • 计算机世界没有魔法

你会如何测试你的监视点实现?

事实上我完全没用到监视点。

强大的GDB

  • 没有使用gdb,之后补吧

断点

我们很容易用这种方式来模拟断点:

1
w $pc=ADDR

:warning: 等等,pc寄存器

是的,pc寄存器不和其他普通的寄存器在一起,普通的调用reg_display并不能打印他。

如何提高断点的效率 (建议二周目思考)

断点的效率差是因为需要在每个指令执行结束的时候检查一遍,并且每次都需要执行一次expr来计算。

如何优化他?我暂且没什么方法,因为pc是可以任意跳转的,并不能预测他的位置。

这里还有一篇文章how debuggers work。调试器的断点工作原理和监视点模拟断点的工作原理不太一样,也许看了这篇文章之后可以理解为什么模拟的速度不够快?

随心所欲的断点

  • 猜测会停不下来?

NEMU的前世今生

  • 模拟器与调试器有什么不同?

  • 与NEMU相比,GDB到底是怎么调试程序的?

  • 我感觉两个问题都不是很trivial,之后补齐

如何阅读手册

在看i386手册时,html的手册只展示到了二级目录所以你会找不到selector这个概念,看pdf版本的就没有这个问题。

必答题

  • riscv32

    • riscv32有哪几种指令格式?

      基础的有四种:R/I/S/U,有两种变种:B/J

    • LUI指令的行为是什么?

      以U型指令的格式加载一个数到寄存器rd,将其低12位置0

    • mstatus寄存器的结构是怎么样的?

      image-20240107223737535

  • shell

    • 完成pa1的内容之后,所有.c和.h一共有多少行代码?

      find 找到所有相关的文件,然后用wc -l统计,最后通过wc自己的筛选去掉中间结果

    • 和框架代码相比,你写了多少代码?

      322 insertions(+), 34 deletions(-)

    • 把这件事写进makefile?

      • 比较好做,但是后面那个去除空行好像不太行

:sunny: 温馨提示: pa1到此结束