本博文记录完成南京大学的ICS2023的PA1过程,包括解决的问题、遇到的坑等,仅供参考
1. 环境配置
按照PA0的Getting Source Code for PAs指导去编译nemu,遇到以下报错
报错中显示llvm-config
找不到,导致最终无法找到头文件MCAsmInfo.h
博主的Linux是Ubuntu 20.04,按照PA指示,使用以下安装了llvm
1 | $ sudo apt-get install llvm-11 llvm-11-dev # only for ubuntu20.04 |
系统中的llvm-config是存在的,只不过名字是llvm-config-11
(路径是/usr/bin/llvm-config-11),因此创建了一个软链接,使得nemu可以找到llvm-config
1 | $ sudo ln -s /usr/bin/llvm-config-11 /usr/bin/llvm-config |
2. RTFSC
2.1 究竟要执行多久?
问题:在cmd_c()
函数中, 调用cpu_exec()
的时候传入了参数-1
, 你知道这是什么意思吗?
解析
函数cpu_exec()
的声明为
1 | void cpu_exec(uint64_t n); |
入参是无符号类型uint64_t
,传入-1,会导致数值溢出,使得入参值为 0xffffffffffffffff,即uint64_t的最大值
2.2 为NEMU编译时添加GDB调试信息
menuconfig已经为大家准备好相应选项了, 你只需要打开它:
1 | Build Options |
然后清除编译结果并重新编译即可. 尝试阅读相关代码, 理解开启上述menuconfig选项后会导致编译NEMU时的选项产生什么变化.
解析
未加入GDB编译信息,make过程输出的日志如下图,可以看出编译选项为 -O2
执行make menuconfig
,配置Enable debug information
,再次make,从输出的日志中可以看到编译选项增加了 -Og -ggdb3
2.3 优美地退出
为了测试大家是否已经理解框架代码, 我们给大家设置一个练习: 如果在运行NEMU之后直接键入q
退出, 你会发现终端输出了一些错误信息. 请分析这个错误信息是什么原因造成的, 然后尝试在NEMU中修复它.
解析
运行nemu,然后直接输入q
,会显示以下报错
查看了下文件native.mk
第38行,日志显示是在$(NEMU_EXEC)处报错
1 | run: run-env |
这个$(NEMU_EXEC)具体做了啥,使用make run -nB
输出make过程,看到最后步是启动nemu主程序
按照make日志,手动启动nemu,然后直接输入q
,检查了下程序返回值,发现是1(程序error)
查了下make手册,发现make执行shell命令时,如果返回值是1,则退出当前rule执行,显示报错(5.5 Errors in Recipes)
阅读了nemu代码,nemu的main函数会调用()
判断返回值,判断逻辑如下:
1 | NEMUState nemu_state = { .state = NEMU_STOP }; |
nemu_state.state
默认值是NEMU_STOP
,如果直接输入q
退出时,state
值为默认值,则程序返回值判断为false,导致程序返回1
解决该问题很简单,函数is_exit_status_bad()
加入默认值NEMU_STOP
判断即可
1 | int is_exit_status_bad() { |
3. 基础设施
本节主要实现几个调试命令:si
、p
以及x
。调试器的命令全集如下
3.1 单步执行
实现单步执行命令:si [N]
解析
之前在RTFSC一节中已经介绍了指令执行主循环cpu_exec()
函数,该函数输入一个参数,为要执行的指令个数,因此单步执行可以直接调用该函数,传入要执行的指令数N
要新加一个命令,需要在结构体数组cmd_table
中新增该命令的信息(命令名字、实现函数)
1 | static struct { |
其中函数cmd_step_instruction
是单步执行命令的实现
1 | static int cmd_step_instruction(char *args) { |
解析命令参数,调用cpu_exec()
执行指令
3.2 打印寄存器
实现打印寄存器命令:info r
打印寄存器就更简单了. 不过既然寄存器的结构是ISA相关的, 我们希望能为简易调试器屏蔽ISA的差异. 框架代码已经为大家准备了如下的API:
1 | // nemu/src/isa/$ISA/reg.c |
执行info r
之后, 就调用isa_reg_display()
, 在里面直接通过printf()
输出所有寄存器的值即可. 如果你从来没有使用过printf()
, 请RTFM或者STFW. 如果你不知道要输出什么, 你可以参考GDB中的输出.
解析
博主当前实现的架构是riscv32,在对应架构目录下的reg.c
实现isa_reg_diaplay(void)
函数
1 | const char *regs[] = { |
寄存器数据存储在cpu.gpr
数组中,具体打印多少寄存器,每个架构下的cpu.gpr
数组大小不一样,具体可以去nemu/src/isa/$ISA/include/isa-def.h
头文件查看定义,riscv32的定义为
1 | typedef struct { |
可知数组大小为MUXDEF(CONFIG_RVE, 16, 32)
。最后再实现命令info r
,调用isa_reg_display()
函数即可
1 | static int cmd_info(char *args) |
3.3 扫描内存
扫描内存的实现也不难, 对命令进行解析之后, 先求出表达式的值. 但你还没有实现表达式求值的功能, 现在可以先实现一个简单的版本: 规定表达式EXPR中只能是一个十六进制数, 例如
1 | x 10 0x80000000 |
这样的简化可以让你暂时不必纠缠于表达式求值的细节. 解析出待扫描内存的起始地址之后, 就可以使用循环将指定长度的内存数据通过十六进制打印出来.
实现了扫描内存的功能之后, 你可以打印0x80000000
或者0x100000
附近的内存, 你应该会看到程序的代码, 和内置客户程序的内容进行对比, 检查你的实现是否正确.
解析
这里关键是如何访问指定内存的数据,在RTFSC一节中介绍nemu初始化时,有一步是加载内置的客户程序读入到内存中,具体实现见函数init_isa()
1 | void init_isa() { |
这里调用了函数guest_to_host()
将物理内存地址转换为内存指针(指针类型为uint8_t*
),拿到这个内存指针后我们就可以访问该内存地址之后的数据,指针每次+1就可以访问一个字节的数据。
扫描内存命令实现如下:
1 | static int cmd_x(char *args) |
这里用了下nemu提供的内存访问接口padd_read()
以及指针转化地址的接口host_to_guest