RISC-V hardfault分析工具,RTTHREAD-RVBACKTRACE

news2025/7/14 2:32:01

RV BACKTRACE

简介

本文主要讲述RV BACKTRACE 的内部主要原理

没有接触过rvbacktrace可以看下面两篇文章,理解一下如何使用RVBACKTRACE

RVBacktrace RISC-V极简栈回溯组件:https://club.rt-thread.org/ask/article/64bfe06feb7b3e29.html

RVBacktrace RISC-V极简栈回溯组件V1.2:https://club.rt-thread.org/ask/article/09737357e4a95b06.html

RVBACKTRACE

https://github.com/Yaochenger/RvBacktrace

rvbacktrace更多的利用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,16

S0的值就是保存的栈信息。默认这个寄存器就给栈回溯用。

主要实现函数如下,获取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,-16sw ra,12(sp)

我们翻阅RISCV手册之后,看到如下的命令:
在这里插入图片描述

当然这个是压缩命令

主要的是计算出它的立即数,imm

参考链接如下:
https://riscv.github.io/riscv-isa-manual/snapshot/unprivileged/#_integer_register_immediate_instructions

static 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 BACKTRACE

https://github.com/armink-rtt-pkgs/CmBacktrace

CM 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2371609.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

matlab 中function的用法

matlab 中function的用法 前言介绍1. 基本语法示例&#xff08;1&#xff09;可以直接输出&#xff08;2&#xff09;调用函数 2.输入参数和输出参数示例多输入参数和输出参数定义一个函数&#xff0c;计算两个数的和与差&#xff1a;调用该函数&#xff1a; 3. 默认参数示例 4…

解锁 LLM 推理速度:深入 FlashAttention 与 PagedAttention 的原理与实践

写在前面 大型语言模型 (LLM) 已经渗透到我们数字生活的方方面面,从智能问答、内容创作到代码辅助,其能力令人惊叹。然而,驱动这些强大模型的背后,是对计算资源(尤其是 GPU)的巨大需求。在模型推理 (Inference) 阶段,即模型实际对外提供服务的阶段,速度 (Latency) 和吞…

4个纯CSS自定义的简单而优雅的滚动条样式

今天发现 uni-app 项目的滚动条不显示&#xff0c;查了下原来是设置了 ::-webkit-scrollbar {display: none; } 那么怎么用 css 设置滚动条样式呢&#xff1f; 定义滚动条整体样式‌ ::-webkit-scrollbar 定义滚动条滑块样式 ::-webkit-scrollbar-thumb 定义滚动条轨道样式‌…

查看jdk是否安装并且配置成功?(Android studio安装前的准备)

WinR输入cmd打开命令提示窗口 输入命令 java -version 回车显示如下&#xff1a;

5月8日直播见!Atlassian Team‘25大会精华+AI实战分享

在刚刚落幕的 Atlassian Team’25 全球大会上&#xff0c;Atlassian发布了多项重磅创新&#xff0c;全面升级其协作平台&#xff0c;涵盖从Al驱动、知识管理到跨团队协作&#xff0c;再到战略执行的各个方面。 为帮助中国用户深入了解这些前沿动态&#xff0c;Atlassian全球白…

Windows系统下使用Kafka和Zookeeper,Python运行kafka(一)

下载和安装见Linux系统下使用Kafka和Zookeeper 配置 Zookeeper Zookeeper 是 Kafka 所依赖的分布式协调服务。在 Kafka 解压目录下,有一个 Zookeeper 的配置文件模板config/zookeeper.properties,你可以直接使用默认配置。 启动 Zookeeper 打开命令提示符(CMD),进入 K…

C++之“继承”

继续开始关于C相关的内容。C作为面向对象的语言&#xff0c;有三大特性&#xff1a;封装&#xff0c;继承&#xff0c;多态。 这篇文章我们开始学习&#xff1a;继承。 一、继承的概念和定义 1. 继承的概念 什么是继承呢&#xff1f; 字面意思理解来看&#xff1a;继承就是…

Webug4.0靶场通关笔记19- 第24关邮箱轰炸

目录 第24关 邮箱轰炸 1.配置环境 2.打开靶场 3.源码分析 4.邮箱轰炸 &#xff08;1&#xff09;注册界面bp抓包 &#xff08;2&#xff09;发送到intruder &#xff08;3&#xff09;配置position &#xff08;4&#xff09;配置payload &#xff08;5&#xff09;开…

java CompletableFuture 异步编程工具用法1

