Skip to content

How to Learn x86-64 Assembly Language for Practical Use: A Step-by-Step Guide

The Problem

I wanted to learn assembly language. But when I tried, I ran into a wall.

I opened an x86-64 assembly tutorial. It showed instructions like mov, push, pop. But I had no context. Why does the stack grow downward? What are these registers? Why do arguments go in different registers?

Then I tried reading a disassembly of a simple C program. Hundreds of instructions. I couldn’t tell what was my code and what was runtime setup.

I was trying to learn assembly without understanding the machine that runs it.

The Solution

The most effective path to learn x86-64 assembly is:

C Fundamentals → C to Assembly Translation → Register Fundamentals →
Essential Instructions → Calling Conventions → System Calls → Practice

You don’t start with assembly. You start with C, then use the compiler as your teacher.

Phase 1: Build the C Foundation

Before touching assembly, I needed to understand what assembly actually does. C is the bridge.

Pointers—The Key Concept

pointer_basics.c
int x = 42;
int *ptr = &x; // ptr holds memory address
printf("%d\n", *ptr); // dereference to get value

In assembly, everything is pointers. You’re always working with addresses and values at those addresses. Understanding C pointers makes assembly natural.

Memory Layout

memory_example.c
void example() {
int stack_var = 10; // Stack - automatic
int *heap_var = malloc(4); // Heap - manual
*heap_var = 20;
free(heap_var);
}

In assembly, you see the stack and heap directly. You manipulate the stack pointer. You understand why memory leaks happen.

Struct Layout and Padding

struct_padding.c
struct Data {
char a; // 1 byte + 3 padding
int b; // 4 bytes
char c; // 1 byte + 3 padding
}; // Total: 12 bytes, not 6

This matters in assembly because you access struct members by offset. The padding affects those offsets.

Phase 2: Use the Compiler as Teacher

The best way to learn assembly is to see what the compiler generates.

Compile C to Assembly

Compile to assembly
# Compile C to assembly
gcc -S -O0 program.c -o program.s
# Use Intel syntax (easier to read)
gcc -S -O0 -masm=intel program.c -o program.s
# Disassemble existing programs
objdump -d program | less

I started with simple functions and examined their assembly:

simple.c
int add(int a, int b) {
return a + b;
}

Becomes:

simple.s
add:
mov eax, edi ; First argument (a) -> eax
add eax, esi ; Add second argument (b)
ret ; Return (result in eax)

This taught me more than any tutorial. I could see exactly how C maps to assembly.

Phase 3: x86-64 Register Fundamentals

x86-64 has many registers, but you only need to know the essential ones.

General Purpose Registers

┌──────────────────────────────────────────────────────────────┐
│ Essential Registers │
├──────────────────────────────────────────────────────────────┤
│ RAX/EAX │ Accumulator │ Return values, arithmetic │
│ RBX/EBX │ Base │ Preserved across calls │
│ RCX/ECX │ Counter │ Loops, shifts │
│ RDX/EDX │ Data │ I/O, arithmetic extension │
│ RSI/ESI │ Source │ String operations │
│ RDI/EDI │ Destination │ String operations │
│ R8-R15 │ Additional │ Extra general purpose (64-bit) │
├──────────────────────────────────────────────────────────────┤
│ RSP/ESP │ Stack pointer │ Top of stack │
│ RBP/EBP │ Base pointer │ Stack frame reference │
│ RIP │ Instruction │ Next instruction address │
│ RFLAGS │ Status flags │ Zero, carry, overflow, etc. │
└──────────────────────────────────────────────────────────────┘

Argument Passing (System V AMD64 ABI)

On Linux, function arguments go in registers:

Integer arguments: RDI, RSI, RDX, RCX, R8, R9
Return value: RAX

This is different from 32-bit x86 where arguments were passed on the stack.

Phase 4: Essential Instructions

x86-64 has over 1000 instructions. You only need about 50 for practical work.

Data Movement

Data movement instructions
mov rax, rbx ; Copy rbx to rax
mov rax, [rbx] ; Load from memory at rbx
mov [rbx], rax ; Store rax to memory at rbx
lea rax, [rbx+rcx] ; Load effective address

Arithmetic

Arithmetic instructions
add rax, rbx ; rax = rax + rbx
sub rax, rbx ; rax = rax - rbx
imul rax, rbx ; rax = rax * rbx (signed)
inc rax ; rax++
dec rax ; rax--

Logic

Logic instructions
and rax, rbx ; Bitwise AND
or rax, rbx ; Bitwise OR
xor rax, rbx ; Bitwise XOR
shl rax, 3 ; Shift left 3 bits
shr rax, 3 ; Shift right 3 bits

Control Flow

Control flow instructions
cmp rax, rbx ; Compare (sets flags)
je label ; Jump if equal
jne label ; Jump if not equal
jg label ; Jump if greater
jl label ; Jump if less
jmp label ; Unconditional jump

Stack Operations

Stack instructions
push rax ; Push rax onto stack
pop rbx ; Pop top of stack into rbx
call function ; Push return address, jump to function
ret ; Pop return address, jump back

