PA2.2-基础设施(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
根据上述内容, 在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
这个功能非常简单, 你已经想好如何实现了: 只需要在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
根据上述内容, 在NEMU中实现ftrace. 你可以自行决定输出的格式. 你需要注意以下内容:
你需要为NEMU传入一个ELF文件, 你可以通过在`parse_args()`中添加相关代码来实现这一功能
你可能需要在初始化`ftrace`时从ELF文件中读出符号表和字符串表, 供你后续使用
关于如何解析ELF文件, 可以参考`man 5 elf`
如果你选择的是riscv32, 你还需要考虑如何从`jal`和`jalr`指令中正确识别出函数调用指令和函数返回指令
注意, 你不应该通过readelf等工具直接解析ELF文件. 在真实的项目中, 这个方案确实可以解决问题; 但作为一道学习性质的题目, 其目标是让你了解ELF文件的组织结构, 使得将来你在必要的时候(例如在裸机环境中)可以自己从中解析出所需的信息. 如果你通过readelf等工具直接解析ELF文件, 相当于自动放弃训练的机会, 与我们设置这道题目的目的背道而驰.

浙公网安备 33010602011771号