【问题标题】:Why GCC 8/9/10 and GCC-7 behaves differently in calculating stack shift amount?为什么 GCC 8/9/10 和 GCC-7 在计算堆栈移位量时表现不同?
【发布时间】:2021-06-19 11:50:36
【问题描述】:

我正在将代码移植到现代编译器,不幸的是,当从 Rust 调用 FFI 函数到 C++ 时,遇到了一个微妙的分段错误。

堆栈跟踪显示,在转换到 C++ 后,Rust 提供的第一个参数神奇地消失了,这误导了 C++ 使用错误的参数。

代码有点私密,所以我不能在这里发布,但程序集显示了一些有趣的东西:

在 GCC-7 中(代码运行没有问题),汇编的前几行如下所示:

   0x0000000000001119 <+0>:     push   rbp
   0x000000000000111a <+1>:     mov    rbp,rsp
   0x000000000000111d <+4>:     push   r13
   0x000000000000111f <+6>:     push   r12
   0x0000000000001121 <+8>:     push   rbx
   0x0000000000001122 <+9>:     sub    rsp,0x128
   0x0000000000001129 <+16>:    mov    QWORD PTR [rbp-0x118],rdi
   0x0000000000001130 <+23>:    mov    rax,rsi
   0x0000000000001133 <+26>:    mov    rsi,rdx
   0x0000000000001136 <+29>:    mov    rdx,rsi
   0x0000000000001139 <+32>:    mov    QWORD PTR [rbp-0x130],rax
   0x0000000000001140 <+39>:    mov    QWORD PTR [rbp-0x128],rdx
   0x0000000000001147 <+46>:    mov    QWORD PTR [rbp-0x120],rcx
   0x000000000000114e <+53>:    mov    QWORD PTR [rbp-0x140],r8
   0x0000000000001155 <+60>:    mov    QWORD PTR [rbp-0x138],r9
   0x000000000000115c <+67>:    lea    rax,[rbp-0x110]
   0x0000000000001163 <+74>:    mov    rdi,rax
   0x0000000000001166 <+77>:    call   0x116b
   0x000000000000116b <+82>:    mov    rax,QWORD PTR [rbp-0x128]
   0x0000000000001172 <+89>:    mov    edx,eax
   0x0000000000001174 <+91>:    mov    rcx,QWORD PTR [rbp-0x130]
   0x000000000000117b <+98>:    lea    rax,[rbp-0x110]
   0x0000000000001182 <+105>:   mov    rsi,rcx
   0x0000000000001185 <+108>:   mov    rdi,rax
   0x0000000000001188 <+111>:   call   0x118d

但是,使用 GCC-8/9/10,程序集变成了

   0x0000000000001125 <+0>:     push   rbp
   0x0000000000001126 <+1>:     mov    rbp,rsp
   0x0000000000001129 <+4>:     push   r12
   0x000000000000112b <+6>:     push   rbx
   0x000000000000112c <+7>:     sub    rsp,0x120
   0x0000000000001133 <+14>:    mov    QWORD PTR [rbp-0x108],rdi
   0x000000000000113a <+21>:    mov    QWORD PTR [rbp-0x110],rsi
   0x0000000000001141 <+28>:    mov    QWORD PTR [rbp-0x120],rdx
   0x0000000000001148 <+35>:    mov    QWORD PTR [rbp-0x118],rcx
   0x000000000000114f <+42>:    mov    QWORD PTR [rbp-0x128],r8
   0x0000000000001156 <+49>:    mov    QWORD PTR [rbp-0x130],r9
   0x000000000000115d <+56>:    lea    rax,[rbp-0x100]
   0x0000000000001164 <+63>:    mov    rdi,rax
   0x0000000000001167 <+66>:    call   0x116c
   0x000000000000116c <+71>:    mov    rax,QWORD PTR [rbp-0x118]
   0x0000000000001173 <+78>:    mov    edx,eax
   0x0000000000001175 <+80>:    mov    rcx,QWORD PTR [rbp-0x120]
   0x000000000000117c <+87>:    lea    rax,[rbp-0x100]
   0x0000000000001183 <+94>:    mov    rsi,rcx
   0x0000000000001186 <+97>:    mov    rdi,rax
   0x0000000000001189 <+100>:   call   0x118e

所以逻辑几乎相同移位量改变?我觉得这种行为很奇怪。

参数列表类似于:

struct Opaque {
  char _inner[0];
}
struct View {
  char * base;
  size_t len;
}

ReturnType ffi_function(Opaque*, View, uint64_t, View, uint64_t, uint64_t);

