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

Why GCC generates strange way to move stack pointer

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

sub    [=10=]xffffffffffffff80,%rsp

这相当于

add    [=11=]x80,%rsp

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

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

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

尝试将两者组合起来,您就会明白为什么。

   0:   48 83 ec 80             sub    [=10=]xffffffffffffff80,%rsp
   4:   48 81 c4 80 00 00 00    add    [=10=]x80,%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 [=19=]x80, %rsp 必须使用 32 位形式 add r/m64, imm32。另一方面,如果我们减去而不是加法,0xffffffffffffff80 就是我们想要的,所以我们可以使用 sub r/m64, imm8,用更小的代码得到同样的效果。

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

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