为了识别运行的嵌入式系统中的堆栈溢出问题,SEGGER编译器通过为每个函数生成检测代码的方式来检查堆栈溢出。该功能可以使用命令行开关-mstack-overflow-check来使能。对于安全系统,必须在溢出的堆栈破坏内存之前检测到堆栈溢出,因此需要在更改堆栈指针和需大量堆栈空间之前进行检查。
一,Embedded Studio的堆栈溢出预防
在Embedded Studio中启用堆栈溢出预防功能,仅需将工程选项Code->Code Generation->Enable Stack Overflow Prevention设置为“Yes”。
如果工程中没有实现错误回调函数__SEGGER_STOP_X_OnError,它默认保持在一个无限循环中。Embedded Studio安装目录$(StudioDir)/samples下的SEGGER_STOP.c中包含一个错误处理的示例实现。
二,编译器生成的代码
如果在编译器中使能堆栈溢出检查,生成的代码将被改变,如下所示:
1、不使用堆栈的函数不会被更改。
2、使用本地堆栈帧但不使用R3传递参数的函数中:堆栈帧的设置(通常使用sub sp, #size指令)被替换为将所需栈大小加载到寄存器R3中,然后调用函数__SEGGER_STOP_GROW_R3()。
3、使用本地堆栈帧并使用R3传递参数的函数中,操作与2相同,但是使用寄存器R4传值,并调用函数__SEGGER_STOP_GROW_R4()。这意味着,R4必须在进入函数时必须入栈。
4、不使用本地堆栈但需在堆栈中保存寄存器的函数中,在寄存器压栈后将调用不带参数的函数__SEGGER_STOP_GROW_0()。
5、需要动态分配堆栈的函数(如使用alloc()函数或可变大小的数组),编译后代码也将调用__SEGGER_STOP_GROW_R3()。因为分配可能发生在函数执行中,需指示寄存器分配器,确保R3可以使用。
然后,被调用的函数可以使用存储在全局变量中的堆栈上限值检查堆栈是否溢出。检查函数将在需要保存的寄存器被压栈后调用。因此,必须计算堆栈上限,以便始终有栈空间用于:
在函数入口处,入栈所有的通用寄存器(R0 - R11, LR),有一些优化可能导致R0 - R3入栈。
所有被调用者需保存的浮点/矢量寄存器(D8 - D15)
中断入口需保存的寄存器 (8个字)
3个(备用)字用于对齐和紧急溢出缓存
可以使用__attribute__((no_stack_overflow_check))禁止生成单个函数的堆栈检查代码。
三、堆栈检查和错误处理
启用“防止堆栈溢出”功能时,必须实现下列堆栈检查函数。在Embedded Studio中,这些函数已添加到标准库中。
__SEGGER_STOP_GROW_R3
__SEGGER_STOP_GROW_R4
__SEGGER_STOP_GROW_0
堆栈溢出时,堆栈检查函数应该跳转到用户提供的错误处理回调函数__SEGGER_STOP_X_OnError()。
堆栈检查函数
编译器生成的代码调用检查函数检查剩余的堆栈大小,在堆栈溢出的情况下函数不能返回。为了提高效率,这些函数没有遵循标准调用约定。因此,函数不能修改除了R12和包含堆栈大小参数的寄存器之外的任何寄存器,函数在返回之前还必须调整堆栈指针。
错误处理回调
为了确保错误处理回调不使用溢出堆栈,它应该在纯汇编中实现,并且在禁用堆栈溢出检查功能状态下进行编译。
在默认实现中,__SEGGER_STOP_X_OnError定义为:
__attribute__((naked, no_stack_overflow_check)) void __SEGGER_STOP_X_OnError(void);
它在堆栈检查函数尾部调用,不遵循常规调用约定。堆栈上限值、新的堆栈指针值和调用者通过R3、R12和LR传递。
错误处理回调可能会将溢出堆栈重置为安全值。同时,它可能会调用其他函数,比如记录错误和重置系统。
示例:
void __SEGGER_STOP_X_OnError(void) { asm( "cpsid i " // Disable interrupts "mov r0, r12 " // Save overflowed SP "mov r1, r3 " // Save SP limit "sub r2, lr, #5 " // Save caller "mrs r3, CONTROL " // Get currently used stack "lsls r3, #30 " "ittee pl " // Reset this stack "ldrpl r12, =__stack_end__ " "msrpl msp, r12 " "ldrmi r12, =__stack_process_end__ " "msrmi psp, r12 " "bl _HandleStackError " // Call error handler "b . " // Stay here ); }
四、堆栈上限
启动代码必须初始化堆栈上限,至少初始化主堆栈上限变量__SEGGER_STOP_Limit_MSP。在默认实现中,该符号由运行时初始化代码自动初始化为默认值。
为了调整上限值,例如改变为保存寄存器保留的空间,以及初始化__SEGGER_STOP_Limit_PSP,应该实现并调用__SEGGER_STOP_X_InitLimits。
使用SEGGER链接器,运行时初始化代码将自动调用__SEGGER_STOP_X_InitLimits:
initialize by calling __SEGGER_STOP_X_InitLimits { section .data.stop.* };
使用GNU链接器时,应该在main中的开始位置调用__SEGGER_STOP_X_InitLimits
int main(void) { int NumItems; #if !defined (__SEGGER_LINKER) // // Optionally initialize stack limits if not done by runtime init. // __SEGGER_STOP_X_InitLimits(); #endif ... }
在运行初始化之前调用函数
当系统在运行初始化之前调用函数时,Cortex-M上默认在Reset_Handler中调用SystemInit, __SEGGER_STOP_Limit_MSP应设置为0,以禁用堆栈检查。
Reset_Handler: .extern __SEGGER_STOP_Limit_MSP // // Initialize main stack limit to 0 to disable stack checks before runtime init // movs R0, #0 ldr R1, =__SEGGER_STOP_Limit_MSP str R0, [R1] // // Call SystemInit // bl SystemInit ...
使用RTOS
当使用RTOS或其他多任务机制时,任务切换程序必须在切换堆栈时更新堆栈上限变量(通常为__SEGGER_STOP_Limit_PSP)。
ChangeTask: ... ldr r0, [r1, #0] // OS.pCurTask ldr r3, [r0, #8] // OS.pCurTask->pStackBottom add r3, #100 // Buffer before stack overflow ldr r2, =__SEGGER_STOP_Limit_PSP str r3, [r2] // Update stack limit ...
建议对所有任务启用堆栈检查。RTOS可以通过将limit变量设置为0来禁用某些任务的堆栈检查。
堆栈溢出几乎在每个系统中都可能遇到。Embedded Studio提供了使用示例,展示简单系统和多任务系统的堆栈溢出行为及处理,下载链接: https://wiki.segger.com/images/2/2f/STOP_Examples.zip。
评论
查看更多