Assembly Language - Arithmetic Instructions
Arithmetic instructions are the foundation of CPU mathematical operations. This chapter details addition, subtraction, multiplication, division, and related carry-operation instructions in the x86 architecture.
ADD - Addition Instruction
ADDAdds the source operand to the destination operand and stores the result in the destination operand.
Example
; ADD instruction example: calculate 100 + 200 + 300
section .data
a dd 100
b dd 200
c dd 300
sum dd 0
section .text
global _start
_start:
; Method 1: register += immediate
mov eax, [a] ; eax = 100
add eax, 200 ; eax = 100 + 200 = 300
; Method 2: register += register
mov ebx, [b] ; ebx = 200
add eax, ebx ; eax = 300 + 200 = 500
; Method 3: register += memory
add eax, [c] ; eax = 500 + 300 = 800
; Store the result
mov [sum], eax ; sum = 800
; Effect of addition on flags
mov eax, 0xFFFFFFFF ; eax = largest 32-bit unsigned number
add eax, 1 ; eax = 0 (overflow wraps around)
; CF = 1 (carry generated)
; ZF = 1 (result is 0)
; OF = 0 (no overflow from signed perspective)
mov eax, 1
mov ebx, 0
int 0x80
SUB - Subtraction Instruction
SUBSubtracts the source operand from the destination operand and stores the result in the destination operand.
Example
; SUB instruction example
section .data
x dd 1000
y dd 300
section .text
global _start
_start:
; Basic subtraction
mov eax, [x] ; eax = 1000
sub eax, [y] ; eax = 1000 - 300 = 700
; Effect of subtraction on flags
mov eax, 10
sub eax, 20 ; eax = -10 (i.e., 0xFFFFFFF6)
; CF = 1 (borrow generated: 10 < 20)
; SF = 1 (result is negative)
; ZF = 0 (result is non-zero)
; OF = 0 (no signed overflow)
; Subtract itself: often used to zero out
mov eax, 12345
sub eax, eax ; eax = 0
; ZF = 1, CF = 0
; This is a classic way to zero a register (more efficient than mov eax, 0)
mov eax, 1
mov ebx, 0
int 0x80
sub eax, eaxis a classic technique to zero a register; it occupies only 2 bytes, whilemov eax, 0occupies 5 bytes. It is commonly used in scenarios requiring extreme size optimization (such as shellcode).
INC / DEC - Increment/Decrement Instructions
Add-1 and subtract-1 instructions more concise than ADD/SUB:
Example
section .data
counter dd 0
section .text
global _start
_start:
mov dword [counter], 0 ; counter = 0
inc dword [counter] ; counter = 1 (memory operand)
inc dword [counter] ; counter = 2
mov ecx, 10
dec ecx ; ecx = 9
dec ecx ; ecx = 8
; INC/DEC do not affect the CF flag (this is an important difference from ADD/SUB)
; But they affect ZF, SF, OF, PF
; Using INC in a loop
mov ecx, 5
mov eax, 0
loop_inc:
inc eax ; eax increments by 1 each time
loop loop_inc ; Repeat 5 times, eax finally = 5
mov ebx, eax ; Return value = 5
mov eax, 1
int 0x80
MUL - Unsigned Multiplication
MULPerforms unsigned multiplication. The rules for multiplication are somewhat special:
| Operand size | Multiplier | Multiplicand (implicit) | Result storage |
|---|---|---|---|
| 1 byte | Any 8-bit register or memory | AL | AX = AL × operand |
| 2 bytes | Any 16-bit register or memory | AX | DX:AX = AX × operand |
| 4 bytes | Any 32-bit register or memory | EAX | EDX:EAX = EAX × operand |
Example
; MUL unsigned multiplication example
section .data
val1 dd 1000
val2 dd 2000
result_low dd 0
result_high dd 0
section .text
global _start
_start:
; 32-bit multiplication: EDX:EAX = EAX × operand
mov eax, [val1] ; eax = 1000
mul dword [val2] ; edx:eax = 1000 × 2000 = 2,000,000
; Result = 2,000,000 = 0x001E8480
; EAX = 0x001E8480 (low 32 bits)
; EDX = 0x00000000 (high 32 bits, because the result did not exceed 32 bits)
; Large number multiplication demo (result exceeds 32 bits)
mov eax, 0xFFFFFFFF ; eax = 4,294,967,295
mov ebx, 2 ; ebx = 2
mul ebx ; edx:eax = 0xFFFFFFFF × 2
; EAX = 0xFFFFFFFE
; EDX = 0x00000001 (high 32 bits)
; CF = 1 (result exceeds 32 bits)
; Save result
mov [result_low], eax
mov [result_high], edx
mov eax, 1
mov ebx, 0
int 0x80
IMUL - Signed Multiplication
IMULUsed for multiplication of signed numbers, with three forms:
Example
; IMUL signed multiplication example
section .data
a dd -100
b dd 3
section .text
global _start
_start:
; Single-operand form: same as MUL
mov eax, [a] ; eax = -100
imul dword [b] ; edx:eax = -100 × 3 = -300
; EAX = -300(0xFFFFFED4)
; EDX = 0xFFFFFFFF (sign extension)
; Two-operand form: reg = reg × operand
mov ebx, [a] ; ebx = -100
imul ebx, [b] ; ebx = -100 × 3 = -300
; The result must fit in 32 bits
; Three-operand form: reg = operand1 × immediate
imul ecx, [a], 5 ; ecx = -100 × 5 = -500
mov eax, 1
mov ebx, 0
int 0x80
MULandIMULThe main difference is the handling of signs:MULInterpreted as unsigned numbers,IMULInterpreted as signed numbers. For example, 0xFFFFFFFF is 4294967295 in MUL, and -1 in IMUL.
DIV - Unsigned Division
DIVPerforms unsigned division, rules are symmetric to MUL:
| Divisor size | Dividend (implicit) | Shang | Remainder |
|---|---|---|---|
| 1 byte | AX | AL | AH |
| 2 bytes | DX:AX | AX | DX |
| 4 bytes | EDX:EAX | EAX | EDX |
Example
; DIV unsigned division example
section .data
dividend dd 1000
divisor dd 7
quotient dd 0
remainder dd 0
section .text
global _start
_start:
; 32-bit division: edx:eax / divisor
; The dividend must first be extended to EDX:EAX
mov eax, [dividend] ; eax = 1000
mov edx, 0 ; edx = 0 (clear high 32 bits)
div dword [divisor] ; edx:eax / 7
; EAX = 142 (quotient)
; EDX = 6 (remainder: 1000 = 142×7 + 6)
mov [quotient], eax ; quotient = 142
mov [remainder], edx ; remainder = 6
; Byte division example: 55 / 4
mov ax, 55 ; Dividend
mov bl, 4 ; Divisor
div bl ; al = 13 (quotient), ah = 3 (remainder)
mov eax, 1
mov ebx, 0
int 0x80
Before doing division, be sure to use
mov edx, 0orxor edx, edxClear EDX! If EDX has stale data, the dividend will be wrong. This is the most common division bug for beginners.
IDIV - Signed Division
IDIVUsed for signed division. Before doing division, you need to useCDQinstruction to sign-extend EAX into EDX:EAX:
Example
; Calculate -100 / 3
mov eax, -100 ; eax = -100
cdq ; Sign extension: edx:eax = -100
; cdq copies the sign bit of eax to all bits of edx
mov ebx, 3 ; Divisor
idiv ebx ; eax = -33 (quotient), edx = -1 (remainder)
; Verify: -100 = -33 × 3 + (-1)
ADC / SBB - Operations with Carry/Borrow
Used forlarge-number arithmetic(data exceeding 32 bits):
Example
; 64-bit addition: adding two 64-bit numbers
section .data
; 64-bit number a = 0x00000001 FFFFFFFF
a_low dd 0xFFFFFFFF
a_high dd 0x00000001
; 64-bit number b = 0x00000000 00000005
b_low dd 0x00000005
b_high dd 0x00000000
; Result
result_low dd 0
result_high dd 0
section .text
global _start
_start:
; Add low 32 bits
mov eax, [a_low]
add eax, [b_low] ; eax = 0xFFFFFFFF + 5 = 0x00000004
; CF = 1 (carry occurred!)
mov [result_low], eax ; result_low = 0x00000004
; Add high 32 bits with carry
mov eax, [a_high]
adc eax, [b_high] ; adc = add + CF
; eax = 1 + 0 + 1(carry) = 2
mov [result_high], eax ; result_high = 2
; Final 64-bit result: 0x00000002 00000004
; Verify: 0x1FFFFFFF + 5 = 0x200000004
; SBB subtraction is similar (with borrow)
mov eax, [a_low]
sub eax, [b_low] ; Subtract the low 32 bits
mov [result_low], eax
mov eax, [a_high]
sbb eax, [b_high] ; sbb = sub - CF
mov eax, 1
mov ebx, 0
int 0x80
Arithmetic Instruction Quick Reference Table
| Instruction | Format | Function | Affected flags |
|---|---|---|---|
| ADD | add dest, src | dest = dest + src | CF, ZF, SF, OF, PF |
| SUB | sub dest, src | dest = dest - src | CF, ZF, SF, OF, PF |
| INC | inc dest | dest = dest + 1 | ZF, SF, OF, PF (does not affect CF) |
| DEC | dec dest | dest = dest - 1 | ZF, SF, OF, PF (does not affect CF) |
| MUL | mul src | Unsigned multiplication | CF, OF |
| IMUL | imul src | Signed multiplication | CF, OF |
| DIV | div src | Unsigned division | Undefined (indeterminate) |
| IDIV | idiv src | Signed division | Undefined (indeterminate) |
| ADC | adc dest, src | dest = dest + src + CF | CF, ZF, SF, OF, PF |
| SBB | sbb dest, src | dest = dest - src - CF | CF, ZF, SF, OF, PF |
| NEG | neg dest | dest = -dest (negate) | CF, ZF, SF, OF, PF |