英文版PDF地址: http://www.iar.com/Global/Resources/Seminars/Working_with_the_Stack_and_Heap.pdf
—————————————— 以下为我翻译的 ——————————————————–
议题
heap(堆)是什么?
怎样决定堆的大小
使用堆时潜在的问题
堆分配注意事项
使用栈(Stack)
决定栈的大小
使用栈时的潜在问题
静态栈检查
Embedded Workbench中的栈插件工具
示例
堆是什么?
堆是内存空间里为动态内存分配保留的一部分区域
当一个应用需要临时使用一定数量的内存时可以从堆空间分配或借用,C中通过调用malloc()函数实现,C++中通过’new’来实现
当这个内存不再需要时可以通过调用free()函数或者使用delet操作符来释放。一旦该内存被释放,可以再次分配使用。
堆的位置和大小是在编译时静态设置的。
为你的应用分配合适的堆空间很重要,否则在运行时可能崩溃。
怎样决定堆大小
考虑应用使用的动态内存时的首要问题是我需要多少堆空间?
相比来说这更像是估计而不是计算
如果你知道你的应用怎样行动,那么你应该大概知道每个时刻应该分配多少内存
另外需要考虑在管理堆空间是有一些额外的开销
堆管理器需要跟踪已经使用的和剩余的空间数量,已经分配了的块大小,通常还包含一个指向下一个可用内存的指针
另外,开发工具可能为了维护内存体系结构保留一定的内存块。譬如,EWARM编译器通常为保证栈对齐而保留8字节倍数的内存块。
潜在的问题
当不同规模的内存块频繁分配、释放时,动态内存分配是最常发生的问题随之而来。
当内存释放时,就会有一个空位
如果下一个要分配的空间比所有的空位都要大,这就会是个问题
这为debugging带来困难,因为堆上总的空闲内存空间比要分配的要大,但是内存申请可鞥因为空闲空间不连续而失败。
例如,假设如下:
堆的位置为0x20000-0x20FFF
一个8字节的内存块分布在该区间的开始位置
紧接着是一个1024字节的块
之后,第一个块被释放并可以再次使用。然而,应用需要分配大于8字节的空间,所以开始 的8字节空间就无法使用。这就是内存碎片化,如果应用用于不在需要8字节或更少的内存空间,那么这8字节的内存空间就浪费了。这个例子也说明了为什么堆中的小内存块是低效的,这种开销等于应用可用数据的数量。
堆的布局
有些情况下确切的知道堆上有些什么,位于什么位置是非常有用的。
堆上包含的不只是分派的数据:
有一些维护堆的额外开销
每一个分配的内存块都包含两个整数,指针的大小取决于CPU的架构,e.g. ARM器件会使用32bit的整数。
第一个整数指明分派的内存块的大小。
最后一个堆对象后会有一个32bit的值来说明剩余的堆空间。
最后一个堆对象开始位置的32bit值说明这个数据的地址。
堆同时跟踪下一个分配的位置,使用一个驻留在堆外部的结构体中。
typedef struct {
__data_Cell __data * __data *_Plast;
__data_Cell __data *_Head; }
__data_Altab;
指针_Head说明堆的当前状态。指针_Plast说明从哪里开始搜索目前未使用的可用块。
__data_Cell结构体定义如下:
typedef struct __data_Cell
{
__data_size_t _Size;
struct __data_Cell __data *_Next;
} __data_Cell;
其中_Size 说明堆的剩余数量, _Next 说明下一个可用分配的内存地址。如果堆空间耗尽,_Next指针则为NULL。
堆的最终思考
如果没有一些工具辅助你分析动态内存需求,直接估计堆空间是比较困难的。
在桌面Java中有这样的工具(HAT,Heap Analysis Tool)
嵌入式C/C++中还没有类似工具
针对资源有限的嵌入式系统,由于额外的开销和可能的堆内存碎片化,动态内存应该尽量少用。
在你的代码中(测试用例中)你可以创建多个结构体或者对象
MISRA C要求所有内存都必须在编译时静态分配,因此你永远也不会跑出堆空间。
EWARM中的堆统计
EWARM中的C-Spy debugger让你可以看到当前堆的使用情况
要看到这个功能,必须在工程中包含 dlmalloc.c
调用 __iar_dlmallinfo() 和 __iar_dlmalloc_stats() 会在Terminal I/O窗口打印堆统计情况。
更多信息请访问:http://supp.iar.com/Support/?note=28545
使用栈(Stack)
栈的概念相对来说比较容易理解
栈是一个LIFO街头,类似于打包东西的盒子
最后放入盒子的是第一个从盒子里移出的物体
同时,栈也有固定的大小,不能溢出
栈时一个固定的内存块,被分为两部分:
分配给当前函数的内存和分配给调用当前函数的函数的内存
可以用于分配的空闲内存
两个区域的分界线称为栈顶,使用栈指针SP表示,这是一个专用的处理器寄存器。
通过移动栈指针来分配栈内存。
函数永远不应该引用栈空间的内存(变量)——如果一个中断发生,中断服务函数会:
申请栈内存
更新栈内存
释放栈内存
上述情况发生时,函数从来不知道自己的内存以及被破坏。
栈的主要优势是程序中不同函数可以使用相同的内存区域存放他们的数据。
与堆空间不同,栈空间永远不会碎片化或者因为内存泄露而一团糟。
可以允许函数调用自身——递归函数——每个调用都能在栈空间存储自身的数据。
决定栈大小
为了决定栈的大小,我们必须理解什么东西会放到栈空间:
auto变量,即没有放到寄存器的局部临时变量和参数
表达式的临时结果
函数的返回值(除非passed in a register)
中断时的处理器状态
函数返回前要恢复的处理器寄存器内容
正如你所见,栈的大小主要取决于你代码中的函数调用数量。
使用栈时的潜在问题
1.栈的工作方式决定了函数返回后他的数据就不存在了。下面的代码演示了一个常见的编码错误。它返回了一个指向函数内部变量的指针。
int *MyFunction()
{
int x;
/* Do something here. */
return &x; /* Incorrect */
}
2.另一个问题是栈溢出。该情况会在函数调用时发生,多层函数调用消耗的栈空间大于栈的总大小。
3.如果栈空间存放大数据对象,则这种风险更高,或者递归函数调用时。
4.如果给定太大的栈空间,RAM就浪费了,如果给定的栈空间太小,会出现两种情况(取决于栈在内存空间的位置):
a 变量会被改写,导致未定义的行为
b 栈会超出内存空间,导致应用的异常终止
因为第二种选择比较用于检测到,你应该考虑将栈设置为朝内存重点增长。
静态栈检查器
有很多静态的C语言检查器
Express Logic’s StackX
Abslnt’s StackAnalyzer
John Regher’s Stack Analysis(for AVR/430 only)
AdaCore’s GNATStack
Embedded Workbench’s Stack Plug-in
EW包含一个易用的栈插件,可以监控CSTACK的大小,同时如果你超出指定的栈门限将输出消息到debug日志。
警告:该插件对于RTOS是无效的,使用RTOS时他始终汇报堆栈溢出。
转载于:https://my.oschina.net/Jr413/blog/411721