Carry-out from both ADC and SBB is based on the whole operation.
If you write sbb
as dst -= CF
, dst -= src
, if either of those produces a borrow then CF should be set.
The parens in the pseudo-code are not trying to tell you which part of the operation actually sets FLAGS. You're reading too much into it, and it's not that easy to emulate a full-adder (adc
or sbb
with carry in and carry out. That's why it's super helpful to have hardware supported instructions for them.)
Your emulation for sbb
is buggy for the case where src+CF
wraps to 0. You don't distinguish the src=-1 CF=1 case (always carry) from src=0, CF=0. (dst-=0 always produces CF=0). Try it on a real CPU with a debugger.
You can think of ADC as dst += src + CF
(or -=
for SBB) where the src+CF
temporary is computed without truncation; e.g. a 33-bit sum for 32-bit operand-size. The dst
and CF outputs of ADD / SUB / ADC / SBB can also be regarded as a 33-bit value.
Simple example:
mov al, 0xFF ; AL=0xFF
stc ; CF=1
adc al, 0xFF ; CF:AL = AL + (0xFF+CF) = AL + 0x100
; AL = 0xFF, CF=1
It's complicated enough to emulate adc
it's more helpful IMO to just understand it in terms of carry-in and carry-out from addition. Or if it helps, like a full adder, or actually a chain of 8, 16, 32, or 64 full adders. (Of course the physical implementation is lower latency than ripple carry).
For me, working out the annoyingly complicated sequence of branching or whatever that would be needed to emulate adc
or sbb
in terms of 32-bit fixed-width operations would be zero help in understanding what they do. They're simple enough to think of as primitive operations, and hard enough to emulate.