摘要:该数字不是随机的,它是确保正确堆栈对齐的计算的一部分。这个数字应该是 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]
离开了。