浅谈嵌入式 MCU 软件开发之应用工程的堆与栈

16 篇文章 1 订阅

转载自:http://huxiongwei.spaces.eepw.com.cn/articles/article/item/136717

 

内容提要

 

概述与案例分析

1. 嵌入式 C 语言应用工程中堆栈的定义和作用

2. 嵌入式 C 语言应用工程的栈大小确定

3. 嵌入式 C 语言应用工程的堆栈溢出定义、危害以及应对措施

 

概述与案例分析

过去工作中,我经常遇到客户非常着急的打电话给我说他们基于MCU的ECU产品工作异常----典型如功能异常,但复位(POR上电复位或者外部RESET管脚复位)之后功能恢复正常,而且这种异常故障往往很难复现,发生概率大概在万分之一以下。

一个具体的真实案例如:某客户使用我们的S12G128开发了一款BCM控制器,当其在整车上工作时,遇到仪表控制器通过CAN总线BootLoader在线更新程序时会造成BCM控制的4个车窗失控(不断升级仪表盘应用工程,大概30~40次才能复现一次这样的异常故障),现场调试使用BDM调试器热同步(hotsync)(具体方法请参考本公众号的后续文章--CodeWarrior IDE使用tips之bug定位绝技--hotsync与attach调试)连接查看发现其MCU控制4个车窗的端口方向寄存器被意外修改为输入了,从而不受端口输出数据寄存器的控制导致车窗控制失效。然而检查客户的整个应用工程代码,只有对该端口方向寄存器的输出配置,无输入配置代码。最后检查发现其BCM的MCU中基于CAN总线的UDS诊断协议栈中有个函数使用了有个768字节的局部变量,将其定义为全局变量后,问题完美解决(具体原因分析见后文)。

遇到这类似的情况,我一般都会建议客户检查工程的堆栈设置,将应用工程的栈(stack)增大或者优化应用工程以解决问题。由此可见,正确的理解C语言应用工程中的堆栈工作原理对我们编写嵌入式软件保证ECU正常工作意义重大。

接下来,我就给大家仔细分析一下嵌入式MCU软件开发之应用工程的堆与栈:

一、嵌入式C语言应用工程中堆栈的定义和作用

C语言因其高效率而成为目前嵌入式微控制器 (MCU) 编程中使用最多的编程语言,堆栈是C语言区别于汇编语言的最大特点,也是其高效运行的基础。事实上,堆栈包括堆(heap)和栈(stack)两个不同的概念,只是因为其二者都占用MCU的片上RAM空间并对系统内存进行分配和管理,而被习惯性放在一起说。

堆(heap)是用于动态分配内存的RAM区域,heap的空间是用户手动申请和释放的:C语言中的malloc(size), calloc(num, size)函数分配heap,释放使用free(*heap)函数;

栈(stack)用于分配函数临时变量和在函数调用或中断产生时保存内核CPU运行时上下文(run time context—包括内核CPU通用寄存器、SP和PC寄存器以及状态寄存器(如S12内核的CCR寄存器,存放内核CPU的计算状态)以及函数当前的临时变量)以及函数参数传递;其占用的RAM大小与CPU架构(不同的内核CPU其位宽和CPU寄存器数量各不相同)和编译器采用的嵌入式应用程序二进制接口(Embedded Application Binary Interface)有关。栈(Stack) 是CPU根据程序运行需求自动分配 (也叫push,压栈:函数调用和中断ISR运行之前,对当前运行函数运行时上下文进行保护) 和释放(也称作pop,出栈:函数返回和中断返回时恢复调用函数和中断发生前函数的运行时环境) 的,无需用户自己维护,但需要在C工程的链接文件中指定其大小。栈可以由高地址向低地址生长,也可以由低地址向高地址生长,具体与CPU架构有关;

以下为S12内核CPU异常/中断产生时的压栈情况:

1.jpg

嵌入式MCU的片内RAM一般会被链接文件“分区”为如下几个段(section):

.bss:未初始化段,MCU启动 (boot/startup)过程中会将该RAM区初始化为0;

