(GCC)STM32进阶详解之栈回溯

news2024/11/15 3:23:36

接上一篇:

函数调用

由上一篇大概了解了函数是如何被调用,中断或者说异常又是如何被调用,而这一篇相当于上一篇知识的一个应用,也是上一篇遗留的思考,即在hardfault中如何判断是从何处触发这个异常的。本来打算自己写demo,但是查到github上有一个开源的CmBacktrace,既然有大牛已经写了开源的库,就直接拿来分析印证吧。项目地址:

CmBacktrace

硬件我使用的是STM32F103ZET6最小系统板,demo是项目中提供的,直接下载即可。

1.原理分析

从串口1输出如下:

实际使用项目中提供的addr2line运行打印出来的地址时,输出如下:

我们回过头来看一下demo,在app.c中的main里有调用fault_test_by_div0()这个函数,它里面故意做了除0运算:

 仔细对比addr2line的输出,我们可以看到,输出结果是完全正确的,包括行号(有空行的情况可能会有点偏差)。其实addr2line这个程序的作用是通过你输入的地址和.elf文件,输出这个地址在.elf文件中表示的函数。原理不难理解,elf文件中完美保留了每个函数的地址,以及每个函数对应的.c文件,自然可以去对应.c中查看该函数行号。可以参考我之前分析elf文件的博文:

(GCC)STM32进阶修炼之ELF文件剖析

所以这里的关键其实在于我们给addr2line传入的三个地址:

addr2line -e CmBacktrace.axf -a -f 08001da6 08001dfc 08000268

打开.axf文件可以看到以上三个地址分别对应的函数:

可以看到最后一个地址指向的08000268其实在源文件中是找不到的而且它保存的也不是某行代码,而是一个全局变量,所以addr2line打印的是问号,表示查找不到。

从这里可以看出,我们的关键就在于找到出错误代码的地址,以及调用这个出错误函数的地方,当然,实际可能有很多个函数层层嵌套调用,而只需要依次找到上一层调用地址,就可以一层一层的递进。

这里,我会再次分析整个函数调用流程,是对上一节内容的一个印证。请注意栈中保存数据在整个流程中的变化

首先我们把断点打在main函数的起始位置,此时现场如下:

由上图我们可以知道几点:

1.调用mian函数结束后, 我们返回到调用main函数的地方,并执行下一个指令,它的地址是0x08000268(为什么减一我前几章有说过),这个地址实际对应:

实际这个地址保存的根本不是可以执行的代码,那为什么还要指明从main执行完后回来执行它?

由上图可以看到,我们实际跳转main是0x08000264,这里使用的指令是BL,这个指令会自动装载下一个指令地址到LR(即执行时PC指向的地址),保证调用完函数后,可以很方便的直接用汇编指令:B LR返回到上层函数,然而实际这个工程中,main函数根本不会返回,所以这个地址即使是错的,也无所谓。

2.因为我们main函数中还要再使用汇编指令BL和寄存器R4,所以我们需要把它们保存在栈里,防止被覆盖,所以可以看到main函数第一行汇编就是把LR和R4保存在了栈里。而此时的栈顶是SP所指向的0x2000 0560,打开.map文件可以看到:

我们分配给栈的空间是从0x2000 0160开始,大小为0x400的空间,因为栈是从上到下生长的,所以栈顶一开始就是0x2000 0560。 

我们再次推进断点位置,这次把断点打在进入fault_test_by_div0函数前,具体现场如下:

可以看到,根据上文的分析,LR和R4已经被压栈,此时栈顶已经变成了0x20000558,我们继续向下推进, 这次把断点打在fault_test_by_div0函数一开始的位置:

继续把断点打在出错的那一行函数,继续运行分析:

对比上面两张图可以看到,栈内数据和我们分析的一模一样,现在我们即将运行错误代码,我们知道,Cortex-M系列的MCU出现错误会跳往编号为3的中断:

