深入浅出地讲解rvbacktrace原理
- 电脑硬件
- 2025-09-04 13:33:02

前情提要:栈
要理解栈回溯,就要首先理解栈是如何运行的
下面看一段非常简单的代码
uint32_t funB(uint32_t b) { return b-1; } void funA(uint32_t a) { uint32_t b = funB(a); } int main(void) { board_init(); funA(3); while (1) { } return 0; }这段代码的逻辑非常简单,调用链也很清晰,在main函数中调用funA,funA中调用funB,main函数的汇编代码如下所示
int main(void) { 8000648c: 1141 add sp,sp,-16 8000648e: c606 sw ra,12(sp) 80006490: c422 sw s0,8(sp) 80006492: 0800 add s0,sp,16 board_init(); 80006494: b16fe0ef jal 800047aa <board_init> funA(3); 80006498: 450d li a0,3 8000649a: b7dfd0ef jal 80004016 <funA>可以看到,当main函数执行时,首先向上开辟了一块栈空间,然后将ra,s0压栈,随后才是用户所写的代码逻辑,对这里用到的两个寄存器进行说明:
-ra:保存了返回地址,当执行jal时,会将下一条指令的地址保存至ra
-s0:又叫fp,frame point,栈帧指针,是回溯需要用到的重要寄存器
这里需要注意一个细节,在调用main函数之前,把fp压栈了,随后将fp指向了当前函数栈底;这样意味着,当运行某个函数时,risc-v的fp寄存器,始终指向该函数的栈底,而该函数的栈中,保留了运行上一个函数时的fp寄存器的值
这么说或许有些抽象,用一个图进行演示
这么一看就清晰多了,也就是在FunB中,可以看到自己的caller FunA的栈底是0x2200110,而caller的栈顶,也就是callee的栈底,也就是0x2200120;
这么一来,对于被调用者来说,可以知道调用者的栈地址,这也是通过FP进行栈回溯的基础
以上基础是理解栈回溯的基础,务必搞清楚后才能理解栈回溯的本质
栈回溯实现 思路有了以上的铺垫之后,我们可以想想如果是要自己写一个栈回溯的函数traceback,该怎么做
首先traceback一定是在某处被调用,我们假设FunB调用了traceback在funB运行时,我们可以从寄存器SP得到FunB的栈顶,从寄存器FP得到栈底我们可以把栈底和栈顶传递给traceback,这样trackback就能得到栈中的所有内容栈内有什么呢,有FunB的返回地址ra,还有之前保存的fp(这里要注意,栈中的fp和当前寄存器FP内的值是两回事,栈中的fp是调用FunB的函数FunA的栈底)接着,我们可以根据栈中的fp跳转至FunA的栈,里面又可以得到FunA的返回地址,以及FunA栈中的fp,也就是main函数的栈底这样一层一层套娃,也就完成了函数的调用链回溯最后需要注意退出条件,fp指向整个栈底的时候意味着调用链结束,应当退出 演示代码 void backtrace() { uintptr_t *fp; // 内联汇编获取当前FP(s0寄存器) asm volatile ("mv %0, s0" : "=r"(fp)); printf("Backtrace:\n"); //如果FP>=栈底,则意味着函数调用链已经结束 while (uintptr_t)FP < (uintptr_t)estack) { // 获取返回地址(RA = fp - 8) uintptr_t ra = *(fp - 1); if (ra == 0) break; // 获取前一个FP(prev_fp = fp - 16) uintptr_t *prev_fp = (uintptr_t *)*(fp - 2); // 打印RA(实际应用中可解析为函数名) printf("#%d: [FP=0x%lx] RA = 0x%lx\n", i, (uintptr_t)fp, ra); // 检查prev_fp有效性(如对齐、地址范围) if ((uintptr_t)prev_fp <= (uintptr_t)fp || prev_fp == NULL) break; fp = prev_fp; } } 细节剖析 uintptr_t ra = *(fp - 1); // 获取前一个FP(prev_fp = fp - 16) uintptr_t *prev_fp = (uintptr_t *)*(fp - 2); // 检查prev_fp有效性(如对齐、地址范围) if ((uintptr_t)prev_fp <= (uintptr_t)fp || prev_fp == NULL) break; fp = prev_fp;1.为什么ra 和prev_fp是 *(fp -1) 和*(fp -2)
对于普通的函数调用(这点很重要,因为ISR,Excpetion可能是另外的情况,下文会展开)来说,程序运行时首先开辟一块栈,将sp指向栈顶,然后将返回地址ra入栈,fp入栈;最后将fp指向栈底,栈底的第一个元素就是ra,第二个元素则是fp,至于为什么不是*fp和*(fp -1),那是因为这是一个自减栈,高地址是栈底,低地址则是栈顶。
2.检查prev_fp有效性
代码的演示中只是简单判断了prev_fp是否非NULL,实际使用中还可以根据栈的地址对齐进行额外校验,例如你的栈如果是16字节对齐,则可以判断prev_fp是否整除16
.stack (NOLOAD) : { . = ALIGN(16); __stack_base__ = .; . += STACK_SIZE; . = ALIGN(16); PROVIDE (_stack = .); PROVIDE (_stack_safe = .); } > DLM Trap的特殊处理 概述RISC-V的trap发生时,会进入trap_handler,然后根据发生trap的原因(中断还是异常),进入不同的处理函数,笔者所使用的RISC-V芯片开启了中断向量,因此中断并不会进入trap_handler,而会直接进入中断处理函数,因此对两种情况分开阐述。
中断当开启中断向量且中断发生后,芯片根据中断id号跳转至中断处理函数。由于中断的退出是通过mret跳转至mepc所指向的地址继续运行,因此在进入中断时并不会保存ra的地址。我们看一段中断的汇编代码如下
可以看到,由于中断是随时发生的,系统并不会保存ra,只保存了s0(也就是fp),对ra的压栈发生在中断对上下文进行保存的过程中,是我们手动实现的,而绿色方框外的代码则是自动生成的。比起常规的函数调用,自动生成的代码中少了对ra的压栈,因此prev_fp是 *(fp -1) 和而非*(fp -2)
异常和中断类似,异常发生时会进入trap_handler,汇编代码如下
同样的,s0和常规函数调用的压栈顺序发生的变化,prev_fp是 *(fp -5) 和而非*(fp -2)
演示代码修改在上文我们提供了backtrac的演示代码,他能很好地处理函数调用的栈回溯,但知道了trap需要的特殊处理后,我们需要对该代码进行修改,根据不同情况对prev_fp取值进行修改,代码如下所示:
void backtrace() { uintptr_t *fp; // 内联汇编获取当前FP(s0寄存器) asm volatile ("mv %0, s0" : "=r"(fp)); printf("Backtrace:\n"); //如果FP>=栈底,则意味着函数调用链已经结束 while (uintptr_t)FP < (uintptr_t)estack) { uint32_index = 0; long *rs0; uint32_t pos[3] = {1,2,5}; //根据不同情况找到prev_fp所在的位置 for (uint32_t i = 0; i < sizeof(pos) / sizeof(pos[0]); i++) { rs0 = (long *) *(b - pos[i]); if (rs0 > (long *) *estack) continue; if (rs0 <= b) continue; if (((char *)rs0 - (char *)b) % 16 == 0) { index = pos[i]; } } //isr并没有对ra自动压栈,在保存上下文时保存了ra if (index == 1) { ra = (unsigned long) *SP; } else { ra = (unsigned long) *(FP - 1); } if (ra == 0) break; // 获取前一个FP(prev_fp = fp - index) uintptr_t *prev_fp = (uintptr_t *)*(fp - index); // 打印RA(实际应用中可解析为函数名) printf("#%d: [FP=0x%lx] RA = 0x%lx\n", i, (uintptr_t)fp, ra); // 检查prev_fp有效性(如对齐、地址范围) if ((uintptr_t)prev_fp <= (uintptr_t)fp || prev_fp == NULL) break; fp = prev_fp; } }深入浅出地讲解rvbacktrace原理由讯客互联电脑硬件栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“深入浅出地讲解rvbacktrace原理”