RISC-V hardfault分析工具,RTTHREAD-RVBACKTRACE
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 pointerwhile (1){frame = (struct stackframe *) (fp - BACKTRACE_LEN); // get frame pointerif ((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 pointerfp = frame->s_fp; // get frame pointerra = frame->s_ra; // get return addresspc = frame->s_ra - 4; // get program counter// print stack interval, return address, program counterBACKTRACE_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 addressnum++;}
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_PRINTFBACKTRACE_PRINTF("Backtrace fail!\r\n");#endifreturn -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 pointerwhile (1){frame = (struct stackframe *)(fp - BACKTRACE_LEN); // get frame pointerif ((uint32_t *)frame > (uint32_t *)(uintptr_t)_rt_eusrstack){rvstack_frame_len = num;return;}sp = fp; // get stack pointerfp = frame->s_fp; // get frame pointerra = frame->s_ra; // get return addresspc = frame->s_ra - 4; // get program counter// print stack interval, return address, program counterBACKTRACE_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 addressnum++;}
看下反汇编
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_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 == 64return imm >> 3; // RV64: 以 8 字节为单位
#elsereturn 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 0x4700if ((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
相关文章:

RISC-V hardfault分析工具,RTTHREAD-RVBACKTRACE
RV BACKTRACE 简介 本文主要讲述RV BACKTRACE 的内部主要原理 没有接触过rvbacktrace可以看下面两篇文章,理解一下如何使用RVBACKTRACE RVBacktrace RISC-V极简栈回溯组件:https://club.rt-thread.org/ask/article/64bfe06feb7b3e29.html RVBacktra…...
xiaopiu原型设计工具笔记
文章目录 有没有行组件是否支持根据图片生成原型呢? 其他官网 做项目要用到原型设计,还是那句话,遇到的必须会用,走起。 支持本地也支持线上。 有没有行组件 是这样,同一行有多个字段,如何弄的准确点呢? 目前只会弄…...

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

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

4个纯CSS自定义的简单而优雅的滚动条样式
今天发现 uni-app 项目的滚动条不显示,查了下原来是设置了 ::-webkit-scrollbar {display: none; } 那么怎么用 css 设置滚动条样式呢? 定义滚动条整体样式 ::-webkit-scrollbar 定义滚动条滑块样式 ::-webkit-scrollbar-thumb 定义滚动条轨道样式…...

查看jdk是否安装并且配置成功?(Android studio安装前的准备)
WinR输入cmd打开命令提示窗口 输入命令 java -version 回车显示如下:...

5月8日直播见!Atlassian Team‘25大会精华+AI实战分享
在刚刚落幕的 Atlassian Team’25 全球大会上,Atlassian发布了多项重磅创新,全面升级其协作平台,涵盖从Al驱动、知识管理到跨团队协作,再到战略执行的各个方面。 为帮助中国用户深入了解这些前沿动态,Atlassian全球白…...

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

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

Webug4.0靶场通关笔记19- 第24关邮箱轰炸
目录 第24关 邮箱轰炸 1.配置环境 2.打开靶场 3.源码分析 4.邮箱轰炸 (1)注册界面bp抓包 (2)发送到intruder (3)配置position (4)配置payload (5)开…...

java CompletableFuture 异步编程工具用法1
1、测试异步调用: static void testCompletableFuture1() throws ExecutionException, InterruptedException {// 1、无返回值的异步任务。异步线程执行RunnableCompletableFuture.runAsync(() -> System.out.println("only you"));// 2、有返回值的异…...
缺乏实体人形机器人的主流高精度仿真方案
在缺乏实体人形机器人的情况下,可通过以下主流仿真方案实现高精度模拟(基于2025年最新技术): 一、基础建模工具链 MATLAB Robotics Toolbox • 通过连杆(Link)和关节(Joint)定义生物力学参数 • 示例代码创建简化模型:…...
基于STM32、HAL库的CP2104 USB转UART收发器 驱动程序设计
一、简介: CP2104是Silicon Labs公司推出的一款USB转UART桥接芯片,具有以下特点: USB 2.0全速兼容 集成USB收发器,无需外部电阻 支持UART数据传输,波特率从300bps到2Mbps 内置EEPROM可配置设备信息 支持RTS/CTS硬件流控制 3.3V I/O电平,内置5V至3.3V稳压器 紧凑的QFN-24…...
ERC-20与ERC-721:区块链代币标准的双星解析
一、代币标准的诞生背景 在以太坊生态中,代币标准是构建去中心化应用(DApps)的基石。ERC-20与ERC-721分别代表同质化与非同质化代币的两大核心标准,前者支撑着90%以上的加密资产流通,后者则开启了数字资产唯一性的新时…...
使用Go语言对接全球股票数据源API实践指南
使用Go语言对接全球股票数据API实践指南 概述 本文介绍如何通过Go语言对接支持多国股票数据的API服务。我们将基于提供的API文档,实现包括市场行情、K线数据、实时推送等核心功能的对接。 一、准备工作 1. 获取API Key 联系服务提供商获取访问密钥(替…...
经典密码学算法实现
# AES-128 加密算法的规范实现(不使用外部库) # ECB模式S_BOX [0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B,0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0,0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0x…...
git 远程仓库管理详解
Git 的远程仓库管理是多人协作和代码共享的核心功能。以下是 Git 远程仓库管理的详细说明,包括常用操作、命令和最佳实践。 1. 什么是远程仓库? 远程仓库(Remote Repository):存储在网络服务器上的 Git 仓库࿰…...
ABP vNext + gRPC 实现服务间高速通信
ABP vNext gRPC 实现服务间高速通信 💨 在现代微服务架构中,服务之间频繁的调用往往对性能构成挑战。尤其在电商秒杀、金融风控、实时监控等对响应延迟敏感的场景中,传统 REST API 面临序列化负担重、数据体积大、通信延迟高等瓶颈。 本文…...

若依框架Ruoyi-vue整合图表Echarts中国地图标注动态数据
若依框架Ruoyi-vue整合图表Echarts中国地图 概述创作灵感预期效果整合教程前期准备整合若依框架1、引入china.json2、方法3、data演示数据4、核心代码 完整代码[毫无保留]组件调用 总结 概述 首先,我需要回忆之前给出的回答,确保这次的内容不重复&#…...
京东(JD)API 商品详情数据接口讲解及 JSON 示例
前言 京东开放平台提供了多种商品详情相关的 API 接口,开发者可以通过这些接口获取商品的详细信息。以下为接口调用方式及 JSON 返回数据的参考示例。 1. 接口调用方式 京东商品详情接口通常采用以下形式: 请求方式:GET/POST关键参数&…...

算法中的数学:约数
1.求一个整数的所有约数 对于一个整数x,他的其中一个约数若为i,那么x/i也是x的一个约数。而其中一个约数的大小一定小于等于根号x(完全平方数则两个约数都为根号x),所以我们只需要遍历到根号x,然后计算出另…...
Python实例题:Python获取喜马拉雅音频
目录 Python实例题 题目 python-get-ximalaya-audioPython 获取喜马拉雅音频脚本 代码解释 get_audio_info 函数: download_audio 函数: 主程序: 运行思路 注意事项 Python实例题 题目 Python获取喜马拉雅音频 python-get-ximala…...

[监控看板]Grafana+Prometheus+Exporter监控疑难排查
采用GrafanaPrometheusExporter监控MySQL时发现经常数据不即时同步,本示例也是本地搭建采用。 Prometheus面板 1,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. 希腊字母(Greek Letters) 名称小写 LaTeX大写 LaTeX显示效果Alpha\alphaAαα, AABeta\betaBββ, BBGamma\gamma\Gammaγγ, ΓΓDelta\delta\Deltaδδ, ΔΔTheta\theta\Thetaθθ, ΘΘPi\pi\Piππ, ΠΠSigma\sigma\Sigmaσσ, ΣΣOmega\omeg…...

剥开 MP4 的 千层 “数字洋葱”:从外到内拆解通用媒体容器的核心
在当今数字化时代,MP4 格式随处可见,无论是在线视频、手机拍摄的短片,还是从各种渠道获取的音频视频文件,MP4 都占据着主流地位。它就像一个万能的 “数字媒体集装箱”,高效地整合和传输着各种视听内容。接下来&#x…...
深度解析语义分割评估指标:从基础到创新实践
一、为什么需要专业评估指标? 在医疗影像分析中,一个3mm的肿瘤漏检可能导致误诊;在自动驾驶场景下,5%的边界识别误差可能引发严重事故。这些真实案例揭示了语义分割评估指标的关键作用。本章将带您深入理解指标背后的数学原理与实践价值。 --- ## 二、基础指标全解析 #…...
浅谈 Shell 脚本编程中引号的妙用
在 Shell 脚本编程中,引号的使用是一项基础却至关重要的技能。无论是单引号、双引号还是不加引号,它们都会显著影响 Shell 对字符串、变量、特殊字符以及命令的解析方式。理解这些差异不仅能帮助开发者编写更健壮的脚本,还能避免因误解引发的…...

从彼得·蒂尔四象限看 Crypto「情绪变迁」:从密码朋克转向「标准化追求者」
作者:Techub 精选编译 撰文:Matti,Zee Prime Capital 编译:Yangz,Techub News 我又带着一篇受彼得蒂尔(Peter Thiel)启发的思想杂烩回来了。作为自封的「蒂尔学派」信徒,我常透过他…...

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

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