这里我们找到中断表,最后可以查到跳转到了HardFault_Handler函数,我们把下个断点打在这里:

可以看到,出现错误后和我们预期一样,注意两个地方:

1.进入中断后,LR的值会被自动更新为特殊的EXC_RETURN,这个值的含义如下:

也就是它告诉了我们,这个中断执行完后,我们返回时该使用何种模式,和使用哪个堆栈指针。

2.正因为LR因为上述原因被占用了,导致我们没有办法再直接通过LR返回到中断被调用前的位置,所以进入中断后,一部分寄存器是硬件自动入栈的:

由进入HardFault_Handler后的现场图也可以看到,压栈的内容与寄存器是一一对应的。也就是说硬件替我们保存了一部分现场,你可能会问了,为何还有一部分寄存器没有被保存,万一在进入中断前我使用过它们比如R5,中断中被破坏了,代码从中断中出来后,再回去执行不是一样会出错吗?这在上一章有讲过,函数调用原则里有分哪些寄存器是需要调用者保护,哪些寄存器是需要被调用者保护的,这里除了硬件压栈的那部分,剩余的都应该由被调用者保护。

这样是不是一切都明了了?思考这样一个问题:

如果此时让你在HardFault_Handler中写一个函数,用它去寻找是由哪条指令执行后,导致进入了HardFault_Handler,而那个指令所在的函数又是被哪里所调用,你会写吗?

一切一切的关键又回到了那三个地址:

对,就是如何从栈中找到这三个地址!

至于为什么可以打印出错误类型是除0,那很简单,即使你没有移植任何代码,在keil中HardFault_Handler打上断点,进入后,查看:

简单来说就是Cortex-M内核提供了一些寄存器,它们保存出错时,错误的大概类型。,只需在进入错误中断后查询相关寄存器即可。

听起来这一切都很简单是不是?但是想写好一个开源库却比想象中难。

2.代码分析

就以上文中demo里不使用OS的情况为例,分析整个代码流程。

首先看main函数,和CmBacktrace相关的初始化就一个函数:

而这个函数里面很简单,仅仅是赋值了一些变量:

这些变量定义如下: 

这里你需要知道一些MDK的知识。在MDK中,使用 AREA 关键字创建的数据段,通常在段名后加$$Base表示起始地址,加$$Limit表示结束地址,比如这里的STACK:

以及:

所以这里的这些宏定义起始就是为了取得其中两个段:

由最开始的解析我们知道,这里查找函数调用的思路就是从栈区找到属于代码段范围的地址。这也就是为什么我们一开始要知道栈区范围和代码段范围。初始化就这么简单就完成了,主要工作都在错误中断中:

这里就是我上一章讲到的函数调用规则,当汇编调用C函数时,需要遵守这个规则。

现在进入到关键函数cm_backtrace_fault里:

 

上述代码都是一些简单的判断与打印,不再详细展开, 看一下下面的打印栈区:

这里如果有堆栈溢出,且上层函数栈顶已经超过栈区最大地址,那什么也不会打印,具体打印数据逻辑如下:

这里的相关打印,我再贴一下:

这和前文中原理分析相关内容,可以一一对应着去看。 下面则是打印的硬件压栈的那些寄存器数据:

实际串口打印如下:

 

接下去就是我说的,通过查找寄存器对应的每个位,来判断当前故障的类型:

具体不再分析,想要知道细节的可以查看《Cortex-M3权威指南》表D.17之后的几个表。

最关键的函数留在了最后:

我们最后打印的就是:

所以关键在函数cm_backtrace_call_stack中:

 

在没有堆栈溢出的情况下,buffer里面会先保存两个值,一个是执行错误代码时的PC,在这个demo中是0x08001da6,然后又保存了执行错误代码时的LR-1,在这里是:0x08001dfd-1=0x08001dfc。

最后就是循环检测栈区,然后把符合的保存下来。  

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

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

相关文章

GO09:整型、浮点、字符类型的介绍和使用细节

