[南大ICS-PA2] 函数调用的踪迹 - ftrace
- 函数调用的踪迹 - ftrace
-
- 消失的符号
- 寻找"Hello World!"
- 实现ftrace
- 测试结果
- 冗余的符号表
- AM作为基础设施
- 测试你的klib
- Differential Testing
- 一键回归测试
函数调用的踪迹 - ftrace
消失的符号
我们在
答:宏在预处理阶段展开,本质上宏并不是一个变量,而只是展开文本而已。函数局部变量可以在进入函数在栈上分配。
寻找"Hello World!"
在Linux下编写一个Hello World程序, 编译后通过上述方法找到ELF文件的字符串表, 你发现"Hello World!"字符串在字符串表中的什么位置? 为什么会这样?
答:在ELF中的0x02000处,
说明字符串在只读节中(rodata)。
实现ftrace
根据上述内容, 在NEMU中实现ftrace. 你可以自行决定输出的格式. 你需要注意以下内容:
- 你需要为NEMU传入一个ELF文件, 你可以通过在
parse_args() 中添加相关代码来实现这一功能- 你可能需要在初始化ftrace时从ELF文件中读出符号表和字符串表, 供你后续使用
- 关于如何解析ELF文件, 可以参考
man 5 elf - 如果你选择的是riscv32, 你还需要考虑如何从
jal 和jalr 指令中正确识别出函数调用指令和函数返回指令
-
参考ICS PA2 实验记录
-
如
mtrace 一样,在KConfig 文件添加配置config FTRACE depends on TRACE && TARGET_NATIVE_ELF && ENGINE_INTERPRETER bool "Enable function call tracer" default y config FTRACE_COND depends on FTRACE string "Only trace function call when the condition is true" default "true"
-
ELF文件
直接读取传入的可执行文件,文件名即
argv[0] -
符号表和字符串表
由讲义可知,现在我们只需要关心
Type 属性为FUNC 的表项就可以了。我们读取ELF的符号表的信息,放入结构体typedef struct { char func_name[64]; // 函数名 paddr_t start; // 起始地址 size_t size; // 函数体大小 }FuncInfo; // [start, start+size) FuncInfo elf_func[1024];
比如:如果一条指令的地址在
[elf_fun[i].start, elf_fun[i].start+elf_fun[i].size) ,那么函数名称就是elf_fun[i].name -
解析ELF文件
[ELF文件解析] 读取ELF文件
-
修改
abstract-machine/scripts/platform/nemu.mk 文件,传入elf 文件NEMUFLAGS += -e $(IMAGE).elf
-
运行一个测试文件
make ARCH=riscv32-nemu ALL=dummy run ,可以看到符号表被解析出来Num: Value Size Name 0: 0000000080000018 32 _trm_init 1: 0000000080000010 8 main
运行
riscv64-linux-gnu-readelf -a build/dummy-riscv32-nemu.elf ,可以看出排除大小为0的函数后,运行正确。ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: RISC-V Entry point address: 0x80000000 Start of program headers: 52 (bytes into file) Start of section headers: 4748 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 80000000 001000 000038 00 AX 0 0 4 [ 2] .sdata2.mainargs PROGBITS 80000038 001038 000001 00 A 0 0 4 [ 3] .comment PROGBITS 00000000 001039 000029 01 MS 0 0 1 [ 4] .symtab SYMTAB 00000000 001064 000160 10 5 7 4 [ 5] .strtab STRTAB 00000000 0011c4 00008a 00 0 0 1 [ 6] .shstrtab STRTAB 00000000 00124e 00003b 00 0 0 1 Symbol table '.symtab' contains 22 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 80000000 0 SECTION LOCAL DEFAULT 1 2: 80000038 0 SECTION LOCAL DEFAULT 2 3: 00000000 0 SECTION LOCAL DEFAULT 3 4: 00000000 0 FILE LOCAL DEFAULT ABS dummy.c 5: 00000000 0 FILE LOCAL DEFAULT ABS trm.c 6: 80000038 1 OBJECT LOCAL DEFAULT 2 mainargs 7: 80000018 32 FUNC GLOBAL DEFAULT 1 _trm_init 8: 80009000 0 NOTYPE GLOBAL DEFAULT 2 _stack_pointer 9: 80000038 0 NOTYPE GLOBAL DEFAULT 1 _etext 10: 80000000 0 NOTYPE GLOBAL DEFAULT ABS _pmem_start 11: 80000039 0 NOTYPE GLOBAL DEFAULT 2 _bss_start 12: 80000039 0 NOTYPE GLOBAL DEFAULT 2 edata 13: 80009000 0 NOTYPE GLOBAL DEFAULT 2 _heap_start 14: 80001000 0 NOTYPE GLOBAL DEFAULT 2 _stack_top 15: 80009000 0 NOTYPE GLOBAL DEFAULT 2 end 16: 80000038 0 NOTYPE GLOBAL DEFAULT 1 etext 17: 80000000 0 FUNC GLOBAL DEFAULT 1 _start 18: 00000000 0 NOTYPE GLOBAL DEFAULT ABS _entry_offset 19: 80000010 8 FUNC GLOBAL DEFAULT 1 main 20: 80000039 0 NOTYPE GLOBAL DEFAULT 2 _data 21: 80009000 0 NOTYPE GLOBAL DEFAULT 2 _end
-
如果你选择的是riscv32, 你还需要考虑如何从
jal 和jalr 指令中正确识别出函数调用指令和函数返回指令跳转并链接指令(jal)具有双重功能。
jal rd, offset x[rd] = pc+4; pc += sext(offset) 跳转并链接 (Jump and Link). J-type, RV32I and RV64I. 把下一条指令的地址(pc+4),然后把 pc 设置为当前值加上符号位扩展的offset。rd 默认为 x1。offset[20|10:1|11|19:12] rd 1101111 - 若将下一条指令 PC + 4 的地址保存 到目标寄存器中,通常是返回地址寄存器 ra,便可以用它来实现过程调用。
- 如果使用零寄存器(x0)替换 ra 作为目标寄存器,则可以实现无条件跳转,因为 x0 不能 更改。
像分支一样,jal 将其 20 位分支地址乘以 2,进行符号扩展后再添加到 PC 上,便得 到了跳转地址。
跳转和链接指令的寄存器版本(jalr)同样是多用途的。
jalr rd, offset(rs1) t =pc+4; pc=(x[rs1]+sext(offset))&~1; x[rd]=t 跳转并寄存器链接 (Jump and Link Register). I-type, RV32I and RV64I. 把 pc 设置为 x[rs1] + sign-extend(offset),把计算出的地址的最低有效位设为 0,并将原 pc+4 的值写入 f[rd]。rd 默认为 x1。offset[11:0] rs1 010 rd 1100111 - 它可以调用地址是动态计算出来的函数,
- 或者也可以实现调用返回(只需 ra 作为源寄存器,零寄存器(x0)作为目的寄存器)。Switch 和 case 语句的地址跳转,也可以使用 jalr 指令,目的寄存器设为 x0。
函数调用
- jal: 目的寄存器为ra(x1)
- jalr: 目的寄存器为ra
函数返回:
- jalr: 源寄存器为ra,目的寄存器为x0
INSTPAT("??????? ????? ????? ??? ????? 11011 11", jal , J, IFDEF(CONFIG_FTRACE, func_trace(s, s->pc + imm)); R(dest) = s->snpc; s->dnpc = (s->pc + imm)); // 要更新动态pc INSTPAT("??????? ????? ????? 000 ????? 11001 11", jalr , I, IFDEF(CONFIG_FTRACE, func_trace(s, s->pc + imm)); R(dest) = s->snpc; s->dnpc = (src1 + imm) & ~0x01); // 也即ret #ifdef CONFIG_FTRACE static FuncInfo *find_func_by_pc(uint32_t pc) { for (int i = 0; i < ELF_FUNC_LEN; ++i) { if (pc >= elf_func[i].start && pc < elf_func[i].end) { return &elf_func[i]; } } return NULL; } static void func_trace(Decode *s, word_t dst_addr) { uint32_t i = s->isa.inst.val; int rd = BITS(i, 11, 7); int rs1 = BITS(i, 19, 15); if (rd == 1) { // 函数调用,以ra(x1)为目的寄存器 FuncInfo *cur_func = find_func_by_pc(s->pc); // 获得当前函数位置 FuncInfo *dst_func = find_func_by_pc(dst_addr); // 目的函数地址 if (cur_func == NULL || dst_func == NULL) { return; } StackEntry *cur = (StackEntry *) malloc (sizeof(StackEntry)); cur->cur_func = cur_func; cur->dst_func = dst_func; cur->type = FUNC_TRACE_CALL; cur->addr = s->pc; cur->next = NULL; func_call_stack_tail->next = cur; func_call_stack_tail = cur; } else if (rd == 0 && rs1 == 1) { // 目的寄存器为0,且元寄存器为ra,函数返回 FuncInfo *cur_func = find_func_by_pc(s->pc); // 获得当前函数位置 FuncInfo *dst_func = find_func_by_pc(R(rs1)); // 目的函数地址 if (cur_func == NULL || dst_func == NULL) { return; } StackEntry *cur = (StackEntry *) malloc (sizeof(StackEntry)); cur->cur_func = cur_func; cur->dst_func = dst_func; cur->type = FUNC_TRACE_RET; cur->addr = s->pc; cur->next = NULL; func_call_stack_tail->next = cur; func_call_stack_tail = cur; } return ; } #endif
-
在
monitor.c 文件函数void init_monitor(int argc, char *argv[]) 中,void init_monitor(int argc, char *argv[]) { /* Perform some global initialization. */ #ifdef CONFIG_FTRACE init_func_trace(); #endif /* Parse arguments. */ parse_args(argc, argv); /*********************/ }
-
在
isa.h 中#ifdef CONFIG_FTRACE #include <elf.h> #define ELF_FUNC_NAME_LEN 127 typedef struct { char func_name[ELF_FUNC_NAME_LEN + 1]; // 函数名 uint32_t start; // 起始地址 uint32_t end; // 结束地址 }FuncInfo; // [start, start+size) #define ELF_FUNC_LEN 1024 FuncInfo elf_func[ELF_FUNC_LEN]; typedef struct __StackEntry{ FuncInfo *cur_func; // 当前位置函数地址 FuncInfo *dst_func; // 目标位置函数地址 paddr_t addr; // 指令所在地址 int type; // call 或 return struct __StackEntry *next; }StackEntry; // 用于记录函数调用栈 StackEntry *func_call_stack_head, *func_call_stack_tail; #define FUNC_TRACE_CALL 0 #define FUNC_TRACE_RET 1 // init.c void init_func_trace(); #endif
-
在
init.c 中#ifdef CONFIG_FTRACE void init_func_trace() { for(int i = 0; i < ELF_FUNC_LEN; ++i) { elf_func[i].func_name[0] = ''; elf_func[i].func_name[ELF_FUNC_NAME_LEN] = ''; } func_call_stack_head = (StackEntry *) malloc (sizeof(StackEntry)); func_call_stack_head->next = NULL; func_call_stack_tail = func_call_stack_head; } #endif
测试结果
测试
-
解析ELF文件如下
-
指令访问如下
-
部分内存访问如下
-
函数调用栈如下
冗余的符号表
gcc -o hello hello.c strip -s hello
此时可以正常运行
gcc -c hello.c strip -s hello.o gcc -o hello hello.o
此时不能正常链接。
/usr/bin/ld: error in test.o(.eh_frame); no .eh_frame_hdr table will be created /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start': (.text+0x24): undefined reference to `main' collect2: error: ld returned 1 exit status
这是因为,在链接时,需要找到对应的符号的实际位置,填充代码中的位置。但是已经链接的文件,二进制代码中已经填好了每个符号的位置,不需要再要符号表了。
AM作为基础设施
全部通过
测试你的klib
Differential Testing
bool isa_difftest_checkregs(CPU_state *ref_r, vaddr_t pc) { return !memcmp(ref_r, &cpu, sizeof(CPU_state)); }
一键回归测试
使用Differential Testing,测试全部通过。
在测试过程中,修改了两个指令的实现
auipc : 这个是在pc上加上高20位,之前的实现中,只是加上了高二十位,没有考虑到pcori :中文翻译的书上对这个指令弄错了,并没有rs2,且这个指令应该是I型指令