基于riscv32的nemu
RTFSC
优美地退出
这一部分主要靠RTFSC
,查看nemu-main.c
,了解程序的整个运行过程,定位在is_exit_status_bad()
函数,此时可以通过printf("%d\n", !good)
查看函数返回值,当直接键入q
退出时,函数返回值为1,锁定问题,现在只要更改函数返回值(即更改good
的值)就可以了。结合good
定义,当键入q
时,将nemu_state.state
设为NEMU_QUIT
,即可将问题解决。
根据 C 语言标准,
main
函数的返回值为int
类型,通常约定:
返回值为 0 表示程序正常结束。
返回值为非零整数(通常为 1 或其他非零值)表示程序异常结束或出错。
基础设施
单步执行
格式: si [N]
函数: strtok、atoi
实现比较简单,直接附代码:
static int cmd_si(char *args)
{
char *arg = strtok(NULL, " ");
int num_line = 0;
if(arg == NULL)
{
cpu_exec(1);
}
else
{
num_line = atoi(arg);
if(num_line == 0)
{
printf("Unknown input, the standard format is 'si [N]'\n");
return 0;
}
cpu_exec(num_line);
}
return 0;
}
打印寄存器
格式: info r
函数: strtok
文档中已经给了我们很明确地提示,当输入info r
时,只需要调用APIisa_reg_display()
,在isa_reg_display()
中我们实现输出所有寄存器的值。
首先,添加cmd_info
函数:
static int cmd_info(char *args)
{
char *arg = strtok(NULL, " ");
if( strcmp(arg, "r") == 0)
isa_reg_display();
else if( strcmp(arg, "w") == 0)
display_wp();
else
printf("Unknown input, the standard format is 'info SUBCMD'\n");
return 0;
}
然后再实现isa_reg_display()
,在实现这个函数时,我们需要RTFSC
找到寄存器的值保存在哪里,通过一番搜索,不难发现,寄存器值保存在结构体成员gpr
中。
void isa_reg_display() {
//bool success = true;
int count = 0;
for(int i = 0; i < 32; i++)
{
printf("%s \t 0x%08x\t", reg_name(i), cpu.gpr[i]);
count++;
if(count == 3 || i == 31)
{
printf("\n");
count = 0;
}
}
}
扫描内存
格式: x N EXPR
函数: strtok、strtol
ANSI_FMT
可用于设置终端输出文本的颜色
想要实现扫描内存的功能,需要使用到memory
中的vaddr_read
来读取内存中的值,每次读取四个字节。
static int cmd_x(char *args)
{
char *arg1 = strtok(NULL, " ");
if(arg1 == NULL)
{
printf("Unknown input, the standard format is 'x N EXPR'\n");
return 0;
}
char *arg2 = strtok(NULL, " ");
if(arg2 == NULL)
{
printf("Unknown input, the standard format is 'x N EXPR'\n");
return 0;
}
int n = strtol(arg1, NULL, 10);
vaddr_t base_addr = strtol(arg2, NULL, 16);
for(int i = 0; i < n;)
{
printf(ANSI_FMT("0x%08x: ", ANSI_FG_BLUE), base_addr);
for(int j = 0; i < n && j < 4; i++,j++)
{
word_t data = vaddr_read(base_addr, 4);
printf("0x%08x\t", data);
base_addr += 4;
}
printf("\n");
}
return 0;
}
表达式求值
格式: p EXPR
函数: strncpy、strtol、strncmp
词法分析
实现词法分析之前,首先要学习一下正则表达式的规则,在这里特别需要注意寄存器的识别,因为在寄存器数组中包含一个$0
的寄存器,所以出现$
或$$
都有可能是寄存器。
首先,定义tokens
可能的类型,并将token
类型分类,以便后面用到。OFTYPES
宏用于判断某个token
的类型是否属于该类
enum {
TK_NOTYPE = 256,
/* TODO: Add more token types */
TK_NEG, TK_POS, TK_DEREF, // + - *
TK_EQ, TK_NEQ, TK_GT, TK_LT, TK_GE, TK_LE, // == != > < >= <=
TK_AND, TK_OR, //&& ||
TK_NUM,
TK_REG,
};
#define OFTYPES(type, types) oftypes(type, types, ARRLEN(types))
static int bound_types[] = {')', TK_NUM, TK_REG};
static int nop_types[] = {'(', ')', TK_NUM, TK_REG};
//static int uo_types[] = {TK_NEG, TK_POS, TK_DEREF}; //Unary operator
static bool oftypes(int type, int types[], int size)
{
for(int i = 0; i < size; i++)
if(type == types[i]) return true;
return false;
}
然后再用正则表达式去匹配这些类型:
static struct rule {
const char *regex;
int token_type;
} rules[] = {
/* TODO: Add more rules.
* Pay attention to the precedence level of different rules.
*/
{" +", TK_NOTYPE}, // spaces
{"\\(", '('}, {"\\)", ')'},
{"\\+", '+'}, {"\\-", '-'},
{"\\*", '*'}, {"\\/", '/'},
{"<", TK_LT}, {">", TK_GT}, {"<=", TK_LE}, {">=", TK_GE},
{"==", TK_EQ}, {"!=", TK_NEQ},
{"&&", TK_AND}, {"\\|\\|", TK_OR},
{"(0[xX][0-9A-Fa-f]+|\\b[0-9]+\\b)", TK_NUM},
//{"(0x)?[0-9]+", TK_NUM},
{"\\${1,2}\\w+", TK_REG},
};
最后,在make_token()
函数中添加代码,实现tokens
数组的生成(tokens
数组用于按顺序存放已经被识别出的token信息)
static bool make_token(char *e) {
int position = 0;
int i;
regmatch_t pmatch;
nr_token = 0;
while (e[position] != '\0') {
/* Try all rules one by one. */
for (i = 0; i < NR_REGEX; i ++) {
if (regexec(&re[i], e + position, 1, &pmatch, 0) == 0 && pmatch.rm_so == 0) {
char *substr_start = e + position;
int substr_len = pmatch.rm_eo;
//用于显示匹配信息
//Log("match rules[%d] = \"%s\" at position %d with len %d: %.*s",
// i, rules[i].regex, position, substr_len, substr_len, substr_start);
position += substr_len;
/* TODO: Now a new token is recognized with rules[i]. Add codes
* to record the token in the array `tokens'. For certain types
* of tokens, some extra actions should be performed.
*/
if(rules[i].token_type == 256) break;
tokens[nr_token].type = rules[i].token_type; //在前面已经匹配过
switch (rules[i].token_type) {
case TK_NUM:
case TK_REG:
strncpy(tokens[nr_token].str, substr_start, substr_len);
tokens[nr_token].str[substr_len] = '\0';
break;
case '*': case '-': case '+':
if(nr_token == 0 || !OFTYPES(tokens[nr_token - 1].type, bound_types))
{
switch(rules[i].token_type)
{
case '-': tokens[nr_token].type = TK_NEG; break;
case '+': tokens[nr_token].type = TK_POS; break;
case '*': tokens[nr_token].type = TK_DEREF; break;
}
}
break;
}
nr_token++;
break;
}
}
if (i == NR_REGEX) {
printf("no match at position %d\n%s\n%*.s^\n", position, e, position, "");
return false;
}
}
return true;
}
递归求值
词法分析的功能实现后,待求值表达式中所有token
的类型都被保存在tokens
数组中,接着根据文档的提示写出求值函数eval()
。
word_t eval(int p, int q, bool *success)
{
*success = true;
if(p > q)
{
*success = false;
return 0;
}
else if(p == q)
{
if(tokens[p].type != TK_NUM && tokens[p].type != TK_REG) //&&误写为||,debug了好久
{
*success = false;
return 0;
}
if(tokens[p].type == TK_NUM)
{
if(strncmp("0x", tokens[p].str, 2) == 0 || strncmp("0X", tokens[p].str, 2) == 0) //16
return strtol(tokens[p].str, NULL, 16);
else
return strtol(tokens[p].str, NULL, 10); //10
}
else
{
return isa_reg_str2val(tokens[p].str, success); //reg
}
}
else if(check_parentheses(p, q) == true)
{
return eval(p + 1, q - 1, success);
}
else
{
int op = find(p, q);
if(op < 0)
{
*success = false;
return 0;
}
//printf("2\n");
bool success1, success2;
word_t val1 = eval(p, op - 1, &success1);
//if(!*success) return 0;
word_t val2 = eval(op + 1, q, &success2);
//if(!*success) return 0;
if(!success2)
{
//printf("4\n");
*success = false;
return 0;
}
if(!success1)
{
//printf("2\n");
switch(tokens[op].type)
{
case TK_NEG: return -val2;
case TK_POS: return val2;
case TK_DEREF: return vaddr_read(val2, 4);
default: *success = false;
return 0;
}
}
else
{
//printf("3\n");
switch(tokens[op].type)
{
case '+': return val1 + val2;
case '-': return val1 - val2;
case '*': return val1 * val2;
case '/': if(val2 == 0)
{
*success = false;
return 0;
}
return (sword_t)val1 / (sword_t)val2;
case TK_EQ: return val1 == val2;
case TK_NEQ: return val1 != val2;
case TK_GT: return val1 > val2;
case TK_LT: return val1 < val2;
case TK_GE: return val1 >= val2;
case TK_LE: return val1 <= val2;
case TK_AND: return val1 && val2;
case TK_OR: return val1 || val2;
default: *success = false; return 0;
}
}
}
return 0; //without it, then will error
}
debug
:在p == q
判断里刚开始将&&
写为了||
,后续debug
了挺久
然后实现check_parentheses()
函数,用于判断表达式是否被一对匹配的括号包围着
bool check_parentheses(int p, int q)
{
if(tokens[p].type == '(' && tokens[q].type == ')')
{
int par = 0;
for(int i = p; i <= q; i++)
{
if(tokens[i].type == '(') par++;
else if(tokens[i].type == ')') par--;
if(par == 0) return i == q;
}
}
return false;
}
最后实现find()
函数来寻找主运算符,主算符的特征如下:
- 非运算符的token不是主运算符
- 主运算符不会出现在括号内
- 主运算符的优先级在表达式中是最低的
- 当有多个运算符优先级都是最低的时,最右边的才是主运算符
int find(int p, int q)
{
int par = 0, op_type = 0, pos = 0;
//printf("1\n");
for(int i = p; i <= q; i++)
{
if(tokens[i].type == TK_NUM)
continue;
else if(tokens[i].type == '(')
par++;
else if(tokens[i].type == ')')
{
if(par == 0) return -1;
par--;
}
else if(OFTYPES(tokens[i].type, nop_types))
continue;
else
{
if(par > 0) continue;
int tmp_type; //是主运算符的优先级
switch(tokens[i].type)
{
case TK_NEG: case TK_POS: case TK_DEREF: tmp_type = 1; break;
case '*': case '/': tmp_type = 2; break;
case '+': case '-': tmp_type = 3; break;
case TK_GT: case TK_LT: case TK_GE: case TK_LE: tmp_type = 4; break;
case TK_EQ: case TK_NEQ: tmp_type = 5; break;
case TK_AND: tmp_type = 6; break;
case TK_OR: tmp_type = 7; break;
default: assert(0);
}
if(tmp_type > op_type)
{
op_type = tmp_type;
pos = i;
}
}
}
if(par != 0) return -1;
//printf("%d\n", pos);
return pos;
}
代码测试
根据文档提示,写出生成表达式的框架:
static void gen_rand_expr() {
//buf[0] = '\0';
if(index_buf > 65530)
printf("oversize\n");
switch(choose(3))
{
case 0: gen_num(); break;
case 1: gen('('); gen_rand_expr(); gen(')'); break;
default: gen_rand_expr(); gen_rand_op(); gen_rand_expr(); break;
}
}
然后再分别实现gen_num()、gen()、gen_rand_op()、choose()
函数,这几个函数实现都比较简单
int choose(int n)
{
return rand() % n;
}
static void gen(char c)
{
buf[index_buf++] = c;
}
static void gen_num()
{
int num = rand() % 100;
//if(num == 0) num += 1;
int len = 0, tmp = num;
while(tmp)
{
tmp /= 10;
len++;
}
int x;
if(len <= 1) x = 1;
else x = (len - 1) * 10;
while(num)
{
char c = num / x + '0';
buf[index_buf++] = c;
num %= x;
x /= 10;
}
}
static void gen_rand_op()
{
char op[4] = {'+', '-', '*', '/'};
int pos = rand() % 4;
buf[index_buf++] = op[pos];
}
notice:
- 过滤除0表达式的方式就是粗暴地开启
-Wall -Werror
参数,因为生成的表达式属于字面量(literal constant),编译时就会计算表达式,如果含有除0的情况gcc
就会执行失败,所以可以通过system
返回值检测编译是否含有除0的情况 - 在
main
函数中每次循环都会生成一个表达式,要注意将index_buf
置为0,并将buf
数组清空 - 使用
./gen-expr 10000 > input
生成一些测试用例时,可能会出错,但是并不影响,可以查看input文件中,正确生成并计算的测试用例结果是否准确
监视点
实现监视点的管理
首先完成监视点结构体的补充:
typedef struct watchpoint {
int NO;
struct watchpoint *next;
/* TODO: Add more members if necessary */
word_t old_value;
char expr[100]; // ** char *expr is error ** //
} WP;
接下来实现new_wp()
和free_wp()
函数,其中new_wp()
表示从free_
链表中返回一个空闲的监视点结构, free_wp()
将wp
归还到free_
链表中
调用
new_wp()
时可能会出现没有空闲监视点结构的情况,此类情况很好的一种解决办法是直接使用assert(free_)
debug
: 在WP
结构体中,expr
应定义为expr
数组,而不是expr
指针
WP* new_wp()
{
if(free_ == NULL)
{
printf("Unused watchpoint\n");
assert(0);
}
WP* tmp = free_;
free_ = free_->next;
tmp->next = head;
head = tmp;
return tmp;
}
void free_wp(WP *wp)
{
if(wp == NULL)
{
printf("No watchpoints are using\n");
assert(0);
}
if(wp->next == NULL)
{
wp->next = free_;
free_ = wp;
head = NULL;
return;
}
else if(wp->next != NULL && wp == head)
{
head = wp->next;
wp->next = free_;
free_ = wp;
return;
}
WP *tmp = head;
while(tmp->next)
{
if(tmp->next == wp)
break;
tmp = tmp->next;
}
tmp->next = wp->next;
wp->next = free_;
free_ = wp;
}
设置监视点
static int cmd_w(char *args)
{
if(args == NULL)
{
printf("Unknown input, the standard format is 'w EXPR'\n");
return 0;
}
bool success;
word_t res = expr(args, &success);
if(!success)
printf("The expression is problematic\n");
else
set_wp(args, res);
return 0;
}
void set_wp(char *arg, word_t value) //set the watchpoint
{
WP *new = new_wp();
new->old_value = value;
strcpy(new->expr, arg);
printf("Hardware watchpoint %d: %s\n", new->NO, new->expr);
}
扫描监视点
文档说明的很详细,根据文档提示即可。
void scan_wp() //scan all watchpoints
{
WP *tmp = head;
//printf("hello\n");
while(tmp)
{
bool success;
word_t new_value = expr(tmp->expr, &success);
//printf("hello\n");
if(new_value != tmp->old_value)
{
printf("The watchpoint %d: %s has changed\n", tmp->NO, tmp->expr);
printf("Old value = %u\n", tmp->old_value);
printf("New value = %u\n", new_value);
tmp->old_value = new_value;
nemu_state.state = NEMU_STOP;
return;
}
tmp = tmp->next;
}
//return 0;
}
打印监视点信息 info w
其实就是遍历一下head
链表,输出相关信息
void display_wp() //display all watchpoints
{
WP *tmp = head;
if(tmp == NULL)
printf("No watchpoints.\n");
else
{
printf("%-20s%-20s%-20s\n", "Num", "What", "Value");
while(tmp)
{
printf("%-20d%-20s%-20u\n", tmp->NO, tmp->expr, tmp->old_value);
tmp = tmp->next;
}
}
}
删除监视点
根据序号在head
链表中找到对应的监视点,然后调用free_wp()
即可。
static int cmd_d(char *args)
{
if(args == NULL)
{
printf("Unknown input, the standard format is 'd N'\n");
return 0;
}
char *arg = strtok(NULL, " ");
int n = strtol(arg, NULL, 10);
delete_wp(n);
return 0;
}
void delete_wp(int n) //delete the watchpoint, its NO is n.
{
WP *tmp = head;
if(tmp == NULL)
printf("The watchpoint for which NO is %d does't exist.\n", n);
else
{
while(tmp->NO != n)
tmp = tmp->next;
if(tmp == NULL)
printf("The watchpoint for which NO is %d does't exist.\n", n);
else
{
free_wp(tmp);
printf("Deleted success\n");
}
}
}
统计代码行数
count:
@echo "The total number of lines in all the .c and .h files in nemu is: "
@find . -type f \( -name "*.c" -o -name "*.h" \) -exec cat {} + | wc -l
count_no_space:
@echo "The total number of lines in all the .c and .h files in nemu without spaces is: "
@find . -type f \( -name "*.c" -o -name "*.h" \) -exec awk 'NF > 0' {} + | wc -l
SUMMARY
函数介绍
strtok
char *strtok(char *str, const char *delim)
函数的作用是从字符串 str
中提取标记,每次调用都会返回下一个标记。参数 delim
是一个包含分隔符字符的字符串,用于指定在哪些字符处进行分割。
PA1中用的最多的就是strtok(NULL, " ")
,由于传入的参数是args
,第一次调用 strtok(NULL, " ")
会在 args
中查找下一个空格,并将空格替换为 \0
。这个调用返回指向第一个标记的指针(即空格之前的部分)。之后的调用 strtok(NULL, " ")
会继续在同一字符串中查找,从上一次找到的标记的下一个位置开始。它会继续查找下一个空格,并将空格替换为 \0
。
strtol
long strtol(const char *nptr, char **endptr, int base)
nptr
:指向要转换的字符串的指针。endptr
:用于存储遇到的第一个非法字符的指针,如果该参数不为NULL
。如果整个字符串都是合法的数字,endptr 将被设置为nptr + strlen(nptr)
。base
:指定进制,可以是 2、8、10(默认)、16 等。
可以搭配strtok
一起使用
char *arg = strtok(NULL, " ");
int n = strtol(arg, NULL, 10);
strcmp
int strcmp(const char *s1, const char *s2)
- 如果
s1
的字典顺序在s2
之前,则返回负数。 - 如果
s1
和s2
相等,则返回零。 - 如果
s1
的字典顺序在s2
之后,则返回正数。
debug
:这个函数也帮助我解决了一个bug。在表达式求寄存器的值时,例如输入p $sp
,此时会调用isa_reg_str2val(const char *s, bool *success)
函数,注意此时传入的*s
是$sp
,而不是sp
,所以在将输入的寄存器与寄存器数组进行匹配时,要使用到strcmp(regs[i], s + 1)
。
word_t isa_reg_str2val(const char *s, bool *success) {
*success = true;
bool sign = false;
int i;
//printf("%s", s);
for(i = 0; i < 32; i++)
{
if(strcmp(regs[i], s + 1) == 0) //** s + 1 , but isn't s **
{
sign = true;
break;
}
}
if(!sign)
{
*success = false;
printf("The register is not exist\n");
return 0;
}
else
{
return cpu.gpr[i];
}
}
strcpy
char *strcpy(char *dest, const char *src)
dest
:目标字符串的指针,接收源字符串的内容。
src
:源字符串的指针,提供要复制的内容。
即将src
复制到dest
。
strncpy
char *strncpy(char *dest, const char *src, size_t n)
dest
:目标字符串的指针,接收源字符串的内容。
src
:源字符串的指针,提供要复制的内容。
n
:最多复制的字符数。
strncmp
int strncmp(const char *s1, const char *s2, size_t n)
s1
和s2
分别是要比较的两个字符串的指针。n
:最多比较的字符数。
如果 s1
的前 n
个字符的字典顺序在 s2
的前 n
个字符之前,则返回负数。
如果 s1
和 s2
的前 n
个字符相等,则返回零。
如果 s1
的前 n
个字符的字典顺序在 s2
的前 n
个字符之后,则返回正数。
PA1
使用:if(strncmp("0x", tokens[p].str, 2) == 0 || strncmp("0X", tokens[p].str, 2) == 0)
宏
MUXDEF
#define COND 1
MUXDEF(COND, puts("hello"), puts("world"));
// 展开后如下
printf("hello");
//相当于
#define COND 1
//注意COND只要被定义,就会输出hello,不管值为0或1
#ifdef COND
puts("hello");
#else
puts("world");
#endif
IFDEF
#define COND 1
IFDEF(COND, puts("hello"));
// 展开后如下
printf("hello");
//相当于
#define COND 1
//注意COND只要被定义,就会输出hello,不管值为0或1
#ifdef COND
puts("hello");
#endif
当然,最重要的还是
STFW
、RTFM
、RTFSC
。