嵌入式系统中堆heap和栈stack的管理——如何计算stack size

ben文一步步介绍了嵌入式系统中为了充分优化利用存储资源稳健设计中的分配堆heap和栈stack的方法。第一部分是如此在系统中测试需要的stack的大小(使用栈保护区和链接器的方法),以及如何检测栈相关的下溢错误信息。

程序运行的各种存储段分类

程序运行时在内存中主要有代码段、数据段、堆栈段(堆空间和栈空间)、进程头、动态链接库等区域。 其中数据使用到的段包括

  • 数据段:静态内存空间,其中数据的总大小和初始值在编译时确定,数据在整个程序运行时一直存在。

  • 栈空间:自动内存空间,其中数据的大小在编译时确定,数据的分配和释放也由编译器在函数进入和退出时插入指令完成,数据生命周期和函数一样。

  • 堆空间:动态(手动)内存空间,其中数据的大小和初始值在运行时确定,数据生命周期不定。

堆heap和stack栈是嵌入式系统的RAM内存分配的基础。设置正确的堆栈大小对于系统的稳定性和可靠性非常重要。堆和栈的空间必须由程序员静态的分配,但计算堆heap和栈stack的空间大小却不是一件简单的事情,即便是对于最小的嵌入式系统。低估栈的使用对带来实时运行中难以预料的错误,并且很难调试。而高估栈stack的空间则会浪费内存空间。堆Heap的溢出也会对系统行为带来严重的影响,同样难以调试。本文介绍了嵌入式系统中的heap和栈stack的使用,讨论可靠的heap和栈stack设计中的基本准则和方法,以及小型嵌入式系统中可能碰到的独特问题。

桌面系统和嵌入式系统有一些共同的堆heap和栈stack设计的考虑点,但更多的是不同之处。一个不同是可用内存空间大小,Windows和Linux默认的栈空间大小分别为1MB和8MB(Linux下可以通过ulimit -s 查看linux的默认栈空间大小, windows下是指默认的线程空间栈大小为1MB),该空间大小还能继续增大。而堆空间大小则受限于可用的物理内存或者页文件的尺寸。另外,嵌入式系统,往往有限的内存资源,因而需要设定最小的足够用的栈stack和堆heap大小。小型嵌入式系统的一个问题是没有虚拟内存机制,栈stack和堆heap、全局数据(如TCP/IP,USB缓存空间等)都是静态的,需要在应用程序构建时就分配好。

栈stack

栈stack是一块程序运行时用来存储临时变量内存RAM空间。栈一般静态分配,并且后进先出(LIFO,last in first out),栈的生命周期从程序的起始直到程序结束。一个函数返回,其用到的栈空间就被释放给后续的函数使用。

存储在栈stack的数据包括

  • 临时变量local variables

    扫描二维码关注公众号,回复: 5784829 查看本文章
  • 返回地址

  • 函数参数

  • 编译器临时空间

  • 中断环境

开发者静态的指定栈内存空间,一般栈向下生长,如果栈空间不足,发生下溢,则栈之下的内存空间被写入,如下图1所示。因而低估栈空间大小可能会造成无法预料的实时运行错误,一些全局变量、野指针或者错误的返回地址,这些类错误都很难发现。决定最坏情况下的栈空间大小对于嵌入式项目非常重要。下面介绍计算需要的栈空间大小的方法。

图1. 当栈下溢时,栈下面的内存空间被写入

计算栈stack空间大小

但是现实中很多复杂的因素让计算最大的stack使用非常困难,大量的深嵌套的函数调用、很多的随时可能触发的中断响应,而且中断还可能嵌套。另外还有通过函数指针的间接调用,而递归以及没有注释的汇编子函数都让计算stack空间困难异常。因而没有一成不变的流程可以遵循。很多微控制器使用多个栈stack,一个系统栈一个用户栈等。多个栈在很多的嵌入式RTOS如?C/OS, ThreadX等都是可行的,任务都是运行在自己的栈空间。实施运行库以及第三方的软件让计算栈空间更为复杂,因为他们的源代码可能拿不到。另外需要记住代码的改变和程序的执行次序会影响栈stack的使用。不同的编译器和不同的优化等级都会产生不同的代码,也会有不同的栈stack需求。因此对于栈stack的最大需求需要随着代码更新而更新。

设置栈stack大小

在设计应用程序时,就需要考虑栈stack的大小,也就是说需要一种测试使用栈stack大小的方法。也许即便整个RAM空间都作为栈stack,也可能都不够的。一种计算栈stack大小的方法是测试最坏情况。测试中,需要一种方法来找出多少stack空间被使用了,可以从print的输出或者在程序执行时找出stack使用的轨迹。在复杂系统中很难激活最坏情况,例如在一个事件驱动的系统中,很多的中断可能在测试中根本没有执行到。

另外的一种方法是计算理论上的最大stack量,分析复杂系统的调用关系的工具能实现快速和准确的计算栈stack的大小。这个工具可以作用于二进制镜像或者源代码,二进制的使用机器码来通过程序计数器的位置来找到最坏的执行路径。源代码的静态分析工具采集应用程序的编译阶段的基本单元。两种方法中的工具都能通过编译单元决定直接的函数调用和间接的函数调用,从而根据整个系统的call graph调用图计算一个保守的栈大小。源代码的分析方法还需要考虑编译器占用的栈stack空间,如对齐和编译器的临时变量,这些可以通过分析目标和可执行代码的工具实现。通常这些工具自己设计实现很困难,商业软件是个好的选择,如独立的静态分析工具PC-Lint,Express Logic提供的StackX等工具。编译器和链接器也能利用一些信息来计算最大的栈stack需求,如为ARM处理器的IAR Embedded Workbench工具。

