Beginner

Operators in Assembly

Learn arithmetic, bitwise, and comparison operators in x86 Assembly using NASM, with hands-on Docker-ready examples

In high-level languages, operators like +, *, or && look like a single thing. In x86 assembly, every operator is an explicit CPU instruction acting on registers or memory, and most of them quietly update the processor’s flags register as a side effect. Those flags — zero, carry, sign, overflow — are what later conditional jumps read to decide where to go next.

Because assembly is untyped, the same value can be interpreted as a signed integer, an unsigned integer, an address, or a bit pattern depending on which instruction you choose. MUL and IMUL use the same bits but treat them differently; JL and JB both branch on “less than” but read different flags. Picking the right instruction is the operator.

This tutorial walks through three families of operators using x86 (32-bit) NASM syntax and Linux syscalls: arithmetic (ADD, SUB, MUL, DIV), bitwise and shifts (AND, OR, XOR, NOT, SHL, SHR), and comparison plus conditional jumps (CMP with JE/JNE/JG/JL). Each example keeps results small enough to print as a single ASCII digit, so we can focus on the operators rather than number formatting.

Arithmetic Operators

Arithmetic in x86 lives in instructions, not infix syntax. ADD dst, src performs dst = dst + src. MUL is one-operand and implicitly uses AL/AX/EAX as the other factor and destination. DIV is even more particular: the dividend lives in AX (for byte division), the divisor is the explicit operand, and the quotient lands in AL with the remainder in AH.

Create a file named arithmetic.asm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
section .data
    line1 db "5 + 3 = X", 10
    line1len equ $ - line1
    line2 db "9 - 4 = X", 10
    line2len equ $ - line2
    line3 db "3 * 2 = X", 10
    line3len equ $ - line3
    line4 db "8 / 3 = X r Y", 10
    line4len equ $ - line4

section .text
    global _start

_start:
    ; Addition: AL = 5 + 3
    mov al, 5
    add al, 3
    add al, '0'              ; convert digit to ASCII
    mov [line1 + 8], al

    ; Subtraction: AL = 9 - 4
    mov al, 9
    sub al, 4
    add al, '0'
    mov [line2 + 8], al

    ; Multiplication: AX = AL * BL (unsigned)
    mov al, 3
    mov bl, 2
    mul bl                   ; AX = 3 * 2 = 6
    add al, '0'
    mov [line3 + 8], al

    ; Division: AL = AX / BL quotient, AH = remainder
    mov ax, 8
    mov bl, 3
    div bl                   ; AL=2, AH=2
    add ah, '0'              ; remainder first (still in AH)
    mov [line4 + 12], ah
    add al, '0'
    mov [line4 + 8], al

    ; write each line to stdout
    mov eax, 4
    mov ebx, 1
    mov ecx, line1
    mov edx, line1len
    int 0x80

    mov eax, 4
    mov ebx, 1
    mov ecx, line2
    mov edx, line2len
    int 0x80

    mov eax, 4
    mov ebx, 1
    mov ecx, line3
    mov edx, line3len
    int 0x80

    mov eax, 4
    mov ebx, 1
    mov ecx, line4
    mov edx, line4len
    int 0x80

    mov eax, 1
    xor ebx, ebx
    int 0x80

A few things to notice:

  • ADD, SUB, and friends are two-operand: add al, 3 means al = al + 3. The first operand is both source and destination.
  • MUL bl is one-operand. The other factor is implicitly AL, and the 16-bit product lands in AX (high byte in AH, low byte in AL).
  • DIV bl divides the implicit dividend in AX by BL, leaving the quotient in AL and the remainder in AH. There is no single “modulo” instruction — you get the remainder for free from DIV.
  • Adding '0' (ASCII 48) to a single-digit value is the smallest possible “integer to string” conversion.

Bitwise and Shift Operators

Below arithmetic sits the layer assembly was made for: manipulating individual bits. AND, OR, XOR, and NOT operate bitwise across all bits of the destination. Shifts (SHL, SHR) move bits left or right and are the assembly-level equivalent of multiplying or dividing by powers of two.

Create a file named bitwise.asm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
section .data
    a1 db "5 AND 3 = X", 10
    a1len equ $ - a1
    a2 db "5 OR  3 = X", 10
    a2len equ $ - a2
    a3 db "5 XOR 3 = X", 10
    a3len equ $ - a3
    a4 db "1 << 3  = X", 10
    a4len equ $ - a4
    a5 db "8 >> 2  = X", 10
    a5len equ $ - a5

section .text
    global _start

_start:
    ; 5 AND 3 = 0101 & 0011 = 0001 = 1
    mov al, 5
    and al, 3
    add al, '0'
    mov [a1 + 10], al

    ; 5 OR 3 = 0101 | 0011 = 0111 = 7
    mov al, 5
    or  al, 3
    add al, '0'
    mov [a2 + 10], al

    ; 5 XOR 3 = 0101 ^ 0011 = 0110 = 6
    mov al, 5
    xor al, 3
    add al, '0'
    mov [a3 + 10], al

    ; 1 << 3 = 8  (logical shift left by 3)
    mov al, 1
    shl al, 3
    add al, '0'
    mov [a4 + 10], al

    ; 8 >> 2 = 2  (logical shift right by 2)
    mov al, 8
    shr al, 2
    add al, '0'
    mov [a5 + 10], al

    ; print all five lines
    mov eax, 4
    mov ebx, 1
    mov ecx, a1
    mov edx, a1len
    int 0x80

    mov eax, 4
    mov ebx, 1
    mov ecx, a2
    mov edx, a2len
    int 0x80

    mov eax, 4
    mov ebx, 1
    mov ecx, a3
    mov edx, a3len
    int 0x80

    mov eax, 4
    mov ebx, 1
    mov ecx, a4
    mov edx, a4len
    int 0x80

    mov eax, 4
    mov ebx, 1
    mov ecx, a5
    mov edx, a5len
    int 0x80

    mov eax, 1
    xor ebx, ebx
    int 0x80

