Lesson 05-Arithmetic and Logical Operation Instructions in Detail

Introduction: Arithmetic Is Where the Computer Earns Its Keep

Let me tell you something that might shock you: your computer spends most of its time moving data around, not actually computing anything.

I know, I know — we like to think of CPUs as these majestic calculation engines, endlessly computing π to a trillion digits and solving differential equations. And they can do those things. But in a typical program, MOV instructions (data transfer) outnumber ADD instructions (arithmetic) by a wide margin.

But when the CPU does do arithmetic — when it adds two numbers, or multiplies them, or shifts bits — that’s where the real work happens. That’s where your program actually computes something.

In this article, we’re going to dive deep into the arithmetic and logical instructions of the x86 architecture. We’ll start with addition and subtraction (including the often-overlooked ADC and SBB instructions for multi-precision arithmetic). We’ll talk about multiplication and division (which are far more complex than addition). We’ll cover bitwise logical operations (AND, OR, XOR, NOT) and the many, many shift and rotate instructions. We’ll touch on BCD (Binary-Coded Decimal) arithmetic (obsolete, but historically important). And we’ll introduce the x87 FPU (Floating-Point Unit) and its stack-based arithmetic.

This is a long article. The x86 instruction set is huge, and the arithmetic/logical instructions are a big chunk of it. But by the end, you’ll understand how the CPU actually computes. And that’s a hell of a thing to understand.

ADD, ADC: Addition With and Without Carry — Why ADC Matters for Multi-Precision Arithmetic

Addition is the most fundamental arithmetic operation. If a CPU can’t add, it can’t do anything useful. (Strictly speaking, a CPU could do everything with just NAND gates, but that’s a different discussion.)

The x86 architecture has two addition instructions:

  1. ADD: Adds two operands and stores the result in the destination operand.
  2. ADC (Add with Carry): Adds two operands plus the Carry Flag (CF), and stores the result in the destination operand.

ADD: The Basic Addition Instruction

ADD is straightforward. It computes destination = destination + source, sets the flags based on the result, and stores the result in destination.

; Basic ADD examples
ADD EAX, EBX        ; EAX = EAX + EBX
ADD AX, 5            ; AX = AX + 5
ADD [EBX], ECX       ; Memory at [EBX] = [EBX] + ECX
ADD EDX, [ESI]       ; EDX = EDX + memory at [ESI]

After ADD, the following flags are updated:

  • CF (Carry Flag): Set if the addition produced a carry-out (i.e., the result doesn’t fit in the destination). For unsigned arithmetic, CF = 1 means “overflow.”
  • ZF (Zero Flag): Set if the result is zero.
  • SF (Sign Flag): Set if the result is negative (bit 31 = 1 for 32-bit operations).
  • OF (Overflow Flag): Set if the addition overflowed for signed arithmetic (i.e., adding two positive numbers gave a negative result, or adding two negative numbers gave a positive result).
  • PF (Parity Flag): Set if the low byte of the result has even parity (even number of 1 bits).
  • AF (Auxiliary Carry Flag): Set if there was a carry-out from bit 3 to bit 4. Used for BCD arithmetic (which we’ll discuss in Section 9).

ADC: Addition with Carry

ADC is like ADD, but it also adds the Carry Flag (CF) to the result.

; ADC example
STC                  ; Set Carry Flag (CF = 1)
MOV AL, 5
ADC AL, 10          ; AL = 5 + 10 + CF = 5 + 10 + 1 = 16

Why would you ever need ADC?

Answer: multi-precision arithmetic. If you need to add numbers that are larger than the CPU’s word size (e.g., adding two 128-bit numbers on a 32-bit CPU), you need to add them in chunks, and you need to propagate the carry from one chunk to the next.

Multi-Precision Addition: 128-bit on a 32-bit CPU

Let’s say you want to add two 128-bit numbers. A 128-bit number is 16 bytes, or 4 × 32-bit chunks.

