为啥 GCC 会产生奇怪的方式来移动堆栈指针

Posted

技术标签:

【中文标题】为啥 GCC 会产生奇怪的方式来移动堆栈指针【英文标题】:Why GCC generates strange way to move stack pointer为什么 GCC 会产生奇怪的方式来移动堆栈指针 【发布时间】:2022-01-23 21:54:51 【问题描述】:

我观察到 GCC 的 C++ 编译器生成以下汇编代码:

sub    $0xffffffffffffff80,%rsp

这相当于

add    $0x80,%rsp

即从堆栈中删除 128 个字节。

为什么 GCC 生成第一个子变体而不是添加变体? add 变体对我来说似乎比利用下溢更自然。

这在相当大的代码库中只发生过一次。我没有最小的 C++ 代码示例来触发它。我正在使用 GCC 7.5.0

【问题讨论】:

【参考方案1】:

尝试组装两者,你会明白为什么。

   0:   48 83 ec 80             sub    $0xffffffffffffff80,%rsp
   4:   48 81 c4 80 00 00 00    add    $0x80,%rsp

sub 版本短了三个字节。

这是因为 x86 上的 addsub 立即指令有两种形式。一个采用 8 位符号扩展立即数,另一个采用 32 位符号扩展立即数。请参阅https://www.felixcloutier.com/x86/add;,相关形式为(在 Intel 语法中)add r/m64, imm8add r/m64, imm32。 32 位的显然要大三个字节。

数字0x80 不能表示为 8 位有符号立即数;由于设置了高位,它将符号扩展到0xffffffffffffff80,而不是所需的0x0000000000000080。所以add $0x80, %rsp 必须使用 32 位格式add r/m64, imm32。另一方面,0xffffffffffffff80 正是我们想要的,如果我们减去而不是加法,因此我们可以使用sub r/m64, imm8,用更小的代码给出相同的效果。

我不会真的说这是“利用下溢”。我只是将其解释为sub $-0x80, %rsp。编译器只是选择发出0xffffffffffffff80 而不是等效的-0x80;它不会费心使用更易于阅读的版本。

请注意,0x80 实际上是唯一可能与此技巧相关的数字;它是唯一的 8 位数字,它是它自己的负模 2^8。任何较小的数字都可以使用add,任何较大的数字无论如何都必须使用32位。事实上,0x80 是我们不能只是从指令集中省略 sub r/m, imm8 并始终使用带有负立即数的 add 的唯一原因。我想如果我们想要对0x0000000080000000 进行 64 位添加,也会出现类似的技巧; sub 可以,但是add 根本不能用,因为没有imm64 版本;我们必须先将常量加载到另一个寄存器中。

【讨论】:

Re: 省略sub-immediate: MIPS 确实做到了,尽管使用了 2 的补码 imm16。但那是因为 MIPS 没有 FLAGS 寄存器。 x86 可以,add $-1, %reg 产生与sub $1, %reg 不同的进位标志输出。 (x86 的 CF 充当减法的借位,而不是 !borrow,因此在 -1 / +1 情况下它实际上总是不同。) 此外,可追溯到 8086 的 x86 操作码遵循一些模式,因此将它们作为子立即执行而不是在没有 #UD 异常的 8086 上执行可能更简单。英特尔本可以选择不记录该指令并保留操作码以供将来使用,但 salc 直到 amd64 才发生。

以上是关于为啥 GCC 会产生奇怪的方式来移动堆栈指针的主要内容,如果未能解决你的问题,请参考以下文章