袋熊的树洞

日拱一卒,功不唐捐

0%

南京大学ICS2023 PA1记录

本博文记录完成南京大学的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
2
Build Options
[*] Enable debug information

然后清除编译结果并重新编译即可. 尝试阅读相关代码, 理解开启上述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
2
3
run: run-env
$(call git_commit, "run NEMU")
$(NEMU_EXEC)

这个$(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
2
3
4
5
6
7
NEMUState nemu_state = { .state = NEMU_STOP };

int is_exit_status_bad() {
int good = (nemu_state.state == NEMU_END && nemu_state.halt_ret == 0) ||
(nemu_state.state == NEMU_QUIT) || (nemu_state.state == NEMU_STOP);
return !good;
}

nemu_state.state默认值是NEMU_STOP,如果直接输入q退出时,state值为默认值,则程序返回值判断为false,导致程序返回1

解决该问题很简单,函数is_exit_status_bad()加入默认值NEMU_STOP判断即可

1
2
3
4
5
int is_exit_status_bad() {
int good = (nemu_state.state == NEMU_END && nemu_state.halt_ret == 0) ||
(nemu_state.state == NEMU_QUIT) || (nemu_state.state == NEMU_STOP);
return !good;
}

3. 基础设施

本节主要实现几个调试命令:sip以及x。调试器的命令全集如下

3.1 单步执行

实现单步执行命令:si [N]

解析

之前在RTFSC一节中已经介绍了指令执行主循环cpu_exec()函数,该函数输入一个参数,为要执行的指令个数,因此单步执行可以直接调用该函数,传入要执行的指令数N

要新加一个命令,需要在结构体数组cmd_table中新增该命令的信息(命令名字、实现函数)

1
2
3
4
5
6
7
8
9
10
11
12
static struct {
const char *name;
const char *description;
int (*handler) (char *);
} cmd_table [] = {
{ "help", "Display information about all supported commands", cmd_help },
{ "c", "Continue the execution of the program", cmd_c },
{ "q", "Exit NEMU", cmd_q },

/* TODO: Add more commands */
{ "si", "Step Instruction", cmd_step_instruction}
};

其中函数cmd_step_instruction是单步执行命令的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int cmd_step_instruction(char *args) {
int n = 1;
errno = 0;
char* arg = strtok(NULL, " ");

if (arg != NULL) {
(void)sscanf(arg, "%d", &n);
if (errno != 0) {
printf("Unknown parameter: %s\n", arg);
return -1;
}
}

cpu_exec(n);

return 0;
}

解析命令参数,调用cpu_exec()执行指令

3.2 打印寄存器

实现打印寄存器命令:info r

打印寄存器就更简单了. 不过既然寄存器的结构是ISA相关的, 我们希望能为简易调试器屏蔽ISA的差异. 框架代码已经为大家准备了如下的API:

1
2
// nemu/src/isa/$ISA/reg.c
void isa_reg_display(void);

执行info r之后, 就调用isa_reg_display(), 在里面直接通过printf()输出所有寄存器的值即可. 如果你从来没有使用过printf(), 请RTFM或者STFW. 如果你不知道要输出什么, 你可以参考GDB中的输出.

解析

博主当前实现的架构是riscv32,在对应架构目录下的reg.c实现isa_reg_diaplay(void)函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const char *regs[] = {
"$0", "ra", "sp", "gp", "tp", "t0", "t1", "t2",
"s0", "s1", "a0", "a1", "a2", "a3", "a4", "a5",
"a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7",
"s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"
};

void isa_reg_display() {
int i;
int numRegs = MUXDEF(CONFIG_RVE, 16, 32);
printf("Reg\tValue\n");
for (i = 0; i < numRegs; i++) {
printf("%s\t0x%08x\n", regs[i], cpu.gpr[i]);
}
}

寄存器数据存储在cpu.gpr数组中,具体打印多少寄存器,每个架构下的cpu.gpr数组大小不一样,具体可以去nemu/src/isa/$ISA/include/isa-def.h 头文件查看定义,riscv32的定义为

1
2
3
4
typedef struct {
word_t gpr[MUXDEF(CONFIG_RVE, 16, 32)];
vaddr_t pc;
} MUXDEF(CONFIG_RV64, riscv64_CPU_state, riscv32_CPU_state);

可知数组大小为MUXDEF(CONFIG_RVE, 16, 32)。最后再实现命令info r,调用isa_reg_display()函数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int cmd_info(char *args)
{
char* arg = strtok(NULL, " ");

if (arg == NULL) {
printf("Need to input subcmd\n");
return -1;
}

if (strcmp(arg, "r") == 0) {
isa_reg_display();
} else {
printf("Unknown subcmd: %s\n", arg);
return -1;
}

return 0;
}

3.3 扫描内存

扫描内存的实现也不难, 对命令进行解析之后, 先求出表达式的值. 但你还没有实现表达式求值的功能, 现在可以先实现一个简单的版本: 规定表达式EXPR中只能是一个十六进制数, 例如

1
x 10 0x80000000

这样的简化可以让你暂时不必纠缠于表达式求值的细节. 解析出待扫描内存的起始地址之后, 就可以使用循环将指定长度的内存数据通过十六进制打印出来.

实现了扫描内存的功能之后, 你可以打印0x80000000或者0x100000附近的内存, 你应该会看到程序的代码, 和内置客户程序的内容进行对比, 检查你的实现是否正确.

解析

这里关键是如何访问指定内存的数据,在RTFSC一节中介绍nemu初始化时,有一步是加载内置的客户程序读入到内存中,具体实现见函数init_isa()

1
2
3
4
5
6
7
void init_isa() {
/* Load built-in image. */
memcpy(guest_to_host(RESET_VECTOR), img, sizeof(img));

/* Initialize this virtual computer system. */
restart();
}

这里调用了函数guest_to_host()将物理内存地址转换为内存指针(指针类型为uint8_t*),拿到这个内存指针后我们就可以访问该内存地址之后的数据,指针每次+1就可以访问一个字节的数据。

扫描内存命令实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static int cmd_x(char *args)
{
int n;
int num_bytes = 0;
paddr_t addr = 0x00;

/* extract the first argument */
char *arg = strtok(NULL, " ");
if (arg == NULL) {
printf("Need to input first param: number of bytes.\n");
return -1;
}
n = sscanf(arg, "%d", &num_bytes);
if (n != 1) {
printf("Failed to parse first param.\n");
return -1;
}

/* extract the second argument */
arg = strtok(NULL, " ");
if (arg == NULL) {
printf("Need to input second param: address.\n");
return -1;
}
n = sscanf(arg, "%x", &addr);
if (n != 1) {
printf("Failed to parse second param.\n");
return -1;
}

printf("Input params: %d 0x%08x\n", num_bytes, addr);
printf("Memory:");
word_t mem = 0;
uint8_t* ptr = guest_to_host(addr);
for (n = 0; n < num_bytes; n++) {
if (n % 4 == 0) {
printf("\n\t");
}
mem = paddr_read(host_to_guest(ptr + n), 1);
printf("%02x ", mem);
}
printf("\n");

return 0;
}

这里用了下nemu提供的内存访问接口padd_read()以及指针转化地址的接口host_to_guest