Phase 5: Calling Conventions

Understanding how functions communicate is essential.

The Linux x86-64 System V ABI

Calling convention example
; Arguments: RDI, RSI, RDX, RCX, R8, R9
; Return: RAX
; Example: Calling printf
section .data
msg db "Hello, %s!", 10, 0
name db "World", 0
section .text
global main
extern printf
main:
push rbp ; Save base pointer
mov rbp, rsp ; Set up stack frame
lea rdi, [rel msg] ; First arg: format string
lea rsi, [rel name] ; Second arg: string
xor eax, eax ; Clear eax (printf uses AL for varargs)
call printf ; Call printf
xor eax, eax ; Return 0
pop rbp
ret

Callee-Saved vs Caller-Saved

This matters when you write your own functions:

Callee-saved (must preserve): RBX, RBP, R12-R15
Caller-saved (can clobber): RAX, RCX, RDX, RSI, RDI, R8-R11

Phase 6: System Calls

Assembly lets you call the kernel directly.

Linux x86-64 System Calls

syscall_example.asm
; Linux x86-64 syscall numbers
; sys_write = 1, sys_exit = 60
section .data
msg db "Hello, Assembly!", 10
len equ $ - msg
section .text
global _start
_start:
; sys_write(fd, buf, count)
mov rax, 1 ; syscall: write
mov rdi, 1 ; fd: stdout
lea rsi, [rel msg] ; buf: message
mov rdx, len ; count: length
syscall ; Make system call
; sys_exit(0)
mov rax, 60 ; syscall: exit
xor rdi, rdi ; status: 0
syscall ; Make system call

Practical Projects

Once I understood the basics, I applied the knowledge:

Project 1: Hello World

hello.asm
; Build: nasm -f elf64 hello.asm && ld -o hello hello.o
; Run: ./hello
section .data
msg db "Hello, x86-64 Assembly!", 10
len equ $ - msg
section .text
global _start
_start:
mov rax, 1 ; write syscall
mov rdi, 1 ; stdout
lea rsi, [rel msg]
mov rdx, len
syscall
mov rax, 60 ; exit syscall
xor rdi, rdi
syscall

Project 2: Array Sum

sum_array.asm
; int sum_array(int *arr, int count)
section .text
global sum_array
sum_array:
xor eax, eax ; Result = 0
test rsi, rsi ; Check if count == 0
jz .done
xor ecx, ecx ; Counter i = 0
.loop:
add eax, [rdi + rcx*4] ; Add arr[i] to result
inc ecx
cmp ecx, esi
jl .loop
.done:
ret

Project 3: Debug with GDB

GDB debugging session
# Compile with debug info
nasm -f elf64 -g -F dwarf program.asm
ld -o program program.o
# Debug
gdb ./program
# Inside GDB:
(gdb) break _start
(gdb) run
(gdb) info registers
(gdb) stepi
(gdb) x/10x $rsp # Show 10 hex values at stack pointer

Common Mistakes

Mistake 1: Learning 32-bit Assembly

Many tutorials still teach 32-bit x86. This wastes time:

32-bit vs 64-bit comparison
; DON'T learn this (32-bit)
push ebp
mov ebp, esp
mov eax, [ebp+8] ; Arguments on stack
pop ebp
ret
; LEARN this (64-bit)
mov rax, rdi ; Arguments in registers
ret

Mistake 2: Trying to Learn All Instructions

Focus on the 20% you’ll use 80% of the time:

  • Data movement (MOV, LEA, PUSH, POP)
  • Arithmetic (ADD, SUB, IMUL, INC, DEC)
  • Logic (AND, OR, XOR, SHL, SHR)
  • Control flow (CMP, JMP, Jcc, CALL, RET)

Mistake 3: Ignoring Calling Conventions

Wrong vs correct register usage
; WRONG: Random register usage
my_function:
mov [rax], rbx ; Clobbering RAX without saving
ret ; RAX should have return value!
; CORRECT: Follow conventions
my_function:
push rbx ; Save callee-saved register
mov rax, rdi ; First argument in RDI
add rax, rsi ; Second argument in RSI
pop rbx ; Restore callee-saved register
ret ; Return value in RAX

Mistake 4: Not Using Tools

Use the right tools:

Essential tools
nasm -f elf64 program.asm # Assembler
ld -o program program.o # Linker
gdb ./program # Debugger
objdump -d program # Disassembler

Summary

In this post, I showed how to learn x86-64 assembly for practical use. The key points:

  1. Start with C—Pointers and memory management map directly to assembly
  2. Use the compiler as teachergcc -S shows you how C becomes assembly
  3. Learn essential registers—RAX, RDI, RSI, RSP, RBP, RIP
  4. Focus on 50 instructions—Not all 1000+ instructions
  5. Understand calling conventions—How functions pass arguments and return values
  6. Practice with real programs—Debug with GDB, write actual code

The path: C fundamentals → C to assembly translation → registers → instructions → calling conventions → system calls → projects.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments