What is the role of the carry flag in Assembly language

What is the role of the carry flag in Assembly language

I didn’t fully appreciate the carry flag until I tried to add two 64-bit numbers together on a 32-bit system and got a result that was just… wrong. Not randomly wrong — consistently off by exactly the amount you’d expect if a carry from the low half never made it into the high half. That’s when the carry flag stopped being an abstract line in a manual and became something I genuinely needed to understand. This post covers what the carry flag is, how it’s set, how it enables multi-precision arithmetic, and how it differs from the closely related (and often confused) overflow flag.

What Is the Carry Flag?

The carry flag (CF on x86/x86-64, C on ARM) is a status bit that records whether an arithmetic operation produced a carry-out (for addition) or a borrow (for subtraction) beyond the width of the destination register, when treating the operands as unsigned numbers.

In other words: if you add two unsigned numbers and the true mathematical result doesn’t fit in the register width, the carry flag captures that “overflow” bit that got dropped.

Where the Carry Flag Lives

ArchitectureRegisterFlag NameBit Position
x86 (32-bit)EFLAGSCFBit 0
x86-64RFLAGSCFBit 0
ARM (AArch32/AArch64)CPSR / PSTATECPart of NZCV

A Concrete Example

Let’s add two 8-bit unsigned numbers: 0xFF + 0x02.

  11111111   (0xFF = 255)
+ 00000010   (0x02 = 2)
-----------
 100000001   (9 bits needed, but register is only 8 bits wide)

The 8-bit register can only hold 00000001 (the low 8 bits), and the 9th bit — the actual carry-out — is captured in the carry flag. So after this addition: the register holds 0x01, and CF = 1, correctly signaling that the true unsigned sum (257) didn’t fit in 8 bits.

; x86-64 example
mov     al, 0xFF
add     al, 0x02        ; al becomes 0x01, CF is set to 1
jc      overflow_occurred   ; jump if carry flag is set
; ARM64 example
MOV     W0, #0xFF
ADDS    W0, W0, #2       ; W0 becomes 1, C flag is set
B.CS    overflow_occurred  ; branch if carry set (CS = Carry Set)

Carry Flag vs. Overflow Flag: The Classic Confusion

This is, without exaggeration, one of the most commonly misunderstood pairs of concepts in assembly programming. Both flags can be set by the exact same instruction, but they answer different questions:

FlagQuestion It AnswersRelevant For
Carry Flag (CF/C)Did the result overflow if the operands are treated as unsigned?Unsigned arithmetic, multi-precision math, pointer arithmetic
Overflow Flag (OF/V)Did the result overflow if the operands are treated as signed (two’s complement)?Signed arithmetic correctness

The same bit pattern can simultaneously be a valid unsigned result with a carry, and an invalid signed result with an overflow, or vice versa — because “correctness” depends entirely on how you’re interpreting the bits.

Example Showing Both Flags in Action

mov     al, 0x7F        ; 127 unsigned, or +127 signed
add     al, 0x01        ; al becomes 0x80
; CF = 0 (127 + 1 = 128, fits fine in unsigned 8-bit range 0-255)
; OF = 1 (as signed: +127 + 1 should be +128, but 0x80 as signed 8-bit is -128 -- overflow!)

This single example proves the point: the same addition sets CF to 0 (no unsigned problem) while setting OF to 1 (a very real signed problem) — because interpreting 0x80 as unsigned (128) is perfectly valid, but interpreting it as signed (-128) is a broken result of adding two positive numbers.

The Carry Flag’s Superpower: Multi-Precision Arithmetic

The carry flag’s most important real-world job is enabling arithmetic on numbers larger than a single register — a technique called multi-precision (or “bignum”) arithmetic, used in cryptography, arbitrary-precision math libraries, and compiler-generated 64-bit arithmetic on 32-bit systems.

x86: ADC and SBB (Add/Subtract With Carry)

; Add two 64-bit numbers using two 32-bit registers each (simulating 64-bit add on a 32-bit system)
; Number A: EDX:EAX (high:low)
; Number B: ECX:EBX (high:low)

