--- title: nju pa1 categories: [lab,nju-pa] --- > 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`即可 ``` c // 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` ``` c 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`具体要执行的东西 ``` c 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 第一步,注册命令 ``` c { "info", "Display the info of registers & watchpoints", cmd_info }, ``` 第二步,编写`cmd_info`,其中调用的`isa_reg_display`函数就是PA文档里介绍的,简易调试器为了屏蔽ISA的差异. 框架代码已经为大家准备了的API之一 ``` c 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` ``` c // 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 第一步,注册命令 ``` c { "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字节 ``` c 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 该命令的实现分为三个板块 ### 命令编写 第一步,注册命令 ``` c {"p", "Usage: p EXPR. Calculate the expression, e.g. p $eax + 1", cmd_p } ``` 第二步,编写`cmd_p`,处理表达式的代码都集中在了`expr`里 ``` c 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`进行计算 ``` c 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]+` - 两个空格匹配`" +"` - `+`匹配`"\+"` - ...以此类推 ``` c 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`是寄存器和变量,暂时还没用到,按照阶段性规定此处只需实现十进制算数表达式 ``` c 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`数组里的位置 ``` c 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` ``` c 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`,先看第一个函数,这个函数的功能就是一句话:如果最外层括号匹配的话,则去除最外层的括号 ``` c 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` ``` c 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条测试用例 ``` shell ./gen-expr 10000 > input ``` `gen-expr.c`代码逻辑是,结合`rand`函数生成随机运算数和运算符,并使用`snprintf`写入缓冲区`buf`中,此处使用`snprintf`而不是`sprintf`防止写缓冲区溢出导致程序崩溃 过滤除0表达式的方式就是粗暴地开启`-Wall -Werror`参数,因为生成的表达式属于字面量(literal constant),编译时就会计算表达式,如果含有除0的情况`gcc`就会执行失败,所以可以通过`system`返回值检测编译是否含有除0的情况 ``` c 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`编译完正则后马上进行测试 ``` c 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 ### 扩展表达式求值 > 你之前已经实现了算术表达式的求值, 但这些表达式都是由常数组成的, 它们的值不会发生变化. 这样的表达式在监视点中没有任何意义, 为了发挥监视点的功能, 你首先需要扩展表达式求值的功能. 实现的扩展主要是新增一元运算符(比如解引用符号`*`)和操作数类型(比如寄存器类型),其他扩展包括增加几个二元运算符(比如逻辑与`&&`),增加的类型与对应的正则如下: ``` c 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的类型是否属于该类 ``` c #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`中进行改动: ``` c 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`进行二元运算符的解析 ``` c 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`处理运算符优先级 ``` c 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`如下,比较简单不解释 ``` c 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_`(单链表的插入删除) ``` c 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`命令,之前已经注册过 ``` c { "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` ``` c 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` ``` c 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()`函数的最后扫描所有的监视点 ``` c 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` ``` c 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编写的[前置知识](https://seisman.github.io/how-to-write-makefile/index.html)。这块补充的部分是写完pa3后再来补充的,可能会含有后面才需要实现的代码,自行甄别 先来看nemu下名为`Makefile`的主控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这个仓库 ``` makefile # 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中用到的时候就知道来自哪里的了 ``` makefile remove_quote = $(patsubst "%",%,$(1)) ``` 定义一个remove_quote函数,移除参数中的双引号 ``` makefile # Extract variabls from menuconfig GUEST_ISA ?= $(call remove_quote,$(CONFIG_ISA)) ENGINE ?= $(call remove_quote,$(CONFIG_ENGINE)) NAME = $(GUEST_ISA)-nemu-$(ENGINE) ``` 这里不说了,加上注释都看得懂 ``` makefile # 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`变量包含了我们所有需要编译的源文件 ``` makefile # 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` ``` makefile # 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`函数中: ``` c 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目前只保存待取指令的内容 ``` c 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`这个宏上: ``` c #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