新建   展示   相册  列表  网摘
 

【MCU】"double 强转 uint64"程序飞了,损失惨重!听说因为代码没"对齐"程序就奔了?(深度剖析)【收藏】get这些技巧,HardFault_Handler排查只需要几分钟

【MCU】"double 强转 uint64"程序飞了,损失惨重!

本文讲述售后反馈升级程序运行卡死问题,经排查是因程序测试不到位,强制类型转换引发异常。以stm32F4芯片项目为例,模拟演示代码问题,解读是因LDRD指令未进行8字节地址对齐导致未对齐故障,还介绍了排查故障的方法。

1、聊一聊

   一听这首歌曲,bug菌的思绪便飞到了十年前,没办法太经典了,As long as you love Me,Bug is always with you!

 

2、正文部分

1

情景 

  • 售后 : X工,现场出大事了,今天升级的程序跑着跑着就挂了!现在整个产线都等着这个设备恢复,能安排个人过来支援下吗?

  • bug菌 : my god !别慌,我问一下负责的A工。

  • bug菌 : 喂,A工,昨天升级的程序有问题,程序卡死,售后在现场你联系一下,支援他一波,顺便把程序发送给我一份,一起看看!

  • A工 : 啊,还有这种事,程序没改什么呀,行,我跟售后联系一下。

经过一番折腾,发现由于程序测试不到位,导致了一个强制类型转化引发的进入异常,这里就分享给大家。

2

bug演示 

    这是一个老项目,采用stm32F4芯片为主控,由于硬件限制而客户又不愿意花大价钱改造,所以程序架构等等都没有再大动作,由于通信上的传输和解析都是字节流,一些小的需求都只是在原来的通信架构上把4个字节拆成2个字节来用,然而这一次实在没办法没改接受数据类型,然后把一个double类型拆成了4个uint16来使用,没想到出问题了。

    公司代码加密,所以这里简单的模拟演示了一下:

    A工用一个double类型取地址,然后把地址强制转为uint64_t类型,以此类型指针取内容,当这段代码执行完程序就跳到了异常中断,导致死机。

    其实这段代码对于经验丰富的人来说,一看就觉得很变扭,但是无论如何也不至于死机呀,毕竟强制类型转化大部分人拿来都是随便用。

3

bug解读 

    当看到A工写的这一套代码,bug菌其实隐隐约约就感觉这块有些问题,但是没敢确定,毕竟整套代码也是前人留下的,全是逻辑没什么精华也没有过细研究,最后看这段代码的汇编才知道问题所在。

    在之前bug菌也曾比较详细的出过一篇分析此类问题的文章,可能这一块并没有吸引到你,不过还是一句话:"出来混都是要还的!"。

听说因为代码没"对齐"程序就奔了?(深度剖析)

    其实问题就出在LDRD这个ARM汇编指令上,LDRD指令表示从指定内存地址取double word,上面图片代码中的LDRD R0,R1,[R2,#0x2EC],可以分解为下面两个ldr步骤 : 

在ARM汇编指令集中LDRD和STRD是一对加载和提取指令,一般都需要使用__align(8)修饰来保证数据对象进行8直接对齐,而使用#pragma pack(8)是来指定结构体成员变量相对于第一个变量的地址的偏移量的对齐方式。

__align指示编译器在 n 字节边界上对齐变量,是一个存储类修饰符,当然也可以以让2字节的对象进行4字节对齐其与8字节对齐是等价的,一定要记得是存储的起始地址为8的整数倍。

对齐可以在一定程度上提高数据提取的效率,一旦起始地址没有对齐会导致对齐错误,所以上面的double浮点类型的结构体变量没有8字节地址对齐,当进行强制类型转化并使用LDRD指令就导致未对齐故障。

3

更专业点 

    当然对于跳转到硬件异常的故障是非常好排查的,下面这篇文章教你如何迅速的定位故障位置和故障信息 :

【收藏】get这些技巧,HardFault_Handler排查只需要几分钟

    对于非对齐指令的执行会导致指令用法上的故障,那么Cortex芯片中相应的故障寄存器标志位会置位。

以上来自于Cortex技术文档,文档中也写得非常的详细。

当CPU尝试做一个未对齐的内存访问,然后就会发生此错误。特别是对于未对齐的LDM/STM/LDRD/STRD指令,所以进入异常中断以后查询芯片内部故障寄存器也是可以找到问题所在的,对于使用仿真器排查是再简单不过了,如果是离线排查就需要进行上篇文章那样打印相关日志来定位问题。

本文到此结束!

3、结束语

     

     所以嵌入式软件的编写不能太过随意,往往你在PC机上跑得飞起的模拟程序,或许真正到嵌入式平台上根本没法运行。

    好了,这里是公众号:“最后一个bug”,一个为大家打造的技术知识提升基地,创作不易,觉得不错给bug菌点个赞。

 

 


 

听说因为代码没"对齐"程序就奔了?(深度剖析)

 

 

1、来聊聊(轻松一刻)

 

    来深圳这么久确实没有看到过下雪,而今天推荐的这首歌却唱出了广漂的小伙伴不少的心声,"...不下雪的广东,不一样的天空,他们都一样彼此有着破碎的梦...";也许这就是生活本来的模样吧。

    好了,不管怎样心中都要充满着热爱!!今天为大家带来嵌入式中关于"对齐"的那些事,比如地址对齐、结构体数据对齐以及一些常用的处理小技巧。

 

2、嵌入式中的那些"周期"

    在之前的《C语言里面嵌入点“机器码”玩一玩》中作者重点把指令与机器码以及数字电路进行了互连,相信大家应该对程序的运行有了一个形象的认识吧。那么执行这些指令的时间节拍到底是这样的呢?这里作者就把时钟周期、机器周期、指令周期等等周期概念跟大家简单的聊一聊。

1)时钟周期 

    当我们使用的晶振或者频率没有经过倍频处理,那么这时候的时钟周期 = 1/振荡频率;如果经过锁相环进行倍频以后那么这个时候的时钟周期 = 1/系统主频。

