0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

99%开发者从未听说过的堆栈模型

痞子衡嵌入式 来源:痞子衡嵌入式 作者:痞子衡嵌入式 2022-11-29 11:19 次阅读


朋友: 你知道如何设置栈最安全么?你知道如何不写一行汇编代码就能设置栈的大小么?你知道如何在链接脚本中使用宏和头文件么?你知道如何在代码中随时随地检查栈的最大使用情况么? 本文从理论到实践,从知其然到知其所以然,一杯奶茶的功夫就给你讲得明明白白。

在中文嵌入式环境中,时不时的总能看到不少朋友”堆”“栈“傻傻分不清楚,我很早之前在文章《漫谈C变量——夏虫不可语冰》介绍过二者的区别,这里就不再深入展开,总之:
栈(Stack)“是我们用来分配局部变量、实现函数调用和在异常响应时保存被打断代码上下文的地方——具体细节不重要,在本文的讨论中,我们只需要记住以下信息
  • Cortex-M系统栈的生长方向是自上而下的,也就是随着更多内容被压入(PUSH)栈中,栈顶指针的地址值是越来越小的——也就是从地址值较大的位置向地址值较小的位置移动。
  • Cortex-M的栈顶指针指向的是“栈顶部的空位”
  • 从最大兼容性角度考虑,Cortex-M架构下栈存储空间必须对齐到8字节。

f73033cc-6f92-11ed-8abf-dac502259ad0.png

“堆(Heap)”是我们使用 malloc 申请动态存储空间时所必须用到的一种数据结构——通常由C语言的系统库提供。
  • 堆本身只是一个内存管理的算法,它所要管理的RAM空间需要用户通过某种手段将指定大小的RAM空间交到Heap算法手里
  • 与栈不同,堆的生长方向其实完全由具体的管理算法决定,而堆的算法数量虽然不能说是灿若星辰,至少一双手肯定数不过来——但一般来说我们可以大体认为堆的生长方向是“自下而上的”——也就是从地址值较小的位置延伸到地址值较大的位置。
  • 堆的对齐要求一般是4字节起步,8字节更好,情况不明的直接就32个字节吧

f7401fda-6f92-11ed-8abf-dac502259ad0.png

【常见的堆栈模型】


从单纯从我不负责任的经验来看,由很多GCC领衔使用的“对向生长”模型可能是嵌入式领域最常见的”大聪明模型“,没有之一。如下图所示:

f74f1620-6f92-11ed-8abf-dac502259ad0.png

先说优点吧:
  • 该模型栈和堆共用同一块连续的地址区间
  • 配置时不需要操心具体栈有多大、堆有多大
  • 配置方法简单:只需要指定这一整块”堆栈“区域的起始地址,以及这一整块堆栈区域的大小
  • 堆和栈的最大可用大小是此消彼长的,理论上可以在某种最优的情况下达到动态的”此消彼长“,可以获得理想状下最大的空间复用效率。
缺点也很明显:
  • 堆和栈的最大可用大小是此消彼长的,在真实场景中,由于”你长我也长谁怕谁”的情况居多,发生随机性的“双向奔赴”从而进行“负距离”的互动可能性从理论上就不可避免,因而是系统稳定性的“一生之敌”
  • 实验室里7x24小时完美通过,一去客户那里就随机性宕机的“挖坑之王”

为了提高系统稳定性,人们简单地将“堆”和“栈”拆开来单独配置,就获得了常见的“两段式堆栈模型”:

f778ca9c-6f92-11ed-8abf-dac502259ad0.png

可以看到,相较之前的模型,虽然仍然是“对向生长”,但由于栈和堆有了自己固定空间,因此可以方便地根据实际用量调整它们的大小(比如留下足够的余量),从而降低彼此入侵带来的稳定性风险。 更有甚者,在二者的边界上引入一个特殊值(比如0xDEADBEEF)所充当的溢出检测”金丝雀(Canary)”——一旦发现这个值与预设的不同,基本就可以断定发生了溢出。

【最安全的“两面包夹芝士”模型】


