Trace实现

itrace

关于反编译的部分, NEMU已经实现了. 我们需要做的只是使用一个合适的数据结构来保存这些信息. 文档中推荐使用环形缓冲区. 作者最初选择仅保存pc, 在需要打印信息时直接通过pc获取指令与反汇编的信息. 但考虑到可能存在自修改程序, 最终决定将指令信息也一并保存.

struct ItraceNode {
  paddr_t pc;
  uint32_t inst;
  struct ItraceNode *next;
};

struct ItraceNode iringbuf[CONFIG_ITRACE_BSIZE];
struct ItraceNode *iringbufNow;
// 初始化环形缓冲
void init_iringbuf(void) {
  iringbufNow = &iringbuf[CONFIG_ITRACE_BSIZE - 1];
  for (int i = 0; i < CONFIG_ITRACE_BSIZE; i++) {
    iringbuf[i].next = &iringbuf[(i + 1) % CONFIG_ITRACE_BSIZE];
    iringbuf[i].inst = 0;
    iringbuf[i].pc = 0;
  }
}

// 存入缓冲, 需要放到正确的位置
void push_inst(paddr_t pc, uint32_t inst) {
  iringbufNow->next->inst = inst;
  iringbufNow->next->pc = pc;
  iringbufNow = iringbufNow->next;
}

打印部分需要先反汇编后再输出. 为了美观, 代码写得稍微复杂了一些, 如果嫌麻烦可以直接顺序打印.

void display_iringbuf(void) {
  char printBuf[CONFIG_TRACE_PBSIZE];
  char* pPrintBuf = printBuf;
  struct ItraceNode *iringbufTemp = iringbufNow->next;
  for (int i = 0; i < CONFIG_ITRACE_BSIZE; i++) {
    if (iringbufTemp->pc == 0) {iringbufTemp = iringbufTemp->next; continue;}; // 跳过空白
    pPrintBuf += sprintf(pPrintBuf, "%sPC: " FMT_PADDR ": ", iringbufTemp == iringbufNow ? " ---> ": "      ", iringbufTemp->pc);
    for (int i = 3; i >= 0; i--)
    {
      pPrintBuf += sprintf(pPrintBuf,"%02x ",((uint8_t*)&iringbufTemp->inst)[i]);
    }
    disassemble(pPrintBuf, printBuf + sizeof(printBuf) - pPrintBuf, iringbufTemp->pc, (uint8_t*)&iringbufTemp->inst, 4);
    iringbufTemp = iringbufTemp->next;
    puts(printBuf);
    pPrintBuf = printBuf;
  }
}

打印效果如下, 控制台可以看到颜色, 网页上就没办法了:

(nemu) trace i
      PC: 0x80000000: 00 00 02 97 auipc t0, 0
      PC: 0x80000004: 00 02 88 23 sb    zero, 0x10(t0)
      PC: 0x80000008: 01 02 c5 03 lbu   a0, 0x10(t0)
 ---> PC: 0x8000000c: 00 10 00 73 ebreak

mtrace

mtraceitrace非常相似, 只是记录的内容不同. 下面直接贴出代码(非完整):

struct MtraceNode {
  bool type;
  uint8_t len;
  word_t data;
  vaddr_t addr;
  struct MtraceNode *next;
};

enum {
  T_READ,
  T_WRITE
};

struct MtraceNode mringbuf[CONFIG_MTRACE_BSIZE];
struct MtraceNode *mringbufNow;
void init_mringbuf(void) {
  mringbufNow = &mringbuf[CONFIG_MTRACE_BSIZE - 1];
  for (int i = 0; i < CONFIG_MTRACE_BSIZE; i++) {
    mringbuf[i].next = &mringbuf[(i + 1) % CONFIG_MTRACE_BSIZE];
    mringbuf[i].type = T_READ;
    mringbuf[i].len = 0;
    mringbuf[i].data = 0;
    mringbuf[i].addr = 0;
  }
}

