nju pa1
note: 基于riscv64,不适配riscv32
RTFSC#
优美地退出#
make run
启动nemu后直接输入q
退出,得到如下最后一行的错误
Welcome to riscv32-NEMU!
For help, type "help"
(nemu) log
Unknown command 'log'
(nemu) q
make: *** [/home/ubuntu/ics2022/nemu/scripts/native.mk:38: run] Error 1
是由于is_exit_status_bad
函数返回了-1,main函数直接返回了此函数返回的结果,make检测到该可执行文件返回了-1,因此报错。通过分析该函数得到解决方案:在输入q
中途退出nemu后,将nemu_state.state
设成NEMU_QUIT
即可
// nemu/src/monitor/sdb/sdb.c
void sdb_mainloop() {
...
int i;
for (i = 0; i < NR_CMD; i ++) {
if (strcmp(cmd, cmd_table[i].name) == 0) {
if (cmd_table[i].handler(args) < 0) {
if (strcmp(cmd, "q") == 0) {
nemu_state.state = NEMU_QUIT; // set "QUIT" state when q
}
return;
}
break;
}
}
if (i == NR_CMD) { printf("Unknown command '%s'\n", cmd); }
}
}
此时再通过make run
运行后中途键入q
命令退出模拟器将不会再报该错误
简易调试器#
单步执行 si [N]#
第一步,在cmd_table注册一条命令si
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 },
{ "si", "Continue the execution in N steps, default 1", cmd_si },
/* TODO: Add more commands */
};
第二步,编写cmd_si
,即si
具体要执行的东西
static int cmd_si(char *args) {
/* extract the first argument */
char *arg = strtok(NULL, " ");
int n;
if (arg == NULL) {
n = 1;
} else {
n = strtol(arg, NULL, 10);
}
cpu_exec(n);
return 0;
}
打印寄存器 info r#
第一步,注册命令
{ "info", "Display the info of registers & watchpoints", cmd_info },
第二步,编写cmd_info
,其中调用的isa_reg_display
函数就是PA文档里介绍的,简易调试器为了屏蔽ISA的差异. 框架代码已经为大家准备了的API之一
static int cmd_info(char *args) {
/* extract the first argument */
char *arg = strtok(NULL, " ");
if (arg == NULL) {
printf("Usage: info r (registers) or info w (watchpoints)\n");
} else {
if (strcmp(arg, "r") == 0) {
isa_reg_display();
} else if (strcmp(arg, "w") == 0) {
// todo
} else {
printf("Usage: info r (registers) or info w (watchpoints)\n");
}
}
return 0;
}
第三步,实现提前设计好的isa_reg_display
// nemu/src/isa/$ISA/reg.c
void isa_reg_display() {
int reg_num = ARRLEN(regs);
int i;
for (i = 0; i < reg_num; i++) {
printf("%-8s%-#20lx%-20ld\n", regs[i], cpu.gpr[i], cpu.gpr[i]);
}
}
扫描内存 x N EXPR#
第一步,注册命令
{ "x", "Usage: x N EXPR. Scan the memory from EXPR by N bytes", cmd_x },
第二步,编写cmd_x
,注意这里有两个参数N和EXPR,因此需要分别检查参数是否存在,并转换类型。然后就是利用vaddr_read
每次读取4个字节并打印。打印格式参照了一下gdb的x命令,每行打印4个4字节
static int cmd_x(char *args) {
char *arg1 = strtok(NULL, " ");
if (arg1 == NULL) {
printf("Usage: x N EXPR\n");
return 0;
}
char *arg2 = strtok(NULL, " ");
if (arg1 == NULL) {
printf("Usage: x N EXPR\n");
return 0;
}
int n = strtol(arg1, NULL, 10);
vaddr_t expr = strtol(arg2, NULL, 16);
int i, j;
for (i = 0; i < n;) {
printf(ANSI_FMT("%#018lx: ", ANSI_FG_CYAN), expr);
for (j = 0; i < n && j < 4; i++, j++) {
word_t w = vaddr_read(expr, 8);
expr += 8;
printf("%#018lx ", w);
}
puts("");
}
return 0;
}
表达式求值 p EXPR#
该命令的实现分为三个板块
命令编写#
第一步,注册命令
{"p", "Usage: p EXPR. Calculate the expression, e.g. p $eax + 1", cmd_p }
第二步,编写cmd_p
,处理表达式的代码都集中在了expr
里
static int cmd_p(char* args) {
bool success;
word_t res = expr(args, &success);
if (!success) {
puts("invalid expression");
} else {
printf("%lu\n", res);
}
return 0;
}
表达式实现#
第一步,编写expr
,首先调make_token
提取符号,再用eval
进行计算
word_t expr(char *e, bool *success) {
if (!make_token(e)) {
*success = false;
return 0;
}
return eval(0, nr_token-1, success);;
}
第二步,实现make_token
和eval
的功能,按代码处理逻辑顺序先写make_token
make_token
逻辑是,每次都用所有的正则来匹配当前位置的字符,如果有匹配成功的就加入这个token(空字符除外),如果都匹配不成功就打印错误信息并返回false
给上层函数
举个例子:1 +2 *3
1
匹配(0x)?[0-9]+
- 两个空格匹配
" +"
+
匹配"\+"
- ...以此类推
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 ++) {
int reg_res = regexec(&re[i], e + position, 1, &pmatch, 0);
if (reg_res == 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;
if (rules[i].token_type == TK_NOTYPE) break;
tokens[nr_token].type = rules[i].token_type;
switch (rules[i].token_type) {
case TK_NUM:
case TK_REG:
case TK_VAR:
strncpy(tokens[nr_token].str, substr_start, substr_len);
tokens[nr_token].str[substr_len] = '\0';
// todo: handle overflow (token exceeding size of 32B)
}
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;
}
make_token
用到的token类型以及对应的正则表达式如下,其中TK_REG
和TK_VAR
是寄存器和变量,暂时还没用到,按照阶段性规定此处只需实现十进制算数表达式
enum {
TK_NOTYPE = 256, TK_EQ,
TK_NUM, // 10 & 16
TK_REG,
TK_VAR,
};
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
{"\\+", '+'}, // plus
{"-", '-'},
{"\\*", '*'},
{"/", '/'},
{"==", TK_EQ}, // equal
{"\\(", '('},
{"\\)", ')'},
{"[0-9]+", TK_NUM}, // TODO: non-capture notation (?:pattern) makes compilation failed
{"\\$\\w+", TK_REG},
{"[A-Za-z_]\\w*", TK_VAR},
};
第三步,再上一步tokens准备好的情况下,下一步就是利用这些提取好的tokens进行计算,按照文档给出的代码框架实现eval
,大体逻辑在文档里已经说了就不再重复,其中find_major
用于获取主运算符在tokens
数组里的位置
word_t eval(int p, int q, bool *ok) {
*ok = true;
if (p > q) {
*ok = false;
return 0;
} else if (p == q) {
if (tokens[p].type != TK_NUM) {
*ok = false;
return 0;
}
word_t ret = strtol(tokens[p].str, NULL, 10);
return ret;
} else if (check_parentheses(p, q)) {
return eval(p+1, q-1, ok);
} else {
int major = find_major(p, q);
if (major < 0) {
*ok = false;
return 0;
}
word_t val1 = eval(p, major-1, ok);
if (!*ok) return 0;
word_t val2 = eval(major+1, q, ok);
if (!*ok) return 0;
switch(tokens[major].type) {
case '+': return val1 + val2;
case '-': return val1 - val2;
case '*': return val1 * val2;
case '/': if (val2 == 0) {
*ok = false;
return 0;
}
return (sword_t)val1 / (sword_t)val2; // e.g. -1/2, may not pass the expr test
default: assert(0);
}
}
}
eval
代码里需要注意的是除法的地方,先看一个例子,假设用户输入的表达式为:(1-2)/2
uint32_t a = (1-2)/2; // a=0
uint32_t a = (uint32_t)(1-2)/2; // a=2147483647
从用户角度,只考虑无符号的情况下,第二个结果是正确的。然而测试用例是通过第一种形式生成的,即进行运算的数都是有符号类型的,因此我们需要将word_t
(无符号)类型先转为sword_t
(有符号)类型,再进行运算,从而满足测试用例。是的,先考虑测试再考虑用户,况且也没有用户
第四步,实现check_parentheses
和find_major
,先看第一个函数,这个函数的功能就是一句话:如果最外层括号匹配的话,则去除最外层的括号
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; // the leftest parenthese is matched
}
}
return false;
}
再看find_major
,其实现找主运算符的功能,根据文档描述:
- 非运算符的token不是主运算符.
- 出现在一对括号中的token不是主运算符. 注意到这里不会出现有括号包围整个表达式的情况, 因为这种情况已经在
check_parentheses()
相应的if
块中被处理了.- 主运算符的优先级在表达式中是最低的. 这是因为主运算符是最后一步才进行的运算符.
- 当有多个运算符的优先级都是最低时, 根据结合性, 最后被结合的运算符才是主运算符. 一个例子是
1 + 2 + 3
, 它的主运算符应该是右边的+
.
因此利用par
判断是否在括号,该变量记录当前位置之前有多少个未匹配的左括号,par>0
证明当前位置处于括号里。同时par
帮助检查括号不匹配的情况。如果找不到主运算符则返回-1,表示非法表达式,比如表达式2 3
int find_major(int p, int q) {
int ret = -1, par = 0, op_type = 0;
for (int i = p; i <= q; i++) {
if (tokens[i].type == TK_NUM) {
continue;
}
if (tokens[i].type == '(') {
par++;
} else if (tokens[i].type == ')') {
if (par == 0) {
return -1;
}
par--;
} else if (par > 0) {
continue;
} else {
int tmp_type = 0;
switch (tokens[i].type) {
case '*': case '/': tmp_type = 1; break;
case '+': case '-': tmp_type = 2; break;
default: assert(0);
}
if (tmp_type >= op_type) {
op_type = tmp_type;
ret = i;
}
}
}
if (par != 0) return -1;
return ret;
}
测试#
实际上就是编写gen-expr.c
,该程序负责生成测试用例。编写完成后在gen-expr
目录下make
(生成在./build目录下)或gcc
生成可执行文件,然后执行产生10000条测试用例
./gen-expr 10000 > input
gen-expr.c
代码逻辑是,结合rand
函数生成随机运算数和运算符,并使用snprintf
写入缓冲区buf
中,此处使用snprintf
而不是sprintf
防止写缓冲区溢出导致程序崩溃
过滤除0表达式的方式就是粗暴地开启-Wall -Werror
参数,因为生成的表达式属于字面量(literal constant),编译时就会计算表达式,如果含有除0的情况gcc
就会执行失败,所以可以通过system
返回值检测编译是否含有除0的情况
static char *buf_start = NULL;
static char *buf_end = buf+(sizeof(buf)/sizeof(buf[0]));
static int choose(int n) {
return rand() % n;
}
static void gen_space() {
int size = choose(4);
if (buf_start < buf_end) {
int n_writes = snprintf(buf_start, buf_end-buf_start, "%*s", size, "");
if (n_writes > 0) {
buf_start += n_writes;
}
}
}
static void gen_num() {
int num = choose(INT8_MAX);
if (buf_start < buf_end) {
int n_writes = snprintf(buf_start, buf_end-buf_start, "%d", num);
if (n_writes > 0) {
buf_start += n_writes;
}
}
gen_space();
}
static void gen_char(char c) {
int n_writes = snprintf(buf_start, buf_end-buf_start, "%c", c);
if (buf_start < buf_end) {
if (n_writes > 0) {
buf_start += n_writes;
}
}
}
static char ops[] = {'+', '-', '*', '/'};
static void gen_rand_op() {
int op_index = choose(sizeof(ops));
char op = ops[op_index];
gen_char(op);
}
static void gen_rand_expr() {
switch (choose(3))
{
case 0: gen_num(); break;
case 1: gen_char('('); gen_rand_expr(); gen_char(')'); break;
default: gen_rand_expr(); gen_rand_op(); gen_rand_expr(); break;
}
}
int main(int argc, char *argv[]) {
int seed = time(0);
srand(seed);
int loop = 1;
if (argc > 1) {
sscanf(argv[1], "%d", &loop);
}
int i;
for (i = 0; i < loop; i ++) {
buf_start = buf;
gen_rand_expr();
sprintf(code_buf, code_format, buf);
FILE *fp = fopen("/tmp/.code.c", "w");
assert(fp != NULL);
fputs(code_buf, fp);
fclose(fp);
int ret = system("gcc /tmp/.code.c -Wall -Werror -o /tmp/.expr");
// filter div-by-zero expressions
if (ret != 0) continue;
fp = popen("/tmp/.expr", "r");
assert(fp != NULL);
uint64_t result;
int _ = fscanf(fp, "%lu", &result);
_ = _;
pclose(fp);
printf("%lu %s\n", result, buf);
}
return 0;
}
测试函数我放在了sdb.c
的init_sdb
中,在init_regex
编译完正则后马上进行测试
void test_expr() {
FILE *fp = fopen("/home/ubuntu/ics2022/nemu/tools/gen-expr/input", "r");
if (fp == NULL) perror("test_expr error");
char *e = NULL;
word_t correct_res;
size_t len = 0;
ssize_t read;
bool success = false;
while (true) {
if(fscanf(fp, "%lu ", &correct_res) == -1) break;
read = getline(&e, &len, fp);
e[read-1] = '\0';
word_t res = expr(e, &success);
assert(success);
if (res != correct_res) {
puts(e);
printf("expected: %lu, got: %lu\n", correct_res, res);
assert(0);
}
}
fclose(fp);
if (e) free(e);
Log("expr test pass");
}
void init_sdb() {
/* Compile the regular expressions. */
init_regex();
/* test math expression calculation */
test_expr();
/* Initialize the watchpoint pool. */
init_wp_pool();
}
正如文档所说,并且也显而易见这个测试只能测试算数表达式,不能测试比如含有变量或寄存器的表达式
监视点 w EXPR / d N#
扩展表达式求值#
你之前已经实现了算术表达式的求值, 但这些表达式都是由常数组成的, 它们的值不会发生变化. 这样的表达式在监视点中没有任何意义, 为了发挥监视点的功能, 你首先需要扩展表达式求值的功能.
实现的扩展主要是新增一元运算符(比如解引用符号*
)和操作数类型(比如寄存器类型),其他扩展包括增加几个二元运算符(比如逻辑与&&
),增加的类型与对应的正则如下:
enum {
TK_NOTYPE = 256,
TK_POS, TK_NEG, TK_DEREF,
TK_EQ, TK_NEQ, TK_GT, TK_LT, TK_GE, TK_LE,
TK_AND,
TK_OR,
TK_NUM, // 10 & 16
TK_REG,
// TK_VAR,
};
static struct rule {
const char *regex;
int token_type;
} rules[] = {
{" +", TK_NOTYPE}, // spaces
{"\\(", '('}, {"\\)", ')'},
{"\\*", '*'}, {"/", '/'},
{"\\+", '+'}, {"-", '-'},
{"<", TK_LT}, {">", TK_GT}, {"<=", TK_LE}, {">=", TK_GE},
{"==", TK_EQ}, {"!=", TK_NEQ},
{"&&", TK_AND},
{"\\|\\|", TK_OR},
{"(0x)?[0-9]+", TK_NUM},
{"\\$\\w+", TK_REG},
// {"[A-Za-z_]\\w*", TK_VAR},
};
另外,将token类型分类,以便后面用到。OFTYPES
宏用于判断某个token的类型是否属于该类
#define OFTYPES(type, types) oftypes(type, types, ARRLEN(types))
static int bound_types[] = {')',TK_NUM,TK_REG}; // boundary for binary operator
static int nop_types[] = {'(',')',TK_NUM,TK_REG}; // not operator type
static int op1_types[] = {TK_NEG, TK_POS, TK_DEREF}; // unary operator type
static bool oftypes(int type, int types[], int size) {
for (int i = 0; i < size; i++) {
if (type == types[i]) return true;
}
return false;
}
按照文档提示,把一元运算符标记出来,与其他运算符区分开来,因为是标记token的类型,所以在make_token
中进行改动:
switch (rules[i].token_type) {
case TK_NUM:
case TK_REG:
// todo: handle overflow (token exceeding size of 32B)
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, nbound_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;
}
eval
函数也进行了改动:
- 操作数在
eval_operand
中单独解析 - 找到主运算符后分别记录左右子表达式的解析结果,如果右表达式解析失败,例如
2+
,右表达式为空,那么整体失败。否则如果左表达式解析失败,例如-1
,右表达式为空,调用calc1
进行一元运算符的解析,否则例如1+1
进行二元运算符的解析
static word_t eval(int p, int q, bool *ok) {
*ok = true;
if (p > q) {
*ok = false;
return 0;
} else if (p == q) {
return eval_operand(p, ok);
} else if (check_parentheses(p, q)) {
return eval(p+1, q-1, ok);
} else {
int major = find_major(p, q);
if (major < 0) {
*ok = false;
return 0;
}
bool ok1, ok2;
word_t val1 = eval(p, major-1, &ok1);
word_t val2 = eval(major+1, q, &ok2);
if (!ok2) {
*ok = false;
return 0;
}
if (ok1) {
word_t ret = calc2(val1, tokens[major].type, val2, ok);
return ret;
} else {
word_t ret = calc1(tokens[major].type, val2, ok);
return ret;
}
}
}
由于新增了运算符,find_major
也进行了相应的改动,其中的switch
处理运算符优先级
static int find_major(int p, int q) {
int ret = -1, par = 0, op_pre = 0;
for (int i = p; i <= q; i++) {
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;
} else {
int tmp_pre = 0;
switch (tokens[i].type) {
case TK_OR: tmp_pre++;
case TK_AND: tmp_pre++;
case TK_EQ: case TK_NEQ: tmp_pre++;
case TK_LT: case TK_GT: case TK_GE: case TK_LE: tmp_pre++;
case '+': case '-': tmp_pre++;
case '*': case '/': tmp_pre++;
case TK_NEG: case TK_DEREF: case TK_POS: tmp_pre++; break;
default: return -1;
}
if (tmp_pre > op_pre || (tmp_pre == op_pre && !OFTYPES(tokens[i].type, op1_types))) {
op_pre = tmp_pre;
ret = i;
}
}
}
if (par != 0) return -1;
return ret;
}
新增的eval_operand
,calc1
,calc2
如下,比较简单不解释
static word_t eval_operand(int i, bool *ok) {
switch (tokens[i].type) {
case TK_NUM:
if (strncmp("0x", tokens[i].str, 2) == 0) return strtol(tokens[i].str, NULL, 16);
else return strtol(tokens[i].str, NULL, 10);
case TK_REG:
return isa_reg_str2val(tokens[i].str, ok);
default:
*ok = false;
return 0;
}
}
// unary operator
static word_t calc1(int op, word_t val, bool *ok) {
switch (op)
{
case TK_NEG: return -val;
case TK_POS: return val;
case TK_DEREF: return vaddr_read(val, 8);
default: *ok = false;
}
return 0;
}
// binary operator
static word_t calc2(word_t val1, int op, word_t val2, bool *ok) {
switch(op) {
case '+': return val1 + val2;
case '-': return val1 - val2;
case '*': return val1 * val2;
case '/': if (val2 == 0) {
*ok = false;
return 0;
}
return (sword_t)val1 / (sword_t)val2; // e.g. -1/2, may not pass the expr test
case TK_AND: return val1 && val2;
case TK_OR: return val1 || 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;
default: *ok = false; return 0;
}
}
监视点池的管理#
维护head
和free_
(单链表的插入删除)
static WP* new_wp() {
assert(free_);
WP* ret = free_;
free_ = free_->next;
ret->next = head;
head = ret;
return ret;
}
static void free_wp(WP *wp) {
WP* h = head;
if (h == wp) head = NULL;
else {
while (h && h->next != wp) h = h->next;
assert(h);
h->next = wp->next;
}
wp->next = free_;
free_ = wp;
}
实现监视点#
第一步,注册命令,其中输出监视点信息的info w
命令属于info
命令,之前已经注册过
{ "w", "Usage: w EXPR. Watch for the variation of the result of EXPR, pause at variation point", cmd_w },
{ "d", "Usage: d N. Delete watchpoint of wp.NO=N", cmd_d },
第二步,实现cmd_info w
,cmd_w
,cmd_d
static int cmd_info(char *args) {
/* extract the first argument */
char *arg = strtok(NULL, " ");
if (arg == NULL) {
printf("Usage: info r (registers) or info w (watchpoints)\n");
} else {
if (strcmp(arg, "r") == 0) {
isa_reg_display();
} else if (strcmp(arg, "w") == 0) {
wp_iterate();
} else {
printf("Usage: info r (registers) or info w (watchpoints)\n");
}
}
return 0;
}
static int cmd_w(char* args) {
if (!args) {
printf("Usage: w EXPR\n");
return 0;
}
bool success;
word_t res = expr(args, &success);
if (!success) {
puts("invalid expression");
} else {
wp_watch(args, res);
}
return 0;
}
static int cmd_d(char* args) {
char *arg = strtok(NULL, "");
if (!arg) {
printf("Usage: d N\n");
return 0;
}
int no = strtol(arg, NULL, 10);
wp_remove(no);
return 0;
}
第三部,实现wp_watch
,wp_remove
,wp_iterate
void wp_watch(char *expr, word_t res) {
WP* wp = new_wp();
strcpy(wp->expr, expr);
wp->old = res;
printf("Watchpoint %d: %s\n", wp->NO, expr);
}
void wp_remove(int no) {
assert(no < NR_WP);
WP* wp = &wp_pool[no];
free_wp(wp);
printf("Delete watchpoint %d: %s\n", wp->NO, wp->expr);
}
void wp_iterate() {
WP* h = head;
if (!h) {
puts("No watchpoints.");
return;
}
printf("%-8s%-8s\n", "Num", "What");
while (h) {
printf("%-8d%-8s\n", h->NO, h->expr);
h = h->next;
}
}
第四步,在trace_and_difftest()
函数的最后扫描所有的监视点
static void trace_and_difftest(Decode *_this, vaddr_t dnpc) {
#ifdef CONFIG_ITRACE_COND
if (ITRACE_COND) { log_write("%s\n", _this->logbuf); }
#endif
if (g_print_step) { IFDEF(CONFIG_ITRACE, puts(_this->logbuf)); }
IFDEF(CONFIG_DIFFTEST, difftest_step(_this->pc, dnpc));
IFDEF(CONFIG_WATCHPOINT, wp_difftest()); // this line
}
第五步,在watchpoint.c
中实现wp_difftest
void wp_difftest() {
WP* h = head;
while (h) {
bool _;
word_t new = expr(h->expr, &_);
if (h->old != new) {
printf("Watchpoint %d: %s\n"
"Old value = %lu\n"
"New value = %lu\n"
, h->NO, h->expr, h->old, new);
h->old = new;
}
h = h->next;
}
}
PA1至此结束,欢迎指出不足之处
补充#
makefile#
看makefile之前呢,需要掌握makefile编写的前置知识。这块补充的部分是写完pa3后再来补充的,可能会含有后面才需要实现的代码,自行甄别
先来看nemu下名为Makefile
的主控makefile,这是第一个接触到的makefile,顺便熟悉一下makefile编写
# Sanity check
ifeq ($(wildcard $(NEMU_HOME)/src/nemu-main.c),)
$(error NEMU_HOME=$(NEMU_HOME) is not a NEMU repo)
endif
首先用wildcard检查是否有nemu-main.c文件,没有的话说明环境变量NEMU_HOME配得不对或者根本没有这个nemu这个仓库
# Include variables and rules generated by menuconfig
-include $(NEMU_HOME)/include/config/auto.conf
-include $(NEMU_HOME)/include/config/auto.conf.cmd
引入configs相关变量,可以查看里面的变量都是以CONFIG_
开头,后面主makefile中用到的时候就知道来自哪里的了
remove_quote = $(patsubst "%",%,$(1))
定义一个remove_quote函数,移除参数中的双引号
# Extract variabls from menuconfig
GUEST_ISA ?= $(call remove_quote,$(CONFIG_ISA))
ENGINE ?= $(call remove_quote,$(CONFIG_ENGINE))
NAME = $(GUEST_ISA)-nemu-$(ENGINE)
这里不说了,加上注释都看得懂
# Include all filelist.mk to merge file lists
FILELIST_MK = $(shell find ./src -name "filelist.mk")
include $(FILELIST_MK)
引入nemu及其子目录下的filelist.mk,这些filelist.mk分别指定了自己负责模块的一些源文件夹和头文件,以及编译参数,比如utils下的filelist.mk检查是否开启了itrace,然后指定itrace相关的源文件和编译参数,然后用filter-out
函数过滤不需要的源文件,最终SRCS
变量包含了我们所有需要编译的源文件
# Extract compiler and options from menuconfig
CC = $(call remove_quote,$(CONFIG_CC))
CFLAGS_BUILD += $(call remove_quote,$(CONFIG_CC_OPT))
CFLAGS_BUILD += $(if $(CONFIG_CC_LTO),-flto,)
CFLAGS_BUILD += $(if $(CONFIG_CC_DEBUG),-Og -ggdb3,)
CFLAGS_BUILD += $(if $(CONFIG_CC_ASAN),-fsanitize=address,)
CFLAGS_TRACE += -DITRACE_COND=$(if $(CONFIG_ITRACE_COND),$(call remove_quote,$(CONFIG_ITRACE_COND)),true)
CFLAGS += $(CFLAGS_BUILD) $(CFLAGS_TRACE) -D__GUEST_ISA__=$(GUEST_ISA)
LDFLAGS += $(CFLAGS_BUILD)
随后根据配置文件配置编译器CC
、配置编译参数CFLAGS
、链接参数LDFLAGS
# Include rules for menuconfig
include $(NEMU_HOME)/scripts/config.mk
然后是与config相关的东西,比如menuconfig的生成。最后就是AM相关的makefile,如果没有AM层,那么就使用native.mk,看一下里面是什么:里面引入了build.mk和difftest.mk,先看看build.mk
build.mk
做的工作如下:
- 指定工作目录WORK_DIR(当前是$NEMU_HOME)、生成目录BUILD_DIR、二进制生成目录OBJ_DIR、可执行二进制文件BINARY等变量
- 指定编译器和flags
- 指定编译规则
- 指定
app
和clean
规则,前者是生成但不执行二进制可执行文件,后者是清理build输出
difftest.mk
与后面PA的difftest有关,做的工作如下:
- 指定difftest_ref目录、二进制可执行文件
- 指定模拟器
--diff
参数保存在ARGS_DIFF
变量 - 指定
difftest
规则
再回到native.mk
,现在可以知道引入的build.mk是通用地用来构建可执行文件的,可以用来构建模拟器,也可以构建别的工具,比如fixdep
工具,在fixdep中的Makefile指定NAME
和SRCS
,最后引入build.mk即可,而difftest.mk用于专门构建difftest工具(其他全系模拟器)
- 指定生成模拟器的时候git commit一下,建议注释掉这个自动commit去掉不然会生成很多没用的commit记录
- 指定
run
、gdb
等规则,前者是运行模拟器,后者是调试模拟器 - 指定
clean
规则
模拟器上程序的执行流程#
当我们运行nemu的时候,是通过SDB这个“窗口”去运行和窥探客户程序的运行的:当键入c
或si
时的原理是一样的,都是调用cpu_exec(n)
,执行n条指令,n是一个无符号整数,传入-1的话变成无符号整数的最大值,可视为把指令不停地执行下去无停顿,否则执行完n条指令后程序会回到sdb_mainloop
中等待下一条用户的sdb命令。
看一下cpu_exec
:前后nemu_state的检查就不说,,流程集中在调用execute(n)
执行n条指令,因此来到execute
函数中:
static void execute(uint64_t n) {
Decode s;
for (;n > 0; n --) {
exec_once(&s, cpu.pc);
g_nr_guest_inst ++;
trace_and_difftest(&s, cpu.pc);
if (nemu_state.state != NEMU_RUNNING) break;
IFDEF(CONFIG_DEVICE, device_update());
}
}
execute
循环n次如下的流程:
- 调用
exec_once
执行每条指令 - 调用
trace_and_difftest
记录trace、执行difftest和检查watchpoint - 检查是否程序应该退出(运行到了最后一条指令或者别的原因退出)
- 更新设备状态
我们现在只关注指令执行流程,因此接下来看exec_once
的流程:
- 调用
isa_exec_once
取指并执行,指令与架构相关,我这里是riscv64的isa_exec_once
- 将
cpu.pc
置为下一条应该执行的指令的地址,这个地址在s->dnpc
中 - 根据是否开启了itrace,将第一步执行的指令记录到s->logbuf中,然后会在
trace_and_difftest
中输出
老样子,这里我们只关注isa_exec_once
函数,这个函数传入一个Decode
结构体,保存待取指令的地址pc、下一条指令的静态地址snpc=pc+4、下一条指令的动态地址dnpc,还有一个字段isa目前只保存待取指令的内容
int isa_exec_once(Decode *s) {
s->isa.inst.val = inst_fetch(&s->snpc, 4);
IFDEF(CONFIG_ITRACE, trace_inst(s->pc, s->isa.inst.val));
return decode_exec(s);
}
isa_exec_once
执行流程如下:
inst_fetch
取指- itrace
decode_exec
译码并执行
重点关注decode_exec
,在这个函数中定义了一些宏比如INSTPAT_INST
、INSTPAT_MATCH
,还有其他一些函数比如decoce_operand
,其他的文件decode.h
等,这些都是为了译码服务的,其中有很多语言上的tricks,宏使用得非常溜,这里不展开讲这些tricks,它们只是让我们代码写得更好看而且不降低运行效率,我们将目光集中在INSTPAT
这个宏上:
#define INSTPAT(pattern, ...) do { \
uint64_t key, mask, shift; \
pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift); \
if ((((uint64_t)INSTPAT_INST(s) >> shift) & mask) == key) { \
INSTPAT_MATCH(s, ##__VA_ARGS__); \
goto *(__instpat_end); \
} \
} while (0)
INSTPAT
这个宏流程如下:
- 调用
pattern_decode
解析传入的第一个参数pattern - 检查指令是否匹配pattern
- 如果匹配的话,使用
INSTPAT_MATCH
宏,其中调用decode_operand
根据指令类型提取操作数、立即数等,并执行指令相应的运算(INSTPAT
宏的最后一个参数),最后跳过下面的pattern,因为已经匹配成功,不用再检查别的pattern
最后decode_exec
再做些收尾工作,比如将$0寄存器置0
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律