2)机器周期 

    我们都知道我们的CPU需要进行取指令、译码、执行,然后CPU进行每项基础的操作都需要时间,这个时间我们认为是机器周期,那么机器周期一般都会由一个或者多个时钟周期构成。

3)指令周期 

    需要知道的是每一条指令都是由一个或者多个机器周期构成的,不过现在随着处理器的进步出现了很多单周期的指令,单周期指令执行时间为一个时钟周期。那么对于多周期指令根据指令的复杂程度其执行时间是不一样的,所以作者在之前的说如何测定程序运行时间中提到:对于通过数指令个数来确定程序运行时间是比较麻烦的。

    好了,这里对于这几个概念不过多解释了,主要是为了后面字节对齐效率分析进行一个铺垫,顺便简单画个图供大家理解下:

图片

3、结构体内部对齐

    小伙伴们对于int类型根据平台不同会存在差异比较熟悉,而对于结构体的大小也可能会因为系统的字节对齐原因产生变化。下面简单体会一下结构体内的字节对齐:

#include <stdio.h>

#include <stdlib.h>

/************************************************

* Fuciton :结构体定义区

* Author :(公众号:最后一个bug)

************************************************/

typedef struct _tag_Test

{

unsigned char byVal1;

int intVal;

unsigned char byVal2;

} stTest;

 

#pragma pack(1)

typedef struct _tag_Test1

{

unsigned char byVal1;

int intVal;

unsigned char byVal2;

} stTest1;

#pragma pack()

 

typedef struct _tag_Test2

{

int intVal;

unsigned char byVal1;

unsigned char byVal2;

} stTest2;

 

/************************************************

* Fuciton :main

* Author :(公众号:最后一个bug)

************************************************/

int main(int argc, char*argv[]) {

printf("sizeof(stTest) : %d\n",sizeof(stTest));

printf("sizeof(stTest1) : %d\n",sizeof(stTest1));

printf("sizeof(stTest2) : %d\n",sizeof(stTest2));

printf("公众号:最后一个bug");

return0;

}

 

    最终数据的结果:

图片

    解析一下:从上面的程序来看,int属于4个字节,那么结构体1采用四字节对齐的方式一共就是12个字节,而结构体2,我们通过使用#pragma pack(1)这样来使得结构体1个字节对齐,同时使用#pragma pack()来进行解除一个字节对齐模式,从而刚好占用6个字节,而结构体3仅仅只是相对结构体1进行变量顺序上的交换,却只有用了8个字节。

    对于结构体3的解释 : 编译器在为结构体成员分配内存的时候,结构体的第一个成员分配在offset = 0的位置,而第二个成员通过计算其成员本身占用大小与当前字节对齐大小进行对比,如果还能够装满字节对齐大小,便直接存储,否则就需要分配到下一个对齐地址处,这样之前没有使用完的部分就被填充,从而在一定程度上浪费了一定的内存空间,而结构体3后两个成员刚好可以放到4字节对齐地址里面,所以内存空间减少。

    所以平时大家也有这样的说法:“把结构体成员中字节占用比较大的放在结构体头部”,这种说法不完全正确,还是要根据成员大小情况具体排列位置,同时对于第二个结构体采用1字节对齐方式的处理办法便能够节省一定的内存,同时也增强了代码的可移植性,不过就是相对比较耗时间,后面作者会解释一下。

 

4、内存对齐

    其实不仅仅只是结构体内部会存在这样的对齐方式,其实对于平时我们分配的全局变量等内存也是存在地址对齐的问题。我们这里想想如果仅仅只是上面的结构体内部成员对齐,而结构体首地址并没有对齐,那从整体上来看结构体内部对齐也就没有什么意义了。

    这里作者就来说说内存对齐,我们都知道CPU在访问内存的时候是通过总线来进行访问,不同CPU其总线都有着不同的宽度,比如16位,32位,64位等,位数越高CPU对数据的吞吐量也就越大,那么一部分CPU为了简化设计加快访问速度,都会只能访问对齐地址上的数据,比如说一些16位的CPU仅仅只能访问偶数地址的内存数据。

图片

    那么对于跨越在两个对齐区域的多字节数据会如何处理呢?

    1)对于支持非对齐地址访问的CPU,一般都会具有对应的非对齐访问指令,通过判断地址是否跨多个对齐区域,然后分别读取多个对齐区域,最后组合以后返回对应数据(如上图所示),这样明显会增加指令的运行时间,降低了CPU的运行效率;有些小伙伴就会问了,我看编译的汇编代码都是执行了一条指令呀,时间应该都是一样的呀?如果你提了这样的问题,记得返回去一下指令周期的定义。

    2 )而对于不支持对齐地址访问的CPU,如果我们在程序中访问不对齐的地址,系统就会抛出异常,比如硬件中断、或者段错误等等。同样结构体对齐也要注意这样的访问问题,所以以后大家在发现程序异常"跑死",定位到异常点以后也可以往地址对齐这方面考虑。

    说到这里很多小伙伴都会非常疑惑,好像我们平时写嵌入式代码并没有考虑这么多呀,也没发现有什么问题呀?听到你这里好像我没定义一个变量都要小心翼翼了。哈哈,是的,确实我们平时大多数时候都不用考虑,因为我们都使用了配套的编译器,编译器会检测不同的变量类型,然后为我们自动的进行内存分配的对齐处理,同样结构体内部对齐也会处理,不过对于有些指针的处理部分编译器并不会特意提示开发人员,比如说:我们把char*ptr指针转化为int*ptr指针进行++访问,便有可能会出现非对齐地址访问的问题。

 

5、最后小结

    对于内存对齐问题,还有很多需要各位小伙伴注意的,比如代码的可移植性,不同平台的网络通信过程中的处理等等,都需要对其进行考虑和处理。这里对于该问题有个感性认识即可,对于部分问题还是需要具体熟悉芯片内核的处理办法进行综合分析,对于结构体还有很多丰富的操作技巧,后续作者会一一跟大家带来。

    好了,这里是公众号:“最后一个bug”,一个为大家打造的技术知识提升基地。同时非常感谢各位小伙伴的支持,我们下期精彩见!

 