void display_mringbuf(void) {
  char printBuf[CONFIG_TRACE_PBSIZE];
  char* pPrintBuf = printBuf;
  struct MtraceNode *mringbufTemp = mringbufNow->next;
  for (int i = 0; i < CONFIG_MTRACE_BSIZE; i++) {
    if (mringbufTemp->addr == 0) {mringbufTemp = mringbufTemp->next; continue;};
    pPrintBuf += sprintf(pPrintBuf, "%s %s at " FMT_WORD ", len: %d ", mringbufTemp == mringbufNow ? " ---> ": "      ", mringbufTemp->type == T_WRITE ? "Write": "Read ", mringbufTemp->addr, mringbufTemp->len);
    pPrintBuf += sprintf(pPrintBuf, "data: " FMT_WORD, mringbufTemp->data);
    mringbufTemp = mringbufTemp->next;
    puts(printBuf);
    pPrintBuf = printBuf;
  }
}

void push_write(vaddr_t addr, int len, word_t data) {
  mringbufNow->next->addr = addr;
  mringbufNow->next->len = len;
  mringbufNow->next->data = data;
  mringbufNow->next->type = T_WRITE;
  mringbufNow = mringbufNow->next;
}

void push_read(vaddr_t addr, int len, word_t data) {
  mringbufNow->next->addr = addr;
  mringbufNow->next->len = len;
  mringbufNow->next->data = data;
  mringbufNow->next->type = T_READ;
  mringbufNow = mringbufNow->next;
}

效果演示:

(nemu) trace m
       Read  at 0x80008fe0, len: 4 data: 0x80000264
       Read  at 0x80000268, len: 4 data: 0x0000f7ff
       Write at 0x800002be, len: 2 data: 0x0000f7ff
       Write at 0x80008fe0, len: 4 data: 0x80000268
       Read  at 0x80008fe0, len: 4 data: 0x80000268
       Read  at 0x8000026c, len: 4 data: 0x0000dfff
       Write at 0x800002c0, len: 2 data: 0x0000dfff
       Write at 0x80008fe0, len: 4 data: 0x8000026c
       Read  at 0x80008fe0, len: 4 data: 0x8000026c
       Read  at 0x80000270, len: 4 data: 0x00007fff
       Write at 0x800002c2, len: 2 data: 0x00007fff
       Write at 0x80008fe0, len: 4 data: 0x80000270
       Read  at 0x80008fe0, len: 4 data: 0x80000270
       Read  at 0x80008ff0, len: 4 data: 0x800001f8
       Read  at 0x80008fec, len: 4 data: 0x00000000
 --->  Read  at 0x80008fe8, len: 4 data: 0x00000000

ftrace

这一段需要学习如何解析ELF文件, 文档中描述得比较简略. 我们需要解析ELF中保存的函数信息, 也就是在符号表(.symtab)中的函数类型符号(STT_FUNC). NEMU如何读取ELF文件可以参考NEMU是如何获得bin文件路径的, 如何添加对应选项可以参考我之前写的实现批处理部分.

解析ELF文件

结构作用重要信息(对于此任务来说)
ELF头(ELF Header)保存整个ELF文件的基本信息节头表(Section Header String Table Index)信息
程序头表(Program Header Table)保存如何加载和执行文件的信息
节头表(Section Header Table)各个节(Section)的信息符号表(.symtab)信息
节(Section)各种类型的数据STT_FUNC类型符号信息
其他

思路

利用elf.h库完成:

  1. 读取ELF文件头: 使用fread函数读取ELF文件头(Elf_Ehdr), 并通过魔数0x7f 0x45 0x4c 0x46验证文件是否为有效的ELF文件.
  2. 遍历节头表: 使用fseekfread函数遍历ELF文件的节头表(Elf_Shdr), 找到符号表节(SHT_SYMTAB).
  3. 读取符号表: 计算符号表的条目数量, 遍历符号表中的每个条目, 找到类型为STT_FUNC的符号.
  4. 保存信息: 将遍历的信息(函数起始位置, 函数大小)存入某一结构中(自己定义).

代码就不贴了, 自己读一读elf.h的手册就行.

记录函数调用

存储信息就不多说了, 用自己喜欢的方式就行. 确定某一次函数调用/返回比较简单, 执行指令时可以得到跳转的目标地址, 遍历从ELF中得到的函数信息, 目标地址在什么函数的范围内就是调用/返回了什么函数.

