,,,,
(1.齐鲁工业大学(山东省科学院),济南 250353;2.山东省科学院自动化研究所;3.山东省汽车电子技术重点实验室)
面向特定应用的嵌入式系统一般会根据实际需求选择规格适中的MCU,采用C语言进行软件开发。在MCU的地址空间中,RAM是一段连续分配的线性空间,全局变量、堆(Heap)、堆栈(Stack)都分配在这段有限的线性空间内,根据实际需要,还可能把FLASH中一段代码重定位到一段RAM空间内运行,以加快程序运行速度[1],提高系统实时性。
由于RAM资源有限,不可能为堆栈分配太大的尺寸,而且,作为一种灵活性很强的高级编程语言,C语言采用线性寻址方式访问RAM空间,堆栈溢出时会继续访问临近堆栈的RAM空间。堆栈尺寸设置过小、局部变量尺寸定义过大、中断优先级设置不合理、中断服务程序过长导致中断嵌套、递归调用、函数调用层次过深等程序设计不当之处都可能导致堆栈溢出,改变临近堆栈的RAM空间中的内容,从而造成程序运行异常[2],发生故障甚至导致重大事故。
通过静态分析方式确定堆栈空间的尺寸时,需要根据源程序中每个函数的局部变量大小确定每个函数的堆栈使用量[3],然后根据编译器生成的函数调用列表为每个函数建立调用树,检查每棵调用树,确定从树根到树叶的调用路径的堆栈使用量,从中选出最大堆栈使用量,同时,还要仔细分析系统用到的所有中断,确定中断服务程序的堆栈使用量。但是,无法得知C标准库函数以及大值整数的乘除、浮点运算等对应的运行库函数的堆栈使用量,这种静态分析方式对开发者的技术水平、对产品代码的理解程度要求非常高,得到的数据并不完善,而且这种方式依赖于具体的应用和源程序实现方式,缺乏通用性。
本文设计了一种检测嵌入式软件堆栈溢出及使用量的方案[4],在不影响系统正常运行的情况下,在不受堆栈溢出影响的定时器中断服务程序中,周期检测堆栈使用量,通过控制LED提示堆栈溢出情况。设置了堆栈溢出缓冲区,隔离堆栈和全局变量分区,堆栈溢出后部分上下文信息被存放在堆栈溢出缓冲区中,将最大堆栈使用量和系统发生堆栈溢出后的堆栈溢出缓冲区数据存入非易失性存储器。系统在实际环境中运行一段时间后,通过查看LED状态以及读取非易失性存储器中的数据,便可以判断堆栈使用情况,通过保存的溢出上下文数据可以分析程序异常位置,从而调整堆栈尺寸或者调整程序设计,以提高系统运行的稳定性。
堆栈的生长方向为自上而下,即向着RAM地址减小的方向增长。如果把堆栈空间设置在RAM的底部,堆栈溢出时会访问不存在的RAM空间,造成代码跑飞,这时无法得到溢出时的上下文数据,也无法对后续的程序修改提供有用信息,所以,在划分RAM空间时,需要将堆栈空间设置在RAM空间的顶部。在链接文件中,按照从顶部到底部的顺序,将RAM空间划分为堆栈区、堆栈溢出缓冲区和全局变量区,根据需要和MCU RAM空间尺寸,还可能设置有代码重定位区[5]。具体划分如图1所示。
图1 RAM空间划分图
在RAM顶部设置大小为STACK_SIZE的堆栈区,STACK_SIZE根据实际应用设置,留有一定的余量,同时受限于RAM资源,STACK_SIZE不能设置过大,栈底为堆栈区的最大地址,记为STACK_BOTTOM,设置为MCU RAM空间的最大地址;紧邻堆栈区,设置尺寸为100字节大小的堆栈溢出缓冲区;紧邻堆栈溢出缓冲区,设置尺寸为APP_RAM_SIZE的全局变量区,MCU的RAM尺寸记为RAM_SIZE。STACK_SIZE、APP_RAM_SIZE和RAM_SIZE的关系为:RAM_SIZE≥STACK_SIZE+100+APP_RAM_SIZE。
堆栈溢出缓冲区位于堆栈和全局变量区之间,可以起到隔离堆栈和全局变量区的作用,当堆栈溢出时,如果溢出深度小于堆栈溢出缓冲区的尺寸,则不会影响全局变量,全局变量的变化也不会改变堆栈内容。
在堆栈溢出缓冲区中定义一个包含100个单字节元素的数组,记为Stack_overflow_buf[100],堆栈溢出时,该数组会存放一部分上下文数据,为后续的程序分析提供关键信息。
系统运行期间,堆栈操作会改变堆栈区数据,堆栈溢出会改变Stack_overflow_buf数据,通过检查堆栈区数据和Stack_overflow_buf,便可以得知系统对堆栈的实际消耗。堆栈溢出可能会改变程序计数器的数值,如果把堆栈检测函数放在中断服务程序之外的其它位置,程序可能无法运行堆栈检测函数,而即使发生了堆栈溢出,中断也会触发MCU进入中断服务程序,因此,将堆栈检测函数放在定时器中断服务程序中。
MCU上电初始化时,将堆栈指针SP初始化为STACK_BOTTOM,堆栈区数据和数组Stack_overflow_buf全部初始化为0x55,最大堆栈使用量记为Stack_size_max,初始化为0,然后开启一个周期为50 ms的定时器。在定时器中断服务程序中,读取堆栈溢出缓冲区和堆栈区的数据,判断堆栈使用情况。
具体方法为:
读取次数记为Read_times,初始化为0,以堆栈溢出缓冲区初始地址为首地址,以堆栈栈底为末地址,循环读取各个RAM地址上的数据,如果读取到的数据等于0x55,读取地址加1,读取次数加1,如果读取到的数据不等于0x55,跳出RAM读取循环。
根据当前最大堆栈使用量=STACK_SIZE+100-Read_times,如果当前最大堆栈使用量大于Stack_size_max,将当前最大堆栈使用量赋值给Stack_size_max,存入非易失性存储器,如果Stack_size_max小于或等于STACK_SIZE,不再进行处理,等待下一次定时中断。否则,判断为堆栈溢出,将数组Stack_overflow_buf的数据作为溢出上下文,存入非易失性存储器;进一步根据Stack_size_max判断是深度溢出还是浅度溢出,如果Stack_size_max小于(STACK_ZIZE + 100),此时的堆栈溢出不会影响全局变量区,不会造成系统运行异常,从而点亮LED进行提示。如果Stack_size_max等于(STACK_ZIZE + 100),此时的堆栈溢出会影响全局变量区,造成系统运行异常,闪烁LED灯进行提示。此时,通过专用设备可以读取存储在非易失性存储器中的最大堆栈使用量和Stack_overflow_buf。根据Stack_overflow_buf数据判断堆栈溢出位置,修改程序设计或者增加堆栈空间的大小。 堆栈溢出检测算法流程图如图2所示。(说明:定时器中断判断分支的“N”分支与本文内容无关,所以未列出。)
图2 堆栈溢出检测算法流程图
[1] 高源,罗秋凤.基于DSP28335程序移植方法的研究与实现[J].电子测量技术,2013,36(3):84-88.
[2] 北京空间飞行器总体设计部.一种适用于多任务软件进程堆栈使用深度检测的方法:中国,201610080939.2 [P].2016-2-14.
[3] 张西超,郭向英.一种用于分析MCS-51目标码堆栈深度的方法[J].空间控制技术与应用,2010,36(2):47-50
[4] 山东省科学院自动化研究所.嵌入式软件堆栈溢出检测方法和装置:中国,201710997905.5[P].2017-10-11.
[5] 山东省科学院自动化研究所.CAN报文滤波解析方法、系统及电子控制单元:中国,201710743658.5 [P].2017-8-25.
马建辉(工程师),研究方向为嵌入式与汽车电子。