【收藏】get这些技巧,HardFault_Handler排查只需要几分钟

 

 

1、聊一聊

 

    今天跟大家推荐的这首歌曲挺有意思的,特别是副歌部分记得加入歌单。

    这篇文章主要是跟大家介绍几种比较使用的方法,用于排除stm32硬件fault,其实bug菌不太喜欢讲一些针对某款固定芯片的特有技术,这里仅仅只是以stm32为例讲解一下如何去处理此类问题,其他芯片效仿即可。

 

2、仿真定位排查

1

提出问题

    对于使用stm32有一段时间的小伙伴,应该都认识HardFault_Handler了吧,可能大部分都是在程序挂掉了以后才知道有这个中断服务函数,所以大家一般就把它与程序死机困在了一块,原因是里面写了一个死循环。

 

图片

 

    其实所谓的异常中断都是通知用户来完成响应操作,在开发阶段一旦有异常触发大部分都会在此处stop,然后观察系统的各状态寄存器等来排查异常触发点,而在实际的产品阶段此处应该是系统软件发生异常的最后补救点。

    不过很多小伙伴都没有去修改官方的中断服务例程,当异常发生一脸懵逼不知道该如何下手排查问题,于是便拿jlink一步一步的仿真调试来查看程序在哪个点开始进入异常中断,如果程序不大可能还好排查;如果程序稍微大一点,花个几天时间应该大有人在,这就大大影响了开发的效率。

    同时一些产品被封起来根本无法使用仿真器,而且有些fault可能非常难以复现,所以必须得使用一种自动检测的手段来排查问题。

 

2

理论依据

    既然bug菌要在这里跟大家介绍一些实用的方法,就一定得有一些理论基础,如果大家对下面的这些知识不熟悉还得好好补充一下:(这里以Cortex-M3为例)

需要补充的知识点:
  • Cortex-M3相关寄存器的作用;

  • Cortex-M3的双堆栈机制;

  • 从用户模式到中断服务例程寄存器的入栈和出栈机制等;

  •  fault 分类以及各自诱因等。

    以上这几点知识大家都可以在<Cortex-M3权威指南>上找到,并且非常的通俗易懂,bug菌就不在这里重复"造轮子"了,如果有些小伙伴平时使用的并不是stm32,所需补充的知识其实大同小异,找到对应芯片内核参考手册,然而根据如上的几个方面进行分析即可。

 

3

排查原理

   对于常规的仿真器调试一旦程序进入HardFault_Handler,那么程序便卡在了中断服务程序中的死循环中,不太熟悉内核的小伙伴一定希望"如果仿真器能够有程序倒退功能该多好呀",也就说大部分的硬件异常只需找到主程序的进入点基本上就能定位具体的fault原因。

    可惜的是目前仿真器并没有此类功能,程序是一直往下执行的,对于开发人员倒是可以通过编程让程序回到入口点,不过处理相对比较麻烦,不过我们可以通过程序运行的各个状态推导出之前的程序的运行状态。

    然而异常中断本质上和普通的定时器中断等等并无差别,那么在中断触发前必然是要保存现场,运行完中断服务函数以后需要恢复现场,然后继续运行之前的程序,同样当触发HardFault同样需要保存现场,那么完全可以根据系统所保存的现场信息推导出进入异常的入口点。

    那么所有的问题都归结到触发中断系统是如何保存中断现场,要回答这个问题大家得看看上面所提到的几点知识。那么下面作者就以两个开源项目中该部分的处理为大家简单介绍一下如何排查fault。

 

3、RTT中的处理

    RTT系统中对HardFault_Handler进行了比较详细的处理,基本上可以把这块参考过来,下面bug菌画了个流程图方便大家阅读:(如下图所示)

 

图片

代码概要分析:
1、代码碎片1

图片

    上面代码实现的是流程图左半部分,很多小伙伴发现r0~r3、r12,lr,pc等等几个寄存器并没有入栈,其实这几个寄存器是硬件上自动压入堆栈中了,不需要我们手动压入。

    为什么需要判断MSP和PSP呢 ? 这个问题大家可以参考<Cortex-M3权威指南>里面的双堆栈机制,一般在RTOS中任务中使用的PSP,而中断中使用的是MSP,但我们进入中断服务函数以后其堆栈指针变成了MSP,为了能够获得任务状态下产生的异常,我们需要找到之前的PSP然后获得其自动入栈的寄存器数据来进行分析,自动入栈的PC和LR都是我们用来定位异常前程序位置的重要寄存器。特别是LR是调用子程序时存储返回地址,从而可以定位发生异常的位置。

2、代码碎片2

 

图片

    

    上图是调用的异常处理函数,其中参数来自r0寄存器的传递(可以查找ARM的函数调用传参形式),那么这个结构体指针参数应该是与入栈寄存器是一一对应的(如下图所示),这样我们便可以通过该指针获得相应的寄存器数据并打印出来,这样对于一些不能使用仿真器的场合是再好用不过了。

 

图片

 

3、代码碎片3

图片

    该部分的处理就是所画流程图的右侧部分实现,其中hard_fault_track函数中主要就是根据具体的每种fault类型寄存器分析fault的原因(其中每个寄存器中的每个位代表什么故障原因都在权威指南中有详细说明)。

 

图片

    

    好了,那么RTT中对HardFault_Handler的处理Bug菌就讲到这里,其输出的相关信息,通过把源程序仿真查看汇编与C的映射栏进行定位异常前的代码位置,进而进一步分析代码。如下图Keil中的汇编与C映射窗口,可以通过直接查找Code地址来定位C代码。

 

图片

 

4、开源故障诊断-"CmBacktrace"

    cmbacktrace是amink开源的一个ARM Cortex-M 系列 MCU 错误追踪库,其可以支持stm32不同系列的内核fault分析,同时也支持不同的RTOS分析,比如RTT,FreeRTOS,Ucos。

      github地址 : https://github.com/armink/CmBacktrace

 

 

