GCC为什么会生成奇怪的移动栈指针方法

7

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

sub    $0xffffffffffffff80,%rsp

这相当于

add    $0x80,%rsp

例如,从堆栈中删除128字节。

为什么GCC生成第一个减法变体而不是加法变体?与利用下溢相比,加法变体对我来说似乎更自然。

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

1个回答

15

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

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

sub指令版本比较短三个字节。

这是因为x86上的addsub immediate指令有两种形式。其中一种采用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实际上是这种技巧相关的唯一可能数字;它是其自身对2^8取负数的唯一8位数字。任何更小的数字都可以使用add,任何更大的数字都必须使用32位。实际上,0x80是我们无法从指令集中省略sub r/m,imm8并始终使用带有负立即数的add的唯一原因。我想类似的技巧也会在需要进行64位加法0x0000000080000000时出现;sub可以完成,但add根本无法使用,因为没有imm64版本;我们必须先将常量加载到另一个寄存器中。


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接