Linux perf_events 注释帧指针混乱

Linux perf_events annotation frame pointer confusion

I 运行 sudo perf record -F 99 find / 后跟 sudo perf report 并选择 "Annotate fdopendir" 这是前七个说明:

push %rbp push %rbx mov %edi,%esi mov %edi,%ebx mov [=14=]x1,%edi sub [=14=]xa8,%rsp mov %rsp,%rbp

第一条指令似乎是在保存调用者的基帧指针。我相信说明 2 到 5 与这个问题无关,但这里是为了完整性。说明 6 和 7 让我感到困惑。 rbp 到 rsp 的分配不应该发生在之前 从 rsp 中减去 0xa8 吗?

x86-64 System V ABI 不需要制作传统/遗留堆栈框架。这看起来 接近 传统的堆栈框架设置,但这绝对不是因为在第一个 push %rbp.

之后没有 mov %rsp, %rbp

我们看到编译器生成的代码只是将 RBP 用作临时寄存器,并用它来保存指向堆栈上局部变量的指针。这只是一个巧合的是,这恰好在 push %rbp 之后的某个时间涉及指令 mov %rsp, %rbp这不是在制作栈帧。

在 x86-64 系统 V 中,RBX 和 RBP 是仅有的 2 个 "low 8" 调用保留的寄存器,因此在某些情况下可以在没有 REX 前缀的情况下使用(例如 push/pop,以及在寻址模式中使用时),节省代码大小。 GCC 更喜欢在 saving/restoring 任何 R12..R15 之前使用它们。 What registers are preserved through a linux x86-64 function call(对于指针,使用 mov 复制它们总是需要 64 位操作数大小的 REX 前缀,因此比 32 位整数节省更少,但 gcc 仍然适用于 RBX 然后 RBP ,按此顺序,当它需要 save/restore 在函数中调用保留的 regs 时。)

我的系统 (Arch Linux) 上 /lib/libc.so.6 (glibc) 的反汇编显示 fdopendir 的代码生成相似但不同。在进行函数调用之前,您过早地停止了反汇编。这阐明了为什么它需要一个调用保留的临时寄存器:它需要在整个调用中使用 reg 中的 var。

00000000000c1260 <fdopendir>:
   c1260:       55                      push   %rbp
   c1261:       89 fe                   mov    %edi,%esi
   c1263:       53                      push   %rbx
   c1264:       89 fb                   mov    %edi,%ebx
   c1266:       bf 01 00 00 00          mov    [=10=]x1,%edi
   c126b:       48 81 ec a8 00 00 00    sub    [=10=]xa8,%rsp
   c1272:       64 48 8b 04 25 28 00 00 00      mov    %fs:0x28,%rax    # stack-check cookie
   c127b:       48 89 84 24 98 00 00 00         mov    %rax,0x98(%rsp)
   c1283:       31 c0                   xor    %eax,%eax
   c1285:       48 89 e5                mov    %rsp,%rbp      # save a pointer
   c1288:       48 89 ea                mov    %rbp,%rdx      # and pass it as a function arg
   c128b:       e8 90 7d 02 00          callq  e9020 <__fxstat>
   c1290:       85 c0                   test   %eax,%eax
   c1292:       78 6a                   js     c12fe <fdopendir+0x9e>
   c1294:       8b 44 24 18             mov    0x18(%rsp),%eax
   c1298:       25 00 f0 00 00          and    [=10=]xf000,%eax
   c129d:       3d 00 40 00 00          cmp    [=10=]x4000,%eax
   c12a2:       75 4c                   jne    c12f0 <fdopendir+0x90>
   ....

   c12c1:       48 89 e9                mov    %rbp,%rcx      # pass the pointer as the 4th arg
   c12c4:       89 c2                   mov    %eax,%edx
   c12c6:       31 f6                   xor    %esi,%esi
   c12c8:       89 df                   mov    %ebx,%edi
   c12ca:       e8 d1 f7 ff ff          callq  c0aa0 <__alloc_dir>
   c12cf:       48 8b 8c 24 98 00 00 00         mov    0x98(%rsp),%rcx
   c12d7:       64 48 33 0c 25 28 00 00 00      xor    %fs:0x28,%rcx     # check the stack cookie
   c12e0:       75 38                   jne    c131a <fdopendir+0xba>
   c12e2:       48 81 c4 a8 00 00 00    add    [=10=]xa8,%rsp
   c12e9:       5b                      pop    %rbx
   c12ea:       5d                      pop    %rbp
   c12eb:       c3                      retq   

这是非常愚蠢的代码生成; gcc 可以在第二次需要它时简单地使用 mov %rsp, %rcx 。我将其称为优化失败。它从不需要调用保留寄存器中的指针,因为它始终知道它相对于 RSP 的位置。

(即使它没有恰好达到 RSP+0,lea something(%rsp), %rdxlea something(%rsp), %rcx 在需要两次时也完全没问题,总成本可能低于 saving/restoring RBP + 所需的 mov 说明。)

或者它可以使用 mov 0x18(%rbp),%eax 而不是 rsp 在该寻址模式下保存一个字节的代码大小。避免在函数调用之间直接引用 RSP 可减少英特尔 CPU 需要插入的堆栈同步微指令的数量。