.data:数据段,该RAM区存放初始化值不为0的全局变量,其初始化值放置在编译结果的.copy(Flash/EEPROM)数据区,每次MCU启动(boot/startup)时,会将其初始值取出对.data区进行初始化;

.stack:栈段,该地址空间的大小在C工程的链接文件中给出,CPU会自动保留该区域,不对其进行任何初始化,但在进入C语言main () 函数之前必须将.stack的起始地址 (stack的最小地址或者最高地址,也称为栈顶—stack top,具体取决于该CPU架构的栈生长方式) 赋值给CPU的栈指针寄存器SP (stack pointer) ,该过程也被称为堆栈初始化;

.heap:堆段,该地址空间的大小在C工程的链接文件中给出,CPU会自动保留该区域,并初始化用于堆管理的指针链表;因为嵌入式MCU的片上RAM资源都非常小,是十分宝贵的资源,而使用heap对RAM空间进行动态管理效率极低,所以在嵌入式编程中极少使用heap,默认的嵌入式MCU C语言应用工程中是没有.heap段的

常见的嵌入式C语言应用工程各数据段、代码段和堆栈的分配如下图所示:

2.jpg

其中放在Flash/EEPROM等NVM(Non-VolatileMemory—非易失性存储器)中的默认段包括:

.text: 代码段,用于存放C应用工程中所有C函数代码的编译结果,比如启动函数startup,main函数等;

.copy:拷贝段,用于存放.data段的初始化值;

.const:常量段,用于存放工程中使用const修饰定义或者#define定义的常量;

interrupt vector table:中断向量表,用于存放包含默认复位向量在内的内核CPU异常和外设中断向量表,其为内核CPU异常或者外设中断的中断服务函数ISR地址数组;

二、 嵌入式C语言应用工程的栈大小确定

由上述stack的用途可知,一个嵌入式C语言应用工程所需的stack大小与其函数调用层数以及是否有中断嵌套密切相关。函数调用层级越多,中断嵌套越多,函数局部变量越多,函数的形参越多其stack消耗也就越多。

嵌入式MCU软件开发集成环境(IDE)中的链接器(linker)会根据工程的链接文件(linkerfile)分配stack的大小的地址范围,在工程编译生成的map(内存映射)文件中能够看到stack占用具体RAM地址范围;当然在map文件中一般也可以看到工程中各函数的调用关系,从而可以分析出工程的最大函数调用层级;然后debug工程,在该最大调用函数中设置一个断点,观察CPU的SP寄存器值,用该值与栈顶相减即可得到该工程函数调用所需的最大stack空间;在该值的基础上考虑中断嵌套,再增加相应的中断嵌套所需的stack消耗,即可估计出整个工程运行时所需的stack大小。当然如果某个函数中使用了大量的局部变量,那可能包含该函数的调用嵌套才是整个工程的“关键路径”,而非真实调用层级最多但不包含该函数的”关键路径”。一般建议再增加一定字节的stack作为系统裕量。

以下为基于一个S12XEP100的实际CodeWarrior5.1 IDE工程map文件的分析:

3.jpg

4.jpg

关于CodeWarrior IDE的map文件细节请参考本公众号的后续文章--CodeWarrior IDE使用tips之map文件详解

目前有专门的付费代码分析工具可以帮助客户分析工程的stack消耗,发现潜在的堆栈溢出问题。

比如IAR的C-STAT C运行时代码分析(C-RUNRuntime analysis)工具ThreadX C-SPY debuggerplugin stack调试工具,当然其作为一个plugin插件只能配合IAR工作,对IAR的嵌入式C工程进行堆栈调试。

5.jpg

关于IAR的C-STAT更多详细信息请参考以下链接:https://www.iar.com/iar-embedded-workbench/add-ons-and-integrations/runtime-analysis/;

三、 嵌入式C语言应用工程的堆栈溢出定义、危害以及应对措施

基于以上对栈的分析,可知,堆栈溢出是指随着程序的运行,栈的使用超出了工程配置时在链接文件中给其分配的空间大小,而内核CPU又未对其进行检查和限制,从而使用相邻的其他RAM段(比如全局变量所在.data段或者.bss段),从而导致的栈修改全局变量或者全局变量修改栈内容的问题。

