【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
背景
上篇 blog 【OS】【Nuttx】【栈溢出】R11 的特殊处理 补充了根据 R10 和 R11,可以推算出应该分配多大的栈空间,并详细分析了要向上补齐 12 字节的原因,其中 8 字节为栈指针 SP 的对齐要求,另外额外 4 字节是考虑一些特殊情况,比如异常 entry 入口,函数 prologue 等,SP 可能要再减 4 字节,下面继续
栈溢出检测
上篇 blog 里提到了,栈空间要多预留 12 字节,下面看一些栈检测的细节

首先 Per-Call Stack Checking 本质上是一种运行时栈溢出的检测机制,通过 gcc 的 -finstrument-functions,在每个函数入口/出口插入检查代码,检查代码会比较当前 SP 和 rBS(R10),如果 SP < rBS,说明栈溢出了,但前提是 rBS 必须已经被正确设置为安全栈的下限
OK,这第一个细节就是,谁负责设置 rBS,在什么时候设置?
首先是第一次发生时的函数调用,__start() 是 C 代码的入口(在 main 之前),__start() 需要在任何函数调用之前,就设置好 rBS,但如果 __start() 本身也被插桩(instrumented),编译器就会在它开头调用 __cyg_profile_func_enter,而这个函数内部会读取 rBS 来做栈检查,但此时 rBS 还没被初始化,就会产生未定义行为,甚至崩溃,所以这里禁止对 __start() 进行插桩
void __start(void) __attribute__((no_instrument_function));
这样 __start 就可以安全地执行,在无栈检查干扰的情况下初始化 rBS

然后是这里的
#ifdef CONFIG_ARMV7M_STACKCHECK
/* Set the stack limit before we attempt to call any functions */
__asm__ volatile ("sub r10, sp, %0" : : "r" (CONFIG_IDLETHREAD_STACKSIZE – 64) : );
#endif
这行汇编代码等价于
r10 = sp – (CONFIG_IDLETHREAD_STACKSIZE – 64);
这个点在之前blog 【OS】【Nuttx】【启动】启动函数分析 分析过,下面再详细说下,其目标是让 rBS 指向栈的安全下限:
- SP 是当前 idle task 的栈顶(高地址)
- CONFIG_IDLETHREAD_STACKSIZE 是 idle task 的总栈大小
- 栈底(最低合法地址) = SP – CONFIG_IDLETHREAD_STACKSIZE
- 但 rBS 被设置为 SP – (CONFIG_IDLETHREAD_STACKSIZE – 64),也就是栈底 + 64
可以看到,rBS 并不指向真正的栈底,而是比栈底高 64 字节的位置

这 64 字节就是故意留出的安全余量(safety margin),原因如下
- 防止刚好溢出:如果 rBS 等于真实栈底,那么当 SP 刚好到栈底时,此时检查认为还没溢出,但下一个 push 就会破坏掉非栈内存,提前 64 字节报警,可以相对留出反应时间
- 补偿异常/中断开销:即使 idle task 本身不深度调用,中断仍可能在 idle 上下文中发生,并使用部分栈空间,而 64 字节可容纳基本 ISR 压栈,比如 8 个寄存器 ≈ 32 字节,并考虑对齐
- 调试友好:栈溢出时,还有 64 字节空间可以写 fault 信息,或触发断点,而不是直接崩溃
注意,这里的 64 字节是额外加的,和前面提到的 +12 字节不冲突:
- +12字节:用于计算新任务栈大小时的余量
- -64字节:用于设置初始 rBS 位置,提供运行时缓冲
所以综上,实际可用的栈空间,会比配置值 CONFIG_IDLETHREAD_STACKSIZE 少 64 字节
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog

![[操作系统]操作系统文件系统与输入输出:架构、行为与调优-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260808030509-6a769ce596f70-220x150.png)


