c - 如何防止 gcc 重新排序 x86 帧指针保存/设置指令?

标签 c assembly gcc x86 callstack

在我使用 flamegraph 进行分析时,我发现即使所有代码库都使用 -fno-omit-frame-pointer 标志编译,调用堆栈有时也会被破坏。通过检查 gcc 生成的二进制文件,我注意到 gcc 可能会重新排序 x86 帧指针保存/设置指令(即 push %rbp; move %rsp, %rbp),有时甚至在 ret 一些分支的指令。如下例所示,push %rbp; move %rsp, %rbp 放在函数的底部。当 perf 恰好在正确设置帧指针之前函数中的示例指令时,它会导致不完整和误导性的调用堆栈。

C 代码:

int flextcp_fd_slookup(int fd, struct socket **ps)
{
  struct socket *s;

  if (fd >= MAXSOCK || fhs[fd].type != FH_SOCKET) {
    errno = EBADF;
    return -1;
  }

  uint32_t lock_val = 1;
  s = fhs[fd].data.s;

  asm volatile (
      "1:\n"
      "xchg %[locked], %[lv]\n"
      "test %[lv], %[lv]\n"
      "jz 3f\n"
      "2:\n"
      "pause\n"
      "cmpl $0, %[locked]\n"
      "jnz 2b\n"
      "jmp 1b\n"
      "3:\n"
      : [locked] "=m" (s->sp_lock), [lv] "=q" (lock_val)
      : "[lv]" (lock_val)
      : "memory");

  *ps = s;
  return 0;
}

CMake Debug 配置文件:

0000000000007c73 <flextcp_fd_slookup>:
    7c73:   f3 0f 1e fa             endbr64 
    7c77:   55                      push   %rbp
    7c78:   48 89 e5                mov    %rsp,%rbp
    7c7b:   48 83 ec 20             sub    $0x20,%rsp
    7c7f:   89 7d ec                mov    %edi,-0x14(%rbp)
    7c82:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
    7c86:   81 7d ec ff ff 0f 00    cmpl   $0xfffff,-0x14(%rbp)
    7c8d:   7f 1b                   jg     7caa <flextcp_fd_slookup+0x37>
    7c8f:   8b 45 ec                mov    -0x14(%rbp),%eax
    7c92:   48 98                   cltq   
    7c94:   48 c1 e0 04             shl    $0x4,%rax
    7c98:   48 89 c2                mov    %rax,%rdx
    7c9b:   48 8d 05 86 86 00 00    lea    0x8686(%rip),%rax        # 10328 <fhs+0x8>
    7ca2:   0f b6 04 02             movzbl (%rdx,%rax,1),%eax
    7ca6:   3c 01                   cmp    $0x1,%al
    7ca8:   74 12                   je     7cbc <flextcp_fd_slookup+0x49>
    7caa:   e8 31 b9 ff ff          callq  35e0 <__errno_location@plt>
    7caf:   c7 00 09 00 00 00       movl   $0x9,(%rax)
    7cb5:   b8 ff ff ff ff          mov    $0xffffffff,%eax
    7cba:   eb 53                   jmp    7d0f <flextcp_fd_slookup+0x9c>
    7cbc:   c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%rbp)
    7cc3:   8b 45 ec                mov    -0x14(%rbp),%eax
    7cc6:   48 98                   cltq   
    7cc8:   48 c1 e0 04             shl    $0x4,%rax
    7ccc:   48 89 c2                mov    %rax,%rdx
    7ccf:   48 8d 05 4a 86 00 00    lea    0x864a(%rip),%rax        # 10320 <fhs>
    7cd6:   48 8b 04 02             mov    (%rdx,%rax,1),%rax
    7cda:   48 89 45 f8             mov    %rax,-0x8(%rbp)
    7cde:   48 8b 55 f8             mov    -0x8(%rbp),%rdx
    7ce2:   8b 45 f4                mov    -0xc(%rbp),%eax
    7ce5:   87 82 c0 00 00 00       xchg   %eax,0xc0(%rdx)
    7ceb:   85 c0                   test   %eax,%eax
    7ced:   74 0d                   je     7cfc <flextcp_fd_slookup+0x89>
    7cef:   f3 90                   pause  
    7cf1:   83 ba c0 00 00 00 00    cmpl   $0x0,0xc0(%rdx)
    7cf8:   75 f5                   jne    7cef <flextcp_fd_slookup+0x7c>
    7cfa:   eb e9                   jmp    7ce5 <flextcp_fd_slookup+0x72>
    7cfc:   89 45 f4                mov    %eax,-0xc(%rbp)
    7cff:   48 8b 45 e0             mov    -0x20(%rbp),%rax
    7d03:   48 8b 55 f8             mov    -0x8(%rbp),%rdx
    7d07:   48 89 10                mov    %rdx,(%rax)
    7d0a:   b8 00 00 00 00          mov    $0x0,%eax
    7d0f:   c9                      leaveq 
    7d10:   c3                      retq     

CMake 发布 配置文件:

