add
/adc
Just Works. So does sub
/sbb
(or sbc
as some ISAs call it, usually the ones where the carry flag is a not-borrow flag after subtraction, like ARM, opposite of x86).
One way of understanding why it works is first to understand that 2's complement add/subtract are the same binary operation as unsigned. (Except divide, and widening multiply). That's one reason we use 2's complement, so we don't need separate add/sub instructions.
A chain of add
/adc
(or sub/sbb) is a software way to propagate carry across the boundary between separate register-width operations, exactly like would happen with a wider hardware adder. The simplest hardware adder is just a chain of full adders, with ripple-carry propagation between every bit, including between bit #7 and bit #8. Doing two 8-bit halves is just using CF to propagate that carry from low to high.
Note that only the high bit of the highest chunk is actually the sign bit (with place-value -2^(n-1)
.) The place-value of the high bit of the low byte is 2^7 = 128, so you could say that each chunk below the highest is unsigned. e.g. 0x00ff
is 255 as a 16-bit 2's complement number, not -128
.
After addition with an add
/adc
chain, if your CPU has a signed-overflow flag (like x86's OF or ARM's V), that flag will correctly reflect whether the whole operation had signed overflow or not. As http://teaching.idallen.com/dat2343/10f/notes/040_overflow.txt explains, one way for an ALU to calculate it is by XORing the carry-in and carry-out from the most-significant bit. Those are exactly the same as they'd be if we'd done a single 16-bit add
instruction instead of 8-bit add
/adc
.
I forget whether it's that easy for subtraction. I think so. (Edits welcome, or I'll update this when I have time to check.)
Borrow propagates strictly from low to high. And subtraction is usually implemented by NOTing one input and feeding a 1
as carry-in to an adder. With these control modifications, you have an adder-subtractor built on top of whatever fast carry-lookahead or carry-save adder you're actually using.
Or for sbc
, flipping the carry input instead of using it directly.