On a 32-bit CPU, you can’t add 128-bit numbers in a single instruction. You need to add them chunk-by-chunk, starting from the least significant chunk, and propagating the carry.

; Add two 128-bit numbers (4 × 32-bit chunks)
; Input: 
;   First number is in EAX:EBX:ECX:EDX  (most significant to least significant)
;   Second number is in ESI:EDI:EBP:ESP  (most significant to least significant)
; Output:
;   Result is in EAX:EBX:ECX:EDX

; Actually, let me redo this with memory operands, which is more realistic.

section .data
    num1 dd 0x12345678, 0x9ABCDEF0, 0x13579BDF, 0x2468ACE0   ; 4 × 32-bit chunks
    num2 dd 0x11111111, 0x22222222, 0x33333333, 0x44444444
    result dd 0, 0, 0, 0

section .text
    ; Load the least significant chunk (chunk 0)
    MOV EAX, [num1]
    ADD EAX, [num2]              ; Add chunk 0, sets CF if carry
    MOV [result], EAX             ; Store chunk 0 result
    
    ; Chunk 1 (with carry from chunk 0)
    MOV EAX, [num1 + 4]
    ADC EAX, [num2 + 4]         ; Add chunk 1 + CF from chunk 0
    MOV [result + 4], EAX       ; Store chunk 1 result
    
    ; Chunk 2 (with carry from chunk 1)
    MOV EAX, [num1 + 8]
    ADC EAX, [num2 + 8]         ; Add chunk 2 + CF from chunk 1
    MOV [result + 8], EAX       ; Store chunk 2 result
    
    ; Chunk 3 (most significant, with carry from chunk 2)
    MOV EAX, [num1 + 12]
    ADC EAX, [num2 + 12]        ; Add chunk 3 + CF from chunk 2
    MOV [result + 12], EAX      ; Store chunk 3 result
    
    ; If CF is set after the last ADC, we have a "carry out" 
    ; (the result doesn't fit in 128 bits)
    ; We'd need a 5th chunk to store it.

This is why ADC exists. Without it, you’d have to manually check the Carry Flag after each ADD and conditionally increment the next chunk. That would be slow and error-prone. ADC does it in hardware, in a single instruction.

The FLAGS After Addition

I mentioned that ADD and ADC update the flags. Let me show you exactly what each flag means after an addition.

; Example 1: Unsigned overflow (CF set)
MOV AL, 0xFF
ADD AL, 1            ; AL = 0x00, CF = 1 (carry out), OF = 0 (no signed overflow)
; Example 2: Signed overflow (OF set)
MOV AL, 0x7F        ; 127 (largest positive signed 8-bit)
ADD AL, 1            ; AL = 0x80 (-128), CF = 0 (no unsigned overflow), OF = 1 (signed overflow!)
; Example 3: Both CF and OF set
MOV AL, 0x80        ; -128 (signed) or 128 (unsigned)
ADD AL, 0x80        ; AL = 0x00, CF = 1 (unsigned overflow), OF = 1 (signed overflow too!)

The key takeaway: CF is for unsigned overflow, OF is for signed overflow. If you’re doing unsigned arithmetic, check CF. If you’re doing signed arithmetic, check OF.

Performance of ADD/ADC

On modern x86 CPUs, ADD and ADC have a latency of 1 cycle and a throughput of 1 per cycle (i.e., the CPU can execute one ADD per cycle, pipelined).

Actually, that’s not quite true for ADC. Due to the dependency on the flags (specifically CF), ADC has historically been slower than ADD. On Intel CPUs before Ice Lake (2019), ADC had a latency of 2-3 cycles and reduced throughput.

But modern CPUs have special handling for ADC (and SBB) to support multi-precision arithmetic efficiently. If you’re doing cryptography or arbitrary-precision arithmetic (big integers), ADC is essential.

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Share your love