如何防止堆栈溢出(在Cortex M上)?

如果您在“大型”计算机上编程,那么您可能不会有这样的问题。 有很多堆栈会溢出它,您需要尝试。 在最坏的情况下,您可以在这样的窗口上单击“确定”,然后找出来。

图片

但是,如果您对微控制器进行编程,则问题看起来会有所不同。 首先,您需要注意堆栈已满。

在本文中,我将讨论有关该主题的我自己的研究。 由于我主要根据STM32和1986年的Milander进行编程,因此我专注于它们。

引言


让我们想象一下最简单的情况-我们编写没有任何操作系统的简单单线程代码,即 我们只有一叠。 如果您像我一样在uVision Keil中编程,则内存将以某种方式分配:



并且,如果您像我一样,认为微控制器上的动态内存是邪恶的,那么就这样:



顺便说一句
如果要禁止使用堆,可以这样做:
#pragma import(__use_no_heap_region) 

详情在这里

好啦怎么了 问题在于,Keil将堆栈直接放在静态数据区域的后面。 而且,Cortex-M中的堆栈朝着地址减少的方向增长。 当它溢出时,它只会爬出分配的内存。 并覆盖任何静态或全局变量。

如果堆栈仅在进入中断时溢出,则特别好。 或者,甚至更好,在嵌套中断中! 并且悄悄破坏了在完全不同的代码部分中使用的某些变量。 并且程序在断言时崩溃。 如果幸运的话。 球形heisenbag,带手电筒可以找一个整整一周。

立即作出保留,如果您使用堆,那么问题就不会解决,只是堆破坏了全局变量而不是全局变量。 没有更好的。

好的,问题很明显。 怎么办

MPU


最简单也是最明显的方法是使用MPU(换句话说,内存保护单元)。 允许您将不同的属性分配给不同的内存; 特别是,您可以在堆栈周围放置只读区域,并在其中写入时捕获MemFault。

例如,在stm32f407中,MPU是。 不幸的是,在其他许多“初级” stm中却不是。 在Milandrovsky 1986VE1中也不存在。

即 解决方案是好的,但并不总是可以负担的。

手动控制


编译时,Keil可以生成(默认情况下执行)带有调用图(链接器选项“ --info = stack”)的html报告。 并且此报告还提供有关使用的堆栈的信息。 Gcc也可以做到这一点(选项-fstack-usage)。 因此,有时您可以查看此报告(或编写为您执行此操作的脚本,并在每次构建之前调用它)。

此外,在报告的开头,将编写一条导致最大程度利用堆栈的路径:



问题是,如果您的代码具有通过指针或虚拟方法进行的函数调用(并且我有它们),则此报告可能会大大低估最大堆栈深度。 好吧,当然不会考虑中断。 不是一个非常可靠的方法。

棘手的堆栈放置


我从本文中学到了这种方法。 这篇文章是关于生锈的,但是主要思想是:



使用gcc时,可以使用“ double link ”完成。

在Keil中,可以使用您自己的链接程序脚本(Keil术语中的分散文件)更改区域的位置。 为此,请打开项目选项,然后取消选中“使用目标对话框中的内存布局”。 然后,默认文件将出现在“分散文件”字段中。 看起来像这样:

 ; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00020000 { ; load region size_region ER_IROM1 0x08000000 0x00020000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { ; RW data .ANY (+RW +ZI) } } 

接下来要做什么? 可能的选项。 官方文档建议定义带有保留名称的节-ARM_LIB_HEAP和ARM_LIB_STACK。 但这带来了不愉快的后果,至少对我来说是这样-堆栈和堆的大小必须在分散文件中设置。

在我使用的所有项目中,堆栈和堆的大小都在汇编程序启动文件(Keil在创建项目时生成)中设置。 我真的不想更改它。 我只想在项目中包含一个新的分散文件,一切都会好起来的。 所以我走了一些不同的方式:

扰流板
 #! armcc -E ; with that we can use C preprocessor #define RAM_BEGIN 0x20000000 #define RAM_SIZE_BYTES (4*1024) #define FLASH_BEGIN 0x8000000 #define FLASH_SIZE_BYTES (32*1024) ; This scatter file places stack before .bss region, so on stack overflow ; we get HardFault exception immediately LR_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load region size_region ER_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } ; Stack region growing down REGION_STACK RAM_BEGIN { *(STACK) } ; We have to define heap region, even if we don't actually use heap REGION_HEAP ImageLimit(REGION_STACK) { *(HEAP) } ; this will place .bss region above the stack and heap and allocate RAM that is left for it RW_IRAM1 ImageLimit(REGION_HEAP) (RAM_SIZE_BYTES - ImageLength(REGION_STACK) - ImageLength(REGION_HEAP)) { *(+RW +ZI) } } 


然后,我说所有名为STACK的对象都应位于REGION_STACK区域中,而所有HEAP对象都应位于REGION_HEAP区域中。 其他所有内容都在区域RW_IRAM1中。 然后他按以下顺序排列区域:操作员的开始,堆栈,堆以及其他所有内容。 计算结果是在汇编器启动文件中,使用以下代码(即,名称为STACK和HEAP的数组)设置了堆栈和堆:

扰流板
 Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit PRESERVE8 THUMB 


好的,您可能会问,但这能给我们带来什么? 这是什么。 现在,从堆栈中移出时,处理器会尝试写入(或读取)不存在的内存。 在STM32上,由于异常-HardFault而发生中断。

由于有MPU,这并不像MemFault那样方便,因为HardFault可能由于多种原因而发生,但至少错误是很大的并且不是很安静。 即 它立即发生,而不是像过去一样在未知的时间段之后发生。