图片

   

    通过包含如上几个文件即可加入到对应的项目工程用于分析故障,其具体的实现思路是与RTT类似的,这里就不具体分析了。

    不过相对功能比较丰富,比如输出错误现场的函数调用栈,也可以在正常状态下使用该库,获取当前的函数调用栈,从而可以更加详细的了解程序运行情况,大家可以参考学习顺便可以了解一下Cortex内核的相关知识,作者也简单的跑了一下例程,其运行结果如下:

 

图片

   

5、最后小结

    对于硬件异常故障的排查bug菌就介绍这么多了,出现硬件异常问题大部分的小伙伴都是由于编码不规范、代码的容错机制不够强大导致的,比如数组越界,调用空指针,堆栈溢出等等常见问题,所以大家在调试阶段可以把断言用上便于开发。

    好了,这里是公众号:“最后一个bug”,一个为大家打造的技术知识提升基地。同时非常感谢各位小伙伴的支持,我们下期精彩见!

   如果有想加入公众号群聊共同讨论技术的小伙伴可以添加下方bug菌微信

 

推荐好文  点击蓝色字体即可跳转

C语言数值常量的“那些事”(细节分析)图片

【C进阶】这种地方别再强制类型转化了,来告诉你个小技巧!图片

goto关键字你不知道的"那些事"(C语言提升)图片

【重磅】剖析MCU的IAP升级软件设计(设计思路篇)

【硬壳】C程序里面嵌点"机器码"玩一玩"(小知识揭露大道理)

【典藏】自制小型GUI界面框架(设计思想篇)

【C进阶】听说用 “ 逗号表达式 ” 仅仅为了秀技?

深度剖析"bit序"与"字节序"

【进阶】嵌入式编程技法之"数据驱动编程"

 

 

推荐好文  点击蓝色字体即可跳转

 

【硬壳】C程序里面嵌点"机器码"玩一玩"(小知识揭露大道理)

 

顿悟,神秘的register关键字(C语言篇) 图片

 

【典藏】深度剖析单片机程序的运行(C程序版) 

 

【连载】通过"库文件"学单片机驱动编程(5)-完结篇

 

☞ C语言为什么一般不在.h中定义函数或者变量?(精华)

 

手把手教你写Modbus-RTU协议(理论篇)

 

深度剖析"bit序"与"字节序"(追思永念)

 

嵌入式编程之动态接口技术(经验干货)

 

【典藏】自制小型GUI界面框架(设计思想篇)

 


有没有类似CmBacktrace(错误追踪库)的软件
 
通义3-235B
 
 
已深度思考

根据现有资料,CmBacktrace作为专为ARM Cortex-M系列MCU设计的错误追踪库,其核心优势在于自动化诊断、多平台支持及开源特性13。尽管搜索结果未直接提及同类竞品,但结合行业实践和功能需求,可推测以下潜在替代方案或互补工具:

1. Segger SystemView

  • 功能定位:实时系统分析工具,支持嵌入式系统运行状态的可视化追踪,包括任务切换、中断触发及异常处理流程。
  • 优势场景:适用于需要深度分析任务调度、时序问题及系统级异常的场景,尤其适合与CmBacktrace配合使用,前者侧重运行时监控,后者专注错误诊断。
  • 兼容性:支持ARM Cortex-M系列及多种RTOS(如FreeRTOS、embOS)1

2. Percepio Tracealyzer for FreeRTOS

  • 功能定位:专为FreeRTOS设计的可视化调试工具,提供任务、队列、信号量等资源的交互图谱,辅助定位死锁、优先级翻转等问题。
  • 优势场景:针对FreeRTOS用户,可与CmBacktrace形成互补,前者分析系统行为,后者直接定位代码级错误。
  • 扩展性:支持自定义事件跟踪,需配合日志输出接口实现3

3. ARM Mbed OS内置诊断模块

  • 功能定位:Mbed OS框架集成的错误处理机制,包含断言检查、故障寄存器解析及堆栈回溯功能。
  • 优势场景:适合已采用Mbed OS的项目,无需额外集成第三方库,但灵活性和定制化程度可能弱于CmBacktrace1
  • 局限性:主要面向Mbed生态,对裸机或非Mbed项目支持有限。

4. 开源项目:CrashCatcher

  • 功能定位:轻量级错误捕捉库,支持ARM Cortex-M架构,提供故障寄存器快照、堆栈回溯及日志存储功能。
  • 优势场景:适用于资源受限场景,代码体积小,但社区活跃度和文档完善度可能不及CmBacktrace。
  • 配置复杂度:需手动适配MCU型号及编译器,依赖开发者对底层机制的理解4

5. 商业工具:IAR Embedded Workbench C-SPY Debugger

  • 功能定位:集成于IAR开发环境的高级调试器,支持硬件断点、实时变量监控及异常分析。
  • 优势场景:适合IAR用户,提供与IDE深度整合的调试体验,但缺乏CmBacktrace的跨平台适配能力(仅限IAR环境)2

选择建议

  • 开源优先:若需免费且灵活的解决方案,可优先考虑CmBacktrace本身,因其已覆盖主流MCU、编译器及RTOS,并提供详尽的文档和社区支持14
  • 商业工具补充:对于追求商业级支持或特定生态(如Mbed、IAR)的团队,可结合上述商业工具提升效率,但需权衡成本与定制化需求。
  • 混合方案:将CmBacktrace与SystemView/Tracealyzer结合使用,实现从代码级错误到系统级行为的全链路分析。

建议进一步查阅GitHub或嵌入式开发者社区(如RT-Thread论坛)获取更多开源项目的更新动态,以匹配具体项目需求34


CmBacktrace(错误追踪库)应用介绍
GaoShengLv5

关于HardFault调试方法可以移步至
https://dadeju.github.io/2025/04/19/HardFault%E5%B8%B8%E8%A7%81%E8%B0%83%E8%AF%95%E6%96%B9%E6%B3%95/
根据故障寄存器信息来定位故障原因及故障代码地址,虽然这样能解决一小部分问题,但是重复的、繁琐的分析过程也会耽误很多时间。而且对于一些复杂问题,只依靠代码地址是难以解决的,必须得还原错误现场的函数调用逻辑关系。虽然连接仿真器可以查看到的函数调用栈,但故障状态下是无法显示的
尤其是下面这种,极难复现的问题
图片1

