ARM嵌入式编程优化之在C/C++中栈的使用

news/2024/10/21 5:48:29/

文章目录

  • Stack的使用场景
  • 如何确定stack空间的使用情况
  • 检查栈使用情况
  • 减少栈使用的方法

Stack的使用场景

栈在C/C++中使用得非常频繁,比如,栈中可以保存:

  • 在调用函数时,保存函数的返回地址。
  • 保存一些寄存器的值,这取决于ARM 架构:the Procedure Call Standard for the Arm Architecture (AAPCS) or the Procedure Call Standard for the Arm 64-bit Architecture (AAPCS64),比如在进入子程序时,将一些寄存器里的状态信息保存到栈中。
  • 局部变量,比如数组、结构体或者联合体。
  • C++中的类,类的数据主要放在栈区或堆区,有可能是堆,也有可能是栈。这取决于实例化对象的方式:
    – A a1 = new A(); //堆
    – A a2; //栈
    还有一些比较不明显的使用场景:
  • 如果局部整型或者浮点型变量溢出了(即没有分配给寄存器),则他们会被存储到栈空间中。
  • 结构体通常会被分配到栈空间,在栈上保留了一个相当于sizeof(struct)填充为字节倍数的空间,其中对于AArch64状态为16,对于AArch32状态为8。但是,编译器可能会尝试将结构体分配给寄存器。
  • 一些优化操作可以引入新的临时变量来保存中间结果。优化包括CSE消除(CSE elimination)、live range拆分和结构体拆分。编译器会尝试将这些临时变量分配给寄存器。如果没有,编译器则会将它们保存到栈中。
  • 如果数组的大小在编译时已知,则编译器会在栈上分配内存。同样,在栈上保留了一个相当于sizeof(array)的空间,填充为字节的倍数,其中对于AArch64状态为16,对于AArch32状态为8。此外,可变长度数组的内存,会在运行时被分配到堆内存空间中。
  • 通常,为只支持16位编码T32指令的处理器编译的代码比A64代码、A32代码和为支持32位编码T32指令的处理器编译的代码更多地使用了栈。这是因为16位编码的T32指令只有8个寄存器可供分配,而A32代码和32位编码的T32指令有14个寄存器可供分配。
  • AAPCS和AAPCS64要求一些函数参数通过堆栈而不是寄存器传递,这取决于它们的类型、大小和顺序。
    • 在ARMv7或者AArch32状态下,当函数参数个数小于或等于4个(或者总的参数大小不大于4*4 bytes)时,使用寄存器(R0,R1,R2,R3)传递。当函数参数个数大于四个时,将通过压栈方式进行传递。我们通常讲的32 bit 状态下可以用寄存器(R0,R1,R2,R3)传递四个参数,这四个参数是指的四个不大于4 bytes类型的参数,如果是参数类型为 8 bytes大小,则只能传递两个。所以我们应该这么理解:如果四个寄存器(R0,R1,R2,R3)不能能存下该函数的所有参数(4 bytes 对齐),则会存储到栈中。
    • 在AArch 64状态下,X0~X7用于函数入参,最多支持8个函数入参,多余则采用入栈方式。

嵌入式应用中,通常对内存有严格限制,因此堆栈上可用的空间也有限。可以使用Arm Compiler for Embedded来确定应用程序代码中的函数使用了多少堆栈空间。函数使用的堆栈量取决于函数参数的数量和类型、函数中的局部变量以及编译器执行的优化等因素。

如何确定stack空间的使用情况

栈的使用情况很难估计,因为它依赖于代码,并且根据程序执行时所采用的代码路径,栈的使用在每次运行之间可能会变化。但是,可以使用以下方法手动估计堆栈利用率的程度:

  • 编译时使用 -g 选项,并且链接时使用 –callgraph 选项,来产生一个静态的统计图,这个callgraph 将会显示所有函数的信息,包括栈的使用情况。
  • 在链接时使用 –info=stack 或者 –info=summarystack选项,将所有全局变量的栈使用情况列出。
  • 使用调试器在栈中的最后一个可用位置上设置一个观察点,并查看是否命中了该观察点。使用-g选项进行编译以生成必要的DWARF信息。
  • 使用调试器时,我们可以:
    • 给栈分配内存空间时,尽量比程序使用的预期要更大。
    • 给栈的内存使用一些已知的数据(比如0xdeadbeaf)进行初始化。
    • 运行应用程序时。目标是在测试运行中使用尽可能多的堆栈空间。例如,尝试执行嵌套最深的函数调用和静态分析发现的最坏情况路径。尝试在适当的地方生成中断,以便将它们包含在栈跟踪中。
    • 在应用程序完成执行之后,检查内存的堆栈空间,看看有多少已知值(比如0xdeadbeaf)被覆盖了。使用 byte作为单位计算出被覆盖区域的内存大小,即可知道栈的使用情况。
    • 使用与目标处理器或体系结构对应的固定虚拟平台(FVP)。使用映射文件,在堆栈正下方定义一个禁止访问的内存区域。如果堆栈溢出到禁止区域,则会发生数据中断,调试器可以捕获该中断。

检查栈使用情况