将“栈(Stack)”和"堆(Heap)"独立配置的“两段式”模型配合边界金丝雀,为预防和检测堆栈溢出提供了可能。但对金丝雀的检测总归有种“事后诸葛亮”的感觉,而且很多时候,我们是想不起来去检查金丝雀的,比如:栈曾经一度跨越雷池入侵到了堆空间,但由于此时堆恰巧分配出去的RAM不多,没有与栈发生实质性的重叠,因而整个系统“安然无恙”——这只能说是运气好,而风险肯定是存在的——正由于系统“安然无恙”,因此我们在系统开发阶段可能不会想起来去检查一下金丝雀(有自动检查机制的RTOS除外),那么这类溢出就有可能被隐藏。

基于上述原因,有没有一种方法可以:

  • 彻底避免栈/堆入侵对系统的破坏
  • 在栈/堆入侵的瞬间就立即表现出来——方便我们在调试阶段立即发现
答案是肯定的,这就是“两面包夹芝士”模型(此前又叫“三明治”模型):

f79f234a-6f92-11ed-8abf-dac502259ad0.png

从上图很容易看出:
  • 该模型属于“两段式模型”的变种
  • 与过去堆和栈的“相向生长”不同,该模型采用了“背向生长”的方式——避免了栈与堆的相互伤害
  • 栈被放在了SRAM的起始位置(Cortex-M从架构上鼓励将SRAM放置在从0x2000-0000开始的地址上),这样一旦发生栈溢出,指针就会指向SRAM存储器以外的无效位置——这在大部分芯片会触发“Bus Fault”,从而产生故障异常——这就实现了对栈溢出的当场捕获,并且不依赖MPU或者“栈底地址限制检测(Stack Limit Checking)”之类的架构特性。

当然有些芯片设计者可能会选择“隐藏这类错误”,不仅不会触发异常,而且会当做无事发生,具体表现为:对无效地址的写入操作将被无视,对无效地址的读取操作将会返回0值。具体可以参考芯片手册,或者干脆做个实验。
  • 堆被放置在了RAM的最后,中间夹着存放静态/全局变量的“RW/ZI区域”,这也是“两面包夹芝士”模型(或者“三明治”模型)名称的由来。这样的安排也彻底杜绝了栈和堆对“RW/ZI区域”发生入侵的可能。当堆溢出时,与栈类似,对大部分芯片来说都会触发故障异常,从而在开发调试阶段第一时间被我们所捕获。

  • 通过链接脚本(比如Arm Compiler的Scatter Script或者gcc、clang的ld)的一些运算功能,我们甚至可以做到“将剩下的空间全留给HEAP”,从而简化系统的配置。

【Arm官方低调推荐的”新“方法】


其实,Arm Compiler 在很久之前就逐步淘汰了“大聪明的单段对向生长模型”,而“两段模型”早已成为主流。比如,我们在汇编启动文件中经常可以见到这样的代码片段:

f872ee50-6f92-11ed-8abf-dac502259ad0.png

这就是“两段式”模型的证据。实际上,在启动代码的尾部,汇编程序通过:

IMPORT __use_two_region_memory

选择了对两段式模型提供支持的libc库:

f8823e96-6f92-11ed-8abf-dac502259ad0.png

看过我前面一期文章《【嵌入式秘术】Cortex-M静态链接库——从入坑到入土》的小伙伴一定会眼前一亮——“原来是这样啊,我们其实是手动选择了对应两段式堆栈模型的库版本呢”。 问题是,我们要如何在Arm Compiler环境下实现“两面包夹芝士”模型呢?我们需要写汇编代码么? 不用担心,即便你的启动文件是汇编的,具体操作方法也非常简单。步骤如下: 步骤一:准备阶段

注意:此步骤只针对使用汇编启动文件的情况。如果你的启动文件是C,则可跳过该步骤。

在工程管理器中找到你的汇编启动文件,它通常以

startup_<芯片型号>.s
的形式命名:

f8a553b8-6f92-11ed-8abf-dac502259ad0.png

找到配置栈和堆大小的部分(红框标注的部分):

f8bf15f0-6f92-11ed-8abf-dac502259ad0.png

将其整体删除(或者注释掉)。注意:请保留这里的 PRESERVE8和THUMB部分。

继续移动到汇编文件的尾部,找到如下的代码:

f8cfc5da-6f92-11ed-8abf-dac502259ad0.png

同理,将其删除(或者注释掉)。注意:这里要保留 END 。

移动到中断向量表的定义处:

f8eee49c-6f92-11ed-8abf-dac502259ad0.png

