结构与参数中数据对齐的差异?
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
都是 push
d 到堆栈,这意味着它们按 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 并创建所有参数的完整数组。)
给定以下代码:
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
都是 push
d 到堆栈,这意味着它们按 8 个字节对齐。为什么会出现这种不一致?
堆栈槽在 x86-64 调用约定中为 8 个字节,例如您正在使用的 x86-64 System V 调用约定,因为 32 位 push/pop 是不可能的,并且更容易
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 并创建所有参数的完整数组。)