RV BACKTRACE简介本文主要讲述RV BACKTRACE 的内部主要原理没有接触过rvbacktrace可以看下面两篇文章理解一下如何使用RVBACKTRACERVBacktrace RISC-V极简栈回溯组件https://club.rt-thread.org/ask/article/64bfe06feb7b3e29.htmlRVBacktrace RISC-V极简栈回溯组件V1.2https://club.rt-thread.org/ask/article/09737357e4a95b06.htmlRVBACKTRACEhttps://github.com/Yaochenger/RvBacktracervbacktrace更多的利用RISCV的一些特性组件支持两种方式栈回溯默认使用配置简单的方式一。方式一不添加编译参数通过调用栈结构进行栈回溯默认是方式一。优点不额外占用系统寄存器 缺点增加代码空间效率较方式二较低方式二通过添加编译参数的方式基于FP寄存器进行栈回溯。优点几乎不增加代码空间 缺点占用s0寄存器rvbacktrace有两种方法一种是编译器编译的时候添加-fno-omit-frame-pointer, 另一个是编译器在默认情况下会优化frame-pointer。我们来理解一下什么是-fno-omit-frame-pointer-fno-omit-frame-pointer: 这里有个no-omit 大概意思是不要忽略或者不要优化frame-pointer是一个寄存器代表是帧指针寄存器。默认情况下是-fomit-frame-pointer: 这里是忽略帧指针。先讲下两个配置类似于一个是优化掉帧指针(代码中不保存帧指针)一个是-fno-omit-frame-pointer不优化帧指针。我们来看下优化和不优化的区别这个是加了-fno-omit-frame-pointer的函数void rvbacktrace_fno() { 8000e02c: 1141 addi sp,sp,-16 8000e02e: c606 sw ra,12(sp) 8000e030: c422 sw s0,8(sp) 8000e032: c226 sw s1,4(sp) 8000e034: 0800 addi s0,sp,16这个是没有加-fno-omit-frame-pointer的函数void rvbacktrace_fno() { 8000d7c6: 1141 addi sp,sp,-16 8000d7c8: c606 sw ra,12(sp) 8000d7ca: c422 sw s0,8(sp)差异是什么呢多了两条指令8000e032: c226 sw s1,4(sp) 8000e034: 0800 addi s0,sp,16来讲下大概原理加了-fno-omit-frame-pointer之后s0寄存器就不能用来通用功能了只能用来作为fp使用用来保存sp的初始值(即进函数之前的值)帧指针就是用来保存上一次的SP的地址然后该地址可以用来读到上一次函数的地址。ra地址就是类似于ARM中的LR的地址就是返回的函数的地址。从下面这张图中可以看到加了-fno-omit-frame-pointer之后默认当前代码的S0就是进该函数刚进来的时候SP值然后固定下面两个寄存器保存的是RA和FP这个位置没有变所以对于rv_backtrace_fno.c中的核心函数就可以按照下面的写法写。sp (unsigned long) _backtrace_threadn-sp; fp ((rt_ubase_t *) (_backtrace_threadn-sp))[BACKTRACE_FP_POS]; // get current frame pointer while (1) { frame (struct stackframe *) (fp - BACKTRACE_LEN); // get frame pointer if ((uint32_t *) frame (uint32_t *) (uintptr_t) _rt_eusrstack) { rvstack_frame_len num; rvbacktrace_addr2line((uint32_t *) rvstack_frame[0]); num 0; break; } sp fp; // get stack pointer fp frame-s_fp; // get frame pointer ra frame-s_ra; // get return address pc frame-s_ra - 4; // get program counter // print stack interval, return address, program counter BACKTRACE_PRINTF([%d]Stack interval :[0x%016lx - 0x%016lx] ra 0x%016lx pc 0x%016lx\n, num, sp, fp, ra, pc); rvstack_frame[num] pc; // save stack frame address num; }RVBACKTRACE的通用改法在编译器还没有加-fno-omit-frame-pointer代码中需要从PC往上找这里第一步就是找到1141这条指令然后计算出立即数16根据当前的SP值然后计算出进函数之前的SP值。void rvbacktrace_fno() { 8000d7c6: 1141 addi sp,sp,-16 8000d7c8: c606 sw ra,12(sp) 8000d7ca: c422 sw s0,8(sp)在函数riscv_backtraceFromStack中就是计算SP值计算也是为了适配64bit和32bit的RISCV计算出SP的原始值然后再往上继续找对应的函数保存下来。/* 1. scan code, find lr pushed */ for (i 0; i BT_FUNC_LIMIT;) { /* FIXME: not accurate from bottom to up. how to judge 2 or 4byte inst */ //CodeAddr (char *)(((long)PC (~0x3)) - i); //非对齐访问 CodeAddr (char *)(PC - i); ins32 *(unsigned int *)(CodeAddr); if ((ins32 0x3) 0x3) { ins16 *(unsigned short *)(CodeAddr - 2); if ((ins16 0x3) ! 0x3) { i 4; framesize riscv_backtrace_framesize_get1(ins32); if (framesize 0) { CodeAddr 4; break; } continue; } } i 2; ins16 (ins32 16) 0xffff; framesize riscv_backtrace_framesize_get(ins16); if (framesize 0) { CodeAddr 2; break; } } if (i BT_FUNC_LIMIT) { /* error branch */ #ifdef BACKTRACE_PRINTF BACKTRACE_PRINTF(Backtrace fail!\r\n); #endif return -1; } /* 2. scan code, find ins: sd ra,24(sp) or sd ra,552(sp) */ for (i 0; CodeAddr i PC;) { ins32 *(unsigned int *)(CodeAddr i); if ((ins32 0x3) 0x3) { i 4; offset riscv_backtrace_ra_offset_get1(ins32); if (offset 0) { break; } } else { i 2; ins16 ins32 0xffff; offset riscv_backtrace_ra_offset_get(ins16); if (offset 0) { break; } } }这段代码根据当前的PC值知道当前的函数的进来的时候SP开启的地址。计算每次SP开始的地址和结束地址然后找到上一次的PC值然后再往上一个函数查找。RVBACKTRACE的加-fno-omit-frame-pointer的用法加了编译参数-fno-omit-frame-pointer之后进函数之前就会记录当前的SP的值经过验证大部分的RISCV64平台的这个选项是默认打开的所以RISCV64可以用这个方法估计RISCV64平台一般都比较大。这个时候我们就要用rv_backtrace_fno.c这个文件来处理栈回溯了。在RVBACKTRACE中就要开启BACKTRACE_USE_FP这个宏void rvbacktrace_fno() { 8000e02c: 1141 addi sp,sp,-16 8000e02e: c606 sw ra,12(sp) 8000e030: c422 sw s0,8(sp) 8000e032: c226 sw s1,4(sp) 8000e034: 0800 addi s0,sp,16S0的值就是保存的栈信息。默认这个寄存器就给栈回溯用。主要实现函数如下获取fp的值为__builtin_frame_address这是一个libc的库函数fp (unsigned long)__builtin_frame_address(0); // get current frame pointer while (1) { frame (struct stackframe *)(fp - BACKTRACE_LEN); // get frame pointer if ((uint32_t *)frame (uint32_t *)(uintptr_t)_rt_eusrstack) { rvstack_frame_len num; return; } sp fp; // get stack pointer fp frame-s_fp; // get frame pointer ra frame-s_ra; // get return address pc frame-s_ra - 4; // get program counter // print stack interval, return address, program counter BACKTRACE_PRINTF([%d]Stack interval :[0x%016lx - 0x%016lx] ra 0x%016lx pc 0x%016lx\n, num, sp, fp, ra, pc); rvstack_frame[num] pc; // save stack frame address num; }看下反汇编36 fp (unsigned long)__builtin_frame_address(0); // get current frame pointer 8000b09c: mv s2,s0其实就是读取s0的值。因为有s0保存了进入函数的时候的栈地址就很容易找到SP进来的时候初始地址然后也比较方便找到上一次的RA和S0。代码实现起来就比较方便不需要一直解析反汇编。其他的线程回溯的方法也是类似的。汇编指令过滤方法知道了上面的两种方法之后比较难的其实是第一种没有编译选项的时候如何根据PC指针找到栈的地址。void rvbacktrace_fno() { 8000d7c6: 1141 addi sp,sp,-16 8000d7c8: c606 sw ra,12(sp) 8000d7ca: c422 sw s0,8(sp例如上面的函数我们就要知道两个命令addi sp,sp,-16和sw ra,12(sp)我们翻阅RISCV手册之后看到如下的命令当然这个是压缩命令主要的是计算出它的立即数imm参考链接如下:https://riscv.github.io/riscv-isa-manual/snapshot/unprivileged/#_integer_register_immediate_instructionsstatic int riscv_backtrace_framesize_get1(unsigned int inst) { unsigned int imm 0; /* addi sp, sp, -im * example * d1010113 addi sp,sp,-752 * from spec addi FROM https://riscv.github.io/riscv-isa-manual/snapshot/unprivileged/#_integer_register_immediate_instructions * bit[31:20] imm[11:0] * bit[19:15] 00010 * bit[14:12] 000 * bit[11:7] 00010 * bit[6:0] 0010011 */ if ((inst 0x800FFFFF) 0x80010113) { imm (inst 20) 0x7FF; imm (~imm 0x7FF) 1; #if __riscv_xlen 64 return imm 3; // RV64: 以 8 字节为单位 #else return imm 2; // RV32: 以 4 字节为单位 #endif } return -1; }CM BACKTRACEhttps://github.com/armink-rtt-pkgs/CmBacktraceCM backtrace 核心代码/* first depth is PC */ buffer[depth] regs.saved.pc; /* fix the LR address in thumb mode */ pc regs.saved.lr - 1; if ((pc code_start_addr) (pc code_start_addr code_size) (depth CMB_CALL_STACK_MAX_DEPTH) (depth size)) { buffer[depth] pc; regs_saved_lr_is_valid true; } size_t cm_backtrace_call_stack_any(uint32_t *buffer, size_t size, uint32_t sp, uint32_t stack_start_addr, uint32_t stack_size) { uint32_t pc; size_t depth 0; /* copy called function address */ for (; sp stack_start_addr stack_size; sp sizeof(size_t)) { /* the *sp value may be LR, so need decrease a word to PC */ pc *((uint32_t *) sp) - sizeof(size_t); /* the Cortex-M using thumb instruction, so the pc must be an odd number */ if (pc % 2 0) { continue; } /* fix the PC address in thumb mode */ pc *((uint32_t *) sp) - 1; if ((pc code_start_addr sizeof(size_t)) (pc code_start_addr code_size) (depth CMB_CALL_STACK_MAX_DEPTH) /* check the the instruction before PC address is BL or BLX */ disassembly_ins_is_bl_blx(pc - sizeof(size_t)) (depth size)) { /* the second depth function may be already saved, so need ignore repeat */ buffer[depth] pc; } } return depth; } static bool disassembly_ins_is_bl_blx(uint32_t addr) { uint16_t ins1 *((uint16_t *)addr); uint16_t ins2 *((uint16_t *)(addr 2)); #define BL_INS_MASK 0xF800 #define BL_INS_HIGH 0xF800 #define BL_INS_LOW 0xF000 #define BLX_INX_MASK 0xFF00 #define BLX_INX 0x4700 if ((ins2 BL_INS_MASK) BL_INS_HIGH (ins1 BL_INS_MASK) BL_INS_LOW) { return true; } else if ((ins2 BLX_INX_MASK) BLX_INX) { return true; } else { return false; } }用一个数组buffer[]用来存放对应的PC和回溯的地址然后从LR往上进行检查PC的值-1之后判断PC是否在代码开始段和结束段如果在则放到回溯的地址里面。总结RVBACKTRACE很好的帮助大家进行栈回溯大家如果试用觉得有用的话欢迎帮忙仓库点个star。如果有建议也可以提issue和PR。https://github.com/Yaochenger/RvBacktrace