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
mtrace
与itrace
非常相似, 只是记录的内容不同. 下面直接贴出代码(非完整):
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
库完成:
- 读取ELF文件头: 使用
fread
函数读取ELF文件头(Elf_Ehdr
), 并通过魔数0x7f 0x45 0x4c 0x46
验证文件是否为有效的ELF文件. - 遍历节头表: 使用
fseek
和fread
函数遍历ELF文件的节头表(Elf_Shdr
), 找到符号表节(SHT_SYMTAB
). - 读取符号表: 计算符号表的条目数量, 遍历符号表中的每个条目, 找到类型为
STT_FUNC
的符号. - 保存信息: 将遍历的信息(函数起始位置, 函数大小)存入某一结构中(自己定义).
代码就不贴了, 自己读一读elf.h
的手册就行.
记录函数调用
存储信息就不多说了, 用自己喜欢的方式就行. 确定某一次函数调用/返回比较简单, 执行指令时可以得到跳转的目标地址, 遍历从ELF中得到的函数信息, 目标地址在什么函数的范围内就是调用/返回了什么函数.
不过事情没那么简单, 在riscv
中, jal
指令只会进行调用. 但是jalr
不是, 根据情况可以是调用也可以是返回, 所以我们需要判断jalr
指令的参数, 从而确定本次跳转究竟是什么类型.
根据手册中可以得知, riscv的通用寄存器中, x1
(ra
)寄存器比较特殊, 通常用于保存返回地址. 返回地址在调用与返回中都很重要, 调用时需要保存返回地址, 返回时需要读取返回地址, 那么判断条件基本明确.
jalr 类型 | 条件 |
---|---|
调用(Call ) | rd == 1 |
返回(Ret ) | rd == 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
也添加上对比代码, 然后就可以愉快的调试啦.