Implementación de un depurador básico para el emulador NEMU (ISA RISC-V 64)

Salida limpia del emulador

Al ejecutar make run y luego introducir q para salir, se producía un error debido a que la función is_exit_status_bad devolvía -1. Para evitarlo, basta con establecer el estado del emulador a NEMU_QUIT cuando se recibe el comando q:

// 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;
          }
          return;
        }
        break;
      }
    }
    if (i == NR_CMD) { printf("Comando desconocido '%s'\n", cmd); }
  }
}

Depurador simple

Ejecución paso a paso: si [N]

Registramos el comando en la tabla:

static struct {
  const char *name;
  const char *description;
  int (*handler) (char *);
} cmd_table [] = {
  { "help", "Muestra información de comandos", cmd_help },
  { "c", "Continúa la ejecución", cmd_c },
  { "q", "Sale de NEMU", cmd_q },
  { "si", "Ejecuta N instrucciones (por defecto 1)", cmd_si },
  // Más comandos...
};

Implementamos cmd_si:

static int cmd_si(char *args) {
  char *arg = strtok(NULL, " ");
  int pasos = (arg == NULL) ? 1 : strtol(arg, NULL, 10);
  cpu_exec(pasos);
  return 0;
}

Mostrar registros: info r

Añadimos el comando info y su subcomando r:

{ "info", "Muestra info de registros y puntos de vigilancia", cmd_info },

static int cmd_info(char *args) {
  char *arg = strtok(NULL, " ");
  if (arg == NULL) {
    printf("Uso: info r (registros) o info w (puntos de vigilancia)\n");
  } else if (strcmp(arg, "r") == 0) {
    isa_reg_display();
  } else if (strcmp(arg, "w") == 0) {
    // pendiente
  } else {
    printf("Uso: info r (registros) o info w (puntos de vigilancia)\n");
  }
  return 0;
}

Implementamos isa_reg_display (en nemu/src/isa/$ISA/reg.c):

void isa_reg_display() {
  int num_regs = ARRLEN(regs);
  for (int i = 0; i < num_regs; i++) {
    printf("%-8s%-#20lx%-20ld\n", regs[i], cpu.gpr[i], cpu.gpr[i]);
  }
}

Explorar memoria: x N EXPR

Registramos el comando:

{ "x", "Uso: x N EXPR. Examina N bytes desde la dirección EXPR", cmd_x },

static int cmd_x(char *args) {
  char *arg1 = strtok(NULL, " ");
  if (!arg1) { printf("Uso: x N EXPR\n"); return 0; }
  char *arg2 = strtok(NULL, " ");
  if (!arg2) { printf("Uso: x N EXPR\n"); return 0; }

  int n = strtol(arg1, NULL, 10);
  vaddr_t dir = strtol(arg2, NULL, 16);

  for (int i = 0; i < n; ) {
    printf(ANSI_FMT("%#018lx: ", ANSI_FG_CYAN), dir);
    for (int j = 0; i < n && j < 4; i++, j++) {
      word_t val = vaddr_read(dir, 8);
      dir += 8;
      printf("%#018lx ", val);
    }
    putchar('\n');
  }
  return 0;
}

Evaluar expresiones: p EXPR

Comando

{ "p", "Uso: p EXPR. Calcula la expresión, ej. p $eax + 1", cmd_p }
static int cmd_p(char* args) {
  bool exito;
  word_t res = expr(args, &exito);
  if (!exito) {
    puts("expresión inválida");
  } else {
    printf("%lu\n", res);
  }
  return 0;
}

Analizador léxico y evaluador

word_t expr(char *e, bool *exito) {
  if (!make_token(e)) {
    *exito = false;
    return 0;
  }
  return eval(0, nr_token-1, exito);
}

make_token recorre la cadena aplicando epxresiones regulares para extraer tokens:

static bool make_token(char *e) {
  int pos = 0;
  regmatch_t pmatch;
  nr_token = 0;
  while (e[pos] != '\0') {
    int i;
    for (i = 0; i < NR_REGEX; i++) {
      int res = regexec(&re[i], e + pos, 1, &pmatch, 0);
      if (res == 0 && pmatch.rm_so == 0) {
        char *inicio = e + pos;
        int largo = pmatch.rm_eo;
        pos += largo;
        if (rules[i].token_type != TK_NOTYPE) {
          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, inicio, largo);
              tokens[nr_token].str[largo] = '\0';
              break;
          }
          nr_token++;
        }
        break;
      }
    }
    if (i == NR_REGEX) {
      printf("sin coincidencia en posición %d\n%s\n%*.s^\n", pos, e, pos, "");
      return false;
    }
  }
  return true;
}

Definiciones de tipos de token:

enum {
  TK_NOTYPE = 256, TK_EQ,
  TK_NUM,
  TK_REG,
  TK_VAR,
};

