一文让你彻底明白“应用工程的堆与栈”
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
应用工程的堆与栈
一、基本知识
C语言因其高效率而成为目前嵌入式微控制器(MCU)编程中使用最多的编程语言,堆栈是C语言区别于汇编语言的最大特点,也是其高效运行的基础。
1、定义
1)基本含义
事实上,堆栈包括堆(heap)和栈(s tack)两个不同的概念,只是因为其二者都占用MCU的片上RAM空间并对系统内存进行分配和管理,而被习惯性放在一起说。
在单片机应用中,堆栈是个特殊的存储区,主要功能是暂时存放数据和地址,通常用来保护断点和现场。
可以理解成堆栈是位于MCU片上RAM空间上的一个特殊存储区。
小注:
✧堆:队列优先,先进先出(FIFO—first in first out)
✧栈,先进后出(FILO—First-In/Last-Out)。
2)详解介绍
①堆(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,即压栈)和释放
(也称作pop,即出栈)的,无需用户自己维护,但需要在C工程的链接文件
中指定其大小。
✧压栈(push):函数调用和中断ISR运行之前,对当前运行函数运行时的上下
文进行保护。
✧出栈(pop):函数返回和中断返回时恢复调用函数和中断发生前函数的运行
时环境。
✧栈可以由高地址向低地址生长,也可以由低地址向高地址生长,具体与CPU
架构有关,以下为S12内核CPU异常/中断产生时的压栈情况:
2、片内RAM段划分
嵌入式MCU的片内RAM一般会被链接文件“分区”为如下几个段(section):1).bss:未初始化段
MCU启动(boot/startup)过程中会将该RAM区初始化为0;
2).data:数据段
该RAM区存放初始化值不为0的全局变量,其初始化值放置在编译结果
的.copy(Flash/EEPROM)数据区,每次MCU启动时,会将其初始值取出对.data 区进行初始化;
3).heap:堆段
该地址空间的大小在C工程的链接文件中给出,CPU会自动保留该区域,并初始化用于堆管理的指针链表。
因为嵌入式MCU的片上RAM资源都非常小,是十分宝贵的资源,而使用heap对RAM空间进行动态管理效率极低,所以在嵌入式编程中极少使用heap,默认的嵌入式MCU C语言应用工程中是没有.heap段的;
4).stack:栈段
该地址空间的大小在C工程的链接文件中给出,CPU会自动保留该区域,不对其进行任何初始化,但在进入C语言main () 函数之前必须将.stack的起始地址(stack的最小地址或最高地址,也称为栈顶—stack top,具体取决于该CPU 架构的栈生长方式)赋值给CPU的栈指针寄存器SP(stack pointer),该过程也被称为堆栈初始化;
常见的嵌入式C语言应用工程各数据段、代码段和堆栈的分配如下图所示:
2、片内Flash/EEPROM段划分
放在Flash/EEPROM等NVM(Non-VolatileMemory—非易失性存储器)中的默认段包括:
1).text: 代码段
用于存放C应用工程中所有C函数代码的编译结果,比如启动函startup,main函数等;
2).copy:拷贝段
用于存放.data段的初始化值;
3).const:常量段
用于存放工程中使用const修饰定义或者#define定义的常量;
4)interrupt vector table:中断向量表
用于存放包含默认复位向量在内的内核CPU异常和外设中断向量表,其为内核CPU异常或者外设中断的中断服务函数ISR地址数组;
二、应用工程栈大小的确定
由上述stack的用途可知,一个嵌入式C语言应用工程所需的栈大小与其函数调用层数以及是否有中断嵌套密切相关。
函数调用层级越多,中断嵌套越多,函数局部变量越多,函数的形参越多其stack消耗也就越多。
1、栈的内存分配
嵌入式MCU软件开发集成环境(IDE)中的链接器(linker)会根据工程的链接文件(linkerfile)分配stack大小的地址范围,在工程编译生成的map(内存映射)文件中能够看到stack占用具体RAM地址范围;当然在map文件中一般也可以看到工程中各函数的调用关系,从而可以分析出工程的最大函数调用层级,然后debug工程。
在该最大调用函数中设置一个断点,观察CPU的SP寄存器值,用该值与栈顶相减即可得到该工程函数调用所需的最大stack空间,在该值的基础上考虑中断嵌套,再增加相应的中断嵌套所需的stack消耗,即可估计出整个工程运行时所需的stack大小。
如果某个函数中使用了大量的局部变量,那可能包含该函数的调用嵌套才是整个工程的“关键路径”,而非真实调用层级最多但不包含该函数的”关键路径”,一般建议再增加一定字节的stack作为系统裕量。
2、map文件的划分
以下为基于一个S12XEP100的实际CodeWarrior5.1 IDE工程map文件的分析:
三、堆栈溢出定义、危害以及应对措施
1、定义
基于以上对栈的分析,可知
堆栈溢出是指随着程序的运行,栈的使用超出了工程配置时在链接文件中给其分配的空间大小,而内核CPU又未对其进行检查和限制,从而使用相邻的其他RAM段(比如全局变量所在.data段或者.bss段),从而导致的栈修改全局变量或者全局变量修改栈内容的问题。
2、危害
由于堆栈上保存了内核CPU运行的关键数据,所以其溢出的危害十分严重,具体如下:
1)栈数据覆盖全局变量:
全局变量意外修改
①被修改全局变量为程序if,while,for,switch语句判断条件,导致程序运
行出错,功能异常;
②被修改全局变量为指针地址或数组索引变量,非法操作/修改系统数据,比
如外设配置寄存器,导致外设工作异常;
对全局变量的修改改变了栈上的数据
①影响栈上保存的调用函数/中断发生前函数的局部变量,数据意外修改,函
数运行异常,功能异常;
②影响栈上保存的函数返回地址(PC寄存器),返回到不确定的地址运行,
导致功能异常甚至死机非法地址复位等;
③影响栈上保存的原函数堆栈指针(SP寄存器),返回后数据(局部变量和全
局变量)操作异常,导致功能失效;
④影响栈上保存的调用函数运行时的内核CPU状态(CCR寄存器),函数判断
语句运行错误(数学逻辑计算结果—N/Z/V/C-bit)、全局中断意外禁止/打开
(I-bit)、低功耗进入意外允许/禁止(S-bit),导致程序跑飞、程序锁死、无法进入低功耗等;
2)基于以上分析,建议在开发嵌入式应用工程代码时遵循以下规则以防止堆栈溢出:
函数参数最好不要超过3个,如果要传递更多的参数,请使用全局变量、指针、数据或结构体;
不要定义过大的局部变量,建议最好保证每个函数的局部变量不大于10个字节,若大于10个字节,尽量使用全局变量;
慎用递归函数;
外设中断嵌套不宜过多,能不用最好不用,要用最多运行3级中断优先级嵌套,并在估计工程stack使用量时将最大嵌套可能性考虑在内;
使用数据指针修改内存时,必须相对其赋值,且不能指向stack区,否则可以造成stack意外修改(保存在stack上的函数返回地址,CPU运行状态CCR寄存器或者影响函数运行的局部变量),从而导致程序跑飞;
若使用了uCOS-III或者FreeRTOS等时时操作系统,使能其内核的堆栈溢出检查功能钩子函数(hook function);下图为MPC5748G SDK(S32DSfor Power V1.2 IDE)中提供的FreeRTOS配置:
如果条件允许,购买使用专业的代码运行时分析工具,比如IAR的C-STAT等对应用工程进行分析评估;。