嵌入式开发中.su文件的作用

reference:

  • https://interrupt.memfault.com/blog/measuring-stack-usage
  • https://github.com/ttsiodras/checkStackUsage

测试代码

#include 

int foo_2(int c) {
  int array[4];
  array[1] = c;
  array[3] = c* c;
  return array[3] - array[1];
}

int foo(int a, int b) {
  int array[10];
  array[1] = a;
  array[9] = foo_2(b);

  return array[1] * array[9];
}

int main() {
  printf("%d\n", foo(1,2));
}

执行结果

$ gcc -c -fstack-usage stack-usage-example.c
$ cat stack-usage-example.su
example/stack-usage/stack-usage-example.c:3:5:foo_2     64      static
example/stack-usage/stack-usage-example.c:10:5:foo      80      static
example/stack-usage/stack-usage-example.c:18:5:main     16      static

我们来详细分析为什么 .su 文件中显示的栈大小是 64、80 和 16 字节,这与 int 是 4 字节(在大多数现代系统上,包括你的 Ubuntu 22.04)看似矛盾。


1. 函数 foo_2 的栈使用(64 字节)

int foo_2(int c) {
  int array[4];       // 4个int = 16字节
  array[1] = c;
  array[3] = c * c;
  return array[3] - array[1];
}
理论计算:
  • array[4]4 * sizeof(int) = 4 * 4 = 16 字节
  • c(参数):4 字节(通过寄存器传递,通常不占用栈
  • 预期栈使用16 字节(仅 array[4]
实际 .su 报告:64 字节

为什么是 64 字节?
GCC 在计算栈大小时会考虑以下额外开销:

  1. 对齐要求
    • 栈帧通常需要按 16 字节对齐(x86-64 的 ABI 要求)。
    • 如果局部变量总大小不满足对齐,编译器会填充额外空间。
  2. 寄存器保存区域
    • 函数调用时,某些寄存器(如 rbp, rbx 等)需要保存在栈上。
    • 在 x86-64 中,可能会额外保留一些空间用于调试或异常处理。
  3. 返回地址和帧指针
    • 返回地址(8 字节)和可选的帧指针(如 rbp,8 字节)也会占用栈空间。
具体分配(x86-64 示例):
|-------------------|
| 保存的寄存器      |  // 可选,如 rbx, r12-r15
| 对齐填充          |  // 确保 16 字节对齐
| array[4] (16字节) |  
| 帧指针 (8字节)    |  // 如果启用帧指针(-fno-omit-frame-pointer)
| 返回地址 (8字节)  |  
|-------------------|

实际栈帧可能被填充到 64 字节以满足对齐和调试需求。


2. 函数 foo 的栈使用(80 字节)

int foo(int a, int b) {
  int array[10];      // 10个int = 40字节
  array[1] = a;
  array[9] = foo_2(b);
  return array[1] * array[9];
}
理论计算:
  • array[10]10 * 4 = 40 字节
  • a, b(参数):通过寄存器传递(不占用栈)
  • 预期栈使用40 字节
实际 .su 报告:80 字节

原因

  1. 对齐填充(从 40 字节填充到 64 字节以满足 16 字节对齐)。
  2. 加上保存的寄存器、返回地址等额外开销,总栈帧扩展到 80 字节。

3. 函数 main 的栈使用(16 字节)

int main() {
  printf("%d\n", foo(1,2));
}
理论计算:
  • main 本身没有局部变量。
  • 参数 foo(1,2) 通过寄存器传递。
实际 .su 报告:16 字节

原因

  1. 返回地址(8 字节)。
  2. 可能的对齐填充(8 字节),确保栈指针按 16 字节对齐。

关键点总结

  1. int 确实是 4 字节,但栈使用不仅由局部变量决定。
  2. GCC 的栈计算包含额外开销
    • 返回地址、帧指针、寄存器保存、对齐填充。
    • 在 x86-64 中,栈帧默认按 16 字节对齐。
  3. 优化级别的影响
    如果启用优化(如 -O2),GCC 可能会减少栈使用(例如省略帧指针)。

验证方法

你可以通过汇编代码验证栈分配。编译时添加 -S 生成汇编:

gcc -S -fstack-usage stack-usage-example.c

查看 foo_2 的汇编(片段):

foo_2:
    pushq   %rbp               ; 保存帧指针(8字节)
    movq    %rsp, %rbp         ; 设置新帧指针
    subq    $48, %rsp          ; 分配栈空间(48 + 16 = 64字节)
    movl    %edi, -20(%rbp)    ; 参数 `c` 保存到栈
    ; ...

这里 subq $48, %rsp 分配了 48 字节,加上 pushq %rbp 的 8 字节和隐含的 8 字节返回地址,总共 64 字节。


如何减少栈使用?

  1. 启用优化
    -O2-Os 会减少栈帧大小(例如省略帧指针):
    gcc -Os -fstack-usage stack-usage-example.c
    
    .su 文件中的值可能会显著减小。
  2. 减少局部变量
    将大数组改为静态或全局(如果线程安全):
    static int array[10];  // 不再占用栈
    
  3. 调整对齐
    使用 __attribute__((aligned(8))) 降低对齐要求(需谨慎)。

总结

  • .su 文件中的栈大小是编译器根据 ABI 规则、对齐要求和调试需求综合计算的结果。
  • 实际栈使用 = 局部变量 + 编译器额外开销(对齐、寄存器保存等)。
  • x86-64 的 16 字节对齐和调试信息可能会导致栈大小远大于局部变量的理论值。

➜  /home/mi/local/p65/1 checkStackUsage.py --help              
Usage: /home/mi/local/checkStackUsage/checkStackUsage.py [-cross PREFIX] ELFbinary root_path_for_su_files [functions...]

where the default prefix is:

	arm-eabi-      for ARM binaries
	sparc-rtems5-  for SPARC binaries
	(no prefix)    for x86/amd64 binaries

Note that if you use '-cross', SPARC opcodes are assumed.

➜  /home/mi/local/p65/1 
➜  /home/mi/local/p65/1 checkStackUsage.py a.out ./
-18446744069414584304: _start (_start(-18446744069414584304))
         0: deregister_tm_clones (deregister_tm_clones(0))
         0: register_tm_clones (register_tm_clones(0))
         0: __do_global_dtors_aux ()
         0: frame_dummy (frame_dummy(0))
        16: _init (_init(16))
        16: _fini (_fini(16))
        64: foo_2 (foo_2(64))
       144: foo (foo(80),foo_2(64))
       160: main (main(16),foo(80),foo_2(64))

你可能感兴趣的:(我的博客,嵌入式)