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

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–

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>