cygwin 上的 gcc 输出使用堆栈 space 外部堆栈帧

gcc output on cygwin using stack space outside stack frame

我在查看 'objdump -S' 的汇编输出时发现了一些奇怪的东西。这是在 cygwin/x86_64 v. 3.1.5 上,gcc 9.3.0 在 Windows 10.

这是特定函数的汇编输出(该函数没有用,只是说明问题):

u_int64_t returnit(u_int64_t x) {
   1004010b9:   55                      push   rbp
   1004010ba:   48 89 e5                mov    rbp,rsp
   1004010bd:   48 83 ec 10             sub    rsp,0x10
   1004010c1:   48 89 4d 10             mov    QWORD PTR [rbp+0x10],rcx
        u_int64_t a = 1;
   1004010c5:   48 c7 45 f8 01 00 00    mov    QWORD PTR [rbp-0x8],0x1
   1004010cc:   00

        return a + x;
   1004010cd:   48 8b 55 f8             mov    rdx,QWORD PTR [rbp-0x8]
   1004010d1:   48 8b 45 10             mov    rax,QWORD PTR [rbp+0x10]
   1004010d5:   48 01 d0                add    rax,rdx
}
   1004010d8:   48 83 c4 10             add    rsp,0x10
   1004010dc:   5d                      pop    rbp
   1004010dd:   c3                      ret

几乎一切看起来都很正常:设置堆栈帧,为局部变量添加额外的 space,并将传递的参数(“x”,在寄存器 rcx 中)复制到堆栈上的某个位置。

这是看起来奇怪的部分:

mov    QWORD PTR [rbp+0x10],rcx

它正在将 rcx 的内容复制到当前堆栈帧之外。局部变量按原样存储在当前堆栈帧中。

我在较旧的 cygwin 安装(32 位,v.2.9.0 和 gcc 6.4.0)上试过这个,它的行为方式相同。

我也在其他平台上尝试过这个 - 一个旧的 ubuntu linux 内核 4.4.0 和 gcc 5.3.1 的 liveboot,以及一个带有 clang 8.0.1 的 FreeBSD 12.1 盒子,都是 64 -bit - 他们做了人们期望的事情,复制在本地堆栈框架内的寄存器中传递的参数值。例如,这是 FreeBSD 上的相关行(它使用 rdi 而不是 rcx):

2012e8:       89 7d fc                mov    DWORD PTR [rbp-0x4],edi

在 cygwin 上这样做有什么特别的原因吗?

此行为符合 Windows x64 ABI。

查看 Microsoft 的 x64 stack usage 页面,我们可以看到 ABI 指定 space 在堆栈上保留四个寄存器参数,即使使用的参数更少。这些是起始地址,充当实际参数寄存器的影子。

此区域可用于保存否则会被覆盖的参数,以帮助调试等。鉴于为极其简单的操作完成的工作量,我假设这是 unoptimised/debugging 代码。代码的优化编译可能会跳过这些冗余存储和加载,并且可能不会触及 ret.

之外的内存。

Windows 使用的 Microsoft x64 调用约定与 Linux、OS X 等在 x86-64 上使用的 System V AMD64 ABI 中看到的调用约定不同.


This example 显示了 MSVC 中的优化效果(不同的编译器,但仍然是 Windows-targeting)。无需在堆栈上实际存储值,可以在一条指令中完成计算。

这是描述此行为的 . The output from the compiler is what you will observe when you use unoptimized code in a 64-bit Windows GCC compilers (MingGW, Cygwin, etc). It is copying the incoming parameters passed via RCX, RDX, R8, R9 into the shadow store (aka Shadow Space or Home Space). This does not apply to 32-bit Windows builds. The code you are reviewing would have been generated at -O0 (usually the default). This behaviour is used to make 64-bit debugging easier. There is a related

This is where the Home space comes into play: It can be used by compilers to leave a copy of the register values on the stack for later inspection in the debugger. This usually happens for unoptimized builds. When optimizations are enabled, however, compilers generally treat the Home space as available for scratch use. No copies are left on the stack, and debugging a crash dump turns into a nightmare

在没有优化的情况下,GCC 的行为是复制通过 RCXRDXR8 传递的参数R9 。如果您将代码修改为:

#include<stdint.h>
uint64_t returnit(uint64_t w, uint64_t x, uint64_t y, uint64_t z) {
        return 0;
}

生成的代码看起来像:

0000000000000000 <returnit>:
   0:   push   rbp
   1:   mov    rbp,rsp
   4:   mov    QWORD PTR [rbp+0x10],rcx
   8:   mov    QWORD PTR [rbp+0x18],rdx
   c:   mov    QWORD PTR [rbp+0x20],r8
  10:   mov    QWORD PTR [rbp+0x28],r9
  14:   mov    eax,0x0
  19:   pop    rbp
  1a:   ret

如果您构建的优化大于 -O0(-O1、O2、O3、-Os、-Og 等),则不会将这些参数的副本复制到影子存储中。


OP 在 中提到:

It just feels like accessing stack space outside your own frame is asking for trouble. It seems like putting the register parameter stack area inside the caller's space rather than the callee's doesn't somehow make more or less vulnerable to overwrites, but it is what it is

编译器可以自由使用影子存储(在 Windows 上),甚至可以在传递参数的堆栈上使用 space。在 C 中,用于堆栈参数的 space 是被调用者拥有的,而不是调用者拥有的。这是因为 C 语言完全按值传递。函数总是从调用者那里获取参数的副本。副作用是 C 编译器可以自由使用函数参数使用的任何堆栈 space,因为它认为合适。