为什么 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上的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 [=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
版本;我们必须先将常量加载到另一个寄存器中。
我观察到 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上的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 [=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
版本;我们必须先将常量加载到另一个寄存器中。