结构与参数中数据对齐的差异?

Difference in data alignment in struct vs parameter?

给定以下代码:

typedef struct tagRECT {
  int left;
  int top;
  int right;
  int bottom;
} RECT;

extern int Func(RECT *a, int b, char *c, int d, char e, long f, int g, int h, int i, int j);

int main() {

}

void gui() {
    RECT x = {4, 5, 6, 7};
    Func(&x, 1, 0, 3, 4, 5, 6, 7, 8, 9);
}

这是 gcc x86_64 大概在 linux 上生成的程序集(我使用了 compiler explorer)。

main:
  mov eax, 0
  ret
gui:
  push rbp
  mov rbp, rsp
  sub rsp, 16
  ; RECT x assignment
  mov DWORD PTR [rbp-16], 4
  mov DWORD PTR [rbp-12], 5
  mov DWORD PTR [rbp-8], 6
  mov DWORD PTR [rbp-4], 7

  ; parameters
  lea rax, [rbp-16]
  push 9
  push 8
  push 7
  push 6
  mov r9d, 5
  mov r8d, 4
  mov ecx, 3
  mov edx, 0
  mov esi, 1
  mov rdi, rax
  call Func
  add rsp, 32
  nop
  leave
  ret

可以看出struct中的int是4字节对齐的。但是函数的最后 4 个参数,所有 int 都是 pushd 到堆栈,这意味着它们按 8 个字节对齐。为什么会出现这种不一致?

堆栈槽在 x86-64 调用约定中为 8 个字节,例如您正在使用的 x86-64 System V 调用约定,因为 32 位 push/pop 是不可能的,并且更容易 . See What are the calling conventions for UNIX & Linux system calls on i386 and x86-64 (it also covers function-calling conventions, as well as system-calling conventions. Where is the x86-64 System V ABI documented?.

但是,

mov 工作得很好,所以将 4 个字节作为堆栈参数的最小单位是一个有效的设计。 (与 x86-16 不同,其中 SP 相对寻址模式是不可能的)。 但除非您引入填充规则,否则您可能会错位 8 字节 args。 因此,为每个 arg 至少提供 8 字节对齐可能是动机的一部分。 (虽然有填充规则来保证 __m128 args 有 16 字节对齐,__m256 有 32 字节等等。而且大概也用于过度对齐的结构,比如 struct { alignas(64) char b[256]; };

对于没有原型的函数,只有 4 字节的槽会更容易中断,并且可能会使可变参数函数更复杂,但 x86-64 System V 已经在堆栈上按值传递更大的对象,因此堆栈 arg 可能需要更多比一个 8 字节 "stack slot".

( 不像 Windows x64,它通过隐藏引用传递,所以每个 arg 恰好是一个堆栈槽。它甚至保留 32 字节的阴影 space 因此可变参数函数可以将其寄存器 args 溢出到shadow space 并创建所有参数的完整数组。)