0000000000007d80 <flextcp_fd_slookup>:
    7d80:   f3 0f 1e fa             endbr64 
    7d84:   81 ff ff ff 0f 00       cmp    $0xfffff,%edi
    7d8a:   7f 44                   jg     7dd0 <flextcp_fd_slookup+0x50>
    7d8c:   48 63 ff                movslq %edi,%rdi
    7d8f:   48 8d 05 6a 85 00 00    lea    0x856a(%rip),%rax        # 10300 <fhs>
    7d96:   48 c1 e7 04             shl    $0x4,%rdi
    7d9a:   48 01 c7                add    %rax,%rdi
    7d9d:   80 7f 08 01             cmpb   $0x1,0x8(%rdi)
    7da1:   75 2d                   jne    7dd0 <flextcp_fd_slookup+0x50>
    7da3:   48 8b 17                mov    (%rdi),%rdx
    7da6:   b8 01 00 00 00          mov    $0x1,%eax
    7dab:   87 82 c0 00 00 00       xchg   %eax,0xc0(%rdx)
    7db1:   85 c0                   test   %eax,%eax
    7db3:   74 0d                   je     7dc2 <flextcp_fd_slookup+0x42>
    7db5:   f3 90                   pause  
    7db7:   83 ba c0 00 00 00 00    cmpl   $0x0,0xc0(%rdx)
    7dbe:   75 f5                   jne    7db5 <flextcp_fd_slookup+0x35>
    7dc0:   eb e9                   jmp    7dab <flextcp_fd_slookup+0x2b>
    7dc2:   31 c0                   xor    %eax,%eax
    7dc4:   48 89 16                mov    %rdx,(%rsi)
    7dc7:   c3                      retq   
    7dc8:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    7dcf:   00 
    7dd0:   55                      push   %rbp
    7dd1:   48 89 e5                mov    %rsp,%rbp
    7dd4:   e8 b7 b7 ff ff          callq  3590 <__errno_location@plt>
    7dd9:   c7 00 09 00 00 00       movl   $0x9,(%rax)
    7ddf:   b8 ff ff ff ff          mov    $0xffffffff,%eax
    7de4:   5d                      pop    %rbp
    7de5:   c3                      retq   
    7de6:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
    7ded:   00 00 00 

有什么方法可以防止 gcc 对这两条指令重新排序吗?

编辑:我在 Ubuntu 22.04 上使用默认工具链 (gcc-11.2.0 + glibc 2.35)。抱歉,没有可重现的示例。 编辑:添加示例函数的源代码。

最佳答案

试试 -fno-shrink-wrap


这看起来像是“收缩包装”优化:仅在需要的代码路径中执行函数序言。通常的好处是在序言之前运行提前检查,而不是通过函数在该路径上保存/恢复一堆寄存器。

但是在这里,GCC 决定只在必须调用另一个函数时才执行序言(设置帧指针)。该函数是错误返回路径中的 __errno_location。哎呀。 :P(并且 GCC 正确地意识到这是不常见的情况,并通过快速路径将其放在 ret 之后。因此快速路径可以是一条直线,没有分支,其他而不是在你的 asm() 中。它不是一个单独的函数,它只是你显示源代码的函数的尾部复制。)

函数的主要路径非常小,只有几个 C 赋值语句和一个 asm() 语句。 GCC 不清楚 asm block 有多大(尽管我认为有一些启发式方法,但仍然很愿意内联一个)。它不知道是否可能存在循环或在 asm block 中花费大量时间。


这是一个已知问题,GCC bug #98018建议 GCC 应该有一个选项来强制在函数的实际顶部设置帧指针。因为目前没有 100% 可靠的选项,除了禁用不可用的优化。 (感谢 @Margaret Bloom for finding & linking this。)

作为comment 6在提到的 GCC 错误中,禁用收缩包装是确保 GCC 在函数本身的顶部设置帧指针所必需的部分,而不仅仅是在需要序言的某些 if 中。

GCC 问题似乎正在考虑停止函数内联的功能,因此回溯将完全反射(reflect) C 抽象机的函数调用嵌套。这超出了您的要求,我认为这只是在优化后在 asm 中存在的函数的入口处设置帧指针。

禁用收缩包装将强制整个序幕发生在那里,包括推送其他 regs,如果有的话。不仅仅是帧指针。 但是这里没有其他人。尽管如此,在一般启用优化的情况下,丢失收缩包装可能非常小。

关于c - 如何防止 gcc 重新排序 x86 帧指针保存/设置指令?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73539739/

相关文章:

C 链表 - 指针值变化

c++ - 更改完全不相关的代码时,Visual Studio C++ 编译器生成的代码速度降低了 3 倍

gcc - GCC 可以在 AIX 上交叉编译吗?

assembly - 如何将二进制整数转换为十六进制字符串?

c - 让 gcc 警告隐式转换

C 预处理无法在 #error 后立即停止

c - int 如何等于另一个返回 long int 的函数?

c - 等待用户输入值并将其分配给 var

c - 我如何解释以下 typedef 语句

c - 使堆栈指针指向 mmap 返回的指针。 (Linux,32 位虚拟机)