我还通过x/-100xg $rbp倾倒了堆栈

0x7fe7a67d41c0: 0x00007fe7a67d41f0      0x0000000014c1b1dc
0x7fe7a67d41d0: 0x000000000000003e      0x0000000000000006
0x7fe7a67d41e0: 0x00007fe7a67d4330      0x00007fe88714b540
0x7fe7a67d41f0: 0x0000000000201000      0x0000000013bd0df1
0x7fe7a67d4200: 0x0000000000000001      0x00007fe7a5e2f4e0
0x7fe7a67d4210: 0x000000000000003e      0x000000000000003b
0x7fe7a67d4220: 0x00007fe7a5e01080      0x00007ffebbd1ddb0
0x7fe7a67d4230: 0x000000001602ff80      0x0000000000000000
0x7fe7a67d4240: 0x0000000000000000      0x0000000000000000
0x7fe7a67d4250: 0x0000000000000000      0x0000000018d59680
0x7fe7a67d4260: 0x0000000018d59680      0x0000000000000000
0x7fe7a67d4270: 0x0000000000000000      0x0000000000000000
0x7fe7a67d4280: 0x00007fe700000000      0x0000000000000000
0x7fe7a67d4290: 0x00007fe7a5e2f4e0      0x00007fe88f1630ed
0x7fe7a67d42a0: 0x00007fe7a67d42f8      0x00007fe7a5e2f4e0
0x7fe7a67d42b0: 0x00007fe7a5e2f4e0      0x00007fe88f0d5f42
0x7fe7a67d42c0: 0x00007fe7a5e2f4e0      0x00007fe7a67d43f0
0x7fe7a67d42d0: 0x00007fe7a67d43f0      0x00007fe88f0ffa04
0x7fe7a67d42e0: 0x0000000000000001      0x0000000000000001
0x7fe7a67d42f0: 0x00007fe7a67d43f0      0x00007fe7a5e2f4e0
0x7fe7a67d4300: 0x00007fe7a5e2f4e0      0x0000000000000001
0x7fe7a67d4310: 0x00007fe7a67d43f0      0x00007fe88f0cf1cf
0x7fe7a67d4320: 0x0000000000000006      0x00007fe88714b540

现在,0x00007ffebbd1ddb0 似乎是第一个参数的正确值,但 C++ 将其读取为 0x00007fe7a5e01080


更新:

就在callq之后,在执行push %rbp之前:

这是我从x/-100xg $rsp ($rsp = 0x7fff0b7d4338) 那里得到的信息

0x7fff0b7d4108: 0x00007fff0ae01080      0x000000000000003e
0x7fff0b7d4118: 0x0000003e00000060      0x00007fff0b7d4ab8
0x7fff0b7d4128: 0x00007fff0b7d4310      0x00007fff0b7d4310
0x7fff0b7d4138: 0x00007fff00000004      0x00007fff0b7d41b8
0x7fff0b7d4148: 0x00007fff0ae2f480      0x00007fff00000004
0x7fff0b7d4158: 0x00007fff0b7d41b8      0x00007fff0ae2f480
0x7fff0b7d4168: 0x0000000000000004      0x0000000000000000
0x7fff0b7d4178: 0x00007fff0b7d41b8      0x00007fff0ae2f498
0x7fff0b7d4188: 0x0000000000000000      0x00007fff0ae2f498
0x7fff0b7d4198: 0x00007ffff3d8219e      0x00007fff0b7d41b8
0x7fff0b7d41a8: 0x00007ffff3da157f      0x00007fff0ae01080
0x7fff0b7d41b8: 0x000000000000003e      0x000000000000003e
0x7fff0b7d41c8: 0x0000000000000002      0x000000000000003e
0x7fff0b7d41d8: 0x00007ffff5d4326d      0x00007fff0ae01080
0x7fff0b7d41e8: 0x000000000000003e      0x00007fff0b7d41b0
0x7fff0b7d41f8: 0x00007fff0ae01080      0x000000000000003e
0x7fff0b7d4208: 0x000000000000003e      0x0000000000000004
0x7fff0b7d4218: 0x00007fff0ae2f480      0x00007fff0ae2f498
0x7fff0b7d4228: 0x0000000000000004      0x00007fff0ae2f480
0x7fff0b7d4238: 0x00007fff0ae2f498      0x00007fff0ae2f480
0x7fff0b7d4248: 0x0000000000000004      0x0000000000000001
0x7fff0b7d4258: 0x00007fff0b7fe2a8      0x00007fff0ae2f480
0x7fff0b7d4268: 0x0000000000000004      0x00007fff0ae2f498
0x7fff0b7d4278: 0x00007fff0ae2f498      0x00007fff0ae2f4e0
0x7fff0b7d4288: 0x0000000000000000      0x00007fff0ae2f4e0
0x7fff0b7d4298: 0x00007ffff3e270ed      0x00007fff0b7d42f8
0x7fff0b7d42a8: 0x00007fff0ae2f4e0      0x00007fff0ae2f4e0
0x7fff0b7d42b8: 0x00007ffff3d99f42      0x00007fff0ae2f4e0
0x7fff0b7d42c8: 0x00007fff0b7d43f0      0x00007fff0b7d43f0
0x7fff0b7d42d8: 0x00007ffff3dc3a04      0x0000000000000001
0x7fff0b7d42e8: 0x0000000000000001      0x00007fff0b7d43f0
0x7fff0b7d42f8: 0x00007fff0ae2f4e0      0x00007fff0ae2f4e0
0x7fff0b7d4308: 0x0000000000000001      0x00007fff0b7d43f0
0x7fff0b7d4318: 0x00007ffff3d931cf      0x00007fff0ae2f4e0
0x7fff0b7d4328: 0x0000000000000001      0x00007fff0b7d43f0

