PA2.2-基础设施(2)

📚 使用须知

  • 本博客内容仅供学习参考
  • 建议理解思路后独立实现
  • 欢迎交流讨论

task : 基础设施(2)

bug诊断的利器 - 踪迹

指令执行的踪迹 - itrace

// nemu/src/cpu/cpu-exec.c
static void exec_once(Decode *s, vaddr_t pc) {
  s->pc = pc;
  s->snpc = pc;
  isa_exec_once(s);
  cpu.pc = s->dnpc;
#ifdef CONFIG_ITRACE
  char *p = s->logbuf;
  /* 
   * p += snprintf(p, sizeof(s->logbuf), "0x%08x:", s->pc);
   * 以十六进制的形式打印出当前pc的值
   * sizeof(s->logbuf) 写入的最大字符数
   * such as : 0x80000000:
   */
  p += snprintf(p, sizeof(s->logbuf), FMT_WORD ":", s->pc);
  /*
   * ilen always = 4
   */
  int ilen = s->snpc - s->pc;
  int i;
  uint8_t *inst = (uint8_t *)&s->isa.inst.val;
  /*
   * 每次以十六进制的形式打印出8bit出来, 因为 uint8_t *inst 
   * all time of print is ilen = 4
   * such as: 00 00 04 13
   */
  for (i = ilen - 1; i >= 0; i --) {
    p += snprintf(p, 4, " %02x", inst[i]);
  }
  int ilen_max = MUXDEF(CONFIG_ISA_x86, 8, 4);
  int space_len = ilen_max - ilen;
  if (space_len < 0) space_len = 0;
  space_len = space_len * 3 + 1;
  memset(p, ' ', space_len);
  p += space_len;
 
  void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte);
  disassemble(p, s->logbuf + sizeof(s->logbuf) - p,
      MUXDEF(CONFIG_ISA_x86, s->snpc, s->pc), (uint8_t *)&s->isa.inst.val, ilen);

#endif
}
 
/**********************************************/
//nemu/src/utils/disasm.cc
/*
 * disassemble(p, s->logbuf + sizeof(s->logbuf) - p,
      MUXDEF(CONFIG_ISA_x86, s->snpc, s->pc), (uint8_t *)&s->isa.inst.val, ilen);
 * from nemu/src/cpu/cpu-exec.c
 * char *str is logbuf which string buf writed to build/nemu-log.txt
 * int size is the remaining memory of logbuf 
 * uint64_t pc is the pc当前指向的地址(have not +4)
 * uint8_t *code is 指向指令的指针
 * int nbyte is 指令的字节长度
 */
extern "C" void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte) {
  MCInst inst;
  llvm::ArrayRef<uint8_t> arr(code, nbyte);
  uint64_t dummy_size = 0;
  gDisassembler->getInstruction(inst, dummy_size, arr, pc, llvm::nulls());
 
  std::string s;
  raw_string_ostream os(s);
  gIP->printInst(&inst, pc, "", *gSTI, os);
 
  int skip = s.find_first_not_of('\t');
  const char *p = s.c_str() + skip;
  assert((int)s.length() - skip < size);
  strcpy(str, p);
}

我们能够看到,在exec_once函数中,每次执行完一条指令,我们就会将这个条指令的地址,指令本身,以及通过disassemble函数得到的指令反汇编保存到s->logbuf

那么s->logbuf是如何写到build/nemu-log中的呢?

// First: 使用Makefile在编译链接的时候传人参数-l(跟我们上面传入参数使得开启批处理模式相同)
// Second: 解析-l的参数 
//nemu/src/monitor/monitor.c
static int parse_args(int argc, char *argv[]) {
  const struct option table[] = {
    {"batch"    , no_argument      , NULL, 'b'},
    {"log"      , required_argument, NULL, 'l'},
    {"diff"     , required_argument, NULL, 'd'},
    {"port"     , required_argument, NULL, 'p'},
    {"help"     , no_argument      , NULL, 'h'},
    {0          , 0                , NULL,  0 },
  };
  int o;
  while ( (o = getopt_long(argc, argv, "-bhl:d:p:", table, NULL)) != -1) {
    switch (o) {
      case 'b': sdb_set_batch_mode(); break;
      case 'p': sscanf(optarg, "%d", &difftest_port); break;
      case 'l': log_file = optarg; break;
      case 'd': diff_so_file = optarg; break;
      case 1: img_file = optarg; return 0;
      default:
        printf("Usage: %s [OPTION...] IMAGE [args]\n\n", argv[0]);
        printf("\t-b,--batch              run with batch mode\n");
        printf("\t-l,--log=FILE           output log to FILE\n");
        printf("\t-d,--diff=REF_SO        run DiffTest with reference REF_SO\n");
        printf("\t-p,--port=PORT          run DiffTest with port PORT\n");
        printf("\n");
        exit(0);
    }
  }
  return 0;
}
 