基本类型的使用 基本介绍 3.8整数的各个类型 func main(){var i int 1 fmt.Println("i", i) var j int8 127 fmt.Println("j", j) }int的无符号的类型: 整型的使用细节 一、Golang各整数部分:有符号和无符号,int u…

信而泰RENIX 802.1ag功能介绍-网络测试仪实操

一、EOAM概述​ 1.以太网​ 1.1以太网优点​ 简单易用​ 价格低廉​ 高拓展性​ 大势所趋,一统天下​ 1.2以太网缺点​ 可管理性差​ 定位故障手段少​ 定位故障速度慢​ 维护成本高​ 2.以太网OAM​ ■EOAM​ 为运营商服务​ 提高以太网可靠性、可维…

非零基础自学Golang 第15章 Go命令行工具 15.6 性能分析 15.6.2 通过文件方式 15.6.3 通过HTTP方式 15.7 小结

非零基础自学Golang 文章目录非零基础自学Golang第15章 Go命令行工具15.6 性能分析15.6.2 通过文件方式15.6.3 通过HTTP方式15.7 小结第15章 Go命令行工具 15.6 性能分析 15.6.2 通过文件方式 为了能分析Go程序的性能,我们需要在程序中导入runtime/pprof来生成性…

飞鹤揭榜“十四五”项目,牵头研制新一代婴配粉

12月22日,在“鲜萃活性营养,更适合中国宝宝体质”中国飞鹤60周年战略升级发布会上,飞鹤对外公布,其牵头申报的——“基于中国母乳研究的新一代婴配乳粉制造技术研究与示范”项目成功获批。 这是继去年飞鹤与江南大学、大连工业大…

大数据基础平台搭建-(二)Hadoop集群搭建

大数据基础平台搭建-(二)Hadoop集群搭建 大数据平台系列文章: 1、大数据基础平台搭建-(一)基础环境准备 2、大数据基础平台搭建-(二)Hadoop集群搭建 大数据平台是基于Apache Hadoop_3.3.4搭建的…

不同存储资源的应用场景及优缺点介绍

容器应用应当根据应用系统的特点,综合考虑容器应用对存储类型、存储性能及数据高可用等方面的要求,选择最适合的存储资源类型。常见的存储资源应用场景包括三类:将存储挂载在外部宿主机上、将存储放置于容器内部和使用外部共享存储。下面对每…

MySQL面试常问问题(数据库架构+存储引擎) —— 赶快收藏

目录 1.说说 MySQL 的基础架构? 2.一条 SQL 查询语句在 MySQL 中如何执行的? 3.MySQL有哪些常见存储引擎? 4.那存储引擎应该怎么选择? 5.InnoDB和MylSAM主要有什么区别? 1.说说 MySQL 的基础架构? MySQL逻辑架构图主要分三…

浅谈古建筑电气火灾成因及防控对策

摘要: 我国古建筑多为砖木结构,当发生火灾事故时具有蔓延快、扑救难的特点,而火灾对古建筑的损害性很大,电气火灾事故在我国火灾事故中比重居高不下。本文通过对古建筑电气火灾成因进行分析,有针对性地提出了古建筑电气火灾防控对…

Java工厂企业工艺管理系统源码 springboot2+vue2前后端分离架构 工艺路线 加工工序管理源码

工艺系统是对车间现场加工工序的管理,根据生产成品或者半成品的工单,以及产品标准工艺路线,系统可以自动生成产品工序加工命令 并且可以根据实际情况再进行工序调整.系统可以根据每道工序,打印工序派工单。工序之间物料转移&#…

【推荐】华为顶级认证HCIE-RS培训教材全套合集

HCIE是华为认证系统中的专家级认证。候选人必须通过笔试,LAB考试和面试,才能最终获得HCIE认证。困难还从另一方面解释了证书的含金量。 该认证具有很高的含金量和行业认可度。此外,获得HCIE证书的工程师将优先获得华为和华为合作伙伴的聘用&a…