检查应用程序中函数使用的栈使用量是一种很好的做法。然后可以考虑重写代码以减少栈使用。
要检查应用程序中的栈使用情况,可以使用链接器选项** --info=stack **。下面的示例代码显示了具有不同数量参数的函数:

__attribute__((noinline)) int fact(int n)
{int f = 1;while (n>0){f *= n--;}return f;
}int foo (int n)
{return fact(n);
}int foo_mor (int a, int b, int c, int d)
{return fact(a);
}int main (void)
{return foo(10) + foo_mor(10,11,12,13);
}

使用armclang --target=arm-arm-none-eabi -march=armv8-a -c -g file.c -o file.o进行编译,使用-g选项可以产生 DWARF 帧信息,然后 armlink可以利用这些信息来估计 栈的使用情况:
armlink file.o --info=stack
以下是armlink对上述代码进行栈使用情况分析的结果:

Stack Usage for fact 0xc bytes.
Stack Usage for foo 0x8 bytes.
Stack Usage for foo_mor 0x10 bytes.
Stack Usage for main 0x8 bytes.

同样也可以使用armlink file.o --callgraph -o FileImage.axf命令,生成callgraph文件,该命令会生成一个名为FileImage.htm的文件,它包含应用程序中各种函数的堆栈使用信息。

fact (ARM, 84 bytes, Stack size 12 bytes, file.o(.text))[Stack]Max Depth = 12
Call Chain = fact[Called By]
>>   foo_mor
>>   foo
foo (ARM, 36 bytes, Stack size 8 bytes, file.o(.text))[Stack]Max Depth = 20
Call Chain = foo >> fact[Calls]
>>   fact[Called By]
>>   main
foo_mor (ARM, 76 bytes, Stack size 16 bytes, file.o(.text))[Stack]Max Depth = 28
Call Chain = foo_mor >> fact[Calls]
>>   fact[Called By]
>>   main
main (ARM, 76 bytes, Stack size 8 bytes, file.o(.text))[Stack]Max Depth = 36
Call Chain = main >> foo_mor >> fact[Calls]
>>   foo_mor
>>   foo[Called By]
>>   __rt_entry_main (via BLX)

详细使用方法见:–info 和–callgraph 。

减少栈使用的方法

通常,您可以通过以下方式降低程序的堆栈要求:

  • 编写只需要几个变量的小函数。
  • 避免使用大型的局部结构体或数组变量。
  • 避免递归调用函数。
  • 在函数的每个点上,在任何给定时间使用的变量的数量最小化。
    使用C作用域语法(extern+全局变量),只在需要的地方声明变量,这样不同的作用域可以使用相同的内存。

参考文章:

https://developer.arm.com/documentation/100748/0620/Writing-Optimized-Code/Stack-use-in-C-and-C–


http://www.ppmy.cn/news/96543.html

相关文章

【BA无标度网络(无限规模)的传播阈值推导过程】

在BA无标度网络中,每个节点的度数是随机增长的,遵循幂律分布。假设节点 v v v的度数为 k v k_v kv​,则节点 v v v将以概率 p k v ∑ k k P ( k ) p\frac{k_v}{\sum_{k} kP(k)} p∑k​kP(k)kv​​选择与之相连的邻居节点,其中 ∑…

Java 的 String、StringBuffer 和 StringBuilder(一文讲透)

提到 String、StringBuffer 和 StringBuilder,就不得不谈及它们的历史,在了解它们的历史之后,我们对它们的理解将更上一级台阶! 发展历史 String 与 StringBuffer 的出现 String 和 StringBuffer 在 Java1.0 中就已经有了&…

如何在华为OD机试中获得满分?Java实现【贪心的商人】一文详解!

✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: Java华为OD机试真题(2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述…

MySQL常见面试题

参考:程序员大斌、javauide、小林coding 1.事务是什么? 由多个操作组成的一个逻辑单元,逻辑单元的多个操作要么同时成功,要么同时失败。 2.事务的四大特性 原子性:一个事务内的操作统一成功或失败 一致性:一个事务执行之前和执行之后都必须处于一致性状态(两人转账…

C++四种类型转化

文章目录 static_castconst_castreinterpret_cast(重新解释)dynamic_caststatic_cast 该运算符把expression转化为type_name类型,static_cast在编译时使用类型信息执行转换,在转换执行必要时的检测(如指针越界,类型检查),但没有运行时类型检查来保证转换的安全性 用于基…

Fedora安装并配置开启SSH服务相关命令

Ubuntu参考我这篇:虚拟机里安装ubuntu-23.04-beta-desktop-amd64,开启SSH(换源、备份),配置中文以及中文输入法等 一、过程 1、检测是否安装了openssh-server $ rpm -qa | grep openssh-serveropenssh-server-7.9p1-5.fc30.x86_642、如果上…

yocto开发-常见的概念

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、BitBake是什么?二、Recipes配方三、Classes四、Configurations五、Layers总结前言 本文介绍在开发过程中经常遇到的概念名词 提示:以下是本篇文章正文内容,下面案例可供参考 一、BitB…

SEO优化

SEO(Search Engine Optimization)是指优化网站结构,使其更好的被搜索引擎收录和排名。以下是一些自定义SEO优化的建议: 关键词研究:你需要了解你的网站所属行业所使用的一些关键词和短语。使用Google AdWords等工具来帮…