[南大ICS-PA2] 函数调用的踪迹 – ftrace

[南大ICS-PA2] 函数调用的踪迹 - ftrace

  • 函数调用的踪迹 - ftrace
    • 消失的符号
    • 寻找"Hello World!"
    • 实现ftrace
    • 测试结果
    • 冗余的符号表
  • AM作为基础设施
  • 测试你的klib
  • Differential Testing
  • 一键回归测试

函数调用的踪迹 - ftrace

消失的符号

我们在am-kernels/tests/cpu-tests/tests/add.c中定义了宏NR_DATA, 同时也在add()函数中定义了局部变量c和形参a, b, 但你会发现在符号表中找不到和它们对应的表项, 为什么会这样? 思考一下, 什么才算是一个符号(symbol)?

答:宏在预处理阶段展开,本质上宏并不是一个变量,而只是展开文本而已。函数局部变量可以在进入函数在栈上分配。

寻找"Hello World!"

在Linux下编写一个Hello World程序, 编译后通过上述方法找到ELF文件的字符串表, 你发现"Hello World!"字符串在字符串表中的什么位置? 为什么会这样?

答:在ELF中的0x02000处,00002000 01 00 02 00 68 65 6c 6c 6f 20 77 6f 72 6c 64 21 |....hello world!|。在符号表中,0x02000位置处的符号为56: 0000000000002000 4 OBJECT GLOBAL DEFAULT 18 _IO_stdin_used,是一个标准io的位置。同时也有18: 0000000000002000 0 SECTION LOCAL DEFAULT 18。在节头部信息中[18] .rodata PROGBITS 0000000000002000 002000 000011 00 A 0 0 4

说明字符串在只读节中(rodata)。

实现ftrace

根据上述内容, 在NEMU中实现ftrace. 你可以自行决定输出的格式. 你需要注意以下内容:

  • 你需要为NEMU传入一个ELF文件, 你可以通过在parse_args()中添加相关代码来实现这一功能
  • 你可能需要在初始化ftrace时从ELF文件中读出符号表和字符串表, 供你后续使用
  • 关于如何解析ELF文件, 可以参考man 5 elf
  • 如果你选择的是riscv32, 你还需要考虑如何从jaljalr指令中正确识别出函数调用指令和函数返回指令
  • 参考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, 你还需要考虑如何从jaljalr指令中正确识别出函数调用指令和函数返回指令

    跳转并链接指令(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

测试结果

测试make ARCH=riscv32-nemu ALL=hello-str run

  • 解析ELF文件如下

    在这里插入图片描述

  • 指令访问如下

    在这里插入图片描述

  • 部分内存访问如下

    在这里插入图片描述

  • 函数调用栈如下

    在这里插入图片描述

冗余的符号表

gcc -o hello hello.c
strip -s hello

此时可以正常运行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位,之前的实现中,只是加上了高二十位,没有考虑到pc
  • ori:中文翻译的书上对这个指令弄错了,并没有rs2,且这个指令应该是I型指令