为什么 C 中的填充对堆栈上分配的 variables/structs 有效?

Why padding in C is valid for variables/structs allocated on stack?

我正在阅读有关 C 中的结构填充的信息:http://www.catb.org/esr/structure-packing/
我不明白为什么在编译期间为 variables/structures 分配在堆栈 上确定的填充在所有情况下在语义上都是有效的。让我举个例子。假设我们要编译这个玩具代码:

int main() {
    int a;
    a = 1;
}

在 X86-64 上 gcc -S -O0 a.c 生成此程序集(删除了不必要的符号):

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    , -4(%rbp)
    movl    [=12=], %eax
    popq    %rbp
    ret

在这种情况下,为什么我们知道 %rbp 的值并因此知道 %rbp-4 是 4 对齐的以适合 storing/loading 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    , -8(%rbp)
    movl    , -4(%rbp)
    movl    [=16=], %eax
    popq    %rbp
    ret

而且我们观察到情况确实如此。但是,又是什么保证 rbp 本身在任意堆栈框架上的值正确对齐? rbp的值不是只有在运行的时候才有吗?如果在编译时对结构起始地址的对齐一无所知,编译器如何对齐结构成员?

正如@P__J__ 指出的(在现已删除的答案中)- C 编译器如何生成代码是一个实现细节。由于您将此标记为 ABI 问题,因此您的真正问题是 "When GCC is targeting Linux, how is it allowed to assume that RSP has any particular minimum alignment?"。 Linux 使用的 64 位 ABI 是 AMD64(x86-64) System V ABI。堆栈的最小对齐 before CALLing 一个符合 ABI 的 1,2 函数(包括 main) 保证是 minimum 16 字节(它可以是 32 字节或 64 字节,具体取决于传递给函数的类型)。 ABI 声明:

3.2.2 The Stack Frame

In addition to registers, each function has a frame on the run-time stack. This stack grows downwards from high addresses. Figure 3.3 shows the stack organization. The end of the input argument area shall be aligned on a 16 (32 or 64, if __m256 or __m512 is passed on stack) byte boundary. In other words, the value (%rsp + 8) is always a multiple of 16 (32 or 64) when control is transferred to the function entry point. The stack pointer, %rsp, always points to the end of the latest allocated stack frame.

您可能会问为什么提到 RSP+8 是 16 的倍数(而不是 RSP+0)。这是因为 CALL 函数的概念意味着一个 8 字节 return 地址将被 CALL 放在堆栈上指令本身。无论函数是被调用还是跳转到(即:tail call),代码生成器总是假定就在执行函数中的第一条指令之前,堆栈总是错位 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    , -8(%rbp)
    movl    , -4(%rbp)
    movl    [=10=], %eax
    popq    %rbp
    ret

对于 64 位代码,从所有这些可以得出的结论是,尽管堆栈指针的实际值在 run-time 处已知,但 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 for this program 的未优化输出以了解 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    , -32(%rsp) 
        movl    , -28(%rsp)
        movl    [=12=], %eax
        leave
        ret

脚注

  • 164 位 Solaris、MacOS 和 BSD 以及 Linux
  • 也使用 AMD64 System V ABI
  • 2 64-bit Microsoft Windows calling convention (ABI) 保证在调用函数之前,堆栈是 16 字节对齐的(8 字节未对齐,恰好在函数的第一条指令之前正在执行)。

In this case why do we know that value of %rbp and consequently %rbp-4 is 4-aligned to be suitable for storing/loading int?

在这种特殊情况下,我们知道我们在 x86 处理器上,在该处理器上任何地址都适合加载和存储整数。调用者可以将先前对齐的 %rbp 递减或偏移 17,除了可能影响性能外,它不会产生任何影响。

然而,它是对齐的。我们之所以知道这是因为它是我们信任的系统的不变量,是 ABI 所要求的。如果堆栈指针未对齐,则表示调用者违反了调用约定的一个方面。

除非我们正在接收来自单独安全域的调用(例如内核接收来自用户 space 的系统调用),否则我们只是信任调用者。 strcmp 函数如何知道它的参数指向有效的 null-terminated 字符串?它信任调用者。同样的事情。

如果函数接收到对齐的 %rsp 并确保对其进行的所有操作都保持对齐,那么 it 调用的任何函数也会收到对齐的 %rsp .编译器确保所有调用都按照所需的堆栈对齐进行。如果您正在编写汇编代码,您必须自己确保。

How can compiler align members of struct if nothing is known about alignment of struct's start address at compile time?

struct 的成员在假定对象的 run-time 基地址将适当对齐的情况下被赋予偏移量,即使是最严格对齐的结构成员。这就是为什么结构的第一个成员只是简单地放置在偏移量零处,而不管其类型如何。

run-time 必须确保为任意对象分配的任何地址都具有任何标准类型中最严格的对齐方式,alignof(maxalign_t)。例如,如果系统上最严格的对齐方式是 16 字节(如在 x86-64 系统 V 中),那么 malloc 必须分发指向 16-byte-aligned 地址的指针。然后任何类型的结构都可以放入生成的内存中。

如果您编写自己的所谓 general-purpose 分配器,在对齐可能严格为 16 的系统上分发 4-byte-aligned 指针,那么这是错误的。


(请注意,__m256__m512 类型不计入 maxalign_tmalloc 仍然只需要确保 x86-64 系统中的 16 字节对齐V,并且对于 __m256 或自定义 struct foo { alignas(32) int32_t a[8]; }; 这样的 over-aligned 类型是不够的。对 over-aligned 类型使用 aligned_alloc()。)

另请注意,ISO C 标准中的措辞是 malloc 返回的内存必须可用于任何类型。无论如何,4 字节分配不能容纳 16 字节类型,因此允许小分配小于 16 字节对齐。