为什么GCC的VLA(可变长度数组)实现中有数字22?

2023-12-27

int read_val();
long read_and_process(int n) {
    long vals[n];
    for (int i = 0; i < n; i++)
        vals[i] = read_val();
    return vals[n-1];
}

x86-64 GCC 5.4编译的汇编语言代码为:

read_and_process(int):
        pushq   %rbp
        movslq  %edi, %rax
>>>     leaq    22(,%rax,8), %rax
        movq    %rsp, %rbp
        pushq   %r14
        pushq   %r13
        pushq   %r12
        pushq   %rbx
        andq    $-16, %rax
        leal    -1(%rdi), %r13d
        subq    %rax, %rsp
        testl   %edi, %edi
        movq    %rsp, %r14
        jle     .L3
        leal    -1(%rdi), %eax
        movq    %rsp, %rbx
        leaq    8(%rsp,%rax,8), %r12
        movq    %rax, %r13
.L4:
        call    read_val()
        cltq
        addq    $8, %rbx
        movq    %rax, -8(%rbx)
        cmpq    %r12, %rbx
        jne     .L4
.L3:
        movslq  %r13d, %r13
        movq    (%r14,%r13,8), %rax
        leaq    -32(%rbp), %rsp
        popq    %rbx
        popq    %r12
        popq    %r13
        popq    %r14
        popq    %rbp
        ret

为什么需要计算 8*%rax+22,然后与 -16 进行 AND,因为可能有 8*%rax+16,这会给出相同的结果并且看起来更自然?

x86-64 GCC 11.2 编译的其他汇编语言代码看起来几乎相同,只是数字 22 被替换为 15。那么这个数字只是随机确定的,还是因为某些原因?


摘要:该数字不是随机的,它是确保正确堆栈对齐的计算的一部分。这个数字应该是 15,而 22 是旧版本 GCC 中一个小错误的结果。


回想起那个x86-64 SysV ABI 要求 16 字节堆栈对齐 https://stackoverflow.com/q/49391001;堆栈指针必须是 16 的倍数call操作说明。因此当我们进入read_and_process,堆栈指针比 16 的倍数小 8,因为call这让我们推送了 8 个字节。所以在打电话之前read_val(),堆栈指针必须减 8,且大于 16 的倍数,即 8 的奇数倍。序言压入奇数个寄存器(5 个,即rbp, r14, r13, r12, rbx),每个 8 个字节。所以剩余的堆栈调整必须是16的倍数。

因此,无论为数组分配多少内存vals,它必须向上舍入为 16 的倍数。执行此操作的标准方法是添加 15,然后与 -16 进行 AND 操作:adjusted = (orig + 15) & -16.

为什么这样有效?-16,由于二进制补码运算,低 4 位被清除,其他位被设置,因此 AND 与-16结果是 16 的倍数 - 但由于 AND 清除了低位,因此结果x & -16小于x;这是四舍五入down。如果我们先加上 15(当然,比 16 少 1),最终效果就是四舍五入up反而。添加 15 至orig会导致它通过 16 的倍数,然后& -16将向下舍入为that16的倍数。Unless orig已经是 16 的倍数了,在这种情况下orig+15向下舍入回到orig本身。所以这在所有情况下都是正确的。

这就是 GCC 从 8.1.0 开始所做的事情。加15烤成一样lea乘以n乘以 8,并与-16几行之后出现。

在这种情况下,由于orig = 8*n已经是 8 的倍数,除了 15 之外还有其他值也可以;例如 8(虽然不是 16,见下文)。但使用 15 在数学上以及代码大小和速度方面是完全等效的,并且由于无论先前的对齐方式如何,15 都可以工作,因此编译器作者可以无条件地使用 15,而无需编写额外的代码来跟踪什么对齐方式orig可能已经有了。


但像旧版 GCC 那样添加 22 显然是错误的。如果orig已经是 16 的倍数了orig = 32, then orig+22是 54,向下舍入为 48。但是 32 字节已经是一个完美的大小,所以我们无缘无故地浪费了 16 字节。 (这里orig is 8*n所以如果输入的话就会发生这种情况n是偶数。)出于类似的原因,您使用 16 而不是 22 的建议也是错误的。

所以22是一个错误。这是一个相当小的错误;生成的代码仍然可以正常工作并符合 ABI,唯一的不良影响是有时会浪费一点堆栈空间。但它在 GCC 8.1.0 中被修复了题为“改善分配对齐”的提交 https://gcc.gnu.org/git/?p=gcc.git;a=commitdiff;h=ae85ad3a95d6df3c4131d02fd327809a29d10b33;hp=54c430044ba9a35a590e591108b184535eba5763. (alloca是一个执行动态堆栈分配的旧非标准函数,编译器编写者经常使用该术语来指代任何堆栈分配。)

显然,问题在于编译器之前的一些传递已确定需要将大小对齐到(至少)8 字节,这可以通过添加 7 并与 -8 进行 AND 运算来完成(稍后可能会在编译器后来意识到n*8已经对齐到 8 字节)。现在,当编译器意识到实际上需要 16 字节对齐时,这个约束应该是多余的,因为 16 的每个倍数都已经是 8 的倍数。但是编译器错误地adds偏移量 7 和 15,当正确的做法是取它们的最大值时(这就是提交所实现的)。 7 + 15 是... 22。

如果您使用 GCC 5.4 并关闭优化来编译代码,您可以看到这两个操作分别发生:

        lea     rdx, [rax+7]  ; add 7 to rax and write to rdx
        mov     eax, 16
        sub     rax, 1        ; now rax = 15
        add     rax, rdx      ; add 15 to rdx

当优化开启时,优化器将这些组合成一个 22 的加法 - 没有注意到 7 的加法一开始就不应该存在。在较新版本的 GCC 中-O0, the lea rdx, [rax+7]离开了。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么GCC的VLA(可变长度数组)实现中有数字22? 的相关文章

随机推荐