当前位置: 首页 > news >正文

深入浅出地讲解rvbacktrace原理

前情提要:栈

        要理解栈回溯,就要首先理解栈是如何运行的

        下面看一段非常简单的代码

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,该怎么做

  1. 首先traceback一定是在某处被调用,我们假设FunB调用了traceback
  2. 在funB运行时,我们可以从寄存器SP得到FunB的栈顶,从寄存器FP得到栈底
  3. 我们可以把栈底和栈顶传递给traceback,这样trackback就能得到栈中的所有内容
  4. 栈内有什么呢,有FunB的返回地址ra,还有之前保存的fp(这里要注意,栈中的fp和当前寄存器FP内的值是两回事,栈中的fp是调用FunB的函数FunA的栈底)
  5. 接着,我们可以根据栈中的fp跳转至FunA的栈,里面又可以得到FunA的返回地址,以及FunA栈中的fp,也就是main函数的栈底
  6. 这样一层一层套娃,也就完成了函数的调用链回溯
  7. 最后需要注意退出条件,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;
    }
}

相关文章:

  • Ubuntu如何利用.ibd文件恢复MySQL数据?
  • 算法基础 -- Fenwick树的实现原理
  • python中使用数据库sqlite3
  • Spring Boot 集成 Kettle
  • QML 部件获得焦点触发的全局槽函数 onActiveFocusItemChanged
  • JavaScript 内置对象-字符串对象
  • Docker Remote API未授权访问漏洞复现
  • DeepSeek 助力 Vue 开发:打造丝滑的日期选择器(Date Picker),未使用第三方插件
  • 在conda虚拟环境中安装jupyter lab-----deepseek问答记录
  • lighten() 函数被弃用:替代方案color.scale()或者color.adjust()
  • C语言基础16:二维数组、字符数组
  • 《DeepSeek训练算法:开启高效学习的新大门》
  • 虚拟机如何添加硬盘
  • Java中CompletableFuture异步工具类
  • 【ENSP】链路聚合的两种模式
  • pypthon字符串与日期转换
  • 6.2.4 基本的数据模型
  • linux笔记:shell中的while、if、for语句
  • Spring 如何解决循环依赖以及那些无法解决的循环依赖
  • 【贝克街迷宫疑云:用侦探思维破解Java迷宫算法】
  • 怎么看一个网站是谁做的/优化关键词是什么意思
  • 淄博市建设业协会网站/360建网站
  • 网站建设免费模版/企业邮箱域名
  • 投票网站做seo如何/十大免费引流平台
  • 海南房产网站建设/百seo排名优化
  • 匠人精神网站建设/站长工具seo综合查询关键词