为什么C中的填充对于在堆栈上分配的变量/结构有效?

dav*_*23r 3 c linux assembly x86-64 abi

我在这里阅读有关C语言中的结构填充的信息:http : //www.catb.org/esr/structure-packing/
我不明白为什么在编译时为堆栈分配的变量/结构确定的填充在所有情况下在语义都是有效的。让我提供一个例子。假设我们要编译以下玩具代码:

int main() {
    int a;
    a = 1;
}
Run Code Online (Sandbox Code Playgroud)

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

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $1, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret
Run Code Online (Sandbox Code Playgroud)

在这种情况下,为什么我们知道4的值%rbp并因此%rbp-4是4对齐的,以适合于int的存储/加载?

让我们尝试使用结构相同的示例。

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $1, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret
Run Code Online (Sandbox Code Playgroud)

从阅读中我推断出结构的填充版本看起来像这样:

struct st{
    char a;
    int b;
}
Run Code Online (Sandbox Code Playgroud)

所以,第二个玩具的例子

struct st{
    char a;      // 1 byte
    char pad[3]; // 3 bytes
    int b;       // 4 bytes
}
Run Code Online (Sandbox Code Playgroud)

产生

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movb    $1, -8(%rbp)
    movl    $2, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret
Run Code Online (Sandbox Code Playgroud)

我们观察到确实如此。但是,再次保证rbp在任意堆栈帧上自身的值正确对齐的保证是什么?难道不是rbp仅在运行时可用的值?如果在编译时对结构的起始地址的对齐一无所知,编译器如何对齐结构的成员?

Mic*_*tch 6

正如@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字节边界对齐(如果在堆栈上通过__m256或__m512,则为32或64)。换句话说,当控制权转移到函数入口点时值(%rsp + 8)始终是16的倍数(32或64)堆栈指针%rsp始终指向最新分配的堆栈帧的末尾。

您可能会问,为什么提到的RSP + 8是16的倍数(而不是RSP + 0)。这是因为调用函数的概念意味着CALL指令本身会将8字节的返回地址放置在堆栈上。无论是调用一个函数还是跳转到某个函数(即tail调用),代码生成器始终假定在执行函数中的第一条指令之前,堆栈始终未对齐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
Run Code Online (Sandbox Code Playgroud)

对于64位代码,可以得出的所有结论是,尽管堆栈指针的实际值在运行时是已知的,但是ABI允许我们推断函数输入时的值具有特定的对齐方式,并且编译器代码生成系统在将a 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;
}
Run Code Online (Sandbox Code Playgroud)

我们已经告诉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
Run Code Online (Sandbox Code Playgroud)

脚注

  • 1 64位Solaris,MacOS和BSD以及Linux也使用AMD64 System V ABI
  • 264位的Microsoft Windows调用约定(ABI) (正在执行8字节对准刚刚之前的函数的第一指令)保证一个函数调用堆栈对准16字节之前。

  • 您可以使用C11`alignas(32)struct st s;`来移植它。C或C ++ 11中的`#include &lt;stdalign.h&gt;`已经是关键字。 (3认同)