不过事情没那么简单, 在riscv中, jal指令只会进行调用. 但是jalr不是, 根据情况可以是调用也可以是返回, 所以我们需要判断jalr指令的参数, 从而确定本次跳转究竟是什么类型.

根据手册中可以得知, riscv的通用寄存器中, x1ra)寄存器比较特殊, 通常用于保存返回地址. 返回地址在调用与返回中都很重要, 调用时需要保存返回地址, 返回时需要读取返回地址, 那么判断条件基本明确.

jalr类型条件
调用(Callrd == 1
返回(Retrd == 0 && rs1 == 1

理解了上面的内容, ftrace就简单了.

// src/isa/riscv32/inst.c
INSTPAT("??????? ????? ????? ??? ????? 11011 11", jal    , J, R(rd) = s->snpc; s->dnpc = imm + s->pc; 
  IFDEF(CONFIG_FTRACE, if (rd == 1) {
    trace_func_call(s->pc, s->dnpc);
}));
INSTPAT("??????? ????? ????? 000 ????? 11001 11", jalr   , I, R(rd) = s->snpc; s->dnpc = (src1 + imm) & (~1);
  IFDEF(CONFIG_FTRACE, if (rd == 1) {
    trace_func_call(s->pc, s->dnpc);
  } else if (rd == 0 && rs1 == 1){
    trace_func_ret(s->pc, s->dnpc);
}));
// src/utils/ftrace.c
void trace_func_call(paddr_t pcAddr, paddr_t dstAddr) {
  // 保存调用信息
}

void trace_func_ret(paddr_t pcAddr, paddr_t dstAddr) {
    // 保存返回信息
}

void display_call(void) {
    // 打印信息
}

difftest

NEMU中的真神, 极大减少debug时间, 本身算是个比较麻烦的功能, 但是框架已经实现好了, 就连需要手动实现的isa_difftest_checkregs也非常简单.

bool isa_difftest_checkregs(CPU_state *ref_r, vaddr_t pc) {
  if (ref_r->pc != cpu.pc) {
    Log("Different values of the PC! REF: " FMT_VADDR " DUT: " FMT_VADDR , ref_r->pc, cpu.pc);
    return false;
  }
  for (int i = 0; i < MUXDEF(CONFIG_RVE, 16, 32); i++) {
    if (ref_r->gpr[i] != gpr(i)) {
      Log("Different values of reg %s! REF: " FMT_WORD " DUT: " FMT_WORD, regs[i], ref_r->gpr[i], gpr(i));
      return false;
    }
  }
  return true;
}

difftest csr

这里就额外说下如何把csr寄存器也加入difftest. 不知道csr是干什么的可以先跳过

假设你已经为nemu添加好了csr, 那么只需要在tools/spike-diff/difftest.cc中添加读取与写入csr寄存器的功能.

找到对应函数并添加代码:

struct diff_context_t {
  word_t gpr[MUXDEF(CONFIG_RVE, 16, 32)];
  word_t pc;
  // 依你的情况添加csr
  vaddr_t mepc;
  word_t mcause;
  word_t mstatus;
  word_t mtvec;
};

void sim_t::diff_get_regs(void* diff_context) {
  struct diff_context_t* ctx = (struct diff_context_t*)diff_context;
  for (int i = 0; i < NR_GPR; i++) {
    ctx->gpr[i] = state->XPR[i];
  }
  ctx->pc = state->pc;
  // 添加
  ctx->mepc = (state->mepc)->read();
  ctx->mcause = (state->mcause)->read();
  ctx->mstatus = (state->mstatus)->read();
  ctx->mtvec = (state->mtvec)->read();
}

void sim_t::diff_set_regs(void* diff_context) {
  struct diff_context_t* ctx = (struct diff_context_t*)diff_context;
  for (int i = 0; i < NR_GPR; i++) {
    state->XPR.write(i, (sword_t)ctx->gpr[i]);
  }
  state->pc = ctx->pc;
  // 添加
  state->mepc->write(ctx->mepc);
  state->mcause->write(ctx->mcause);
  state->mstatus->write(ctx->mstatus);
  state->mtvec->write(ctx->mtvec);
}

最后记得在isa_difftest_checkregs也添加上对比代码, 然后就可以愉快的调试啦.