使用CmBacktrace能够很好的应对这类场景,可以将错误信息输出到控制台上,CmBacktrace 输出的信息包括函数调用栈、故障诊断结果、堆栈、故障寄存器及产品固件信息,极大的提升了错误定位的效率及准确性。

移植过程

1.添加库文件到工程
把CmBacktrace项目文件download到本地https://github.com/armink/CmBacktrace
图片2

需要注意的是fault_handler目录下的cmb_fault.S文件需要和自己使用的平台对应
图片3

打开C99支持
图片4

配置cmb_cfg.h文件

图片5

注释之前的HardFault_Handler函数

图片6

初始化cm_backtrace

cm_backtrace_init(“KeyInterrupt”, “V1.0”, “V1.0”);

在\CmBacktrace-1.4.1\demos\non_os路径下找一个主动触发硬件错误的demo
图片7

C
void fault_test_by_div0(void) {
volatile int * SCB_CCR = (volatile int *) 0xE000ED14; // SCB->CCR
int x, y, z;

*SCB_CCR |= (1 << 4); /* bit4: DIV_0_TRP. */

x = 10;
y = 0;
z = x / y;
printf("z:%d\n", z);
}
 

甚至能提示HardFault的原因
图片8

根据下面的提示,在MDK-ARM\Objects目录下运行:

C
addr2line -e KeyInterrupt.axf -a -f 0800157c 080016fa 

addr2line是一个能将地址映射到源代码位置的工具(函数名 + 文件 + 行号)
-e KeyInterrupt.axf 指定带有调试信息的 ELF/AXF 文件
-a 显示地址(Address)本身
-f 显示函数名(Function)

使用 addr2line 命令,查看函数调用栈详细信息,并定位错误代码
图片9


 

CmBacktrace: ARM Cortex-M 系列 MCU 错误追踪库

中文页 | English

  

0、CmBacktrace 是什么

CmBacktrace (Cortex Microcontroller Backtrace)是一款针对 ARM Cortex-M 系列 MCU 的错误代码自动追踪、定位,错误原因自动分析的开源库。主要特性如下:

  • 支持的错误包括:
    • 断言(assert)
    • 故障(Hard Fault, Memory Management Fault, Bus Fault, Usage Fault, Debug Fault)
  • 故障原因 自动诊断 :可在故障发生时,自动分析出故障的原因,定位发生故障的代码位置,而无需再手动分析繁杂的故障寄存器;
  • 输出错误现场的 函数调用栈(需配合 addr2line 工具进行精确定位),还原发生错误时的现场信息,定位问题代码位置、逻辑更加快捷、精准。也可以在正常状态下使用该库,获取当前的函数调用栈;
  • 支持 裸机 及以下操作系统平台:
    • RT-Thread
    • UCOS
    • FreeRTOS(需修改源码)
  • 根据错误现场状态,输出对应的 线程栈 或 C 主栈;
  • 故障诊断信息支持多国语言(目前:简体中文、英文);
  • 适配 Cortex-M0/M3/M4/M7 MCU;
  • 支持 IAR、KEIL、GCC 编译器;

1、为什么选择 CmBacktrace

入门新人 :对于从 C51 、MSP430 等简单单片机转而使用更加复杂的 ARM 新人来说,时不时出现的 "hard falut" 死机会让新人瞬间懵掉。定位错误的方法也往往是连接上仿真器,一步步 F10/F11 单步,定位到具体的错误代码,再去猜测、排除、推敲错误原因,这种过程十分痛苦。

熟练老手 :慢慢的大家知道可以通过故障寄存器信息来定位故障原因及故障代码地址,虽然这样能解决一小部分问题,但是重复的、繁琐的分析过程也会耽误很多时间。而且对于一些复杂问题,只依靠代码地址是无法解决的,必须得还原错误现场的函数调用逻辑关系。虽然连接仿真器可以查看到的函数调用栈,但故障状态下是无法显示的,所以还是得一步步 F10/F11 单步去定位错误代码的位置。另外,还有两种场景,

  • 1、很多产品真机调试时必须断开仿真器
  • 2、问题确实存在,但是极难被重现

所以定位这类问题就显得难上加难。

使用本库 :上述所有问题都迎刃而解,可以将错误信息输出到控制台上,还可以将错误信息使用 EasyFlash 的 Log 功能保存至 Flash 中,设备死机后重启依然能够读取上次的错误信息。CmBacktrace 输出的信息包括函数调用栈、故障诊断结果、堆栈、故障寄存器及产品固件信息,极大的提升了错误定位的效率及准确性。

俗话说,工欲善其事,必先利其器。所以有时候做事效率低的原因也许是,你会用的工具种类太少。

合作、贡献 :开源软件的发展离不开大家的支持,欢迎大家多提建议,也希望更多的人一起参与进来,共同提高 。如果觉得这个开源项目很赞,可以点击 项目主页 (Github|OSChina|Coding) 右上角的 Star ,同时把它推荐给更多有需要的朋友。

2、CmBacktrace 如何使用

2.1 演示