1、测试异步调用&#xff1a; static void testCompletableFuture1() throws ExecutionException, InterruptedException {// 1、无返回值的异步任务。异步线程执行RunnableCompletableFuture.runAsync(() -> System.out.println("only you"));// 2、有返回值的异…

若依框架Ruoyi-vue整合图表Echarts中国地图标注动态数据

若依框架Ruoyi-vue整合图表Echarts中国地图 概述创作灵感预期效果整合教程前期准备整合若依框架1、引入china.json2、方法3、data演示数据4、核心代码 完整代码[毫无保留]组件调用 总结 概述 首先&#xff0c;我需要回忆之前给出的回答&#xff0c;确保这次的内容不重复&#…

算法中的数学:约数

1.求一个整数的所有约数 对于一个整数x&#xff0c;他的其中一个约数若为i&#xff0c;那么x/i也是x的一个约数。而其中一个约数的大小一定小于等于根号x&#xff08;完全平方数则两个约数都为根号x&#xff09;&#xff0c;所以我们只需要遍历到根号x&#xff0c;然后计算出另…

[监控看板]Grafana+Prometheus+Exporter监控疑难排查

采用GrafanaPrometheusExporter监控MySQL时发现经常数据不即时同步&#xff0c;本示例也是本地搭建采用。 Prometheus面板 1&#xff0c;Detected a time difference of 11h 47m 22.337s between your browser and the server. You may see unexpected time-shifted query res…

LaTeX印刷体 字符与数学符号的总结

1. 希腊字母&#xff08;Greek Letters&#xff09; 名称小写 LaTeX大写 LaTeX显示效果Alpha\alphaAαα, AABeta\betaBββ, BBGamma\gamma\Gammaγγ, ΓΓDelta\delta\Deltaδδ, ΔΔTheta\theta\Thetaθθ, ΘΘPi\pi\Piππ, ΠΠSigma\sigma\Sigmaσσ, ΣΣOmega\omeg…

剥开 MP4 的 千层 “数字洋葱”:从外到内拆解通用媒体容器的核心

在当今数字化时代&#xff0c;MP4 格式随处可见&#xff0c;无论是在线视频、手机拍摄的短片&#xff0c;还是从各种渠道获取的音频视频文件&#xff0c;MP4 都占据着主流地位。它就像一个万能的 “数字媒体集装箱”&#xff0c;高效地整合和传输着各种视听内容。接下来&#x…

从彼得·蒂尔四象限看 Crypto「情绪变迁」:从密码朋克转向「标准化追求者」

作者&#xff1a;Techub 精选编译 撰文&#xff1a;Matti&#xff0c;Zee Prime Capital 编译&#xff1a;Yangz&#xff0c;Techub News 我又带着一篇受彼得蒂尔&#xff08;Peter Thiel&#xff09;启发的思想杂烩回来了。作为自封的「蒂尔学派」信徒&#xff0c;我常透过他…

Java线程安全问题深度解析与解决方案

一、线程安全问题的本质 并发编程的核心挑战&#xff1a;当多个线程同时访问共享资源时&#xff0c;由于操作系统的抢占式调度特性&#xff0c;可能导致不可预期的结果。这种因非原子操作和竞态条件引发的数据不一致问题&#xff0c;称为线程安全问题。 二、经典线程安全问题案…

Mybatis解决以某个字段存在,批量更新,不存在批量插入(高效)(一)

背景 在开发企业级应用时&#xff0c;我们经常需要处理批量数据的插入和更新操作。传统的逐条处理方式性能低下&#xff0c;而简单的REPLACE INTO或INSERT ... ON DUPLICATE KEY UPDATE在某些场景下又不够灵活。本文将介绍一种基于临时表的高效批量插入/更新方案&#xff0c;解…

【时时三省】(C语言基础)怎样定义和引用二维数组

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 有的问题需要用二维数组来处理。例如&#xff0c;有3个小分队&#xff0c;每队有6名队员&#xff0c;要把这些队员的工资用数组保存起来以备查。这就需要用到二维数组&#xff0c;如下图&…

杨校老师竞赛课之C++备战蓝桥杯初级组省赛

目录 1. 灯塔 题目描述 输入描述 输出描述 输入样例1 输出样例1 输入样例2 输出样例2 数据说明 2. 子区间 题目描述 输入描述 输出描述 输入样例 输出样例 数据说明 3. 染色 题目描述 输入描述 输出描述 输入样例1 输出样例1 输入样例2 输出样例2 数据…

Matlab 基于Hough变换的人眼虹膜定位方法

1、内容简介 Matlab220-基于Hough变换的人眼虹膜定位方法 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略