[英]Why padding in C is valid for variables/structs allocated on stack?
我在这里阅读C中的结构填充: http : //www.catb.org/esr/structure-packing/ 。
我不明白为什么在编译期间为堆栈上分配的变量/结构确定的填充在所有情况下在语义上都是有效的。 让我举个例子。 假设我们要编译这个玩具代码:
int main() {
int a;
a = 1;
}
在X86-64上gcc -S -O0 ac
生成此程序集(删除了不必要的符号):
main:
pushq %rbp
movq %rsp, %rbp
movl $1, -4(%rbp)
movl $0, %eax
popq %rbp
ret
在这种情况下,为什么我们知道%rbp
和%rbp-4
是4对齐的,以适合存储/加载int?
让我们尝试使用结构相同的例子。
struct st{
char a;
int b;
}
从阅读中我推断,填充版本的结构看起来像这样:
struct st{
char a; // 1 byte
char pad[3]; // 3 bytes
int b; // 4 bytes
}
所以,第二个玩具的例子
int main() {
struct st s;
s.a = 1;
s.b = 2;
}
生成
main:
pushq %rbp
movq %rsp, %rbp
movb $1, -8(%rbp)
movl $2, -4(%rbp)
movl $0, %eax
popq %rbp
ret
我们观察到这种情况确实如此。 但同样, rbp
本身在任意堆栈帧上的值是否正确对齐的保证是什么? rbp
的值是否仅在运行时可用? 如果在编译时没有任何关于struct的起始地址对齐的知识,编译器如何对齐struct的成员?
正如@P__J__指出的那样(在一个现在删除的答案中) - C编译器如何生成代码是一个实现细节。 由于您将此标记为ABI问题,因此您的真正问题是“当GCC针对Linux时,如何假设RSP具有任何特定的最小对齐?”。 Linux使用的64位ABI是AMD64(x86-64)System V ABI 。 在 调用符合ABI的1,2函数(包括main
) 之前 ,堆栈的最小对齐保证最小为16个字节(根据传递给函数的类型,它可以是32个字节或64个字节)。 ABI声明:
3.2.2堆栈帧
除寄存器外,每个函数在运行时堆栈上都有一个框架。 这个堆栈从高地址向下增长。 图3.3显示了堆栈组织。 输入参数区域的末尾应在16 (32或64,如果__m256或__m512在栈上传递) 字节边界上对齐 。 换句话说, 当控制转移到函数入口点时 , 值(%rsp + 8)始终是16 (32或64) 的倍数 。 堆栈指针%rsp始终指向最新分配的堆栈帧的末尾。
您可能会问为什么提到RSP + 8是16的倍数(而不是RSP + 0 )。 这是因为调用函数的概念意味着CALL指令本身将8字节的返回地址放在堆栈上。 无论函数是被调用还是跳转到(即: 尾调用 ),代码生成器总是假设在执行函数中的第一条指令之前,堆栈始终未对齐为8.虽然堆栈将是自动保证在8字节边界上对齐。 如果从RSP中减去8,则保证再次对齐16字节。
值得注意的是,下面的代码保证在PUSHQ
,堆栈在16字节边界上对齐,因为PUSH
指令将RSP降低8并再次将堆栈对齐到16字节边界:
main:
# <------ Stack pointer (RSP) misaligned by 8 bytes
pushq %rbp
# <------ Stack pointer (RSP) aligned to 16 byte boundary
movq %rsp, %rbp
movb $1, -8(%rbp)
movl $2, -4(%rbp)
movl $0, %eax
popq %rbp
ret
对于64位代码,可以从中得出的结论是,尽管堆栈指针的实际值在运行时是已知的,但ABI允许我们推断在进入函数时的值具有特定的对齐并且编译器代码生成系统可以在堆栈上放置struct
时使用它。
一个逻辑问题是 - 如果在进入函数时可以保证的堆栈对齐不足以对齐放置在堆栈上的结构或数据类型,那么GCC编译器会做什么? 考虑对您的程序进行此修订:
struct st{
char a; // 1 byte
char pad[3]; // 3 bytes
int b; // 4 bytes
};
int main() {
struct st s __attribute__(( aligned(32)));
s.a = 1;
s.b = 2;
}
我们告诉GCC变量s
应该是32字节对齐的。 一个可以保证16字节堆栈对齐的函数不能保证32字节对齐(32字节对齐确保16字节对齐,因为32可以被16整除)。 GCC编译器必须生成函数序言,以便s
可以正确对齐。 你可以看一下这个程序的未经优化的godbolt输出,看看GCC如何实现这个目标:
main:
pushq %rbp
movq %rsp, %rbp
andq $-32, %rsp # ANDing RSP with -32 (0xFFFFFFFFFFFFFFE0)
# rounds RSP down to next 32 byte boundary
# by zeroing the lower 5 bits of RSP.
movb $1, -32(%rsp)
movl $2, -28(%rsp)
movl $0, %eax
leave
ret
在这种情况下,为什么我们知道%rbp和%rbp-4的值是4对齐的,以适合存储/加载int?
在这种特殊情况下,我们知道我们在x86处理器上,任何地址都适合加载和存储整数。 调用者可以将先前对齐的%rbp
递减或偏移17,除了可能的性能之外,它不会产生差异。
然而,它是一致的。 为什么我们知道这是ABI要求的我们信任的系统的不变量。 如果堆栈指针未对齐,则表示调用者违反了调用约定的一个方面。
除非我们接收来自单独安全域的呼叫(例如从用户空间接收系统调用的内核),否则我们只信任调用者。 strcmp
函数如何知道它的参数指向有效的,以null结尾的字符串? 它信任来电者。 一样。
如果函数接收到对齐的%rsp
并确保它的所有操作都保持对齐,那么它调用的任何函数也会收到对齐的%rsp
。 确保编译器确保所有调用都使用所需的堆栈对齐。 如果您正在编写汇编代码,则必须确保自己。
如果在编译时没有任何关于struct的起始地址对齐的知识,编译器如何对齐struct的成员?
在假设对象的运行时基址对于最严格对齐的struct成员适当对齐的情况下,给予struct
的成员偏移。 这就是为什么结构的第一个成员只是放在零偏移处,无论其类型如何。
运行时必须确保为任意对象分配的任何地址都具有任何标准类型alignof(maxalign_t)
的最严格对齐。 例如,如果系统上最严格的对齐是16字节(如x86-64 System V中),则malloc
必须分配指向16字节对齐地址的指针。 然后可以将任何类型的结构放入生成的内存中。
如果您编写自己的通用分配器,在系统上发出4字节对齐的指针,其中对齐可能与16一样严格,那么这是错误的。
(注意, __m256
和__m512
类型不计入maxalign_t
: malloc
仍然只需要确保x86-64 System V中的16字节对齐,并且不足以用于过度对齐的类型,如__m256
或自定义struct foo { alignas(32) int32_t a[8]; };
.对于过度对齐的类型使用aligned_alloc()
。)
另请注意,ISO C标准中的措辞是malloc
返回的malloc
必须可用于任何类型。 无论如何,4字节分配不能保持16字节类型,因此允许小分配小于16字节对齐。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.