在我使用 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/