计算栈stack的深度


一个计算栈stack深度的方法是利用当前的栈stack指针。简单的取得函数的参数或者临时变量的地址,然后回朔到main函数,计算使用最大使用的栈stack空间。下面是一个例子,假设栈是向下生长的,即从高地址到低地址。
char *highStack, *lowStack;
int main(int argc, char *argv[])
{
    highStack = (char *)&argc;
    // ...
    printf("Current stack usage: %d\n", highStack - lowStack);
}

void deepest_stack_path_function(void)
{
    int a;
    lowStack = (char *)&a;
    // ...
}
这种通过栈指针的地址的方法适用于小的确定性系统,但对于大型的复杂的系统并不适用。另外这种方法还没有考虑到中断函数使用的栈空间。
以上方法的一个变种包括使用高频时钟(10到250 kHz)中断周期性的采样栈指针,这种方法不用手动的分析函数来计算最大的栈深度, 而且如果允许中断抢断,可以通过其他中断函数来计算栈空间大小。但需要注意的是,因为中断函数通常很小,需要确认中断函数内能有至少一个时钟采样。

void sampling_timer_interrupt_handler(void)
{
    char* currentStack;
    int a;
    currentStack = (char *)&a;
    if (currentStack < lowStack) lowStack = currentStack;
}

栈stack保护区

栈保护区是一块分配在栈之下的一块内存空间(假设栈stack是向下生长的),如图2所示,这样当栈stack下溢时,就能在保护区留下痕迹trace。这种方法适用于桌面系统,因为有操作系统可以检测因栈下溢导致的内存保护错误。而对于小的嵌入式系统没有MMU,一个栈的保护区还是可以插入来检测栈下溢。同时为了突出保护区的意义,需要设置合适的栈大小来保证保护区有写入的情况。

图2. 栈stack保护区来跟踪栈下溢


通过软件的方法来检查保护区是否还完整(即填充保护区相同的数据,如0xff的数据,然后检测保护区是否被写入)来确定栈下溢情况。更好的方式还是有内存保护单元MPU的MCU,MPU能触发当保护区被写入时,任何保护区的写入都能触发异常,后续的异常处理来决定栈空间大小。

检测栈下溢—填充栈空间为固定模式


把栈空间填充成固定的值可以检测栈空间的下溢,如在程序开始前填充0xCD。当程序终止时,栈内存可以从栈底端搜索模式直到0xCD没找到。这样就能得到从栈顶端的栈空间大小。很多的调试器都采用这种方法来检测栈使用,调试器还可以用图形化的方法来显示栈使用。通常debugger检测的栈下溢都是有滞后的,即之前的执行有溢出。

链接器计算的最大栈使用

现在来看编译器和链接器来计算栈大小,如前面提到的IAR Embedded Workbench。链接器能通过调用图来计算最大的栈使用。对于嵌套和递归函数,需要开发者给出调用层次深度,这样编译器才能对栈使用给出合理的需求大小。

图3. IAR Embedded Workbench显示的使用固定模式跟踪栈调用

通常编译器会产生每个C函数的基本信息,但一些情况下还需要提供额外的栈相关的信息,如间接调用的函数指针情况。使用pragma指示字能提供一些栈空间大小的信息。如下例所示。
void
foo(int i)
{
    #pragma calls = fun1, fun2, fun3
    func_arr[i]();
}
如果使用栈使用的控制问卷,你能额外提供模块内的函数的栈使用情况。链接器还能产生警告信息如果一些必要的信息缺失时,如下面的一些情况:

  • 有函数没有给出栈的使用情况;
  • 有间接调用没给出可能的调用函数
  • 有没有被包含进调用图的函数;
  • 有递归调用
  • 有调用调用图的主函数的函数;


下面是给出的一个栈使用情况的图例:


图4: 链接器给出的最大栈使用情况


然后系统总的最大栈的使用情况就能用累加得到:

500+24+24+12+92+8+1144+8+24+32+152 = 2020 bytes

需要注意的是,链接器给出的最大栈使用情况是最快情况的栈使用,因而给出的可能是过于悲观的数据。

References

1. Nigel Jones, "Computing Your Stack Size: Stack Overflow
2. John Regehr, "Say no to stack overflow," EE Times Design, 2004.
3. Carnegie Mellon University, "Secure Coding in C and C++, Module 4, Dynamic Memory Management," 2010.

http://www.embedded.com/design/debug-and-optimization/4394132/Mastering-stack-and-heap-for-system-reliability--Part-1---Calculating-stack-size?cid=Newsletter+-+Embedded.com+Tech+Focus

http://houh-1984.blog.163.com/

http://www.embedded.com/design/debug-and-optimization/4397656/Mastering-stack-and-heap-for-system-reliability--Part-2---Properly-allocating-stacks?cid=Newsletter+-+Embedded.com+Tech+Focus

小结

本文一步步介绍了嵌入式系统中为了充分优化利用存储资源稳健设计中的分配堆heap和栈stack的方法。第一部分是如此在系统中测试需要的stack的大小(使用栈保护区和链接器的方法),以及如何检测栈相关的下溢错误信息。

猜你喜欢

转载自blog.csdn.net/lyw851230/article/details/88374621