该演示分如下几个步骤:

  • 1、制造除零异常(IAR 工程,点击查看源码
  • 2、查看错误诊断信息
  • 3、查看函数调用栈基本信息
  • 4、通过命令行工具进入项目工程存放可执行文件的路径
  • 5、使用 addr2line 命令,查看函数调用栈详细信息,并定位错误代码

2.2 Demo

目录平台链接
\demos\non_os\stm32f10x 裸机 STM32 Cortex-M3 点击查看
\demos\os\rtthread\stm32f4xx RT-Thread STM32 Cortex-M4 点击查看
\demos\os\ucosii\stm32f10x UCOSII STM32 Cortex-M3 点击查看
\demos\os\freertos\stm32f10x FreeRTOS STM32 Cortex-M3 点击查看

2.3 移植说明

2.3.1 准备工作

  • 1、查看 \demos 目录下有没有合适自己的 Demo ,如有类似,则建议在其基础上修改
  • 2、明确操作系统/裸机平台及 CPU 平台
  • 3、将 \src 下的全部源文件添加至产品工程中,并保证源码目录被添加至头文件路径
  • 4、cmb_fault.s 汇编文件(点击查看)可以选择性添加至工程,添加后需要把项目原有的 HardFault_Handler 注释掉
  • 5、把 cm_backtrace_init 函数放在项目初始化地方执行
  • 6、将 cm_backtrace_assert 放在项目的断言函数中执行,具体使用方法参照下面的 API 说明
  • 7、如果第 4 步骤没有将 cmb_fault.s 汇编文件启用,则需要将 cm_backtrace_fault 放到故障处理函数(例如: HardFault_Handler )中执行,具体使用方法参照下面的 API 说明

2.3.2 配置说明

配置文件名: cmb_cfg.h ,针对不同的平台和场景,用户需要自自行手动配置,常用配置如下:

配置名称功能备注
cmb_println(...) 错误及诊断信息输出 必须配置
CMB_USING_BARE_METAL_PLATFORM 是否使用在裸机平台 使用则定义该宏
CMB_USING_OS_PLATFORM 是否使用在操作系统平台 操作系统与裸机必须二选一
CMB_OS_PLATFORM_TYPE 操作系统平台 RTT/UCOSII/UCOSIII/FREERTOS
CMB_CPU_PLATFORM_TYPE CPU平台 M0/M3/M4/M7
CMB_USING_DUMP_STACK_INFO 是否使用 Dump 堆栈的功能 使用则定义该宏
CMB_PRINT_LANGUAGE 输出信息时的语言 CHINESE/ENGLISH

注意:以上部分配置的内容可以在 cmb_def.h 中选择,更多灵活的配置请阅读源码

2.4 API 说明

2.4.1 库初始化

void cm_backtrace_init(const char *firmware_name, const char *hardware_ver, const char *software_ver)
参数描述
firmware_name 固件名称,需与编译器生成的固件名称对应
hardware_ver 固件对应的硬件版本号
software_ver 固件的软件版本号

注意 :以上入参将会在断言或故障时输出,主要起了追溯的作用

2.4.2 获取函数调用栈

size_t cm_backtrace_call_stack(uint32_t *buffer, size_t size, uint32_t sp)
参数描述
buffer 存储函数调用栈的缓冲区
size 缓冲区大小
sp 待获取的堆栈指针

示例:

/* 建立深度为 16 的函数调用栈缓冲区,深度大小不应该超过 CMB_CALL_STACK_MAX_DEPTH(默认16) */
uint32_t call_stack[16] = {0};
size_t i, depth = 0;
/* 获取当前环境下的函数调用栈,每个元素将会以 32 位地址形式存储, depth 为函数调用栈实际深度 */
depth = cm_backtrace_call_stack(call_stack, sizeof(call_stack), cmb_get_sp());
/* 输出当前函数调用栈信息
 * 注意:查看函数名称及具体行号时,需要使用 addr2line 工具转换
 */
for (i = 0; i < depth; i++) {
    printf("%08x ", call_stack[i]);
}

2.4.3 追踪断言错误信息

void cm_backtrace_assert(uint32_t sp)
参数描述
sp 断言环境时的堆栈指针

注意 :入参 SP 尽量在断言函数内部获取,而且尽可能靠近断言函数开始的位置。当在断言函数的子函数中(例如:在 RT-Thread 的断言钩子方法中)使用时,由于函数嵌套会存在寄存器入栈的操作,此时再获取 SP 将发生变化,就需要人为调整(加减固定的偏差值)入参值,所以作为新手 不建议在断言的子函数 中使用该函数。

2.4.4 追踪故障错误信息

void cm_backtrace_fault(uint32_t fault_handler_lr, uint32_t fault_handler_sp)
参数描述
fault_handler_lr 故障处理函数环境下的 LR 寄存器值
fault_handler_sp 故障处理函数环境下的 SP 寄存器值

该函数可以在故障处理函数(例如: HardFault_Handler)中调用。另外,库本身提供了 HardFault 处理的汇编文件(点击查看,需根据自己编译器进行选择),会在故障时自动调用 cm_backtrace_fault 方法。所以移植时,最简单的方式就是直接使用该汇编文件。

2.5 常见问题

2.5.1 编译出错,提示需要 C99 支持

点击查看教程:一步开启 Keil/IAR/GCC 的 C99 支持

2.5.2 如何查看到函数调用栈中函数的具体名称及代码行号

点击查看教程:如何使用 addr2line 工具获取函数调用栈详细信息

2.5.3 故障处理函数:HardFault_Handler 重复定义

在使用了本库提供的 cmb_fault.s 汇编文件时,因为该汇编文件内部已经定义了 HardFault_Handler ,所以如果项目中还有其他地方定义了该函数,则会提示 HardFault_Handler 被重复定义的错误。此时有两种解决方法:

  • 1、注释/删除其他文件中定义的 HardFault_Handler 函数,仅保留 cmb_fault.s 中的;
  • 2、将 cmb_fault.s 移除工程,手动添加 cm_backtrace_fault 函数至现有的故障处理函数,但需要注意的是,务必 保证该函数数入参的准备性 ,否则可能会导致故障诊断功能及堆栈打印功能无法正常运行。所以如果是新手,不推荐第二种解决方法。

2.5.4 初始化时提示无法获取主栈(main stack)信息

在 cmd_def.h 中有定义默认的主栈配置,大致如下:


#if defined(__CC_ARM)
    /* C stack block name, default is STACK */
    #ifndef CMB_CSTACK_BLOCK_NAME
    #define CMB_CSTACK_BLOCK_NAME          STACK
    #endif
    ...
#elif defined(__ICCARM__)
    /* C stack block name, default is 'CSTACK' */
    #ifndef CMB_CSTACK_BLOCK_NAME
    #define CMB_CSTACK_BLOCK_NAME          "CSTACK"
    #endif
    ...
#elif defined(__GNUC__)
    /* C stack block start address, defined on linker script file, default is _sstack */
    #ifndef CMB_CSTACK_BLOCK_START
    #define CMB_CSTACK_BLOCK_START         _sstack
    #endif
    /* C stack block end address, defined on linker script file, default is _estack */
    #ifndef CMB_CSTACK_BLOCK_END
    #define CMB_CSTACK_BLOCK_END           _estack
    #endif
    ...
#else

比如在 Keil-MDK 编译器下会默认选择 STACK 作为主栈 block 的名称,但在一些特殊平台下,项目的主栈 block 名称可能不叫 STACK,导致 CmBacktrace 无法获取到正确的主栈信息,所以在初始化时会有如上的错误提示信息。

解决这个问题一般有两个思路

  • 1、在 cmb_cfg.h 中重新定义主栈的信息,此时 CmBacktrace 会优先使用 cmb_cfg.h 中的配置信息;
  • 2、修改项目配置,如果是 Keil-MDK ,则在启动文件的开头位置,将主栈的名称修改为默认的 STACK ,其他编译器一般很少出现该问题。

2.6 视频讲解

2.7 许可

采用 MIT 开源协议,细节请阅读项目中的 LICENSE 文件内容。  //https://github.com/armink/CmBacktrace 


 

 

 

STM32 调试之栈回溯和 CmBacktrace 的使用

3 人赞同了该文章
✅作者简介:热爱科研的嵌入式开发者,修心和技术同步精进
❤欢迎关注我的知乎:对error视而不见
代码获取、问题探讨及文章转载可私信。
☁ 愿你的生命中有够多的云翳,来造就一个美丽的黄昏。
获取更多嵌入式资料可点击链接进群领取,谢谢支持!

一、引言

在 STM32 开发过程中,调试是一个至关重要的环节。当程序出现异常(如硬件错误、软件崩溃等)时,定位问题的根源往往是一项具有挑战性的任务。栈回溯(Stack Backtrace)是一种强大的调试技术,它可以帮助我们了解程序崩溃时的函数调用栈信息,从而快速定位问题所在。而 CmBacktrace 是一个专门为 ARM Cortex - M 系列 MCU 设计的开源栈回溯库,它能够方便地在 STM32 上实现栈回溯功能。

二、栈回溯原理

2.1 函数调用栈

在程序执行过程中,每个函数调用都会在栈上分配一块内存,称为栈帧(Stack Frame)。栈帧包含了函数的局部变量、返回地址、寄存器保存值等信息。当函数调用发生时,系统会将当前的上下文信息(如寄存器值)压入栈中,并跳转到被调用函数执行;当函数返回时,系统会从栈中恢复上下文信息,并跳转到返回地址继续执行。

2.2 栈回溯过程

栈回溯的基本思想是从当前的栈顶开始,逐步向上遍历栈帧,提取每个栈帧中的返回地址,从而得到函数调用的顺序。通过解析这些返回地址,我们可以知道程序在崩溃时是从哪些函数调用过来的。

三、CmBacktrace 介绍

3.1 概述

CmBacktrace 是一个轻量级的、开源的栈回溯库,专门为 ARM Cortex - M 系列 MCU 设计。它具有以下特点:

  • 支持多种 ARM Cortex - M 内核,包括 M0、M3、M4、M7 等。
  • 占用资源少,对系统性能影响小。
  • 提供简单易用的 API,方便集成到现有的项目中。

3.2 安装和配置

首先,从 CmBacktrace 的 GitHub 仓库(https://github.com/armink/CmBacktrace)下载源码。将源码中的 cm_backtrace 文件夹复制到你的 STM32 项目中。

在项目中包含 cm_backtrace.h 头文件,并进行初始化配置:

#include "cm_backtrace.h"

// 初始化 CmBacktrace
void cm_backtrace_init(void) {
    cm_backtrace_config_t config;
    config.arch = CM_BACKTRACE_ARCH_ARM_CM;
    config.max_depth = 10; // 最大栈回溯深度
    config.symbol_table = NULL; // 符号表,可用于解析函数名
    cm_backtrace_init_with_config(&config);
}

四、使用 CmBacktrace 进行栈回溯

4.1 异常处理函数中调用栈回溯

在 STM32 中,当发生硬件异常(如 HardFault、MemManageFault 等)时,会进入相应的异常处理函数。我们可以在异常处理函数中调用 CmBacktrace 进行栈回溯:

#include "cm_backtrace.h"

// HardFault 异常处理函数
void HardFault_Handler(void) {
    // 获取异常上下文信息
    cm_backtrace_exception_info_t info;
    cm_backtrace_get_exception_info(&info);

    // 执行栈回溯
    cm_backtrace_t backtrace;
    cm_backtrace_get(&backtrace, &info);

    // 输出栈回溯信息
    cm_backtrace_print(&backtrace);

    // 进入无限循环,避免程序继续运行
    while (1);
}

4.2 手动触发栈回溯

除了在异常处理函数中调用栈回溯,我们还可以在程序的其他地方手动触发栈回溯,例如在调试过程中:

#include "cm_backtrace.h"

void manual_backtrace(void) {
    // 获取当前上下文信息
    cm_backtrace_context_t context;
    cm_backtrace_get_current_context(&context);

    // 执行栈回溯
    cm_backtrace_t backtrace;
    cm_backtrace_get(&backtrace, &context);

    // 输出栈回溯信息
    cm_backtrace_print(&backtrace);
}

五、符号表解析

5.1 符号表的作用

符号表(Symbol Table)包含了程序中函数名、变量名等符号与其对应的内存地址的映射关系。通过符号表,我们可以将栈回溯得到的返回地址解析为具体的函数名,从而更直观地了解函数调用顺序。

5.2 生成和使用符号表

在 Keil MDK 中,可以通过在链接器选项中勾选 “Generate Browse Information” 来生成符号表文件(.map 文件)。然后,将 .map 文件中的符号信息提取出来,转换为 CmBacktrace 支持的格式,并传递给 cm_backtrace_init_with_config 函数:

#include "cm_backtrace.h"

// 符号表数组
const cm_backtrace_symbol_t symbol_table[] = {
    // 手动添加符号表项,格式为 {函数名, 函数起始地址}
    {"main", 0x08000000},
    {"func1", 0x08000100},
    // ...
};

// 初始化 CmBacktrace 并使用符号表
void cm_backtrace_init_with_symbol_table(void) {
    cm_backtrace_config_t config;
    config.arch = CM_BACKTRACE_ARCH_ARM_CM;
    config.max_depth = 10;
    config.symbol_table = symbol_table;
    config.symbol_table_size = sizeof(symbol_table) / sizeof(cm_backtrace_symbol_t);
    cm_backtrace_init_with_config(&config);
}

六、总结

栈回溯是一种强大的调试技术,在 STM32 开发中能够帮助我们快速定位程序崩溃的原因。CmBacktrace 作为一个专门为 ARM Cortex - M 系列 MCU 设计的栈回溯库,提供了简单易用的 API,方便我们在项目中实现栈回溯功能。通过合理使用栈回溯和 CmBacktrace,我们可以提高调试效率,减少开发周期。


CmbackTrace 移植手册与异常定位

 

移植过程

1、建议在项目中创建一个名为 SERVp_cmBackTrace 的组,用于组织和管理与cmBackTrace相关的所有文件。这有助于保持项目的结构清晰,便于管理和维护。将 对应文件夹的内容增加到项目工程中
image

2、添加文件至项目工程
将cmBackTrace相关文件夹的内容添加到项目工程中,并将其放置在 SERVp_cmBackTrace 组内。确保所有必要的源文件和头文件都已正确添加到项目中。
image

3、配置cmBackTrace
image

当前调试服务使用RTT打印,但在RTT缓冲区较小的情况下,可能会导致数据被覆盖或打印不完整。

为了解决这一问题,建议采用IAR的Semihosting方式来输出HardFault异常信息。

经过初步测试,使用Semihosting方式进行异常信息打印表现良好。鉴于异常信息打印的频率不高,且对实时性的要求不高,Semihosting是一个合理的选择。经过测试,该方案能够成功定位到引发异常的函数。

开启Semihosting

image

4、对 freeRTOS 继续修改
1、注释掉原有的HardFault处理函数
在FreeRTOS中,需要注释掉原有的HardFault_Handler处理函数,以便新的异常处理逻辑能够生效。
image

1、修改 task.ctask.c文件配置修改。
(1)在FreeRTOS/tasks.c文件中新增uxSizeOfStack字段。
(2)同时添加了vTaskStackAddr()、vTaskStackSize()和vTaskName()三个辅助函数,用于获取当前任务的堆栈地址、大小以及名称。
image

点击查看代码
 	/*------------------------------------------------------------------------*/
    /*< Support For CmBacktrace >*/
    #else
        UBaseType_t     uxSizeOfStack;          /*< Support For CmBacktrace >*/
/*------------------------------------------------------------------------*/

image

点击查看代码
/*-----------------------------------------------------------*/
/*< Support For CmBacktrace >*/
uint32_t * vTaskStackAddr()
{
    return pxCurrentTCB->pxStack;
}

uint32_t vTaskStackSize()
{
    #if ( portSTACK_GROWTH > 0 )
    
    return (pxNewTCB->pxEndOfStack - pxNewTCB->pxStack + 1);
    
    #else /* ( portSTACK_GROWTH > 0 )*/
    
    return pxCurrentTCB->uxSizeOfStack;
    
    #endif /* ( portSTACK_GROWTH > 0 )*/
}

char * vTaskName()
{
    return pxCurrentTCB->pcTaskName;
}
/*-----------------------------------------------------------*/

2、修改prvInitialiseNewTask函数
在static void prvInitialiseNewTask函数中,
新增初始化uxSizeOfStack字段的代码,确保每个新创建的任务都能正确记录其堆栈大小。

pxNewTCB->uxSizeOfStack = ulStackDepth; /< Support For CmBacktrace >/

image

3、修改 FreeRTOS.h 文件配置
最后,在FreeRTOS.h文件中的typedef struct xSTATIC_TCB定义里,根据堆栈增长方向的不同,有条件地添加uxSizeOfStack字段。

点击查看代码
#if(portSTACK_GROWTH <= 0)
    UBaseType_t     uxSizeOfStack;      /*< Support For CmBacktrace >*/
#endif /* ( portSTACK_GROWTH > 0 )*/

![image](https://img2024.cnblogs.com/blog/2327000/202505/2327000-20250509074309926-1045479887.png)

测试Cmbacktrace效果

测试环境设置

为了验证cmBacktrace的有效性,在一个特定的线程 BLLm_RTOS_legalSysThread 中调用了一个故意构造的 mcmcpy 异常错误函数。该函数将触发HardFault异常,以便测试cmBacktrace的异常定位能力。

image

image

配置调试环境:

进入仿真环境
打开调试器的 Terminal IO 窗口,以便查看异常信息的输出。

image

触发异常:

运行项目,使 BLLm_RTOS_legalSysThread 线程执行并调用 mcmcpy 异常错误函数。

观察异常信息:

在 Terminal IO 窗口中,可以看到HardFault异常发生时的详细信息。这些信息包括异常发生的上下文、堆栈跟踪等。
image

使用异常定位工具:

image

打开异常定位工具(如cmBacktrace提供的工具)。
输入从 Terminal IO 窗口中获取的异常信息。

验证定位结果:

通过异常定位工具,成功定位到了引发异常的具体位置。
此时,程序计数器(PC)的值为 0x0000a43c。
实际上,通过cmBacktrace工具,我们可以更精确地定位到异常信息的值为 0x00028040。

image

由此可见成功定位

image

测试结论

通过上述测试步骤,验证了cmBacktrace工具的有效性。
即使在PC指针值为 0x0000a43c 的情况下,cmBacktrace仍然能够成功定位到具体的异常位置 0x00028040。
这表明cmBacktrace工具在HardFault异常定位方面能够提高调试效率和准确性。

 
posted @ 2026-01-12 09:32  前沿风暴  阅读(1)  评论(0)    收藏  举报

校内网 © 2004-2026

京公网安备 33010602011771号 京ICP备2021040463号-3