Instruction Set Architectures: CISC, RISC, x86-64, ARM and RISC-V
Today the distinction is blurred: x86 cores execute micro-ops internally, and ARM has grown many instructions.
Concept
What an ISA Defines Instructions: arithmetic, logic, data movement, control flow, system instructions.
Registers: number, width and roles.
Data types and sizes.
Addressing modes: how operands are found.
Instruction encoding: the binary format.
Memory model: byte order, alignment, ordering between cores (Module 26).
Privilege levels, interrupts, exceptions (Modules 8, 12).
RISC (ARM, RISC-V, MIPS) Many, complex, some do memory + arithmetic at once Fewer, simple, uniform Variable length (x86: 1–15 bytes)
Fixed length (usually 4 bytes; compressed 2-byte forms optional)
Most instructions can use memory operands Load/store architecture: only loads/stores touch memory Fewer (x86-64: 16 GPRs)
Complex; modern x86 CPUs translate instructions into internal RISC-like micro-ops
Today the distinction is blurred: x86 cores execute micro-ops internally, and ARM has grown many instructions. ISAs now differ more in licensing, ecosystem and power efficiency.
CISC, backward compatible to 1978's 8086 ARM64 (AArch64)
Phones, Apple Silicon, cloud servers, automotive SoCs RISC, licensed by Arm
Microcontrollers, accelerators, research, growing Linux support
Open, modular (base + extensions like M, A, F, D, C, V) ARM Cortex-M (Thumb-2) 16/32-bit mixed encoding x86-64 (Intel syntax) Constant in the instruction add rax, rbx Operand in a register Direct / absolute mov eax, [0x601040] mov eax, [rbx] Address in a register Base + displacement mov eax, [rbp-8] Local variables, struct fields Base + index × scale mov eax, [rdi+rsi*4] lea rsi, [rip+msg]
Why It Matters
Porting software between x86 and ARM (servers, Apple Silicon, automotive ECUs), reading crash dumps, writing startup code for microcontrollers and understanding compiler output all require ISA knowledge.
Syntax
Two assembly syntaxes exist for x86: AT&T (movq (%rsi), %rax — source first, % registers, size suffixes; GNU default) and Intel (mov rax, QWORD PTR [rsi] — destination first; gcc -masm=intel).
Example
5.1 The same C function compiled for three ISAs Compiled with Clang -O2 for each target (directives removed):
5.2 Variable-length x86-64 encoding
/* The same C function, compiled for three instruction set architectures */
long dot3(const long *a, const long *b)
{
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
int is_negative(int x)
{
return x < 0;
}# ---- x86_64 ----
dot3:
movq (%rsi), %rax
movq 8(%rsi), %rcx
imulq (%rdi), %rax
imulq 8(%rdi), %rcx
addq %rax, %rcx
movq 16(%rsi), %rax
imulq 16(%rdi), %rax
addq %rcx, %rax
retq
is_negative:
movl %edi, %eax
shrl $31, %eax
retq
# ---- aarch64 ----
dot3:
ldp x8, x11, [x1]
ldp x9, x10, [x0]
mul x8, x8, x9
ldr x9, [x0, #16]
madd x8, x11, x10, x8
ldr x10, [x1, #16]
madd x0, x10, x9, x8
ret
is_negative:
lsr w0, w0, #31
ret
# ---- riscv64 ----
dot3:
ld a2, 0(a1)
ld a3, 8(a1)
ld a4, 0(a0)
ld a5, 8(a0)
ld a0, 16(a0)
ld a1, 16(a1)
mul a2, a2, a4
mul a3, a3, a5
add a2, a2, a3
mul a0, a1, a0
add a0, a0, a2
ret
is_negative:
srliw a0, a0, 31
ret$ gcc -O2 -fcf-protection=none -c f.c -o f.o && objdump -d f.o | grep -E '^\s+[0-9a-f]+:'
0: 8d 47 01 lea 0x1(%rdi),%eax
3: c3 ret
4: 66 66 2e 0f 1f 84 00 data16 cs nopw 0x0(%rax,%rax,1)
b: 00 00 00 00
f: 90 nop
10: 48 89 f8 mov %rdi,%rax
13: 48 0f af c6 imul %rsi,%rax
17: c3 retExpected Result
The listings show the classic differences:
x86-64 multiplies directly from memory (imulq (%rdi), %rax) — a CISC memory operand.
ARM64 and RISC-V load everything into registers first (load/store architecture). ARM64 uses ldp (load a *pair* of registers) and madd (multiply-add in one instruction); RISC-V uses only simple ld, mul, add.
Arguments arrive in rdi/rsi (x86-64), x0/x1 (ARM64) and a0/a1 (RISC-V), results return in rax, x0/w0 and a0.
is_negative is a single shift of the sign bit (by 31) on all three ISAs — no branch at all.
The objdump output shows x86-64 instructions of 1 byte (c3 = ret), 3 bytes (8d 47 01 = lea 0x1(%rdi),%eax) and 4 bytes (48 0f af c6 = imul %rsi,%rax); the long nopl is padding that aligns the next function to 16 bytes.
Common Mistakes
Mixing up AT&T and Intel operand order.
Assuming RISC means "fewer instructions in total" — RISC programs often have *more* instructions, each simpler.
Assuming ARM code runs on x86 (it needs recompilation or emulation such as Rosetta 2 or QEMU).
Forgetting that unaligned memory access may be slow or fault on some ISAs.
Best Practices
Write portable C and let the compiler target each ISA; use intrinsics or assembly only in measured hot spots.
Cross-compile early for every target you ship (CI with aarch64 and riscv64 builds).
Use Compiler Explorer (godbolt.org) to compare code generation quickly.
Interview Relevance
High: "CISC vs RISC?", "what is a load/store architecture?", "name x86 addressing modes", "why is RISC-V important?", "what are micro-ops?".
Practice Questions
Compile a loop summing an array for x86-64 and ARM64 and count the instructions in the loop body.
Which addressing mode would you use for a[i] where a is an int*?
Look up the RISC-V base integer instruction count (RV32I) and compare it with x86.
Find two instructions in the listings that exist on ARM64 but have no single-instruction equivalent on RISC-V.
Quick Quiz
Q1. (Multiple choice) Which ISA uses variable-length instructions? A) RISC-V base B) ARM64 C) x86-64 D) MIPS
Answer: C (verified: 1-, 3- and 4-byte instructions in the dump).
Q2. (Predict the output) On ARM64, which register holds the first integer argument?
Answer: x0 (or w0 for 32 bits) — see is_negative (verified).
Q3. (True/False) In a load/store architecture, add can take one operand directly from memory.
Answer: False. Only load and store instructions access memory (verified in the RISC-V listing).
Q4. (Find the bug) movq %rax, %rbx was read as "copy rbx into rax".
Answer: In AT&T syntax the source comes first: it copies rax into rbx.
Coding Challenge
Write int popcount(unsigned x) in portable C (Kernighan's trick: x &= x - 1 clears the lowest set bit) and test it; then compare with __builtin_popcount, which compiles to a single popcnt instruction when the CPU supports it.
#include <stdio.h>
static int popcount(unsigned x)
{
int n = 0;
while (x) { x &= x - 1; n++; }
return n;
}
int main(void)
{
unsigned values[] = {0, 1, 0xFF, 0x18FEF100, 0xFFFFFFFF};
for (int i = 0; i < 5; i++)
printf("0x%08X: %2d bits set (builtin %2d)\n", values[i], popcount(values[i]), __builtin_popcount(values[i]));
return 0;
}