为啥 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 会产生奇怪的方式来移动堆栈指针的主要内容,如果未能解决你的问题,请参考以下文章

为啥移动 numpy uint8 会产生负值?

为啥 gcc 会产生这个奇怪的程序集 vs clang?

为啥将 32 位寄存器移动到堆栈然后从堆栈移动到 xmm 寄存器?

为啥 GCC 会在堆栈上推送一个额外的返回地址?

为啥 GCC 以这种方式对堆栈中的整数进行排序?

为啥 GCC 生成的代码会从堆栈中读取垃圾?