Skip to content

11 — Bitwise Operations

Bitwise operations are fundamental to assembly programming. You use them to manipulate individual bits, pack/unpack data, implement fast arithmetic, and work with hardware registers and flags.


The Core Operations

AND — Clear Bits (Masking)

and dst, src    ; dst = dst & src

Use AND to extract or clear specific bits:

and rax, 0x0F    ; keep only the low nibble (bits 0-3)
and rax, 0xFF    ; keep only the low byte
and rax, ~1      ; clear bit 0 (round down to even)

Truth table for a single bit:

A  B  A & B
0  0    0
0  1    0
1  0    0
1  1    1     ← only set if BOTH are 1

OR — Set Bits

or dst, src    ; dst = dst | src

Use OR to set specific bits:

or rax, 0x01    ; set bit 0
or rax, 0x20    ; set bit 5 (converts uppercase ASCII to lowercase)
or rax, rbx     ; combine flags

Truth table:

A  B  A | B
0  0    0
0  1    1
1  0    1
1  1    1     ← set if EITHER is 1

XOR — Toggle Bits

xor dst, src    ; dst = dst ^ src

Use XOR to toggle bits, or to zero a register:

xor rax, rax     ; rax = 0 (idiom: faster than mov rax, 0)
xor rax, 0xFF    ; toggle the low byte
xor rax, rbx     ; toggle bits where rbx has 1s

Truth table:

A  B  A ^ B
0  0    0
0  1    1
1  0    1
1  1    0     ← set only if inputs DIFFER

NOT — Complement

not rax    ; flip all 64 bits
0b11001010  →  0b00110101

Shift Instructions

SHL — Shift Left Logical (multiply by 2ⁿ)

shl rax, 1     ; rax *= 2
shl rax, 3     ; rax *= 8
shl rax, cl    ; shift by value in CL

Bits shifted out go into CF. Zeros fill from the right.

SHR — Shift Right Logical (unsigned divide by 2ⁿ)

shr rax, 1     ; rax /= 2  (unsigned)
shr rax, 4     ; rax /= 16 (unsigned)

Zeros fill from the left. The original sign bit is NOT preserved.

SAR — Shift Right Arithmetic (signed divide by 2ⁿ)

sar rax, 1    ; rax /= 2  (signed, preserves sign bit)
sar rax, 3    ; rax /= 8  (signed)

The sign bit (MSB) is replicated — negative numbers stay negative.

SHR:  1000_0000  >>1  →  0100_0000   (sign bit lost)
SAR:  1000_0000  >>1  →  1100_0000   (sign bit preserved)

ROL / ROR — Rotate Left / Right

rol rax, 1    ; rotate left:  MSB wraps to LSB
ror rax, 8    ; rotate right by 8 bits

No bits are lost in a rotation — they wrap around.

Shift Amount

The shift amount can be: - An immediate (compile-time constant) - The CL register (low byte of RCX) for dynamic shifts

shl rax, 3     ; immediate: shift left 3
mov cl, 5
shl rax, cl    ; dynamic: shift by value in CL

Bit Manipulation Idioms

Test a Specific Bit

test rax, (1 << 7)    ; is bit 7 set?
jnz  bit_is_set       ; ZF=0 means the bit was 1

Set a Specific Bit

or  rax, (1 << 5)    ; set bit 5

Clear a Specific Bit

and rax, ~(1 << 5)   ; clear bit 5
; NASM: and rax, 0xFFFFFFFFFFFFFFDF

Toggle a Specific Bit

xor rax, (1 << 3)    ; toggle bit 3

Extract a Bit Field

To extract bits [hi:lo] (a range of bits):

; Extract bits 11:8 from RAX (4 bits)
mov rbx, rax
shr rbx, 8          ; shift right to bring target bits to position 0
and rbx, 0x0F       ; mask to keep only 4 bits

Count Trailing Zeros (BSF)

bsf rax, rbx    ; rax = index of lowest set bit in rbx
                ; (undefined if rbx = 0)

Count Leading Zeros (BSR)

bsr rax, rbx    ; rax = index of highest set bit in rbx
                ; (undefined if rbx = 0)

POPCNT — Population Count (count 1-bits)

popcnt rax, rbx    ; rax = number of 1-bits in rbx

LZCNT / TZCNT (BMI1)

lzcnt rax, rbx    ; count leading zeros (defined even for rbx=0)
tzcnt rax, rbx    ; count trailing zeros

Fast Arithmetic Using Shifts

Multiplying and dividing by powers of 2 with shifts is faster than MUL/DIV:

; Multiply by 2
shl rax, 1      ; rax *= 2

; Multiply by 8
shl rax, 3      ; rax *= 8

; Multiply by 5 (using LEA)
lea rax, [rax + rax*4]    ; rax = rax + rax*4 = rax*5

; Multiply by 9
lea rax, [rax + rax*8]    ; rax*9

; Divide by 4 (unsigned)
shr rax, 2

; Divide by 4 (signed)
sar rax, 2

Packing and Unpacking Data

Assembly often works with packed data (multiple values in one register).

Pack Two Bytes into One Word

; Pack: high byte = al, low byte = bl → ax
mov ah, al     ; ah = al
mov al, bl     ; al = bl
; ax now contains both values

Pack with Shifts

; Pack two 32-bit values into a 64-bit register
; rax = (high << 32) | low
mov  eax, dword [low_val]    ; eax = low (zero-extends to rax)
mov  rbx, qword [high_val]
shl  rbx, 32
or   rax, rbx                ; rax = high:low

Unpack

; Unpack: high 32 bits of rax into rbx
mov  rbx, rax
shr  rbx, 32                 ; rbx = high 32 bits
and  eax, 0xFFFFFFFF         ; eax = low 32 bits (or use movzx)

XOR Swap (Without Temporary Register)

; Swap rax and rbx without a temp register
xor rax, rbx    ; rax = rax ^ rbx
xor rbx, rax    ; rbx = (rax^rbx) ^ rbx = rax
xor rax, rbx    ; rax = (rax^rbx) ^ rax  = rbx

Curiosity, not recommended — use xchg or a temp register in practice.


Complete Example: Byte Manipulation

; bits.asm — various bit manipulation examples
section .text
global _start

_start:
    mov rax, 0xDEADBEEF12345678

    ; Extract byte 3 (0-indexed from right): value = 0x56
    mov rbx, rax
    shr rbx, 24              ; shift byte 3 to position 0
    and rbx, 0xFF            ; rbx = 0x56

    ; Set bit 0 of rax
    or  rax, 1

    ; Clear bits 7:4 (low nibble of second byte)
    and rax, ~(0xF0)         ; clear bits 7:4

    ; Toggle bit 31
    xor rax, (1 << 31)

    ; Count set bits in rbx
    popcnt rcx, rbx

    ; Is rax even? (bit 0 == 0?)
    test rax, 1
    jnz  .odd
    ; rax is even
.odd:

    mov rax, 60
    xor rdi, rdi
    syscall

Key Takeaways

Goal Instruction Pattern
Zero a register xor rax, rax
Test if zero test rax, rax
Test bit N test rax, (1 << N)
Set bit N or rax, (1 << N)
Clear bit N and rax, ~(1 << N)
Toggle bit N xor rax, (1 << N)
Extract low N bits and rax, (1 << N) - 1
Multiply by 2ⁿ shl rax, N
Unsigned divide by 2ⁿ shr rax, N
Signed divide by 2ⁿ sar rax, N
Count set bits popcnt rax, rbx

Next: 12 — Arrays and Strings