100xg:

0x7fff0b7d4338: 0x00007ffff3dc42fe      0x0000000000000007
0x7fff0b7d4348: 0x0000000000000006      0x00007ffff637393e
0x7fff0b7d4358: 0x00007fff0ae2f480      0x00007fff0b7d4388
0x7fff0b7d4368: 0x00007fff0ae2f480      0x0000000000000060
0x7fff0b7d4378: 0x00007fff0ae01080      0x000000000000003e
0x7fff0b7d4388: 0x0000000000000001      0x00007fff0ae2f4e0
0x7fff0b7d4398: 0x000000000000003e      0x00007fff0ae01080
0x7fff0b7d43a8: 0x0000000013bd0d88      0x00007fffffffbfe0
0x7fff0b7d43b8: 0x00007fff0b7d4420      0x00007fffffffbfa8
0x7fff0b7d43c8: 0x00007fffffffbf50      0x00007fff0b7d4ab8
0x7fff0b7d43d8: 0x000000000000003b      0x0000000000000007
0x7fff0b7d43e8: 0x0000000000000006      0x00007fff0ae2f4e0
0x7fff0b7d43f8: 0x0000000000000004      0x0000000000000001
0x7fff0b7d4408: 0x00007fff0ae2f480      0x0000000000000004
0x7fff0b7d4418: 0x0000000000000001      0x00007fff0ae01080
0x7fff0b7d4428: 0x000000000000003e      0x000000000000003e
0x7fff0b7d4438: 0x00007fffffffbf50      0x00007fff0b7d4ab8
0x7fff0b7d4448: 0x000000000000003b      0x0000000000000007
0x7fff0b7d4458: 0x0000000000000006      0x00007fff0ae2f2a0
0x7fff0b7d4468: 0x000000000000003e      0x00007fff0ae01080
0x7fff0b7d4478: 0x000000000000003e      0x00007fff0ae2f4e0
0x7fff0b7d4488: 0x0000000000000001      0x00007fff0b7d45b0
0x7fff0b7d4498: 0x000001ff0ae2f2a0      0x00007fff0ae2f480
0x7fff0b7d44a8: 0x0000000000000000      0x00007ffff7bb2c60
0x7fff0b7d44b8: 0x00007ffff3daebc6      0x0000000000201000
0x7fff0b7d44c8: 0x0000000000000000      0x00007fffebd4f080
0x7fff0b7d44d8: 0x00007fff0ae2f2a0      0x000000000000003e
0x7fff0b7d44e8: 0x0100000000010301      0x00007fff0ae2f2a0
0x7fff0b7d44f8: 0x000000000000003e      0x000000000000003e
0x7fff0b7d4508: 0x00007fff0ae2f2a0      0x000000000000003e
0x7fff0b7d4518: 0x00007fff0ae2f2a0      0x000000000000003e
0x7fff0b7d4528: 0x00007fff0ae2f2a0      0x0000000060cc0baf
0x7fff0b7d4538: 0x00007fff0ae32180      0x00007fffffffbf50
0x7fff0b7d4548: 0x0000000000000000      0x00007fff0ae32240
0x7fff0b7d4558: 0x00007fff0ae32000      0x0000000000000006
0x7fff0b7d4568: 0x0000000000000007      0x000000000000003b
0x7fff0b7d4578: 0x00007fff0ae3e000      0x00007fff0b7d50b0
0x7fff0b7d4588: 0x00007fff0b7d50b0      0x00007fff0b7d4ab8
0x7fff0b7d4598: 0x00007fff0ae2f480      0x0000000000000004
0x7fff0b7d45a8: 0x0000000000000001      0x00007fff0ae32240
0x7fff0b7d45b8: 0x00007fff0ae32240      0x0000000000000000
0x7fff0b7d45c8: 0x00007fff0ae2f2a0      0x000000000000003e
0x7fff0b7d45d8: 0x0000000000000001      0x00007fff0ae2f480
0x7fff0b7d45e8: 0x0000000000000004      0x0000000000000001
0x7fff0b7d45f8: 0x0000000000000000      0x00007fff0ae3e000
0x7fff0b7d4608: 0x000000000000003b      0x0000000000000007
0x7fff0b7d4618: 0x0000000000000006      0x00007fffebda59d0
0x7fff0b7d4628: 0x00007ffff21bf5cd      0x00007fff0ae32180
0x7fff0b7d4638: 0x00007fff0ae32180      0x00007fff0ae32180
0x7fff0b7d4648: 0x0000000000000000      0x00007fff0b7d4858

