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_tokeneval的功能,按代码处理逻辑顺序先写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_REGTK_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_parenthesesfind_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.cinit_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_operandcalc1calc2如下,比较简单不解释

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;
  }
}

监视点池的管理#

维护headfree_(单链表的插入删除)

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 wcmd_wcmd_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_watchwp_removewp_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
  • 指定编译规则
  • 指定appclean规则,前者是生成但不执行二进制可执行文件,后者是清理build输出

difftest.mk与后面PA的difftest有关,做的工作如下:

  • 指定difftest_ref目录、二进制可执行文件
  • 指定模拟器--diff参数保存在ARGS_DIFF变量
  • 指定difftest规则

再回到native.mk,现在可以知道引入的build.mk是通用地用来构建可执行文件的,可以用来构建模拟器,也可以构建别的工具,比如fixdep工具,在fixdep中的Makefile指定NAMESRCS,最后引入build.mk即可,而difftest.mk用于专门构建difftest工具(其他全系模拟器)

  • 指定生成模拟器的时候git commit一下,建议注释掉这个自动commit去掉不然会生成很多没用的commit记录
  • 指定rungdb等规则,前者是运行模拟器,后者是调试模拟器
  • 指定clean规则

模拟器上程序的执行流程#

当我们运行nemu的时候,是通过SDB这个“窗口”去运行和窥探客户程序的运行的:当键入csi时的原理是一样的,都是调用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_INSTINSTPAT_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

posted @   NOSAE  阅读(7831)  评论(19编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示