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.