static struct rule {
  const char *regex;
  int token_type;
} rules[] = {
  {" +", TK_NOTYPE},
  {"\\+", '+'}, {"-", '-'}, {"\\*", '*'}, {"/", '/'},
  {"==", TK_EQ},
  {"\\(", '('}, {"\\)", ')'},
  {"[0-9]+", TK_NUM},
  {"\\$\\w+", TK_REG},
  {"[A-Za-z_]\\w*", TK_VAR},
};

Función eval:

word_t eval(int p, int q, bool *ok) {
  *ok = true;
  if (p > q) { *ok = false; return 0; }
  if (p == q) {
    if (tokens[p].type != TK_NUM) { *ok = false; return 0; }
    return strtol(tokens[p].str, NULL, 10);
  }
  if (check_parentheses(p, q)) return eval(p+1, q-1, ok);
  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;
    default: assert(0);
  }
}

Funciones auxiliares:

bool check_parentheses(int p, int q) {
  if (tokens[p].type != '(' || tokens[q].type != ')') return false;
  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;
}

int find_major(int p, int q) {
  int ret = -1, par = 0, op_prior = 0;
  for (int i = p; i <= q; i++) {
    if (tokens[i].type == '(') { par++; continue; }
    if (tokens[i].type == ')') {
      if (par == 0) return -1;
      par--;
      continue;
    }
    if (par > 0) continue;
    if (tokens[i].type == TK_NUM) continue;
    int prior = 0;
    switch (tokens[i].type) {
      case '*': case '/': prior = 1; break;
      case '+': case '-': prior = 2; break;
      default: assert(0);
    }
    if (prior >= op_prior) { op_prior = prior; ret = i; }
  }
  if (par != 0) return -1;
  return ret;
}

Pruebas automáticas

Se genera un programa gen-expr.c que produce 10000 expresiones aleatorias, las compila con gcc -Wall -Werror para filtrar división por cero, y compara el resultado con el obteenido por el evaluador.

static char *buf_ptr = buf;
static char *buf_lim = buf + sizeof(buf);

static int elegir(int n) { return rand() % n; }

static void gen_espacio() {
  int tam = elegir(4);
  if (buf_ptr < buf_lim) {
    int n = snprintf(buf_ptr, buf_lim - buf_ptr, "%*s", tam, "");
    if (n > 0) buf_ptr += n;
  }
}

static void gen_num() {
  int num = elegir(INT8_MAX);
  if (buf_ptr < buf_lim) {
    int n = snprintf(buf_ptr, buf_lim - buf_ptr, "%d", num);
    if (n > 0) buf_ptr += n;
  }
  gen_espacio();
}

static void gen_char(char c) {
  int n = snprintf(buf_ptr, buf_lim - buf_ptr, "%c", c);
  if (n > 0) buf_ptr += n;
}

static char ops[] = {'+', '-', '*', '/'};
static void gen_operador() {
  gen_char(ops[elegir(4)]);
}

static void gen_expr_aleatoria() {
  switch (elegir(3)) {
    case 0: gen_num(); break;
    case 1: gen_char('('); gen_expr_aleatoria(); gen_char(')'); break;
    default: gen_expr_aleatoria(); gen_operador(); gen_expr_aleatoria(); break;
  }
}

int main(int argc, char *argv[]) {
  srand(time(0));
  int n_casos = (argc > 1) ? atoi(argv[1]) : 1;
  for (int i = 0; i < n_casos; i++) {
    buf_ptr = buf;
    gen_expr_aleatoria();
    sprintf(code_buf, code_format, buf);
    FILE *f = fopen("/tmp/.code.c", "w");
    fputs(code_buf, f); fclose(f);
    if (system("gcc /tmp/.code.c -Wall -Werror -o /tmp/.expr") != 0) continue;
    f = popen("/tmp/.expr", "r");
    uint64_t resultado;
    fscanf(f, "%lu", &resultado);
    pclose(f);
    printf("%lu %s\n", resultado, buf);
  }
  return 0;
}

La función de prueba se inserta en init_sdb:

void test_expr() {
  FILE *fp = fopen("tools/gen-expr/input", "r");
  if (!fp) { perror("test_expr error"); return; }
  word_t correcto;
  char *linea = NULL;
  size_t len = 0;
  bool ok;
  while (fscanf(fp, "%lu ", &correcto) != -1) {
    getline(&linea, &len, fp);
    linea[strlen(linea)-1] = '\0';
    word_t obtenido = expr(linea, &ok);
    assert(ok);
    if (obtenido != correcto) {
      printf("Fallo: %s, esperado %lu, obtenido %lu\n", linea, correcto, obtenido);
      assert(0);
    }
  }
  fclose(fp);
  free(linea);
  Log("Prueba de expresiones superada");
}

void init_sdb() {
  init_regex();
  test_expr();
  init_wp_pool();
}

Puntos de vigilancia: w EXPR / d N

Extensión del evaluador de expresiones