XOR is worth special attention. The idiom xor reg, reg zeros a register more compactly than mov reg, 0 — the Hello World example already used xor ebx, ebx for exactly this reason. SHL by n is identical to multiplication by 2^n for unsigned values, and SHR is the unsigned counterpart of integer division by a power of two; SAR (arithmetic shift right) preserves the sign bit when you need signed semantics.

Comparison and Conditional Jumps

Comparison in assembly is a two-step process: a CMP instruction sets flags, and a conditional jump reads those flags. CMP a, b does the same arithmetic as SUB a, b but throws away the result — it only keeps the flags. The flags then drive a family of jump instructions:

MnemonicBranch whenReads flag(s)
JE / JZequal / zeroZF=1
JNE / JNZnot equalZF=0
JGgreater (signed)ZF=0 and SF=OF
JLless (signed)SF≠OF
JAabove (unsigned)CF=0 and ZF=0
JBbelow (unsigned)CF=1

Create a file named compare.asm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
section .data
    msg_eq  db "7 == 7  -> equal", 10
    eqlen   equ $ - msg_eq
    msg_gt  db "9 >  4  -> greater", 10
    gtlen   equ $ - msg_gt
    msg_lt  db "2 <  5  -> less", 10
    ltlen   equ $ - msg_lt
    msg_no  db "branch not taken", 10
    nolen   equ $ - msg_no

section .text
    global _start

_start:
    ; --- equality test ---
    mov eax, 7
    cmp eax, 7
    jne not_equal           ; skip if not equal
    mov eax, 4
    mov ebx, 1
    mov ecx, msg_eq
    mov edx, eqlen
    int 0x80
    jmp gt_test
not_equal:
    mov eax, 4
    mov ebx, 1
    mov ecx, msg_no
    mov edx, nolen
    int 0x80

gt_test:
    ; --- signed greater-than ---
    mov eax, 9
    cmp eax, 4
    jle not_greater         ; jump if 9 <= 4 (it isn't)
    mov eax, 4
    mov ebx, 1
    mov ecx, msg_gt
    mov edx, gtlen
    int 0x80
    jmp lt_test
not_greater:
    mov eax, 4
    mov ebx, 1
    mov ecx, msg_no
    mov edx, nolen
    int 0x80

lt_test:
    ; --- signed less-than ---
    mov eax, 2
    cmp eax, 5
    jge not_less            ; jump if 2 >= 5 (it isn't)
    mov eax, 4
    mov ebx, 1
    mov ecx, msg_lt
    mov edx, ltlen
    int 0x80
    jmp done
not_less:
    mov eax, 4
    mov ebx, 1
    mov ecx, msg_no
    mov edx, nolen
    int 0x80

done:
    mov eax, 1
    xor ebx, ebx
    int 0x80

Notice the inverted logic: we test JLE to skip the “greater than” message, because falling through means the comparison held. This pattern — “test the negation, jump past the body” — is how if blocks are typically lowered to assembly by compilers.

Running with Docker

Each .asm file is assembled, linked, and executed in one step by the image:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the NASM assembly image
docker pull esolang/x86asm-nasm:latest

# Run the arithmetic example
docker run --rm -v $(pwd):/code -w /code esolang/x86asm-nasm x86asm-nasm arithmetic.asm

# Run the bitwise example
docker run --rm -v $(pwd):/code -w /code esolang/x86asm-nasm x86asm-nasm bitwise.asm

# Run the comparison example
docker run --rm -v $(pwd):/code -w /code esolang/x86asm-nasm x86asm-nasm compare.asm

Expected Output

Running arithmetic.asm:

5 + 3 = 8
9 - 4 = 5
3 * 2 = 6
8 / 3 = 2 r 2

Running bitwise.asm:

5 AND 3 = 1
5 OR  3 = 7
5 XOR 3 = 6
1 << 3  = 8
8 >> 2  = 2

Running compare.asm:

7 == 7  -> equal
9 >  4  -> greater
2 <  5  -> less

Key Concepts

  • Operators are instructions. Every arithmetic, bitwise, or comparison “operator” is a named CPU instruction (ADD, XOR, CMP, …) that consumes register or memory operands rather than infix syntax.
  • Two-operand form. Most instructions are op dst, src, where the destination is also the first source: add eax, ebx means eax = eax + ebx.
  • MUL and DIV are special. They use implicit AL/AX/EAX operands and produce wider results, leaving the remainder in AH for byte division — there is no separate modulo instruction.
  • Signed vs unsigned is per-instruction. Use MUL/DIV and JA/JB for unsigned values, IMUL/IDIV and JG/JL for signed. The bits are identical; the interpretation comes from which mnemonic you choose.
  • Flags are the bridge to control flow. CMP a, b updates ZF, SF, CF, and OF without storing a result; the following conditional jump reads those flags. This is how if becomes assembly.
  • xor reg, reg is the idiomatic zero. It’s shorter than mov reg, 0 and is recognized by the CPU as a register-clearing pattern.
  • Shifts are powers of two. SHL x, n is x * 2^n and SHR x, n is unsigned x / 2^n — much faster than MUL/DIV when you can use them.

Running Today

All examples can be run using Docker:

docker pull esolang/x86asm-nasm:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining