汽车电子expert成长之路

本博客发布的个人原创精品----嵌入式系统技术文章,欢迎大家参考学习,并转发分享!

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

0
阅读(3652)

内容提要

概述与案例分析

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异常/中断产生时的压栈情况:

2.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语言应用工程各数据段、代码段和堆栈的分配如下图所示:

 

3.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文件的分析:

4.jpg

5.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工程进行堆栈调试。

 

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

u  被修改全局变量为

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

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

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

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

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

7.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配置:

8.jpg


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


  胡恩伟

NXP汽车电子FAE


若对本文观点有任何意见和建议欢迎留言指出。

如果喜欢,可以关注本人公众号,阅读更多精彩内容。

1-1.jpg