2024 NJU PA2.2
在上一节中,我们实现了riscv-32指令集中的多条指令,并通过了am-kernels/tests/cpu-tests中的若干测试用例。目前,我们离一台实用的计算机还有些距离,包括但不限于:
- 输入/输出
- 虚拟内存管理
- 上下文管理
本节的任务如下:
- 通过
cpu-tests中的测试用例string和hello-str. - 实现
itrace,mtrace和ftrace.
cpu-tests的尾巴
RISC-V指令测试
提示
“应测尽测”!
测试很简单,克隆项目riscv-tests-am,在此目录中运行make ARCH=riscv32-nemu run:

嗖嗖嗖,测试结束!可以看到,除了未实现的几条指令,其它指令都测试通过(div是已经实现但没有通过的,不用管它),这让我们更有信心迎接以后的挑战。
通过测试用例string
查看测试用例am-kernels/tests/cpu-tests/tests/string.c, 需要实现位于$AM_HOME/klib/src/string.c中的几个函数:除了strcmp和memcmp的返回值,没有需要特别注意的地方。
提示
如果出现以下报错:
查看
build/string-riscv32-nemu.txt, 发现调用函数putch时出错,实际上触发了panic("XXX"), 说明你有某个函数未实现。
通过测试用例hello-str
实现位于$AM_HOME/klib/src/stdio.c的函数sprintf,第一次实现参数可变的函数,可能会有些复杂。
trace工具
itrace: iringbuf
实现iringbuf非常简单:数组+模运算即可。以下是最简单的实现,只记录了最近访问指令的地址:
#ifdef CONFIG_ITRACE
#define IRINGBUF_SIZE 16
static vaddr_t iringbuf[IRINGBUF_SIZE];
static int iringbuf_pointer = -1;
void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte);
#define DIS_BUFSIZE 64
char dis_buf[DIS_BUFSIZE];
void print_itrace_iringbuf() {
for (int i = 0; i < IRINGBUF_SIZE; i++) {
if (i == iringbuf_pointer) {
printf("--->");
}
vaddr_t addr = iringbuf[i];
if (addr > 0) { // 针对iringbuf未装满的情形
int inst = inst_fetch(&addr, 4); // 取指令
printf("\t0x%08x", addr); // 输出地址
disassemble(dis_buf, DIS_BUFSIZE, addr, (uint8_t*)&inst, 4); // 输出指令的反汇编结果
printf("\t%s\n", dis_buf);
}
}
}
#endif
上面用到了函数disassemble,在$NEMU_HOME/src/cpu/cpu-exec.c中能找到一个如何使用的例子。
很多地方都可以记录指令,如exec_once, isa_exec_once等。我放在了函数isa_exec_once($NEMU_HOME/src/isa/riscv32/inst.c):
int isa_exec_once(Decode *s) {
#ifdef CONFIG_ITRACE
iringbuf_pointer = (iringbuf_pointer + 1) % IRINGBUF_SIZE;
iringbuf[iringbuf_pointer] = s->snpc; // 记录指令
#endif
s->isa.inst = inst_fetch(&s->snpc, 4);
return decode_exec(s);
}
需要特别注意记录的位置:因为调用inst_fetch后,s->snpc的值会发生变化。
最后是输出iringbuf的位置,我放在了$NEMU_HOME/src/memory/paddr.c:
void print_itrace_iringbuf(); // 记得声明
static void out_of_bound(paddr_t addr) {
#ifdef CONFIG_ITRACE
print_itrace_iringbuf(); // 地址访问越界后,输出iringbuf
#endif
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);
}
当内存访问越界后,会调用out_of_bound, 并输出iringbuf.
mtrace
mtrace的实现很简单,首先添加配置项MTRACE,具体操作在PA 1.3中提到过:
config MTRACE
bool "Enable memory tracer"
default y
配置完成后make menuconfig下,在位于$NEMU_HOME/src/memory/paddr.c的函数paddr_read, paddr_write中添加输出逻辑即可:
word_t paddr_read(paddr_t addr, int len) {
#ifdef CONFIG_MTRACE
printf("mtrace: read memory from 0x%08x, %d bytes\n", addr, len);
#endif
if (likely(in_pmem(addr))) return pmem_read(addr, len);
......
}
void paddr_write(paddr_t addr, int len, word_t data) {
#ifdef CONFIG_MTRACE
printf("mtrace: write memory from 0x%08x, %d bytes\n", addr, len);
#endif
if (likely(in_pmem(addr))) { pmem_write(addr, len, data); return; }
......
}
OK,现在关闭mtrace, 实现ftrace!
ftrace
和之前的mtrace一样,添加一个CONFIG_FTRACE宏,但这一步不是必需的。
解析elf文件
可以先写好解析elf的逻辑,测试无误后再粘贴到项目中。我设计了以下两个接口:
void read_elf(char* elf_path):解析elf文件,并提取出函数项;void print_ftrace(uint32_t inst_addr, uint32_t func_addr, int is_enter):输出ftrace的内容。其中,inst_addr是指令地址;func_addr是用于查找函数名的地址;is_enter区分call和ret.
函数read_elf从elf文件中提取函数的起始地址、长度和函数名,为此,需要读取符号表和字符串表。这一步并不难,不需要完全掌握elf格式,按需了解即可,但还是比较繁琐。(解析elf文件时可能用到的工具都在头文件elf.h里)
具体实现因人而异,因此不再赘述。以下代码仅供参考:
查看代码
#ifdef CONFIG_FTRACE
#include <elf.h>
typedef struct ElfFunc {
uint32_t addr; // 函数起始地址
uint32_t size; // 函数体的大小
char* name; // 函数名
} ElfFunc;
static ElfFunc* elfuncs = NULL; // 函数项数组
static int elfunc_num = 0; // 函数项的个数
static char* elfunc_strtab = NULL; // string table
void read_elf(const char* elf_path) {
// 读取elf文件
FILE* fp = fopen(elf_path, "rb");
fseek(fp, 0, SEEK_END);
int elf_size = ftell(fp);
fseek(fp, 0, SEEK_SET);
unsigned char* elf = (unsigned char*)malloc(elf_size);
if(fread(elf, 1, elf_size, fp) != elf_size) {
panic("failed to read elf file %s", elf_path);
}
fclose(fp);
// 解析elf文件
elfunc_num = 0;
elfuncs = NULL;
elfunc_strtab = NULL;
Elf32_Ehdr* elf_header = (Elf32_Ehdr*)elf;
Elf32_Shdr* elf_shdrs = (Elf32_Shdr*)(elf + elf_header->e_shoff);
// 读取string table
for (int si = 0; si < elf_header->e_shnum; si++) {
Elf32_Shdr shdr = elf_shdrs[si];
if (shdr.sh_type == SHT_STRTAB && si != elf_header->e_shstrndx) {
elfunc_strtab = (char*)malloc(shdr.sh_size);
memcpy(elfunc_strtab, elf + shdr.sh_offset, shdr.sh_size);
break;
}
}
// 解析symbol table
for (int si = 0; si < elf_header->e_shnum; si++) {
Elf32_Shdr shdr = elf_shdrs[si];
if (shdr.sh_type == SHT_SYMTAB) {
Elf32_Sym* symtab = (Elf32_Sym*)(elf + shdr.sh_offset);
// 获取symbol table中函数项的个数
for (int i = 0; i < shdr.sh_size / shdr.sh_entsize; i++) {
Elf32_Sym sym = symtab[i];
if (ELF32_ST_TYPE(sym.st_info) == STT_FUNC) {
elfunc_num++;
}
}
elfuncs = (ElfFunc*)malloc(sizeof(ElfFunc) * elfunc_num);
ElfFunc* item = elfuncs;
for (int i = 0; i < shdr.sh_size / shdr.sh_entsize; i++) {
Elf32_Sym sym = symtab[i];
if (ELF32_ST_TYPE(sym.st_info) == STT_FUNC) {
item->addr = sym.st_value;
item->size = sym.st_size;
item->name = elfunc_strtab + sym.st_name;
item += 1;
}
}
}
}
free(elf);
}
static int print_ftrace_level = 0;
void print_ftrace(uint32_t inst_addr, uint32_t func_addr, int is_enter) {
printf("0x%08x: ", inst_addr);
if (is_enter != 1) {
print_ftrace_level--;
}
for (int i = 0; i < print_ftrace_level; i++) {
printf(" ");
}
if (is_enter == 1) {
print_ftrace_level++;
printf("call ");
}
else {
printf("ret ");
}
for (int i = 0; i < elfunc_num; i++) {
if (func_addr >= elfuncs[i].addr && func_addr < elfuncs[i].addr + elfuncs[i].size) {
printf("%s\n", elfuncs[i].name);
return;
}
}
printf("???\n");
}
#endif
传入elf文件
在上次添加批处理(-b)的位置(abstract-machine/scripts/platform/nemu.mk)添加elf选项(假设通过-e指定,例如-e demo.elf,换成其它字母也可以):
NEMUFLAGS += -e $(IMAGE).elf
接下来,在函数parse_args($NEMU_HOME/src/monitor/monitor.c)中添加处理elf参数项的逻辑:
static char *elf_file = NULL; // elf文件路径
static int parse_args(int argc, char *argv[]) {
const struct option table[] = {
......
{"elf" , required_argument, NULL, 'e'}, // 添加解析elf文件的项
......
while ( (o = getopt_long(argc, argv, "-bhl:d:p:e:", table, NULL)) != -1) { // 注意这里添加了 e: ,冒号表示-e选项需要参数
switch (o) {
......
case 'e': elf_file = optarg; break; // 获取-e参数
......
}
将上一步实现的elf解析代码放在此文件中,并在函数init_monitor中调用函数read_elf.
识别函数调用
通过以下特征识别伪指令call和ret:
- 函数调用(
call)使用jal或jalr指令,会向1号寄存器$ra中写入返回地址,即rd==1, - 函数返回(
ret)使用jalr指令,且rd==0, rs1==1, imm==0.
根据上述特征,在$NEMU_HOME/src/isa/riscv32/inst.c中添加识别逻辑:
void print_ftrace(uint32_t inst_addr, uint32_t func_addr, int is_enter);
static void ftrace_jal(uint32_t inst_addr, uint32_t func_addr, int rd) {
#ifdef CONFIG_FTRACE
if (rd == 1) {
print_ftrace(inst_addr, func_addr, 1);
}
#endif
}
static void ftrace_jalr(uint32_t inst_addr, uint32_t func_addr, int rd, int rs1, int imm) {
#ifdef CONFIG_FTRACE
if (rd == 0 && rs1 == 1 && imm == 0) {
print_ftrace(inst_addr, inst_addr, 0);
}
else if (rd == 1) {
print_ftrace(inst_addr, func_addr, 1);
}
#endif
}
以及:
INSTPAT("??????? ????? ????? ??? ????? 11011 11", jal , J, R(rd) = s->pc + 4, s->dnpc = s->pc + imm, ftrace_jal(s->pc, s->dnpc, rd));
INSTPAT("??????? ????? ????? 000 ????? 11001 11", jalr , I, R(rd) = s->pc + 4, s->dnpc = ((src1 + imm) >> 1) << 1, ftrace_jalr(s->pc, s->dnpc, rd, BITS(s->isa.inst, 19, 15), imm));
文档中提到,这种实现有点小问题,暂时不必管它。最后的效果如下:

至此,PA 2.2全部结束。


浙公网安备 33010602011771号