在嵌入式系统中,堆栈是静态分配的,不会依据堆栈的使用情况自动增加堆栈深度,存在堆栈溢出的风险。一旦发生堆栈溢出,后果很严重,可能会立即导致死机,也可能埋了一颗定时炸弹,在随后的开发过程随时导致死机。事实上,堆栈溢出导致死机还算是不错的状况,更糟糕的情况是,有时候正常,有时候功能错误,捉摸不定,如果又是在修改了与这个堆栈压根就没什么关系的代码的情况下出现,那真的要吐血,因为第一感觉会告诉你,刚刚修改的代码存在错误。好在可以预防这种情况,稍微专业点的RTOS就会提供堆栈检查API,eCos当然也不例外,在有堆栈检查API的RTOS中,即使已经发生了堆栈溢出,也可以快速诊断哪个堆栈产生溢出。
堆栈检查选项
堆栈检查通常是发生在开发和测试阶段,因此在eCos中堆栈检查功能是一个可选项,要使用eCos堆栈检查功能,首先要打开对应的选项。
> eCos kernel
> Thread-related options
> Measure stack usage
堆栈检查API
使能Measure stack usage选项后,cyg_thread_measure_stack_usage函数可用,函数原型如下:
cyg_uint32 cyg_thread_measure_stack_usage(cyg_handle_t thread)
这个函数以线程句柄为入参,返回该线程已使用的堆栈深度,以字节为单位。
仅有已使用堆栈深度是不够的,使用cyg_thread_get_stack_size获取线程堆栈大小,使用cyg_thread_get_stack_base获取线程堆栈基址。通过这三个函数可以确定线程堆栈存储位置,分配空间大小,已使用堆栈大小,通过已使用堆栈大小与分配空间大小相比可以评估线程堆栈是否存在堆栈溢出的风险。此外,可以使用cyg_thread_get_info函数一次性获取上述三个参数。
代码示例
使用cyg_thread_get_info可以一次性地获取上述三个参数,而且还可以获取其他线程信息,使用cyg_thread_get_next可以枚举当前运行环境下的所有线程。下面的代码使用cyg_thread_get_next函数枚举所有线程,然后调用cyg_thread_get_info获取线程信息,并打印线程名称和堆栈使用情况。如果堆栈检查选项未使能,那么thread_info.stack_used为0。
cyg_handle_t thread = 0;
cyg_uint16 id;
cyg_thread_info thread_info;
while(cyg_thread_get_next(&thread, &id))
{
if(cyg_thread_get_info(thread, id, &thread_info))
{
diag_printf("name: %s, handle: 0x%08x, id: 0x%04x, "
"stack_base: 0x%08x, stack_size: %d, stack_used: %d\n",
thread_info.name, thread_info.handle, thread_info.id,
thread_info.stack_base, thread_info.stack_size, thread_info.stack_used);
}
else
{
diag_printf("ERROR: get thread info failed, handle: 0x%08x, id: 0x%04x\n",
thread, id);
}
}
影响堆栈深度的因素
一、函数调用深度,每调用1次函数至少需要8字节堆栈空间用来保存LR寄存器以及堆栈对齐,因此嵌入式系统应当尽量避免递归调用,递归调用将大大增加对堆栈需求并且引入不确定性,最终可能导致堆栈溢出。
二、函数复杂度,函数越复杂使用的临时变量越多,临时变量可以保存在寄存器或堆栈中,如果要保存在寄存器中,那么首先要将原寄存器内容压栈保存,因此临时变量无论是保存在寄存器或堆栈都会增加堆栈深度。
三、编译选项,不同的编译参数会影响堆栈深度,例如使用-O0编译出的代码要比-O2编译出的代码使用更多的堆栈。因为使用-O0时,会将所有变量压栈,而不仅是保存在寄存器,这有利于调试,但会增加堆栈深度。
四、中断,产生中断时,中断服务函数将被中断线程的上下文保存到被中断线程的堆栈,因此中断也会加深线程堆栈深度,中断嵌套将会加深中断堆栈。
堆栈检查原理
在线程初始化时,将线程堆栈所有空间填入预设值,eCos预设值为0xDEADBEEF,在调用cyg_thread_measure_stack_usage或者cyg_thread_get_info时从堆栈底部开始检查堆栈存储的内容是否为预设值,如果是预设值说明该地址可能未被使用,如果不是那么说明该地址已经被使用。预设值不是0而是0xDEADBEEF的原因是:变量值为0的可能性非常大,而为0xDEADBEEF的可能性非常小,减少因为变量值与预设值相同而导致计算偏差的可能性。
// kernel//include/thread.inl:222
inline void Cyg_HardwareThread::attach_stack(CYG_ADDRESS s_base, cyg_uint32 s_size)
{
......
#ifdef CYGFUN_KERNEL_THREADS_STACK_MEASUREMENT
{
CYG_WORD *base = (CYG_WORD *)s_base;
cyg_uint32 size = s_size/sizeof(CYG_WORD);
cyg_ucount32 i;
for (i=0; i<size; i++) {
base[i] = 0xDEADBEEF;
}
}
#endif
......
}
// kernel//include/thread.inl:157
inline cyg_uint32 Cyg_HardwareThread::measure_stack_usage(void)
{
CYG_WORD *base = (CYG_WORD *)stack_base;
cyg_uint32 size = stack_size/sizeof(CYG_WORD);
cyg_ucount32 i;
for (i=0; i<size; i++) {
if (base[i] != 0xDEADBEEF)
break;
}
return (size - i)*sizeof(CYG_WORD);
}
中断堆栈
eCos提供了线程堆栈检查,但我没有发现中断堆栈检查(如果你发现eCos已经提供了该功能,请告诉我,谢谢),中断堆栈同样会有溢出的风险,因此我编写了stkinfo组件对中断堆栈进行检查,此外该组件还可以对不包含内核情况下的应用程序主堆栈进行检查。
stkinfo组件
点击这里下载stkinfo扩展组件包,目前该组件仅支持Cortex-M架构,可能支持其他架构,但未经验证。
stkinfo扩展组件包提供的接口如下:
typedef struct _cyg_stack_info
{
CYG_ADDRWORD base;
cyg_uint32 size;
cyg_uint32 used;
}cyg_stack_info;
// initialize interrupt stack with known value.
void cyg_interrupt_stack_measure_init(void);
// Measure the stack usage of the interrupt.
void cyg_get_interrupt_stack_info(cyg_stack_info* info);
// initialize main stack with known value.
void cyg_main_stack_measure_init(void);
// Measure the stack usage of the user program.
void cyg_get_main_stack_info(cyg_stack_info* info);
(1)堆栈信息数据结构,包括堆栈基址,堆栈大小,已使用堆栈大小。
(9)中断堆栈检查初始化,这个函数将中断堆栈写入预设值,但是仅写到堆栈的下半段,因为eCos使用中断堆栈作为初始化过程的堆栈,因此中断的上半段已经在使用中,不能写入预设值,从这里可以看出,只有在中断堆栈使用量超过堆栈大小一半时检查结果才比较精确,但是用来判断堆栈溢出已经足够啦。
(12)获取中断堆栈信息,包括堆栈已使用大小,如果堆栈已使用量小于堆栈的一半,那么总是返回堆栈的一半大小值,例如堆栈分配1024字节,实际仅使用378字节,该函数返回的使用量是512字节,如果堆栈已使用量超过堆栈的一半,那么返回堆栈的实际使用量。
(15)应用程序主堆栈初始化,与中断堆栈一样仅初始化下半段,如果当前配置包含内核,那么该函数什么都不做。
(18)获取应用程序主堆栈信息,包括堆栈已使用大小,如果当前配置包含内核,那么info数据结构的所有值为0。
安装stkinfo组件
使用ecosadmin.tcl脚本安装stkinnfo组件,该脚本在eCos源代码的packages子目录下。
cd /packages
tclsh ecosadmin.tcl add /stkinfo-.epk
也可以手动安装该组件。
tar -xf /stkinfo-.epk
cat /pkgadd.db >> /packages/ecos.db
cp -R /services /packages/
使用stkinfo组件
在对堆栈进行检查之前,首先要对堆栈进行预设值填充,为了保证预设值不会覆盖当前正在使用的堆栈空间,要在初始化的早期对堆栈进行预设值填充,因此在hal_system_init函数中调用cyg_interrupt_stack_measure_init进行堆栈预设值填充。hal_system_init是平台HAL的一部分,在系统初始化早期被架构HAL调用。这里以Olimex LPC-1766-STK目标机为例。
// hal/cortexm/lpc17xx/lpc1766stk/<version>/src/lpc1766stk_misc.c:78
......
#ifdef CYGPKG_STKINFO
# include <cyg/stkinfo/stkinfo.h>
#endif
......
__externC void
hal_system_init(void)
{
#ifdef CYGPKG_STKINFO
cyg_interrupt_stack_measure_init();
cyg_main_stack_measure_init();
#endif
......
}
......
初始化过后,可以在任意时刻调用cyg_get_interrupt_stack_info获取堆栈信息,并打印堆栈的使用情况,将上面打印堆栈使用情况的代码示例添加上中断堆栈后的示例如下,这个示例的完整代码见stkinfo组件的测试用例。
cyg_handle_t thread = 0;
cyg_uint16 id;
cyg_thread_info thread_info;
cyg_stack_info stack_info;
cyg_get_interrupt_stack_info(&stack_info);
diag_printf("name: %s, "
"stack_base: 0x%08x, stack_size: %d, stack_used: <=%d\n",
"ISR/DSR",
stack_info.base, stack_info.size, stack_info.used);
while(cyg_thread_get_next(&thread, &id))
{
if(cyg_thread_get_info(thread, id, &thread_info))
{
diag_printf("name: %s, handle: 0x%08x, id: 0x%04x, "
"stack_base: 0x%08x, stack_size: %d, stack_used: %d\n",
thread_info.name, thread_info.handle, thread_info.id,
thread_info.stack_base, thread_info.stack_size, thread_info.stack_used);
}
else
{
diag_printf("ERROR: get thread info failed, handle: 0x%08x, id: 0x%04x\n",
thread, id);
}
}
(4)声明中断堆栈信息变量。
(6)调用cyg_get_interrupt_stack_info获取堆栈信息。
(7)打印中断堆栈基址、大小、已使用大小。