最棒的是,我们为此不支付任何费用,没有额外的运行时间! 哇 但是有一个问题。

这不适用于Milander。

是的 当然,在Milandra上(我主要对1986BE1和BE91感兴趣),存储卡看起来有所不同。 在STM32中,在操作员开始之前,什么都没有,并且在Milandra上,在操作员之前位于外部总线的区域。

但是,即使您不使用外部总线,也不会收到任何HardFault。 或者也许得到它。 或者也许得到它,但不是马上。 我找不到有关此主题的任何信息(这对Milander而言并不奇怪),并且实验没有给出任何可理解的结果。 如果堆栈大小是256的倍数,则有时会发生HardFault。 有时,如果堆栈到不存在的内存中的距离太远,则会发生HardFault。

但这无关紧要。 如果不是每次都发生HardFault,那么简单地将堆栈移动到RAM的开头就不再节省我们。 坦白地说,STM也没有义务同时抛出异常,Cortex-M核心规范似乎对此没有什么具体说明。

因此,即使在STM上,它也更像是骇客,但不是很脏。

因此,您需要寻找其他方式。

记录访问断点


如果我们将堆栈移到RAM的开头,则堆栈的极限值将始终相同-0x20000000。 我们可以在该单元格的记录中添加一个断点。 这可以通过命令完成,甚至可以使用.ini文件在自动运行中注册:

 // breakpoint on stackoverflow BS Write 0x20000000, 1 

但这不是一种非常可靠的方法。 每次初始化堆栈时都会触发此断点。 单击“杀死所有断点”很容易使它意外被击败。 而且,只有在调试器存在的情况下,他才会保护您。 不好

动态溢出保护


快速搜索此主题后,我想到了Keil的选项-protect_stack和--protect_stack_all。 不幸的是,有用的选项不是防止整个堆栈溢出,而是防止将另一个函数弹出到堆栈框架中。 例如,如果您的代码超出了数组的范围或因可变数量的参数而失败。 Gcc当然也可以做到这一点(-fstack-protector)。

此选项的本质如下:“保护变量”被添加到每个堆栈帧,即保护编号。 如果退出该函数后此数字已更改,则将调用错误处理程序函数。 详细信息在这里

有用的东西,但不是我所需要的。 我需要一个更简单的检查-以便在输入每个函数时,将SP(堆栈指针)寄存器的值与先前已知的最小值进行比较。 但是,不要在每个功能的入口都用手书写此测试吗?

动态SP控制


幸运的是,gcc有一个很棒的选项“ -finstrument-functions”,当您输入每个函数和退出每个函数时,它都允许您调用用户定义的函数。 通常用于输出调试信息,但是有什么区别?

更幸运的是,Keil故意复制了gcc功能,并且在名称“ --gnu_instrument”下有相同的选项( 详细信息 )。

之后,您只需要编写以下代码:

扰流板
 //   ,    //   ,         scatter- extern unsigned int Image$$REGION_STACK$$RW$$Base; //    ,   static const uint32_t stack_lower_address = (uint32_t) &( Image$$REGION_STACK$$RW$$Base ); //         extern "C" __attribute__((no_instrument_function)) void __cyg_profile_func_enter( void * current_func, void * callsite ) { (void)current_func; (void)callsite; ASSERT( __current_sp() >= stack_lower_address ); } //   -   extern "C" __attribute__((no_instrument_function)) void __cyg_profile_func_exit( void * current_func, void * callsite ) { (void)current_func; (void)callsite; } 


瞧! 现在,在进入每个函数(包括中断处理程序)后,将执行堆栈溢出检查。 如果堆栈溢出,将有一个断言。

一点解释:
  • 是的,当然,您需要以一定的余量检查溢出,否则存在“跳越”堆栈顶部的风险。
  • 基元是使用链接程序生成的常量获取有关存储区信息的一种特殊方法。 此处的详细信息(尽管在某些地方不是很清楚)。


解决方案是否完美? 当然不是

首先,这项检查远非免费,它的代码膨胀了10%,那么,代码将运行得更慢(尽管我没有测量)。 关键是否关键取决于您; 我认为,这是安全的合理价格。

其次,这在使用预编译的库时最有可能无法使用(但是由于我根本不使用它们,因此我没有检查)。

但是,由于我们自己进行验证,因此该解决方案可能适用于多线程程序。 但是我还没有真正考虑过这个想法,所以我暂时坚持一下。

总结一下


原来是为stm32和Milander找到了可行的解决方案,尽管对于后者,我不得不付出一些开销。

对我而言,最重要的是思维范式的微小转变。 在上述文章之前我根本没有想到您可以以某种方式保护自己免受堆栈溢出的影响。 我不认为这是需要解决的问题,而是某种自然现象-有时下雨,有时堆栈溢出,好了,没什么可做的,您必须硬着头皮容忍。

我通常会为自己(和其他人)注意到这一点-而不是在Google上花费5分钟并找到一个简单的解决方案-我已经忍受了多年的困扰。

这就是我的全部。 我知道我还没有发现任何根本性的新东西,但是我没有遇到任何做出这样决定的现成文章(至少Joseph Yu本人没有在有关此主题的文章中直接提供此内容)。 我希望他们在评论中能够告诉我我是否正确,以及这种方法的陷阱是什么。

UPD:如果在添加分散文件时,Keil开始发出难以理解的警告“ AppData \ Local \ Temp \ p17af8-2(33):警告:#1-D:文件的最后一行没有换行符结束”-但是该文件本身不是打开,因为它是临时的,只需在散点图文件中添加换行符和最后一个字符即可。

Source: https://habr.com/ru/post/zh-CN425071/


All Articles