我观察到GCC的C++编译器生成了以下汇编代码:
sub $0xffffffffffffff80,%rsp
这相当于
add $0x80,%rsp
例如,从堆栈中删除128字节。
为什么GCC生成第一个减法变体而不是加法变体?与利用下溢相比,加法变体对我来说似乎更自然。
这在相当大的代码库中只发生了一次。我没有最小的C++代码示例来触发此操作。我正在使用GCC 7.5.0。
我观察到GCC的C++编译器生成了以下汇编代码:
sub $0xffffffffffffff80,%rsp
这相当于
add $0x80,%rsp
例如,从堆栈中删除128字节。
为什么GCC生成第一个减法变体而不是加法变体?与利用下溢相比,加法变体对我来说似乎更自然。
这在相当大的代码库中只发生了一次。我没有最小的C++代码示例来触发此操作。我正在使用GCC 7.5.0。
试着组装两者,你就会明白为什么。
0: 48 83 ec 80 sub $0xffffffffffffff80,%rsp
4: 48 81 c4 80 00 00 00 add $0x80,%rsp
sub
指令版本比较短三个字节。
这是因为x86上的add
和sub
immediate指令有两种形式。其中一种采用8位符号扩展立即数,另一种采用32位符号扩展立即数。请参见https://www.felixcloutier.com/x86/add;相关的格式(使用Intel语法)是add r/m64, imm8
和add 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 Cordessalc
中直到amd64才发生这种情况。 - Peter Cordes