I have observed that GCC's C compiler generates the following assembler code:
sub $0xffffffffffffff80,%rsp
This is equivalent to
add $0x80,%rsp
i.e. remove 128 bytes from the stack.
Why does GCC generate the first sub variant and not the add variant? The add variant seems way more natural to me than to exploit that there is an underflow.
This only occurred once in a quite large code base. I have no minimal C code example to trigger this. I am using GCC 7.5.0
CodePudding user response:
Try assembling both and you'll see why.
0: 48 83 ec 80 sub $0xffffffffffffff80,%rsp
4: 48 81 c4 80 00 00 00 add $0x80,%rsp
The sub
version is three bytes shorter.
This is because the add
and sub
immediate instructions on x86 has two forms. One takes an 8-bit sign-extended immediate, and the other a 32-bit sign-extended immediate. See https://www.felixcloutier.com/x86/add; the relevant forms are (in Intel syntax) add r/m64, imm8
and add r/m64, imm32
. The 32-bit one is obviously three bytes larger.
The number 0x80
can't be represented as an 8-bit signed immediate; since the high bit is set, it would sign-extend to 0xffffffffffffff80
instead of the desired 0x0000000000000080
. So add $0x80, %rsp
would have to use the 32-bit form add r/m64, imm32
. On the other hand, 0xffffffffffffff80
would be just what we want if we subtract instead of adding, and so we can use sub r/m64, imm8
, giving the same effect with smaller code.
I wouldn't really say it's "exploiting an underflow". I'd just interpret it as sub $-0x80, %rsp
. The compiler is just choosing to emit 0xffffffffffffff80
instead of the equivalent -0x80
; it doesn't bother to use the more human-readable version.
Note that 0x80 is actually the only possible number for which this trick is relevant; it's the unique 8-bit number which is its own negative mod 2^8. Any smaller number can just use add
, and any larger number has to use 32 bits anyway. In fact, 0x80 is the only reason that we couldn't just omit sub r/m, imm8
from the instruction set and always use add
with negative immediates in its place. I guess a similar trick does come up if we want to do a 64-bit add of 0x0000000080000000
; sub
will do it, but add
can't be used at all, as there is no imm64
version; we'd have to load the constant into another register first.