将红框中所标注的代码选中:
__VectorsDCD__initial_sp
替换为如下内容:
                IMPORT   |Image$$ARM_LIB_STACK$$ZI$$Limit|


__VectorsDCD|Image$$ARM_LIB_STACK$$ZI$$Limit|
即:

f9063ad4-6f92-11ed-8abf-dac502259ad0.png

保存启动文件。

此时,如果你着急编译,当你当你开启了microLib时,很可能会看到如下的链接错误:

f90fa718-6f92-11ed-8abf-dac502259ad0.png

即:
Error: L6218E: Undefined symbol __initial_sp (referred from entry2.o).
或者你没有开启 microLib,则会看到一个不同的错误:

f926fd28-6f92-11ed-8abf-dac502259ad0.png

即:
Error: L6915E: Library reports error: The semihosting __user_initial_stackheap cannot reliably set up a usable heap region if scatter loading is in use
这都是正常的,不必惊慌。这类错误会在完成后面的步骤后自然消失。
步骤二:获取链接脚本(Scatter Script)

打开工程配置窗口“Options for Target”,切换到“Linker”选项卡:

f93274fa-6f92-11ed-8abf-dac502259ad0.png

首先,一定要确保你勾选了图中的“Use Memory Layout from Target Dialog”选项。在这一前提下,再次取消对它的勾选:

f93f09cc-6f92-11ed-8abf-dac502259ad0.png

我们会看到,MDK基于当前的Memory Layout,为我们在Out目录下生成了一个与工程同名的链接脚本(比如图中的工程名叫example,因此生成的链接脚本为 example.sct)。

单击 Edit 按钮,可以看到脚本的内容:

f95980e0-6f92-11ed-8abf-dac502259ad0.png

先别着急半路开香槟——该文件是系统自动生成的,如果我们不移动它的位置,那么只要哪次手抖勾选了“Use Memory Layout from Target Dialog”,它的内容就会立即被覆盖掉——意味着我们在后续步骤中所做的改就会付诸东流。

为了避免该问题,应该将它从 Out 目录中移动到工程目录下。具体步骤为,右键单击脚本文件名:

f9790492-6f92-11ed-8abf-dac502259ad0.png

选择“Open Container Folder”来打开文件所在目录:

f991f6a0-6f92-11ed-8abf-dac502259ad0.png

找到Scatter Script脚本文件后,将其拷贝到上一级目录下(也就是工程目录):

f9a82704-6f92-11ed-8abf-dac502259ad0.png

重新打开工程配置窗口:

f9c4323c-6f92-11ed-8abf-dac502259ad0.png

确保我们“没有”选中“Use Memory Layout from Target Dialog”选项,并在Scatter File文本框中直接填写我们刚刚拷贝出来的脚本文件名(由于我们直接放在工程目录下,因此这里直接用相对路径"./example.scat"或者"example.scat"就行)。单击OK保存配置。 步骤三:在链接脚本中部署堆和栈

在编辑器中打开我们的脚本文件:

f9de471c-6f92-11ed-8abf-dac502259ad0.png

图中选中的部分实际上包含了RAM中的所有内容,包括静态变量、全局变量、栈和堆:

f9f632c8-6f92-11ed-8abf-dac502259ad0.png

是的,你的猜测没错:当我们没有特别说明时,Stack和Heap都以ZI的形式存在于上述空间内,其位置任由Linker摆布——这当然也带来了很多不确定性

接下来我们要做的就是按照我们的设计——“两面包夹芝士”来明确的指定栈和队列的大小和位置:

f79f234a-6f92-11ed-8abf-dac502259ad0.png

我们要做的是首先将一个名为ARM_LIB_STACK的execution region放置到RAM的起始位置:
  ARM_LIB_STACK 0x20000000 ALIGN 8 EMPTY 0x800 {}
这里:
  • 起始地址是 0x20000000
  • STACK的大小是 0x800
  • ALIGN 8 指定对齐是8个字节
  • EMPTY是必须要保留的,它用来说明 ARM_LIB_STACK 是一个大数组,里面默认填充了0。
  • 如果你想修改填充的内容还可以通过关键字 FILL <填充值> 来指定填充的32bit数值,比如:
 ARM_LIB_STACK 0x20000000 ALIGN 8 FILL 0xDEADBEEF EMPTY 0x800 {}
