【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
背景
上篇 blog
【Nuttx】【OS】【启动】回归!继续 Nuttx 探索(Stack Monitor)
在 Gitee 上新建了个仓库,用来同步 Github Nuttx 源仓库,然后分析了栈溢出检测中的 Stack Monitor 原理,并分析了其中可能会漏掉的栈溢出场景,最后准备开始分析 Per function Call
栈溢出检测
还是 Per function Call 这里

除了保留一个 CPU 寄存器(R10),编译器(GCC)还可以通过 -finstrument-functions 选项,在每个函数的入口和出口自动插入检查代码
- 入口:插入
__cyg_profile_func_enter - 出口:插入
__cyg_profile_func_exit
OK,然后是入口的检查逻辑,在每次函数被调用时,先执行检查如下的伪代码
c
if ( (current SP - 安全边距) < rBS ) {
触发 Hard Fault(硬件故障)
}
这里的安全边距 = 64 字节 + (如果启用 FPU)136 字节 = 最多约 200 字节,其检查过程大致如下
-
栈底存入 R10 寄存器

-
发生函数调用时,对 SP 指针进行运算,并和 R10 寄存器进行比较

-
有两种可能,一种是(SP - 安全距离)< R10 寄存器,意味着 SP 指针的使用已经不在栈空间之内,此时检查函数
__cyg_profile_func_enter需要上报 Hard Fault 硬件故障通知栈溢出

-
另一种可能就是 (SP - 安全距离)> R10 寄存器,这时候 SP 指针就还算使用安全,有足够的安全余量

OK,接下来这段话解释了为什么每次函数调用进行栈检查时,要从当前栈指针 SP 减去一个安全距离,再去和栈底 rBS 比较

这里的核心内容是,即使任务栈看起来还没用完,也要预留至少 200 字节的应急空间,以防中断突然发生导致栈溢出
这里需要分成两种系统配置场景来考虑:没有独立的 ISR 中断栈,和有独立的 ISR 中断栈
-
没有独立中断栈时 :在这种配置下,当中断发生时,CPU 会直接把上下文(比如寄存器信息等)压入当前任务的栈中 ,这里需要保存的内容包括基础寄存器(
R0-R3,R12,LR,PC,xPSR),大概 32 字节,如果启用了浮点单元 FPU,则还要额外保存 16 个浮点寄存器(S0-S15),大概 64 字节,然后可能还有对齐,嵌套中断等额外开销,可能总计需要 100 ~ 200 字节(这里取了个整数 200 字节)

所以,如果此时任务栈已经用到了离栈底只剩 50 字节,这时候来个中断,就会直接溢出并破坏内存

这时候,在检查栈是否安全时,得提前扣除这 200 字节,也就是说,只要(SP - 200)<
rBS,就算危险! -
有独立的中断栈时 :在这种情况下,中断发生时,CPU 就会切换到专用的中断栈,不再使用任务栈,但切换过程本身还是会需要一点任务栈空间 ,比如保存部分寄存器,跳转到中断处理程序前的准备代码,这部分内容通常只需要几十字节,剩下的 100+ 字节就是额外的安全余量,可以应对如编译器优化导致的栈使用波动,未来代码修改增加局部变量,测量误差或对齐填充等

所以即使有独立中断栈,这里也建议预留 200 字节,因为多一点安全余量总比溢出强
最后总结下:
- 安全边距 = 64 + 136(启用 FPU)= 200 字节
- 没有独立中断栈:CPU 把上下文(包括通用寄存器 + FPU 寄存器)压入当前任务栈,需要预留空间
- 有独立中断栈:切换到中断栈时,仍需要少量栈空间做跳转,其余作为安全余量
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog
【Nuttx】【OS】【启动】栈溢出检测(二)