add     eax, ebx        ; add low 32 bits, sets CF if there's a carry-out
adc     edx, ecx        ; add high 32 bits PLUS the carry from the previous addition

ADC (Add with Carry) is the instruction that makes chained, multi-word addition possible — it adds the carry-in from the previous operation as an extra +1 when needed, letting you “ripple” a carry across as many register-widths as your number requires.

ARM64: ADCS (Add with Carry, Setting Flags)

; Adding two 128-bit numbers stored across two 64-bit register pairs
ADDS    X0, X2, X4       ; add low halves, sets carry flag
ADC     X1, X3, X5       ; add high halves plus carry-in from previous ADDS

ARM provides the same conceptual capability via ADCS/ADC and SBCS/SBC (subtract with carry/borrow), used identically to chain arithmetic across multiple register-widths.

Internal Working: How ADC Uses the Carry Flag

sequenceDiagram
    participant ALU as ALU
    participant Flags as Flags Register (CF)
    participant Reg as Destination Register

    Note over ALU,Reg: Step 1 - Add low words
    ALU->>ALU: Compute low_A + low_B
    ALU->>Flags: Set CF = 1 if result overflowed register width
    ALU->>Reg: Store low result

    Note over ALU,Reg: Step 2 - Add high words with carry-in
    ALU->>Flags: Read current CF value
    ALU->>ALU: Compute high_A + high_B + CF
    ALU->>Flags: Update CF based on this new addition
    ALU->>Reg: Store high result

The critical detail is that ADC reads the carry flag before the addition (as an extra input bit) and then writes a new carry flag value based on this combined addition — allowing an arbitrary chain of ADC instructions to correctly propagate a carry across as many words as needed.

Practical Use Cases

  1. Big integer arithmetic: Cryptographic libraries (RSA, elliptic curve operations) implement arbitrary-precision addition/multiplication using chains of ADC/ADCS across many register-widths.
  2. 64-bit arithmetic on 32-bit systems: Before 64-bit CPUs were ubiquitous, compilers generated exactly this ADD/ADC pattern to implement 64-bit integer addition using pairs of 32-bit registers.
  3. Checksum and hashing algorithms: Some checksum algorithms (like the Internet checksum used in IP headers) explicitly rely on carry propagation across word boundaries during summation.
  4. Pointer and address arithmetic bounds checking: Detecting unsigned overflow when computing buffer offsets, a real concern in security-sensitive code.

Debugging the Carry Flag

In GDB:

(gdb) p $eflags
(gdb) info registers eflags
eflags 0x203 [ CF IF ]

Seeing CF present in that bracketed flag list confirms the carry flag is currently set — extremely useful when stepping through hand-written or compiler-generated multi-precision arithmetic routines to verify carries propagate correctly.

Optimization Considerations

Comparison: Carry Flag vs. Related Flags

FlagMeaningTypical Use
Carry Flag (CF/C)Unsigned overflow/borrowMulti-precision math, unsigned comparisons
Overflow Flag (OF/V)Signed overflowSigned arithmetic correctness
Zero Flag (ZF/Z)Result is exactly 0Equality checks
Sign Flag (SF/N)Result is negativeSigned comparisons

Common Mistakes

Best Practices

FAQs

Is the carry flag the same as the overflow flag? No — they answer different questions about the same arithmetic result: CF is about unsigned overflow/borrow, OF is about signed overflow. They can differ or agree depending on the specific values involved.

Which instructions read the carry flag as an input? ADC/SBB on x86, and ADCS/SBCS/ADC/SBC on ARM, all read the current carry flag as an extra input bit, in addition to producing an updated carry flag as output.

Does the carry flag matter on 64-bit systems where I rarely need bignum math? Yes — it’s still used for compiler-generated arithmetic on wide types (__int128), cryptographic library implementations, and any unsigned overflow detection logic, even if you personally never write ADC by hand.

Summary and Key Takeaways

References

Exit mobile version