C++的帧信息:

 called by frame at 0x7fff0b7d44c0
 source language c++.
 Arglist at 0x7fff0b7d4330, args: server=0x7fff0ae2f498, region_buff=..., peer_id=62, snaps=..., index=62, term=140737324202302
 Locals at 0x7fff0b7d4330, Previous frame's sp is 0x7fff0b7d4340
 Saved registers:
  rip at 0x7fff0b7d4338

预期的第一个参数:0x7fffffffbfe0

预期的第二个参数:(0x7fff0ae01080, 0x3e)

预期的第三个参数:0x3b

预期的第四个参数:(0x7fff0ae2f4e0, 1)

预计第 5、6 个参数:76

【问题讨论】:

  • 为什么您认为您的论点缺失?参数可能通过寄存器传入(如果它是堆栈,您会看到 rbp 的正偏移量)。编译器通常会在版本之间生成不同的汇编指令,这并不意味着编译器存在问题。很可能,问题出在 c++ 代码中。
  • 第一个参数(Opaque*,在从 rust 转换到 c++ 之后)变为arg2.base 等等。参数列表似乎发生了变化。
  • 其实我怀疑 C++ 代码中存在 UB,因为使用相同的 rust 共享库,gcc-7 编译的代码运行流畅,但没有其他编译器的可执行文件。但是在段错误的位置(C++ 使用arg2.len 作为地址,应该是base 字段),我的观察是参数转移错误。
  • 我期待 extern "C" 所以编译器使用 C ABI。
  • 我怀疑 gcc 8/9/10 都有一个导致此问题的重大错误。更有可能您的 c++ 代码不符合正确创建 c API 的要求,而对于 gcc 7,您很幸运它创建了一个可以工作的二进制文件。

标签: c++ gcc rust x86-64 abi


【解决方案1】:

较新的 GCC 在函数顶部少了一个 push(少了一个调用保留寄存器)。所以它只需要在下一次调用之前将 RSP 移动 8 的偶数倍 (0x120) 即可达到 16 字节对齐边界,同时保留足够的空间,而不是 0x128。

这两件事都会改变 RBP 和 RSP 之间的距离,因此[RBP-0x130] 偏移量是到达 RSP 正上方的空间所必需的,以便在堆栈上按值传递大型结构。

我没有检查数学,但如果您仍然认为 GCC 可能存在正确性错误,您可以再次检查自己。


struct View 只有两个指针大小的成员。因此,x86-64 System V ABI 可以在一对寄存器中传递它。

而且它没有构造函数或析构函数或其他任何东西,因此 C++ ABI 也随之而来。 (我忘记了用于决定一个对象是否必须具有地址并因此将其传递到堆栈上的确切规则。如果您的 C++ 与 Rust 代码对此不同意,可能会导致他们不同意如何传递它。)

更新,因为问题确实是一个不平凡的结构:

版本 12 首次出现在 G++ 8 中,更正了 x86_64 目标上的空类以及仅删除了复制/移动构造函数的类的调用约定。它意外地更改了具有已删除复制构造函数和普通移动构造函数的类的调用约定。

版本 13 首次出现在 G++ 8.2 中,修复了版本 12 中的意外更改。