它实现了往0x20000000开始的0x800(2KB)大小的栈空间中填充0xDEADBEEF的功能:

fa3acb36-6f92-11ed-8abf-dac502259ad0.png

熟悉“水印法”测量栈用量的小伙伴一定大喜。

为了让ZI/RW紧随其后——放在STACK的后面,我们需要对 RW_IRAM1 的描述进行修改,即从:

RW_IRAM1 0x20000000 0x00020000  {
修改为:
RW_IRAM1+0{
即:

fa58cdca-6f92-11ed-8abf-dac502259ad0.png

这里,我们在原本放置地址0x20000000的位置用"+0"表示“紧随其后”,并删除了原本的大小0x00020000——这样做就是告诉编译器“RW_IRAM1”不限制大小。

接下来,我们要用类似的方法紧随 RW_IRAM1 之后放置名为 ARM_LIB_HEAP 的execution region——用来指定堆的位置和大小:

ARM_LIB_HEAP +0 ALIGN 8 EMPTY 0x200 {}
可以看到,这里与栈的设置方式几乎一样,而“+0”则同样告诉linker:请将ARM_LIB_HEAP紧邻前面的 RW_IRAM1 放置。最终的效果如下:
LR_IROM1 0x00000000 0x00040000  {    
  ER_IROM1 0x00000000 0x00040000  {  
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
   .ANY (+XO)
  }


  ARM_LIB_STACK 0x20000000 ALIGN 8 EMPTY 0x800 {}


  ;RW_IRAM1 0x20000000 0x00020000  {  ; RW data
  RW_IRAM1 +0  {  ; RW data
   .ANY (+RW +ZI)
  }


ARM_LIB_HEAP+0ALIGN8EMPTY0x200{}
}

fa6bee5a-6f92-11ed-8abf-dac502259ad0.png

还记得我们前面删除了原本对RW_IRAM1的尺寸限制(也就是0x0002000)么?这意味着,现阶段的脚本文件对我们实际使用的RAM空间是没有任何限制的——换句话说,如果超出了芯片实际的SRAM大小,编译器也是不会报告错误的。为了重新加入这一限制,我们可以在 ARM_LIB_HEAP的后面加入下面的语句:

ScatterAssert(ImageLimit(ARM_LIB_HEAP) <= 0x20000000 + 0x20000)
这里:
  • ScatterAssert() 是让linker对括号中的内容进行检查
  • ImageLimit() 是在编译时刻获得括号内指定execution region的终止地址
  • 0x20000000+0x20000是例子中整个RAM的终止地址(这里假设RAM从0x20000000开始,大小是0x20000
  • 综合来说,上述代码的作用是在linker的链接阶段计算HEAP的终止地址,确认它是否落在了RAM的有效范围内。
如果超出了范围,我们就会看到如下的编译错误:
Error:L6388E:ScatterAssertexpression(ImageLimit(ARM_LIB_HEAP)<= 0x20000000 + 0x20000) failed on line 22 : (0x20001220 <= 0x20020000)
最终效果如下:

fa772d10-6f92-11ed-8abf-dac502259ad0.png

对应的“两面包夹芝士”图示如下:

fa927c96-6f92-11ed-8abf-dac502259ad0.png

编译工程:

fabb7588-6f92-11ed-8abf-dac502259ad0.png

【“虽迟但到”的宏和头文件】


是的,你猜没错,我们可以在链接脚本中使用编译预处理,这意味着:
  • 我们可以使用宏
  • 我们可以include头文件
  • 我们可以进行条件编译

具体方法并不难,只需要在链接脚本的“第一行”,注意一定要是第一行(Number One)——前面不能有任何内容,空行或者注释都不行——放置如下的内容:

#! armclang --target=arm-arm-none-eabi -mcpu=cortex-m0 -E -xc

faf17d22-6f92-11ed-8abf-dac502259ad0.png

然后我们就可以在脚本文件中愉快地使用宏和include了。看到脚本中这么多的常数了么?地址啊、大小啊,这下都可以用宏替代了。比如:
#define RAM1_SIZE    0x00020000
#define RAM1_BASE    0x20000000
#define RAM1_LIMIT   (RAM1_BASE + RAM1_SIZE)


#define STACK_SIZE   0x800
#define HEAP_SIZE    0x200

fb145658-6f92-11ed-8abf-dac502259ad0.png

其实我们还可以把宏的定义部分放置到专门的配置头文件中——通过#include来包含——从而真正做到一个配置头文件定天下。 至于宏可以有哪些骚操作,感兴趣的小伙伴可以关注【裸机思维】公众号后,发送关键字“”来获取相关文章,这里就不再赘述。
需要注意的是:
  • 在较新版本的MDK中,上述方法“应该”同时支持Arm Compiler 5(armcc)Arm Compiler 6(armclang)。你可以关注【裸机思维】公众号后,发送关键字“MDK”来获取最新的MDK。
对于某些较老的MDK来说,如果你使用的是 Arm Compiler 5,则需要把添加在 scatter script 第一行的命令行修改为:
#! armcc --cpu Cortex-M0 -E
...

以解决可能出现的编译错误。

  • 如果你的头文件并没有“直接”放置在工程目录下,而是存在一个相对路径,则可以通过在上述命令行中追加 -I <路径> 的形式来告知编译器去哪里搜索我们的头文件。比如:
#!armclang--target=arm-arm-none-eabi-mcpu=cortex-m0-E-xc-I../../cfg

或者

#!armcc--cpuCortex-M0-E-I../../cfg

则是告诉编译器从相对路径 "../../cfg" 下去搜索头文件。

  • 当你通过修改头文件的方式来更新scatter script的内容后,第一次编译,请务必一定要以“Rebuild All”的形式进行,否则你的修改不会生效。

别说我没提醒过你哦!


【如何把剩余的空间都留给堆】


很多时候,把剩余空间都留给堆是一个不错的想法,这样“两面包夹芝士”模型就获得了和“单段相向生长”模型一样的优势——配置简单。由于我们已经有了宏的帮助,借助 ImageLimit() 我们可以将 HEAP_SIZE 的宏定义修改为:
#define HEAP_SIZE    (RAM1_LIMIT - ImageLimit(RW_IRAM1))
它的意思是:用RAM1的终止地址减去 RW_IRAM1的终止地址,获得中间的差额,其图示如下:

fb5ca96c-6f92-11ed-8abf-dac502259ad0.png

看似完美,有的小伙伴一编译就会报告如下的错误:

fb6a5cd8-6f92-11ed-8abf-dac502259ad0.png

即:

Error: L6388E: ScatterAssert expression (ImageLimit(ARM_LIB_HEAP) <= (0x20000000 + 0x20000)) failed on line 29 : (0x20020004 <= 0x20020000)

奇怪,我们的计算公式应该没错啊——Heap的尺寸应该就是使用整个 RAM的终止地址减去 RW_IRAM1 的终止地址啊,为什么提示差4个字节呢?

聪明的小伙伴一定已经注意到了,我们在 ARM_LIB_HEAP 的定义中,指定了其首地址的对齐为8字节:

ARM_LIB_HEAP +0 ALIGN 8 EMPTY HEAP_SIZE {}

RW_IRAM1 的尺寸不一定是8的整倍数,当它只是“4的整倍数”而不满足“8的整倍数”这一条件时,ImageLimit(RW_IRAM1)的后面与 ARM_LIB_HEAP的起始地址之间就会产生一个4字节的气泡

fb75b5c4-6f92-11ed-8abf-dac502259ad0.png

要解决这一问题也很简单,我们可以使用 scatter script 脚本为我们提供的一个专门来进行地址对齐的函数:

AlignExpr(<地址数值>,<对齐要求>)

比如:

AlignExpr(ImageLimit(RW_IRAM1), 8)

就表示对 RW_IRAM1 的终止地址进行 8 字节对齐。借助它的帮助,我们可以修改脚本如下:

#defineHEAP_SIZE
    (RAM1_LIMIT-AlignExpr(ImageLimit(RW_IRAM1),8))

即:

fb8d26e6-6f92-11ed-8abf-dac502259ad0.png

再编译时,已然没有问题。

【如何随时随地的了解栈的最大使用情况】


水印法是实现“最大栈用量统计”的最有效方式。其原理也不复杂:

  1. 先用指定的水印常数(比如 0xDEADBEEF)将整个栈填满;
  2. 从栈空间的最初顶部(栈存储空间的终止地址)向下开始搜索之前填充的水印常数——一旦碰到水印,就将当前已经经历过的RAM总量作为栈的最大深度(最大用量);

fbf32b30-6f92-11ed-8abf-dac502259ad0.png

对于步骤1来说,可以通过前面介绍的 FILL 关键字来完成对栈空间的填充:
ARM_LIB_STACK0x20000000ALIGN8FILL0xDEADBEEFEMPTYSTACK_SIZE
{
}

然后借助下面的代码完成统计工作:

#if defined(__clang__)
#   pragma clang diagnostic push
#   pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#   pragma clang diagnostic ignored "-Wdouble-promotion"
#endif
uint32_t calculate_stack_usage_topdown(void)
{
    extern uint32_t Image$$ARM_LIB_STACK$$Limit[];
    extern uint32_t Image$$ARM_LIB_STACK$$Length;


    uint32_t *pwStack = Image$$ARM_LIB_STACK$$Limit;
    uint32_t wStackSize = (uintptr_t)&Image$$ARM_LIB_STACK$$Length / 4;
    uint32_t wStackUsed = 0;




    do {
        if (*--pwStack == 0xDEADBEEF) {
            break;
        }
        wStackUsed++;
    } while(--wStackSize);
    
    
    printf("
Stack Usage: [%d/%d] %2.2f%%
", 
            wStackUsed * 4, 
            (uintptr_t)&Image$$ARM_LIB_STACK$$Length,
            (   (float)wStackUsed * 400.0f 
            /   (float)(uintptr_t)&Image$$ARM_LIB_STACK$$Length));


    return wStackUsed * 4;
}
#if defined(__clang__)
#   pragma clang diagnostic pop
#endif

这里有几点需要说明一下:

  • armlink 为我们提供了通用的语法来获取 execution region 的起始地址、大小和终止地址:

extern uint32_t Image$$$$Base[];
externuint32_tImage$$$$Length;
extern uint32_t Image$$$$Limit[];

这里,BaseLimit被定义成了不定长数组的形式,因此我们可以直接把它们当做常量指针来使用——获取所需的地址。Length被定义成了一个普通的uint32_t型的变量,按照官方文档的要求,虽然很反直觉,但如果要获取它的值——也就是对应execution region的大小,必须要对其进行&操作,并随后强制转化为整形数值。这么说也许有点抽象,不妨对照前面的代码来看:

#include 
...
extern uint32_t Image$$ARM_LIB_STACK$$Limit[];
extern uint32_t Image$$ARM_LIB_STACK$$Length;


uint32_t *pwStack = Image$$ARM_LIB_STACK$$Limit;
uint32_t wStackSize = (uintptr_t)&Image$$ARM_LIB_STACK$$Length / 4;

这里,我们通过 Image$ARM_LIB_STACK$$Limit[] 将栈的终止地址赋值给了(uint32_t *)型的指针 pwStack。以表达式 (uintptr_t)&Image$$ARM_LIB_STACK$$Length 获取了 ARM_LIB_STACK 的实际大小。

  • 普通情况下,在变量名中使用 “$” 会在Arm Compiler 6引发警告:

warning:'$'inidentifier[-Wdollar-in-identifier-extension]

为了让编译器闭嘴,我们临时对函数 calculate_stack_usage_topdown() 在编译时刻做了屏蔽warning的操作:

#if defined(__clang__)
#   pragma clang diagnostic push
#   pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#   pragma clang diagnostic ignored "-Wdouble-promotion"
#endif
uint32_t calculate_stack_usage_topdown(void)
{
    ...
}
#if defined(__clang__)
#pragmaclangdiagnosticpop
#endif

-Wdouble-promotion 则是由printf中的百分比运算引起的,一并屏蔽即可。

在任意时刻,当我们想要知道当前系统的最大栈用量时,可以直接调用函数 calculate_stack_usage_topdown(),比如:
int main(void)
{
    ...
calculate_stack_usage_topdown();
...
}

一个可能的执行结果如下:

fc1dcf66-6f92-11ed-8abf-dac502259ad0.png

自上而下统计栈用量的方法优点是:当栈空间很大而实际栈用量较小时,可以较快的完成统计;缺点是:如果恰好栈里因为任何原因(比如用户定义了一个局部变量,然后恰好给他赋予了我们的水印常数),就会造成统计错误——没能实际获得最大深度。

针对这一问题,我们可以修改搜索策略,从占空间的起始地址(也就是基地址)处向上搜索“非水印常数”——一旦碰到,就可以用已知的栈空间尺寸减去已经经历过的RAM总量作为栈的最大深度(最大用量)。

fc3ff47e-6f92-11ed-8abf-dac502259ad0.png

该方法的优点是:不容易发生误判;缺点是:当栈空间很大而实际栈用量较小时往往较为耗时。对应的代码如下:

#if defined(__clang__)
#   pragma clang diagnostic push
#   pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#   pragma clang diagnostic ignored "-Wdouble-promotion"
#endif
uint32_t calculate_stack_usage_bottomup(void)
{
    extern uint32_t Image$$ARM_LIB_STACK$$Base[];
    extern uint32_t Image$$ARM_LIB_STACK$$Length;


    uint32_t *pwStack = Image$$ARM_LIB_STACK$$Base;
    uint32_t wStackSize = (uintptr_t)&Image$$ARM_LIB_STACK$$Length;
    uint32_t wStackUsed = wStackSize / 4;


    do {
        if (*pwStack++ != 0xDEADBEEF) {
            break;
        }
    } while(--wStackUsed);
    
    printf("
Stack Usage: [%d/%d] %2.2f%%
", 
            wStackUsed * 4, 
            wStackSize,
((float)wStackUsed*400.0f/(float)wStackSize));


    return wStackUsed * 4;
}
#if defined(__clang__)
#   pragma clang diagnostic pop
#endif

【后记】


在这篇文章中,我们介绍了栈和堆在存储器中的常见排布模型,比较了它们的优劣,并提出了一种被称为“两面包夹芝士”的两段式模型。该模型:
  • 可以有效避免堆栈溢出破坏常规变量
  • 溢出发生时可以在大部分芯片中第一时间触发异常——被我们捕捉到

后面,我们以MDK为例介绍了如何在Arm Compiler环境下应用这一模型,并引入了使用宏对其进行进一步拓展的方法。

值得说明的是,这一方法对Arm Compiler 5(armcc)Arm Compiler 6(armclang)同样适用。支持MicroLib和非MicroLib的情况。无论启动文件是否为汇编,都可以正常工作。

实际上,使用链接脚本而非汇编启动文件来对两段式堆栈模型进行配置是Arm公司一直以来所提倡的。随着Arm Compiler 6的逐步普及,更多的芯片公司正在追随Arm的脚步将原本的汇编启动文件替换为 CMSIS 目录下所提倡的纯C语言启动文件。

作为【反复横跳】系列的一部分,我希望通过这篇文章能帮助大家扫清从Arm Compiler 5Arm Compiler 6过渡图中与栈相关的障碍。希望对你有所帮助。


审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 嵌入式
    +关注

    关注

    4984

    文章

    18300

    浏览量

    288656
  • 堆栈
    +关注

    关注

    0

    文章

    171

    浏览量

    19535
  • 变量
    +关注

    关注

    0

    文章

    597

    浏览量

    28114

原文标题:99%开发者从未听说过的堆栈模型

文章出处:【微信号:pzh_mcu,微信公众号:痞子衡嵌入式】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    开发者手机 AI - 目标识别 demo

    功能简介 该应用是在Openharmony 4.0系统上开发的一个目标识别的AI应用,旨在从上到下打通Openharmony AI子系统,展示Openharmony系统的AI能力,并为开发者提供AI
    发表于 04-11 16:14

    比无缝漫游更高级的无感漫游,你听说过吗?

    关于无缝漫游无缝漫游,很多人都听说过。无缝漫游是指设备在移动过程中穿越不同网络覆盖区域时,能够从一个接入点(AP)自动切换到另一个接入点,而无需中断网络服务或手动进行任何操作,确保网络连接在移动
    的头像 发表于 03-28 08:15 110次阅读
    比无缝漫游更高级的无感漫游,你<b class='flag-5'>听说过</b>吗?

    源码开放,开发者手机 buff 叠满

    开发者手机开源代码编译指导 编译环境建议: ubuntu20.04 Linux 系统内存:最低 16G Pyhon 3.8 安装必要工具: sudo apt-get update sudo
    发表于 03-04 14:29

    鸿蒙开发者预览版如何?

    在24年的华为鸿蒙发布会中表示。预览版已经向开发者开放申请,首批支持的机型有三款分别为华为 Mate 60、华为Mate 60 Pro、华为Mate X5。 其HarmonyOS NEXT去除
    发表于 02-17 21:54

    鸿蒙系统优缺点,能否作为开发者选择

    星河版已经是纯血鸿蒙,但是它的发展一些周期。生态圈的建立难度大,各大厂商加入鸿蒙原生开发需要时间累积。 鸿蒙开发人才空缺,由于鸿蒙作为一款新型的系统,程序员们都是从0学起。所以市面上很少有鸿蒙开发者
    发表于 02-16 21:00

    您有一份OpenHarmony开发者论坛2023年度总结,请查收~

    2023 年 11 月,OpenHarmony 开发者论坛 1.0 版本正式上线。 感谢各位开发者对 OpenHarmony 的大力支持和热爱,成为 OpenHarmony 开发者论坛的第一批
    发表于 01-26 17:27

    HarmonyOS SDK,助力开发者打造焕然一新的鸿蒙原生应用

    六大领域的开发能力,为开发者带来简洁、高效的开发体验,开发者只需通过 API 调用即可实现丰富的鸿蒙原生应用功能和独特体验。同时,在开发
    发表于 01-19 10:31

    欢迎加入飞腾派开发者社区,感谢每一位开发者

    发烧友论坛一起策划了飞腾派开发板测评活动,受到了广大开发者的喜爱。 通过这次活动,飞腾派成功地吸引了众多高质量开发者的关注和参与,进一步扩大了其在开发者社区中的影响力。此次活动将电子
    发表于 12-11 16:11

    鸿蒙原生应用/元服务开发-开发者如何进行真机测试

    前提条件:已经完成鸿蒙原生应用/元服务开发,已经能相对熟练使用DevEco Studio,开发者自己有鸿蒙4.0及以上的真机设备。 真机测试具体流程如下 1.手机打开开发者模式 2.在项目中,左上角 文件(F)->项目结构
    发表于 11-30 09:46

    OpenHarmony开发者论坛正式上线,盖楼赢惊喜好礼~

    如何参与和贡献? 你们的声音,我们都有认真聆听! 你们的期待,就是我们前进的动力! 值此OpenHarmony开发者论坛正式上线之际,为了答谢广大开发者的关注与支持,我们发起了 OpenHarmony开发者论坛”盖楼有礼“活动
    发表于 11-15 09:56

    各位开发者期待已久的开源鸿蒙开发者手机已经开放购买啦!!

    各位开发者期待已久的开源鸿蒙开发者手机已经开放购买啦!! “开源鸿蒙开发者手机”,本质上是手机形态的开发板,为广大 OpenHarmony 开发者
    发表于 10-10 18:32

    开发者福利月】听说点开这篇文章,假期快乐可以延续哦~

    官宣 (1012) 到程序员节 (1024) 的庆祝活动 再到谷歌开发者公众号的生日回馈 (1030) 每一天都充满了惊喜和期待 愿与开发者们共度快乐十月 代表开发者福利,看看你能找到多少个 DevFest 线下活动官宣
    的头像 发表于 10-09 10:10 208次阅读
    【<b class='flag-5'>开发者</b>福利月】<b class='flag-5'>听说</b>点开这篇文章,假期快乐可以延续哦~

    开发者如何使用讯飞星火认知大模型API?

    之前我们使用网页文本输入的方式体验了讯飞星火认知大模型的功能(是什么让科大讯飞1个月股价翻倍?),本篇博文将从开发者角度来看看如何使用讯飞星火认知大模型API。
    的头像 发表于 08-15 12:22 4419次阅读
    <b class='flag-5'>开发者</b>如何使用讯飞星火认知大<b class='flag-5'>模型</b>API?

    机械屏你听说过

    听说过会跳舞的显示屏吗,随着社会经济的稳步发展能让一些产业走向衰落,也能让一些古老的产业重新焕发光彩,在商显行业里的各类显示终端产品竞争尤为激烈,随之,各种新兴领域市场发展迅速。户外传媒、广告业
    的头像 发表于 07-10 15:47 342次阅读

    HarmonyOS/OpenHarmony应用开发- Stage模型概述

    UIAbility通过WindowStage持有了一个窗口,该窗口为ArkUI提供了绘制区域。 Context 在Stage模型上,Context及其派生类向开发者提供在运行期可以调用的各种能力
    发表于 05-25 17:44