由于堆栈上保存了内核CPU运行的关键数据,所以其溢出的危害十分严重,具体如下:

 

Ø  栈数据覆盖全局变量:

n  全局变量意外修改:

u  被修改全局变量为程序if,while, for, switch语句判断条件à导致程序运行出错,功能异常;

u  被修改全局变量为指针地址(或数组索引变量)à非法操作/修改系统数据,比如外设配置寄存器,导致外设工作异常;

Ø  对全局变量的修改改变了栈上的数据:

n  影响栈上保存的调用函数/中断发生前函数的局部变量à数据意外修改,函数运行异常,功能异常;

n  影响栈上保存的函数返回地址(PC寄存器)à返回到不确定的地址运行,导致功能异常甚至死机非法地址复位等;

n  影响栈上保存的原函数堆栈指针(SP寄存器)à返回后数据(局部变量和全局变量)操作异常,导致功能失效;

n  影响栈上保存的调用函数运行时的内核CPU状态(CCR寄存器)à函数判断语句运行错误(数学逻辑计算结果—N/Z/V/C-bit)、全局中断意外禁止/打开(I-bit)、低功耗进入意外允许/禁止(S-bit),导致程序跑飞、程序锁死、无法进入低功耗等;

6.jpg

基于以上分析,我建议大家在开发嵌入式应用工程代码时遵循以下规则以防止堆栈溢出:

1.      函数参数最好不要超过3个,如果要传递更多的参数,请使用全局变量、指针、数据和结构体;

2.      不要定义过大的局部变量,建议最好保证每个函数的局部变量不大于10个字节;若大于10个字节,尽量使用全局变量;

3.      慎用递归函数;

4.      外设中断嵌套不宜过多,能不用最好不用,要用最多运行3级中断优先级嵌套,并在估计工程stack使用量时将最大嵌套可能性考虑在内”;

5.      使用数据指针修改内存时,必须相对其赋值,且不能指向stack区,否则可以造成stack意外修改(保存在stack上的函数返回地址,CPU运行状态CCR寄存器或者影响函数运行的局部变量),从而导致程序跑飞;

6.      若使用了uCOS-III或者FreeRTOS等时时操作系统,使能其内核的堆栈溢出检查功能钩子函数(hook function);下图为MPC5748G SDK(S32DSfor Power V1.2  IDE)中提供的FreeRTOS配置:

7.jpg

 

7.      如果条件允许,购买使用专业的代码运行时分析工具,比如IAR的C-STAT等对应用工程进行分析评估;

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嵌入式MCU软件开发中,中断是非常重要的一种机制,可以有效地处理硬件外设的状态变化,提高系统响应速度和效率。在中断处理过程中,中断优先级和中断嵌套是需要考虑的两个重要问题。 中断优先级是指当多个中断同时发生时,系统按照一定的优先级顺序来处理这些中断。一般来说,系统有多个硬件外设,每个外设都会产生中断,中断的优先级高低可以通过设置相应的寄存器来实现。在中断处理过程中,如果当前正在处理一个中断,而此时又有一个优先级更高的中断发生,系统会立即中断当前中断的处理,转而去处理优先级更高的中断,这就是中断优先级的作用。 中断嵌套是指在处理一个中断的过程中,又发生了另一个中断。这种情况下,系统需要先处理当前正在处理的中断,再去处理发生的新中断。为了实现中断嵌套,系统需要有一个中断嵌套的机制,一般来说,这个机制是通过设置一个中断屏蔽寄存器来实现的。当一个中断正在处理时,系统会将该中断的优先级设置为最高,然后将其他中断的优先级都设置为低于当前中断的优先级,这样可以保证当前中断处理完毕后,仍然按照优先级顺序来处理其他中断。 在实际的嵌入式MCU软件开发中,中断优先级和中断嵌套是需要根据具体应用场景来设置的。如果系统中有多个硬件外设,可以根据外设的性质和重要程度来设置中断优先级。如果系统需要支持中断嵌套,需要考虑中断处理的顺序和优先级,以保证系统的稳定性和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值