Struts2中的数据校验

Struts2中的数据校验1、Action控制器2、jsp页面3、struts.xml配置4、测试1、Action控制器 如果要使用校验,则需要继承ActionSupport类,覆写validate()方法,如果是实现Action接口,则无法覆写此方法。直接在之前的控制器基础上进行…

Spring 中使用Nacos配置管理

添加依赖 <dependency><groupId>com.alibaba.nacos</groupId><artifactId>nacos-spring-context</artifactId><version>${latest.version}</version> </dependency>本文使用的版本为&#xff1a;1.1.1 注&#xff1a;我们在N…

【nowcoder】笔试强训Day1

目录 一、选择题 二、编程题 2.1组队竞赛 2.2删除公共字符串 一、选择题 1.在 Java 中&#xff0c;存放字符串常量的对象属于&#xff08; &#xff09;类对象。 A Character B String C StringBuffer D Vector 字符串分为两大类&#xff0c;一类是字符串常量&#xf…

ESLint + StyleLint + Prettier + VSCode 打造最优雅的前端开发体验

ESLint StyleLint Prettier VSCode 打造最优雅的前端开发体验 引言 对于一个成熟的前端团队&#xff0c;统一的编码规范和提交规范尤其重要。要保证秩序井然、风格统一、整齐有序&#xff0c;光把规范写在文档里是没有太多实际价值的。没有人愿意去一条一条看规则&#xff…

ovn:中央节点ip变更导致节点失联

1.当ovn的中央节点ip突然变更&#xff08;从3.197->1.114&#xff09;后&#xff0c;便再无法同节点之间进行信息的同步。 2.已将节点的ovn-remote变更到最新的中央节点ip 3.但是进行ovn-controller 检测时却显示失败 4.通过telnet 测试6642 端口失败 5.最后发现原来是中央…

【UE4 第一人称射击游戏】05-设置角色动画

素材资料地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1epyD62jpOZg-o4NjWEjiyg 密码&#xff1a;jlhr 步骤&#xff1a; 1.新建一个名为“Character”的文件夹 将Swat.fbx导入Character文件夹中 新建一个名为“Animation”的文件夹 将Animation文件夹内的所…

大数据基础平台搭建-(一)基础环境准备

大数据基础平台搭建-&#xff08;一&#xff09;基础环境准备 大数据平台系列文章&#xff1a; 1、大数据基础平台搭建-&#xff08;一&#xff09;基础环境准备 2、大数据基础平台搭建-&#xff08;二&#xff09;Hadoop集群搭建 目录大数据基础平台搭建-&#xff08;一&#…

docker安装minio集群

docker安装minio集群 文章目录docker安装minio集群1、所有节点配置主机名解析2、配置时间同步&#xff0c;关闭防火墙和selinux。3、所有节点安装docker4、部署minio集群&#xff0c;3个节点每个节点挂载2个目录10.20.138.5210.20.138.5310.20.138.545、访问任意节点的9000端口…

vm vh移动端布局及 bilibili官网移动端首页布局

vm和vh是啥&#xff1f; 市场上的移动端大多数为flex布局&#xff0c;此时我们用到了rem这个单位&#xff0c;但是rem需要媒体查询&#xff0c;要根据页面是尺寸进行修改&#xff0c;而vm/vh省去各种判断和修改&#xff0c;像B站就通过vue和vm写的。 vm/vh是一个相对单位&…

【Quarkus技术系列】「云原生架构实战」配置参考指南相关的功能机制配置介绍分析

回顾Quarkus介绍 Quarkus的概念定义 Quarkus是一个为Java虚拟机&#xff08;JVM&#xff09;和原生编译而设计的全堆栈 Kubernetes 原生 Java 框架&#xff0c;用于专门针对容器优化 Java&#xff0c;并使其成为无服务器、云和 Kubernetes 环境的高效平台。 Quarkus与框架整…