Se añaden operadores unarios (* como desreferencia, + y - unarios), operadores relacionales y lógicos (==, !=, <, >, <=, >=, &&, ||) y soporte para registros.

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

static struct rule rules[] = {
  {" +", TK_NOTYPE},
  {"\\(", '('}, {"\\)", ')'},
  {"\\*", '*'}, {"/", '/'},
  {"\\+", '+'}, {"-", '-'},
  {"<", TK_LT}, {">", TK_GT}, {"<=", TK_LE}, {">=", TK_GE},
  {"==", TK_EQ}, {"!=", TK_NEQ},
  {"&&", TK_AND},
  {"\\|\\|", TK_OR},
  {"(0x)?[0-9]+", TK_NUM},
  {"\\$\\w+", TK_REG},
};

En make_token se detectan operadores unarios según el contexto:

case '*': case '-': case '+':
  if (nr_token==0 || !es_tipo_limite(tokens[nr_token-1].type, ...)) {
    if (rules[i].token_type == '-') tokens[nr_token].type = TK_NEG;
    else if (rules[i].token_type == '+') tokens[nr_token].type = TK_POS;
    else tokens[nr_token].type = TK_DEREF;
  }

Actualización de eval:

static word_t eval(int p, int q, bool *ok) {
  *ok = true;
  if (p > q) { *ok = false; return 0; }
  if (p == q) return eval_operando(p, ok);
  if (check_parentheses(p, q)) return eval(p+1, q-1, ok);
  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) return calc2(val1, tokens[major].type, val2, ok);
  else     return calc1(tokens[major].type, val2, ok);
}

Funciones nuevas:

static word_t eval_operando(int i, bool *ok) {
  if (tokens[i].type == 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);
  } else if (tokens[i].type == TK_REG) {
    return isa_reg_str2val(tokens[i].str, ok);
  } else {
    *ok = false;
    return 0;
  }
}

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

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

Gestión del pool de puntos de vigilancia

static WP* nuevo_wp() {
  assert(free_);
  WP* ret = free_;
  free_ = free_->next;
  ret->next = head;
  head = ret;
  return ret;
}

static void liberar_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;
}

Comandos w, d y mejora de info w

{ "w", "Uso: w EXPR. Vigila cambios de EXPR", cmd_w },
{ "d", "Uso: d N. Elimina el punto de vigilancia N", cmd_d },

static int cmd_info(char *args) {
  char *arg = strtok(NULL, " ");
  if (!arg) printf("Uso: info r (registros) o info w (puntos de vigilancia)\n");
  else if (strcmp(arg, "r") == 0) isa_reg_display();
  else if (strcmp(arg, "w") == 0) iterar_wp();
  else printf("Uso: info r (registros) o info w (puntos de vigilancia)\n");
  return 0;
}

static int cmd_w(char* args) {
  if (!args) { printf("Uso: w EXPR\n"); return 0; }
  bool ok;
  word_t res = expr(args, &ok);
  if (!ok) { puts("expresión inválida"); return 0; }
  vigilar(args, res);
  return 0;
}

static int cmd_d(char* args) {
  char *arg = strtok(NULL, "");
  if (!arg) { printf("Uso: d N\n"); return 0; }
  int num = strtol(arg, NULL, 10);
  eliminar_wp(num);
  return 0;
}

Implementación de las funciones asociadas:

void vigilar(char *expr, word_t res) {
  WP* wp = nuevo_wp();
  strcpy(wp->expr, expr);
  wp->old = res;
  printf("Punto de vigilancia %d: %s\n", wp->NO, expr);
}

void eliminar_wp(int num) {
  assert(num < NR_WP);
  WP* wp = &wp_pool[num];
  liberar_wp(wp);
  printf("Punto de vigilancia %d eliminado: %s\n", wp->NO, wp->expr);
}

void iterar_wp() {
  WP* h = head;
  if (!h) { puts("No hay puntos de vigilancia."); return; }
  printf("%-8s%-8s\n", "Num", "Qué");
  while (h) {
    printf("%-8d%-8s\n", h->NO, h->expr);
    h = h->next;
  }
}

En trace_and_difftest se añade la comprobación:

static void trace_and_difftest(Decode *_this, vaddr_t dnpc) {
  ...
  IFDEF(CONFIG_WATCHPOINT, verificar_wp());
}

void verificar_wp() {
  WP* h = head;
  while (h) {
    bool _;
    word_t nuevo = expr(h->expr, &_);
    if (h->old != nuevo) {
      printf("Punto de vigilancia %d: %s\n"
             "Valor antiguo = %lu\n"
             "Valor nuevo   = %lu\n",
             h->NO, h->expr, h->old, nuevo);
      h->old = nuevo;
    }
    h = h->next;
  }
}

Con esto finaliza la implementación del depurador básico para PA1.

Etiquetas: NEMU RISC-V64 depurador SDB expresiones

Publicado el 6-20 18:22