//third: 
//nemu/src/cpu/cpu-exec.c
//在execut中执行trace_and_difftest(&s, cpu.pc);
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());
  }
}
 
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, checkWatchPoint());
}

trace_and_difftest函数中ITRACE_COND这个宏是通过我们使用gcc -D ITRACE_COND=true 传过来的,源代码中并未定义

-D 选项是 GCC 编译器的一个选项,用于定义预处理器宏。通过 -D 选项,我们可以在编译时为源代码中的宏指定一个值。

log_write("%s\n", _this->logbuf);这条语句的作用便是将我们s->logbuf中的内容写到我们通过-l传入的文件中了

if (g_print_step) { IFDEF(CONFIG_ITRACE, puts(_this->logbuf)); }这条语句是通过g_print_step判断是否要直接打印到终端中,一个例子是当我们再NEMU中使用命令si的时候可以看到我们将s->logbuf的内容打印到终端了

指令环形缓冲区 - iringbuf

task : 实现iringbuf

根据上述内容, 在NEMU中实现iringbuf. 你可以按照自己的喜好来设计输出的格式, 如果你想输出指令的反汇编, 可以参考itrace的相关代码; 如果你不知道应该在什么地方添加什么样的代码, 你就需要RTFSC了.

在哪添加代码?

想想当出现访问物理内存越界的时候是哪里在报错?

//nemu/src/memory/paddr.c
static void out_of_bound(paddr_t addr) {
  panic("address = " FMT_PADDR " is out of bound of pmem [" FMT_PADDR ", " FMT_PADDR "] at pc = " FMT_WORD,
      addr, PMEM_LEFT, PMEM_RIGHT, cpu.pc);
}
//nemu/include/debug.h
#define panic(format, ...) Assert(0, format, ## __VA_ARGS__)
 
/* 
 * 看来是通过Assert来实现的报错的,我们不妨看看Assert中的内容
 */
