论坛原始地址(持续更新):http://www.armbbs.cn/forum.php?mod=viewthread&tid=99514
第10章 ThreadX任务栈大小确定及其溢出检测
本章节为大家讲解ThreadX任务栈大小的确定方法以及栈溢出检测方法。给任务分配多大的栈空间,一直是初学者比较头疼的问题,本章就主要为大家讲解如何解决此问题。
10.1 任务栈大小的确定
10.2 什么是栈溢出
10.3 ThreadX的栈溢出检测机制
10.4 实验例程
10.6总结
10.1 任务栈大小的确定
在基于RTOS的应用设计中,每个任务都需要自己的栈空间,应用不同,每个任务需要的栈大小也是不同的。将如下的几个选项简单的累加就可以得到一个粗略的栈大小:
1、 函数的嵌套调用,针对每一级函数用到栈空间的有如下四项:
- 函数局部变量。
- 函数形参,一般情况下函数的形参是直接使用的CPU寄存器,不需要使用栈空间,但是这个函数中如果还嵌套了一个函数的话,这个存储了函数形参的CPU寄存器内容是要入栈的。所以建议大家也把这部分算在栈大小中。
- 函数返回地址,针对M3、 M4和M7内核的MCU,一般函数的返回地址是专门保存到LR(Link Register)寄存器里面的,如果这个函数里面还调用了一个函数的话,这个存储了函数返回地址的LR寄存器内容是要入栈的。所以建议大家也把这部分算在栈大小中。
- 函数内部的状态保存操作也需要额外的栈空间。
2、 任务切换,任务切换时所有的寄存器都需要入栈,对于带FPU浮点处理单元的M4/M7内核MCU来说,FPU寄存器也是需要入栈的。
3、 针对M3内核和M4/M7内核的MCU来说,在任务执行过程中,如果发生中断:
- M3内核的MCU有8个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈。
- M4/M7内核的MCU有8个通用寄存器和18个浮点寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套都是用的系统栈。
4、 进入中断以后使用的局部变量以及可能发生的中断嵌套都是用的系统栈,这点要注意。
实际应用中将这些都加起来是一件非常麻烦的工作,上面这些栈空间加起来的总和只是栈的最小需求,实际分配的栈大小可以在最小栈需求的基础上乘以一个安全系数,一般取1.5-2。上面的计算是我们用户可以确定的栈大小,项目应用中还存在无法确定的栈大小,比如调用printf函数就很难确定实际的栈消耗。又比如通过函数指针实现函数的间接调用,因为函数指针不是固定的指向一个函数进行调用,而是根据不同的程序设计可以指向不同的函数,使得栈大小的计算变得比较麻烦。
另外还要注意一点,建议不要编写递归代码,因为我们不知道递归的层数,栈的大小也是不好确定的。
一般来说,用户可以事先给任务分配一个大的栈空间,然后通过第8章介绍的调试方法打印任务栈的使用情况,运行一段时间就会有个大概的范围了。这种方法比较简单且实用些。
- 函数栈大小确定
函数的栈大小计算起来是比较麻烦的,那么有没有简单的办法来计算呢?有的,一般IDE开发环境都有这样的功能,比如MDK会生成一个htm文件,通过这个文件用户可以知道每个被调用函数的最大栈需求以及各个函数之间的调用关系。但是MDK无法确定通过函数指针实现函数调用时的栈需求。另外,发生中断或中断嵌套时的现场保护需要的栈空间也不会统计。
关于MDK生成的map和htm文件的使用,我们安富莱电子有出过一期视频教程,可以在这里查看:
http://www.armbbs.cn/forum.php?mod=viewthread&tid=15408
STM32H7的BSP驱动手册第10章节:
http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980 。
10.2 什么是栈溢出
前面为大家讲解了如何确定任务栈的大小,那什么又是栈溢出呢?简单的说就是用户分配的栈空间不够用了,溢出了。下面我们举一个简单的实例,栈生长方向从高地址向低地址生长(M4/M7和M3是这种方式)。
(1) 上图标识1的位置是RTOS的某个任务调用了函数test()前的SP栈指针位置。
void test (void); { int i; int array[10]; : : // Code }
(2) 上图标识2的位置是调用了函数test需要保存返回地址到栈空间。这一步不是必须的,对于M3和M4/M7内核是先将其保存到LR寄存器中,如果LR寄存器中有保存上一级函数的返回地址,需要将LR寄存器中的内容先入栈。
(3) 上图标识3的位置是局部变量int i和int array[10]占用的栈空间,但申请了栈空间后已经
越界了。这个就是所谓的栈溢出了。如果用户在函数test中通过数组array修改了这部分越界区的数据且这部分越界的栈空间暂时没有用到或者数据不是很重要,情况还不算严重,但是如果存储的是关键数据,会直接导致系统崩溃。
(4) 上图标识4的位置是局部变量申请了栈空间后,栈指针向下偏移(返回地址+变量i+10个数组元素)*4 =48个字节。
(5) 上图标识5的位置可能是其它任务的栈空间,也可能是全局变量或者其它用途的存储区,如果test函数在使用中还有用到栈的地方就会从这里申请,这部分越界的空间暂时没有用到或者数据不是很重要,情况还不算严重,但是如果存储的是关键数据,会直接导致系统崩溃。
10.3 ThreadX的栈溢出检测机制
10.3.1 实现原理
(注:有些应用场景,这种栈检测是检测不出来的)。
ThreadX提供了在运行时检查每个任务的栈是否损坏的功能。默认情况下,ThreadX在创建过程中使用0xEF数据模式填充任务的每个字节。如果应用程序使能了宏定义TX_ENABLE_STACK_CHECKING编译工程,则ThreadX将检查每个任务的栈在挂起或恢复时是否损坏。如果检测到栈损坏,则ThreadX将调用用户使用函数tx_thread_stack_error_notify设置的回调函数。否则,如果未指定堆栈错误处理程序,则ThreadX将调用内部_tx_thread_stack_error_handler例程。
- 栈溢出检测方法
除了TreadX提供的栈溢出检测机制,还有其它的栈溢出检测机制,大家可以在Mircrium官方发布的如下这个博文中学习:
https://www.micrium.com/detecting-stack-overflows-part-2-of-2/
10.3.2 实现方法
- 使能栈检测
推荐直接在tx_port.h里面使能:
#define TX_ENABLE_STACK_CHECKING
- 注册回调:
大家可以随意设置注册的函数名:
tx_thread_stack_error_notify(my_stack_error_handler);
- 回调函数的实现
代码如下:
void my_stack_error_handler(TX_THREAD *thread_ptr) { App_Printf("===============================================================\r\n"); App_Printf("如下任务被检测出栈溢出\r\n"); App_Printf("===============================================================\r\n"); App_Printf(" 任务优先级 任务栈大小 当前使用栈 最大栈使用 任务名\r\n"); App_Printf(" Prio StackSize CurStack MaxStack Taskname\r\n"); TX_THREAD *p_tcb; /* 定义一个任务控制块指针 */ p_tcb = &AppTaskStartTCB; /* 遍历任务控制列表TCB list),打印所有的任务的优先级和名称 */ do { if(p_tcb != (TX_THREAD *)thread_ptr) { p_tcb = p_tcb->tx_thread_created_next; } else { App_Printf(" %2d %5d %5d %5d %s\r\n", p_tcb->tx_thread_priority, p_tcb->tx_thread_stack_size, (int)p_tcb->tx_thread_stack_end - (int)p_tcb->tx_thread_stack_ptr, (int)p_tcb->tx_thread_stack_end - (int)p_tcb->tx_thread_stack_highest_ptr, p_tcb->tx_thread_name); while(1); } }while(1); }
为方便起见,我们这里直接将栈溢出任务的任务名打印出来,方便大家查看那个任务出问题了,然后通过while(1)阻塞在这里:
10.4 实验例程
配套例子:
V6-3005_ThreadX Task Stack Checking
实验目的:
- 学习ThreadX任务管理。
实验内容:
1、共创建了如下几个任务,通过按下按键K1可以通过串口或者RTT打印任务堆栈使用情况
===================================================
OS CPU Usage = 1.94%
===================================================
Prio StackSize CurStack MaxStack Taskname
2 4092 383 391 App Task Start
3 4092 543 659 App Msp Pro
4 4092 391 391 App Task UserIF
5 4092 543 659 App Task COM
30 1020 519 519 App Task STAT
31 1020 143 71 App Task IDLE
0 1020 391 391 System Timer Thread
串口软件可以使用SecureCRT或者H7-TOOL RTT查看打印信息。
App Task Start任务 :启动任务,这里用作BSP驱动包处理。
App Task MspPro任务 :消息处理,这里未使用。
App Task UserIF任务 :按键消息处理。
App Task COM任务 :这里用作LED闪烁。
App Task STAT任务 :统计任务
App Task IDLE任务 :空闲任务
System Timer Thread任务:系统定时器任务
2、(1) 凡是用到printf函数的全部通过函数App_Printf实现。
(2) App_Printf函数做了信号量的互斥操作,解决资源共享问题。
3、默认上电是通过串口打印信息,如果使用RTT打印信息
(1) MDK AC5,MDK AC6或IAR通过使能bsp.h文件中的宏定义为1即可
#define Enable_RTTViewer 1
(2) Embedded Studio继续使用此宏定义为0, 因为Embedded Studio仅制作了调试状态RTT方式查看。
实验操作:
- K1按键按下打印任务执行情况。
- K2按键按触发任务AppTaskUserIF栈溢出,并且会打印出问题的任务名,并将程序阻塞运行。
串口打印信息方式(AC5):
波特率 115200,数据位 8,奇偶校验位无,停止位 1
RTT打印信息方式(AC5):
程序执行框图:
10.5 总结
本章节主要为大家讲解了任务栈大小的确定以及栈溢出检测的两种方法,建议实际操作下本章节配套的例子,对栈溢出有一个感性的认识,随着以后的学习再深入理解并运用。