2024 NJU PA2.2

在上一节中,我们实现了riscv-32指令集中的多条指令,并通过了am-kernels/tests/cpu-tests中的若干测试用例。目前,我们离一台实用的计算机还有些距离,包括但不限于:

  • 输入/输出
  • 虚拟内存管理
  • 上下文管理

本节的任务如下:

  • 通过cpu-tests中的测试用例stringhello-str.
  • 实现itrace, mtraceftrace.

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中的几个函数:除了strcmpmemcmp的返回值,没有需要特别注意的地方。

提示

如果出现以下报错:

查看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区分callret

函数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.

识别函数调用

通过以下特征识别伪指令callret

  • 函数调用(call)使用jaljalr指令,会向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全部结束。

posted @ 2025-03-10 10:27  overxus  阅读(723)  评论(0)    收藏  举报