因此,前 6 个参数(将结构计为两个参数)将在 RDI、RSI、RDX、RCX、R8、R10(按此顺序)中传递,其余的在堆栈中。您可以编写一个简单的函数来查看它在哪里查找 args:

int ffi_function(Opaque *server, View region_buf, uint64_t peer_id,
                View snaps, uint64_t index, uint64_t term)
{
    //return region_buf.len;
    return region_buf.len - snaps.len + index;
}

GCC -Og -fverbose-asm makes the following asm,Godbolt 上的 GCC11:

ffi_function(Opaque*, View, unsigned long, View, unsigned long, unsigned long):
        sub     rdx, r9   # _3, tmp103
        mov     rax, rdx  # _3, _3
        add     rax, QWORD PTR [rsp+8]    # _3, index
        ret

(您可以看到index 是第一个堆栈参数,就在返回地址的正上方;更早的堆栈参数在寄存器中。snaps.len 是最后一个寄存器参数,在 R9 中。)

我猜uint64_t 是您的 ReturnType。如果它是一个大于 2 个寄存器的结构,那么您将有一个指向返回值对象的指针作为 RDI 中的第一个 arg,而其他 args 则增加 1。


您的转储与您预期的 args 匹配得很好。我重新格式化了它,以便为每个 qword(8 字节)堆栈槽添加注释。

RSP:
0x7fff0b7d4338: 0x00007ffff3dc42fe (return addr)
                0x0000000000000007 (expected 5th arg = 7)
                0x0000000000000006 (expected 6th arg = 6)
                0x00007ffff637393e (part of the caller's stack frame, not args)
                ...

前面的参数都在寄存器中。

如果调试信息没有完美地找到它们,那可能是因为您编译时进行了优化;事情变得棘手,被调用者可能不会在任何地方保留其传入参数的副本。或者即使没有,让调试信息正常工作也可能很棘手。

【讨论】:

  • 我更新了转储的堆栈。介意看看:D
  • @SchrodingerZHU:第一个参数是指针类型,所以在call的那一刻,它在RDI中,而不是在堆栈上。第一个 stack arg 将是第一个 View,将直接位于返回地址之上,16 字节对齐。 RBP 不是 arg 传递的一部分,因此您应该在 call 指令之前或之后相对于 RSP 进行转储。 (如果您希望任何人查看任何内容,请确保准确说明堆栈快照是在哪个版本中拍摄的。)
  • 我又更新了。和一些附加信息:我确认参数在我们自己的代码中的第一个 callq 之前搞砸了。因此,应该认为是c++代码导致的堆栈损坏。
  • @SchrodingerZHU:我看到了你的更新,并且已经在做我自己的一个了。您的参数正是调用约定所说的位置,堆栈中只有最后 2 个。 (我之前没有注意到您的结构足够小以适合寄存器,但确实如此。)
【解决方案2】:

对不起,我的回复晚了。再次感谢@Peter Cordes。 我的一位同事终于找到并解决了这个问题。


事实证明,View 的定义并非纯粹是微不足道的。结构中添加了一个自定义的ctor。

我的同事做了两件事来解决这个问题:

  • 删除所有 ctor 以保持结构简单
  • 将所有相关的 ffi 函数移至外部 C 范围。

我还没有查看生成的代码,但根据报告,段错误已经消失。


话虽如此,我仍然认为可能需要仔细考虑一些事情。如果我们严格遵循 Itanium ABI,可以看到 View 仍然是trivial for the purpose of call 并且应该在 C 的约定中通过。

https://itanium-cxx-abi.github.io/cxx-abi/abi.html#non-trivial-parameters

所以即使原始代码有些“混乱”,我认为它可以在 FFI 中使用? (毕竟 gcc-7 在这方面做得很好,而且 -Wabi=11/12/13 没有对此提出任何抱怨)。


更新:

刚刚检查了组件。更改后,gcc-10 生成的代码与gcc-7 完全相同。

【讨论】:

  • 哦,所以 g++-8 更改了您的结构的 C++ ABI 规则?是的,那会做到的。 gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html 向我指出 gcc.gnu.org/onlinedocs/gcc/… 哪个文档 g++ -fabi-version=0(默认)与 第 12 版,第一次出现在 G++ 8,[...]。它意外地更改了具有已删除复制构造函数和普通移动构造函数的类的调用约定。
  • 很可能相关。但是,我确实认为原始代码不应该受到影响。无论如何,不​​择手段地使用危险代码是不明智的。
猜你喜欢
  • 2012-09-05
  • 2013-08-12
  • 2011-05-07
  • 1970-01-01
  • 1970-01-01
  • 2022-08-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多