A Guide to x64 Arithmetic Instructions
This guide is a deep dive into the arithmetic capabilities of modern x86_64 processors, covering integer operations at the assembly level.
Table of Contents
- Table of Contents
- Assembler Setup
- ADD Instruction
- SUB Instruction
- MUL Instruction
- IMUL Instruction
- DIV Instruction
- IDIV Instruction
- Sample Putting It All Together
- Appendix: 8-bit, 16-bit, and 32-bit Arithmetic Variants
Assembler Setup
.intel_syntax noprefix: This directive specifies Intel syntax without register prefixes.- 64-bit code: Use
as --64for 64-bit mode. When linking, ensure you link for 64-bit binaries as well.
Below is a skeleton for our examples:
.intel_syntax noprefix # Use Intel syntax, no % prefix for registers
.data
val1: .quad 10
val2: .quad 20
.text # Place code in the .text section
.global _start # Define entry point for the linker
_start:
# Your assembly code goes here
# Example exit routine:
mov rax, 60 # 60 is the sys_exit syscall number on x86_64 Linux
xor rdi, rdi # Set exit code to 0
syscallExample commands to assemble and link:
as --64 -o myfile.o myfile.asm
ld -o myprog myfile.o
./myprogADD Instruction
The add instruction performs an addition of the second operand to the first operand. The result is stored in the first operand. Valid permutations include register-to-register, register-to-immediate, memory-to-register, and register-to-memory.
Example 1: Register, Register
add rax, rbx # rax = rax + rbxExample 2: Register, Immediate
add rbx, 0x10 # rbx = rbx + 0x10Example 3: Memory, Register
- Uses the 64-bit value at memory location rax.
add [val1], rbx # [val1] = [val2] + rbxExample 4: Register, Memory (Complex Addressing)
add rax, [rdx + rbx*8 + 32] # rax = rax + [rdx + rbx*8 + 32]SUB Instruction
The sub instruction performs a subtraction of the second operand from the first operand, storing the result in the first operand. Valid permutations include register-to-register, register-to-immediate, memory-to-register, and register-to-memory.
Example 1: Register, Register
sub rcx, rdx # rcx = rcx - rdxExample 2: Register, Immediate
sub rax, 0x100 # rax = rax - 0x100Example 3: Memory, Register
sub [val1], rcx # [val1] = [val1] - rcxExample 4: Register, Memory (Complex Addressing)
sub rbx, [rax + rdi*4] # rbx = rbx - [rax + rdi*4]MUL Instruction
The mul instruction is the unsigned multiplication instruction in x64. It uses implicit operands:
raxis multiplied by the specified operand, and the full 128-bit product is placed inrdx:rax.
Example 1: Unsigned Multiply Register
mov rax, 10
mov rbx, 20
mul rbx # rax:rdx = rax * rbx (unsigned)
# rax = 200, rdx = 0Example 2: Unsigned Multiply Memory
mov rax, 10
mul qword ptr [val1] # 64-bit operand from memory
# rax:rdx = rax * [val1]IMUL Instruction
The imul instruction is the signed multiplication instruction. It has multiple variants: one operand (like mul), two-operand, or three-operand forms.
Example 1: One Operand (Signed)
mov rax, -5
mov rdi, 3
imul rdi # rax:rdx = rax * rdi (signed)
# rax = -15, high part in rdxExample 2: Two Operands (Destination, Source)
mov rbx, -4
mov rax, 6
imul rbx, rax # rbx = rbx * rax (signed)
# rbx = -24Example 3: Three Operands
mov rax, -3
imul rbx, rax, 12 # rbx = rax * 12 (signed), with an immediate
# rbx = -36DIV Instruction
The div instruction is the unsigned division, taking the 128-bit dividend from rdx:rax and dividing by the specified operand. The quotient is placed in rax, and the remainder goes to rdx.
mov rax, 100
xor rdx, rdx # zero-extend for unsigned
mov rbx, 10
div rbx # rax = 10, rdx = 0
# (rdx:rax) / rbx -> rax (quotient), rdx (remainder)IDIV Instruction
The idiv instruction is the signed division, also reading a 128-bit dividend from rdx:rax. The quotient goes to rax, and the remainder goes to rdx.
mov rax, -64
cqo # sign-extend rax into rdx:rax
mov rsi, 8
idiv rsi # rax = quotient, rdx = remainder
# (rdx:rax) / rsi -> rax (quotient), rdx (remainder)Note:
cqo(convert quadword to octaword) sign-extendsraxintordx:rax. This is needed before signed division to properly extend the sign bit.
Sample Putting It All Together
Below is a small snippet demonstrating many of these instructions in sequence. You can name this file arithmetic.asm, assemble, link, and run it on a typical Linux system:
as --64 -o arithmetic.o arithmetic.asm
ld -o arithmetic arithmetic.o
./arithmetic.intel_syntax noprefix
.text
.global _start
_start:
# 1. Demonstrate ADD
mov rax, 5
mov rbx, 10
add rax, rbx # rax = 15
# 2. Demonstrate SUB
sub rax, 2 # rax = 13
# 3. Demonstrate MUL (unsigned)
# rax:rdx = rax * rcx
mov rcx, 3
mul rcx # rax = 39, rdx = 0
# 4. Demonstrate IMUL (signed, 3-operand)
# rbx = rax * 4
imul rbx, rax, 4 # rbx = 156
# 5. Demonstrate DIV (unsigned)
# (rdx:rax) / rbx -> rax
xor rdx, rdx # Clear rdx
mov rax, 200
mov rbx, 10
div rbx # rax = 20, rdx = 0
# 6. Demonstrate IDIV (signed)
# (rdx:rax) / rbx -> rax
mov rax, -100
cqo # sign extend
mov rbx, 7
idiv rbx # rax = -14, rdx = -2
# Exit
mov rax, 60 # sys_exit
xor rdi, rdi # exit code 0
syscallAppendix: 8-bit, 16-bit, and 32-bit Arithmetic Variants
While x86‐64 primarily uses 64-bit registers (rax, rbx, etc.), you can also work with smaller sizes: 8-bit (AL, BL, etc.), 16-bit (AX, BX, etc.), and 32-bit (EAX, EBX, etc.). The operations work similarly but use smaller registers and, in some cases, different implicit registers for multiplication and division. Below is a brief overview:
8-bit Arithmetic
- Registers: AL, BL, CL, DL, etc.
- add/sub:
add al, bl ; AL = AL + BL
sub al, 0x5 ; AL = AL - 5- mul:
- Uses AL implicitly. The product is placed into AX (
AH:AL).
- Uses AL implicitly. The product is placed into AX (
; Unsigned multiply: AX = AL * BL
mul bl- imul:
- For the one-operand version, AX = AL * r8 (signed).
- div:
- 16-bit dividend in AX is divided by r8. The quotient is in AL, remainder in AH.
; AX / BL -> AL (quotient), AH (remainder)
div bl- idiv:
- Same concept, but signed.
16-bit Arithmetic
- Registers: AX, BX, CX, DX, etc.
- add/sub:
add ax, bx ; AX = AX + BX
sub ax, 0x10 ; AX = AX - 0x10- mul:
- Uses AX as the implicit operand. The product goes into DX:AX.
; Unsigned multiply: DX:AX = AX * BX
mul bx- imul:
- For the one-operand version, DX:AX = AX * r16 (signed).
- div:
- 32-bit dividend in DX:AX is divided by r16. The quotient goes to AX, remainder to DX.
; DX:AX / BX -> AX, remainder DX
div bx- idiv:
- Signed variant; sign-extend AX into DX:AX with
cwd(convert word to doubleword) before dividing.
- Signed variant; sign-extend AX into DX:AX with
32-bit Arithmetic
- Registers: EAX, EBX, ECX, EDX, etc.
- add/sub:
add eax, ebx ; EAX = EAX + EBX
sub eax, 0x100 ; EAX = EAX - 0x100- mul:
- Uses EAX as the implicit operand. The product goes into EDX:EAX.
; Unsigned multiply: EDX:EAX = EAX * EBX
mul ebx- imul:
- For the one-operand version, EDX:EAX = EAX * r32 (signed).
- div:
- 64-bit dividend in EDX:EAX is divided by r32. The quotient goes to EAX, remainder to EDX.
; EDX:EAX / EBX -> EAX, remainder EDX
div ebx- idiv:
- Signed variant; sign-extend EAX into EDX:EAX with
cdq(convert doubleword to quadword) before dividing.
- Signed variant; sign-extend EAX into EDX:EAX with
In each case, the “smaller” variants of the instructions behave the same way conceptually as their 64-bit counterparts, just with different registers and, for multiplication/division, different implicit register pairs. By mixing and matching these sizes as needed, you have fine-grained control over your arithmetic operations in x86‐64 assembly.