1. 栈溢出基础:什么是任务栈、栈溢出原理、栈溢出对系统的影响

1.1 任务栈到底是什么?

咱们先聊聊任务栈。说白了,它就是每个任务在内存里的一块私有空间。VxWorks 里每个任务都有自己的栈,就像每个人都有自己的办公桌一样。

任务栈里存的是啥?主要是三样东西:

  • 局部变量——函数里定义的临时数据
  • 函数调用信息——返回地址、参数、寄存器现场
  • 中断嵌套的上下文——中断来了,CPU 得把当前状态先存起来

我刚开始做 VxWorks 项目时,总觉得栈空间给大点就完事了。后来发现,嵌入式设备的内存就那么点,你给每个任务多分 1KB,10 个任务就是 10KB,这在资源紧张的系统里可不是小数目。

核心概念:任务栈 = 任务私有的 LIFO(后进先出)内存区域。栈指针从高地址向低地址生长,栈底在高地址,栈顶在低地址。

1.2 栈溢出是怎么发生的?

栈溢出的原理其实很简单。你想想看,每个任务栈都有固定大小,比如 4KB。当函数调用层数太深,或者局部变量太大,数据就会超出栈的边界。

为什么会这样?常见原因有:

  1. 递归调用太深——每次递归都压栈,一不小心就爆了
  2. 局部变量太大——比如在函数里定义一个 2KB 的数组,栈才 4KB,两三次调用就没了
  3. 中断嵌套过多——中断来了要压栈,嵌套多了栈空间就紧张了
  4. 函数调用链太长——A 调 B,B 调 C,C 调 D……每层都占空间

我记得有个项目,任务栈设了 8KB,看起来挺大。结果有个函数里定义了一个结构体数组,光这个数组就占了 6KB。再加上函数调用链,栈直接就溢出了。嗯,这种问题最难查,因为不是每次都崩,看运气。

注意:栈溢出不会立即报错。它可能先覆盖相邻内存,等用到那块被污染的数据时,系统才崩溃。这就是为什么栈溢出问题特别难定位。

1.3 栈溢出对系统的影响有多大?

栈溢出不是小事。我见过最轻的,是某个任务偶尔跑飞,重启后又能正常工作。最严重的,是整个系统死机,连看门狗都救不回来。

具体来说,栈溢出会造成:

影响类型 具体表现 排查难度
任务崩溃 单个任务异常退出,其他任务正常 中等
数据污染 栈溢出覆盖了相邻的全局变量或堆数据
系统死机 栈溢出破坏了内核关键数据结构 极高
偶发异常 只在特定条件下触发,复现困难 极高

你想想看,栈溢出覆盖了相邻的全局变量,那个变量可能是个标志位。本来应该为 0,被覆盖成了 0xFF,程序逻辑就全乱了。而且这种问题,你用调试器看,变量值确实变了,但不知道是谁改的。

我曾经遇到过一个案例:系统运行 3 天后必死。查了整整一周,最后发现是一个任务的栈溢出了 8 个字节,刚好覆盖了另一个任务的信号量控制块。嗯,从那以后,我对栈大小就特别敏感了。

1.4 如何判断栈是否溢出?

VxWorks 提供了几个工具,我常用的有:

  • checkStack()——检查当前任务的栈使用情况
  • taskShow()——显示任务信息,包括栈大小和已使用量
  • 栈填充模式——创建任务时用 0xEE 填充栈,运行时检查边界是否被改写

我的习惯:每个任务创建后,先跑一轮压力测试,然后用 checkStack() 看峰值使用量。给这个峰值加上 30% 的余量,就是最终的栈大小。别问我为什么是 30%,吃过亏就知道了。

1.5 栈溢出检测的常见方法

实际项目中,我一般用这几种方法:

  1. 边界标记法——在栈底和栈顶放特殊标记,定期检查是否被改写
  2. 栈使用率监控——用定时任务定期打印每个任务的栈使用率
  3. 硬件 MPU 保护——如果芯片支持 MPU,可以给栈区域设置访问权限
  4. 静态分析——在代码审查时,人工检查递归深度和局部变量大小

说白了,没有万能的方法。我建议你把这几种方法组合使用。比如开发阶段用边界标记法,测试阶段用栈使用率监控,发布前用静态分析再过一遍。

一句话总结:栈溢出是嵌入式开发中最隐蔽的问题之一。它不会立刻报错,但一旦爆发,后果很严重。提前做好检测和预防,比事后排查要高效得多。

下一章,我会详细讲怎么用 VxWorks 的工具链来检测栈溢出,包括具体的代码示例和调试技巧。到时候咱们再聊。