PA实验问题及记录
PA0
平常使用linux较多,较为熟悉,未碰到环境配置方面的困难
实验环境:
OS:Manjaro 20.2 Nibia
Kernal:x86_64 Linux 5.9.11-3-MANJARO
-
初始化相关环境变量
根目录init.sh中注释掉第十行return
后执行 bash init.sh ,使用的不是默认shell,需要更改20-21 行
-
设置$ISA环境变量
-
nemu/makefile 中
-
第33行:
CFLAGS += -O0 -g -MMD -Wall -ggdb3 -std=c99 $(INCLUDES) -D__ISA__=$(ISA) -fomit-frame-pointer -mmanual-endbr -fcf-protection=none #-Werror 禁止编译器优化,调试时使用
-
第71行:
@$(LD) -O0 -rdynamic $(SO_LDLAGS) -o $@ $^ -lSDL2 -lreadline -ldl
-
注释掉第25行关闭自动添加commit历史 -
···
-
-
make 报错 找不到optarg
src/monitor/monitor.c: In function ‘parse_args’: src/monitor/monitor.c:67:3: warning: implicit declaration of function ‘getopt’ [-Wimplicit-function-declaration] while ( (o = getopt(argc, argv, "-bl:d:a:")) != -1) { ^ src/monitor/monitor.c:70:28: error: ‘optarg’ undeclared (first use in this function) case 'a': mainargs = optarg; break; ^ src/monitor/monitor.c:70:28: note: each undeclared identifier is reported only once for each function it appears in make: *** [build/obj-mips32/monitor/monitor.o] Error 1
在include/common.h 中 #include<getopt.h> 解决
-
调试环境
gdb调试
PA1
选择ISA为:riscv-32
task实现如下:
单步执行
解析出执行步数(无参数时默认为一),直接调用cpu_exec(num)即可
static int cmd_si(char *args) {
char * arg = strtok(args, " ");
if (arg == NULL) {
cpu_exec(1);
return 0;
}
int num = atoi(arg);
cpu_exec(num);
return 0;
};
打印寄存器
完善isa_reg_display()函数即可,riscv的寄存器为32个通用寄存器加上$PC
void isa_reg_display() {
printf("General reg: ----------------------------------------------------------- \n");
int i, j;
for(i = 0 ; i < 32 ; i++) {
printf("%-3s :0x%08x | ", regsl[i], cpu.gpr[i]._32);
if ((i+1)%4 == 0)
printf("\n");
}
printf("Special reg: ----------------------------------------------------------- \n");
printf("$pc :0x%08x\n\n", cpu.pc);
}
扫描内存
通过 strtok 分别获得字符串型的地址和扫描长度,调用expr(后续实现)求得表达式结果作为地址,调用 vaddr_read 函数扫描内存后按格式打印即可。
...
vaddr_t addr = expr(EXPR, &success);
...
for (int i = 0; i < n; i++) {
uint32_t data = vaddr_read(addr + i * 4, 4);
printf("0x%08x ", addr + i * 4);
for (int j = 0; j < 4; j++) {
printf("0x%02x " , data & 0xff);
data = data >> 8;
}
printf("\n");
}
...
表达式求值
-
首先需要完善正则匹配:
{" +", TK_NOTYPE, 0}, // spaces {"0[xX][0-9a-fA-F]+", TK_HEX, 0}, // hex {"[0-9]+", TK_DEX, 0}, // dex {"(\\$[0a-zA-Z]+)|([xX][0-9]+)",TK_REG, 0}, // register $ OR x0~x31 {"\\|\\|", TK_OR ,1}, // or {"&&", TK_AND, 2}, // and {"==", TK_EQ, 3}, // equal {"!=", TK_NEQ, 3}, // not equal {"\\+", '+', 4}, // plus {"-", '-', 4}, // sub {"\\*", '*', 5}, // mul {"/", '/', 5}, // div {"!", '!', 6}, // not {"\\(", '(', 7}, // bra_l {"\\)", ')', 7}, // bra_r
在原来rule的基础上增加了优先级字段,用于后续在表达式递归下降的过程中求得主运算符。
这里有几个需要注意的地方:
- 像是*或者 ( 这样的符号需要经过两次转义,一次是c语言字符串转义,一次是正则引擎需要的转义
- regex.h 不支持正则中类似于 <! 这样的前置匹配,所以无法直接配置十进制,这里采用优先匹配十六进制再匹配十进制的方式绕过去
-
接着完善make_token()函数
识别出表达式中的每一个token。在for循环中,用regexec()函数匹配目标文本串和前面定义的rules[i]中的正则表达式比较,pmatch.rm_so==0表示匹配串在目标串中的第一个位置,pmatch.rm_eo表示结束位置,position和substr_len表示读取完后的位置和读取长度。成功识别到对应规则后,进行type、pri、str的复制即可
-
接着进入对与表达式的处理eval函数,首先是对于表达式是否合法进行判断,这里需要满足start<= end 的条件,当start == end 的时候表示进行到了递归结束点,进行相应位置表达式(HEX、DEX、REG)的求值即可。对于start < end 的情况,首先需要判断表达式括号是否匹配,这里增加了一个函数
static check_bra check_parentheses(int start, int end),参数为表达式起始和结束位置,返回值为枚举类型check_bra
typedef enum { BRA_SURRONDED, MATCH, DISMATCH } check_bra;
其中BRA_SURROUNDED代表表达式被()包裹且括号内为括号匹配的表达式,MATCH代表括号匹配但不满足前一种情况的表达式,DISMATCH代表不满足括号匹配的表达式。
当遇到DISMATCH时停止解析返回即可;当遇到BRA_SURRONDED时直接解析内层表达式即可;当遇到MATCH时才是我们需要进行处理的部分
-
在MATCH的情况下,为了进行表达式的递归下降,首先要找到表达式中最后进行运算的符号,即主运算符,以此讲表达式分成前后两部分后分别进行计算,由函数 static int dominant_operator(int start, int end) 实现。
static int dominant_operator(int start, int end) { int op = start, pri_min = 10; for (int i = start; i <= end;i ++) { if (tokens[i].type == TK_HEX || tokens[i].type == TK_DEX || tokens[i].type == TK_REG) continue; int bra_count = 0; bool flag = true; for (int j = i - 1; j >= start; j--) { if (tokens[j].type == '(') { if (bra_count == 0) { flag = false; break; } bra_count--; } if (tokens[j].type == ')') bra_count++; } if (!flag) continue; if (tokens[i].pri <= pri_min) { op = i; pri_min = tokens[op].pri; } } return op; }
此函数根据之前数据结构中存储的优先级结构,来确定最后运算的符号,为优先级数字最小且不在括号中的符号,返回其index
再求得主运算符之后,左右分别递归后运算即可。
-
后续eval增加了对于解引用运算(*)和负号(-)的处理,因为无法在正则匹配中区分出来,所以需要在匹配完成后在进行符号的判断和优先级的修正,将这两个符号设为一个较高的优先级,在后续求得主运算符后,判断为这两个运算符时直接进行求值即可
for (int i = 0; i < nr_token; i ++) { if (tokens[i].type == '*' && (i == 0 || (tokens[i - 1].type != TK_DEX && tokens[i - 1].type != TK_HEX && tokens[i - 1].type != TK_REG && tokens[i - 1].type !=')') )) { tokens[i].type = TK_DEREF; tokens[i].pri = 6; } if (tokens[i].type == '-' && (i == 0 || (tokens[i - 1].type != TK_DEX && tokens[i - 1].type != TK_HEX && tokens[i - 1].type != TK_REG && tokens[i - 1].type !=')') )) { tokens[i].type = TK_MINUS; tokens[i].pri = 6; } }
碰到的问题:
- 在解析类似于表达式p (1+2)*(3+4) 的时候出错,实现括号匹配解析的时候,不能将其归为BRA_SURROUNDED类型,会把这个表达式识别为不合法的表达式,因此增加了必须左侧有两个连续左括号的条件,但因此又导致了对于简单的表达式p (1+2) 回被认为是MATCH类型,进而求主运算符,导致错误;最后将BRA_SURROUNDED的条件设置为上述两种情况的并集,成功解决问题。
监视点
-
监视点管理操作
WP* wp_new (); // 申请新监视点 void wp_free(WP * wp); // 释放监视点 void wp_delete(int num); // 删除对应序号的监视点 void wp_display(); //打印所有的监视点 bool wp_check(); //监测监视点的值是否变化
wp_new 从free链表中取一个结点给head链表。
wp_free 函数是遍历head链表直到找出对应NO的结点,从head中删除,添加到free链表中。
wp_chcek 函数是便利head链表中的监视点,求出其值后与上一次的值进行比较,打印出变化的监视点并返回false
-
实现监视点
修改cpu_exec.c,每执行完一条命令,调用wp_check() 函数,进行相应监视点的值的判断
if (!wp_check()) { nemu_state.state = NEMU_STOP; }
必答题
-
我选择ISA为:riscv-32
-
理解基础设施:
450200.5=4500(min)=75(h)
-
查阅手册:
-
riscv32有哪几种指令格式?
-
LUI指令的行为是什么?
-
mstatus寄存器的结构是怎么样的?
-
-
代码统计
-
nemu/
目录下的所有.c和.h和文件总共有多少行代码?find . -name "*.c" -or -name "*.h" | xargs grep -Ev "^$" | wc -l #4432
-
和框架代码相比, 你在PA1中编写了多少行代码?
git checkout pa0 find . -name "*.c" -or -name "*.h" | xargs grep -Ev "^$" | wc -l #4009
增加了423行代码
-
加入 make count 命令
COUNT_L := $(shell find . -name "*.h" -or -name "*.c" | xargs grep -Ev "^$$" | wc -l) COUNT_ADD := $(shell expr $(COUNT_L) - 4009) ... count: @echo $(COUNT_L) lines in nemu @echo $(COUNT_ADD) lines added into the frame code
-
-
解释gcc中的
-Wall
和-Werror
有什么作用? 为什么要使用-Wall
和-Werror
?-Wall,打开gcc的所有警告。
-Werror,它要求gcc将所有的警告当成错误进行处理。发现代码中潜在的问题
实验总结
pa1的实验难度较小,基本没有设计到和具体指令集相关的内容,因为实习原因,平时时间较紧,导致pa1总的时间线比较长,希望pa2能够集中时间完成。