为啥 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 上的 add
和 sub
立即指令有两种形式。一个采用 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 实际上是唯一可能与此技巧相关的数字;它是唯一的 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 会产生奇怪的方式来移动堆栈指针的主要内容,如果未能解决你的问题,请参考以下文章