//nemu/include/debug.h
#define Assert(cond, format, ...) \
  do { \
    if (!(cond)) { \
      MUXDEF(CONFIG_TARGET_AM, printf(ANSI_FMT(format, ANSI_FG_RED) "\n", ## __VA_ARGS__), \
        (fflush(stdout), fprintf(stderr, ANSI_FMT(format, ANSI_FG_RED) "\n", ##  __VA_ARGS__))); \
      IFNDEF(CONFIG_TARGET_AM, extern FILE* log_fp; fflush(log_fp)); \
      extern void assert_fail_msg(); \
      assert_fail_msg(); \
      assert(cond); \
    } \
  } while (0)
 
// 出现了个assert_fail_msg()函数,有点眼熟
//nemu/src/cpu/cpu-exec.c
void assert_fail_msg() {
  isa_reg_display();
  statistic();
}

看来我们要在assert_fail_msg输出它

img

//nemu/src/cpu/cpu-exec.c
void assert_fail_msg() {
  isa_reg_display();
  IFDEF(CONFIG_IRINGTRACE, iringbuf_display());
  statistic();
}
 
//nemu/src/isa/riscv32/inst.c
int isa_exec_once(Decode *s) {
  s->isa.inst.val = inst_fetch(&s->snpc, 4);
  IFDEF(CONFIG_IRINGTRACE, iringbuf_get(*s));
  return decode_exec(s);
}
 
//nemu/src/utils/trace.c 我自己新建立的文件
#include <common.h>
#include <cpu/decode.h>
#define IRINGBUF_SIZE 16
 
static Decode iringbuf[IRINGBUF_SIZE];
/*The next instruction should be placed at the index in iringbuf*/
static int iringbuf_nextIdx = 0;
 
void iringbuf_get(Decode s){
  iringbuf[iringbuf_nextIdx++] = s;
  if (iringbuf_nextIdx >= IRINGBUF_SIZE)
    iringbuf_nextIdx = 0;
}
 
static void iringbuf_translate(Decode *s){
  char *p = s->logbuf;
  p += snprintf(p, sizeof(s->logbuf), FMT_WORD ":", s->pc);
  int ilen = s->snpc - s->pc;
  int i;
  uint8_t *inst = (uint8_t *)&s->isa.inst.val;
  for (i = ilen - 1; i >= 0; i --) {
    p += snprintf(p, 4, " %02x", inst[i]);
  }
  int ilen_max = MUXDEF(CONFIG_ISA_x86, 8, 4);
  int space_len = ilen_max - ilen;
  if (space_len < 0) space_len = 0;
  space_len = space_len * 3 + 1;
  memset(p, ' ', space_len);
  p += space_len;
 
#ifndef CONFIG_ISA_loongarch32r
  void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte);
  disassemble(p, s->logbuf + sizeof(s->logbuf) - p,
      MUXDEF(CONFIG_ISA_x86, s->snpc, s->pc), (uint8_t *)&s->isa.inst.val, ilen);
#else
  p[0] = '\0'; // the upstream llvm does not support loongarch32r
#endif
}
 
/*
 * 一般来说, 我们只会关心出错现场前的trace, 在运行一些大程序的时候, 运行前期的trace大多时候没有查看甚至输出的必要. 
 * 一个很自然的想法就是, 我们能不能在客户程序出错(例如访问物理内存越界)的时候输出最近执行的若干条指令呢?
 * 要实现这个功能其实并不困难, 我们只需要维护一个很简单的数据结构 - 环形缓冲区(ring buffer)即可
*/
void iringbuf_display(){
  int iringbuf_nowIdx = (iringbuf_nextIdx - 1) < 0 ? 31 : iringbuf_nextIdx - 1; 
  int i;
 
 
  for (i = 0; i < IRINGBUF_SIZE; i++){
    if (i == iringbuf_nowIdx)
      printf("%-4s","-->");
    else 
      printf("%-4s","   ");
    iringbuf_translate(&iringbuf[i]);
    printf("%s\n",iringbuf[i].logbuf);
  }
}

内存访问的踪迹 - mtrace

task : 实现mtrace

这个功能非常简单, 你已经想好如何实现了: 只需要在paddr_read()paddr_write()中进行记录即可. 你可以自行定义mtrace输出的格式.

不过和最后只输出一次的iringbuf不同, 程序一般会执行很多访存指令, 这意味着开启mtrace将会产生大量的输出, 因此最好可以在不需要的时候关闭mtrace. 噢, 那就参考一下itrace的相关实现吧: 尝试在Kconfig和相关文件中添加相应的代码, 使得我们可以通过menuconfig来打开或者关闭mtrace. 另外也可以实现mtrace输出的条件, 例如你可能只会关心某一段内存区间的访问, 有了相关的条件控制功能, mtrace使用起来就更加灵活了.

void mtraceRead_display(paddr_t addr, int len){
  printf("read address = " FMT_PADDR " at pc = " FMT_WORD " with byte = %d\n",
      addr, cpu.pc, len);
}
 
void mtraceWrite_display(paddr_t addr, int len, word_t data){
  printf("write address = " FMT_PADDR " at pc = " FMT_WORD " with byte = %d and data =" FMT_WORD "\n",
      addr, cpu.pc, len, data);
}

函数调用的踪迹 - ftrace

task : 实现ftrace

根据上述内容, 在NEMU中实现ftrace. 你可以自行决定输出的格式. 你需要注意以下内容:

你需要为NEMU传入一个ELF文件, 你可以通过在`parse_args()`中添加相关代码来实现这一功能
你可能需要在初始化`ftrace`时从ELF文件中读出符号表和字符串表, 供你后续使用
关于如何解析ELF文件, 可以参考`man 5 elf`
如果你选择的是riscv32, 你还需要考虑如何从`jal`和`jalr`指令中正确识别出函数调用指令和函数返回指令

注意, 你不应该通过readelf等工具直接解析ELF文件. 在真实的项目中, 这个方案确实可以解决问题; 但作为一道学习性质的题目, 其目标是让你了解ELF文件的组织结构, 使得将来你在必要的时候(例如在裸机环境中)可以自己从中解析出所需的信息. 如果你通过readelf等工具直接解析ELF文件, 相当于自动放弃训练的机会, 与我们设置这道题目的目的背道而驰.

posted @ 2025-12-12 19:56  mo686  阅读(2)  评论(0)    收藏  举报