If you are interested in operating systems, compilers, reverse engineering, embedded systems, executable formats, or simply understanding how computers work at a low level, learning assembly language can be extremely valuable.
There are many assemblers available today, but one that deserves special attention is Flat Assembler, commonly called FASM.
FASM is a fast, powerful, and flexible assembler that gives you a very direct relationship with the machine code generated by your program.
In this article, we'll look at what FASM is, how it works, why it is interesting, and what you can build with it.
What Is Flat Assembler?
Flat Assembler (FASM) is an assembler for the x86 and x86-64 architectures.
An assembler converts assembly language instructions into machine code that the CPU can execute.
For example:
mov rax, 60
The CPU does not actually understand the text:
mov rax, 60
Instead, FASM translates the instruction into machine-code bytes.
Conceptually:
Assembly source
|
v
FASM
|
v
Machine code
|
v
Executable
|
v
CPU
This is the fundamental job of an assembler.
Why Is FASM Interesting?
FASM has several characteristics that make it different from many other assemblers.
1. It is self-hosting
FASM is written in assembly language.
That alone makes it an interesting project for people who want to study low-level programming.
2. It has a powerful macro system
FASM's macro system allows you to build your own higher-level abstractions while still working with assembly.
For example:
macro exit {
mov rax, 60
xor rdi, rdi
syscall
}
Then:
exit
FASM expands the macro during assembly.
3. It can generate different executable formats
FASM supports multiple output formats.
For example, you can create:
format ELF64 executable
for a Linux x86-64 executable.
You can also work with Windows PE formats and other output types.
4. It gives you direct control
FASM does not try to hide the machine.
You can explicitly work with:
- registers
- memory
- instructions
- sections
- symbols
- addresses
- stack frames
- calling conventions
- system calls
- executable formats
This makes it excellent for learning how software actually works.
FASM vs NASM
If you have already learned or seen NASM, you may wonder:
Why learn FASM instead?
Both are excellent assemblers, but their philosophies differ.
A simplified comparison:
| Feature | FASM | NASM |
|---|---|---|
| x86/x86-64 | Yes | Yes |
| Linux | Yes | Yes |
| Windows | Yes | Yes |
| Powerful macros | Yes | Yes |
| Multiple output formats | Yes | Yes |
| Direct machine-level control | Yes | Yes |
| Self-hosting implementation | Yes | No |
| Minimal syntax | Yes | Yes |
The biggest reason to explore FASM is its macro system and overall design philosophy.
FASM tries to provide powerful facilities while keeping the assembler itself relatively small and flexible.
Your First FASM Program
Let's start with a simple Linux x86-64 program.
format ELF64 executable
segment readable executable
entry main
main:
mov rax, 60
xor rdi, rdi
syscall
This program simply exits.
Let's understand it.
format
format ELF64 executable
This tells FASM what kind of output we want.
Here:
ELF64
means a 64-bit Linux ELF executable.
And:
executable
means we want an executable file.
So FASM knows how to construct the final executable.
segment
segment readable executable
This creates a segment containing executable code.
The permissions indicate that the segment can be:
readable
executable
The CPU will execute instructions from this region.
entry
entry main
This tells FASM that main is the entry point of the executable.
In other words, when the operating system starts the program, execution begins at:
main:
Labels
This:
main:
is a label.
A label gives a name to a location in the program.
For example:
main:
mov rax, 60
The label main represents the address of that instruction.
You can create other labels:
loop_start:
...
or:
my_function:
...
Labels are extremely important in assembly programming.
Registers
When programming x86-64 assembly, you will work heavily with registers.
Some important registers include:
RAX
RBX
RCX
RDX
RSI
RDI
RBP
RSP
R8
R9
R10
R11
R12
R13
R14
R15
Each register is a small, extremely fast storage location inside the CPU.
For example:
mov rax, 123
means:
RAX = 123
You can then use the value:
add rax, 10
Now:
RAX = 133
The mov Instruction
One of the most important instructions is:
mov
It copies data.
For example:
mov rax, 10
means:
RAX <- 10
Another example:
mov rbx, rax
means:
RBX <- RAX
It is important to understand that mov is fundamentally a copy operation.
Memory
Registers are very small.
When you need larger amounts of data, you work with memory.
FASM allows memory operands using brackets:
mov rax, [address]
The brackets mean:
Access the memory located at this address.
For example:
mov rax, [rbx]
means:
Use the value in RBX as an address
|
v
Memory[RBX]
|
v
RAX
Compare:
mov rax, rbx
with:
mov rax, [rbx]
The first copies the address/value stored in RBX.
The second reads the memory at the address stored in RBX.
This distinction is fundamental in assembly.
Data Definitions
FASM provides directives for creating data.
Some important ones are:
db
dw
dd
dq
They represent different data sizes.
db
Define byte:
db 10
dw
Define word:
dw 10
dd
Define double word:
dd 10
dq
Define quad word:
dq 10
You will use these constantly when creating data structures, buffers, strings, tables, and other binary data.
Strings
A string can be defined using:
message db 'Hello, world!', 0
The 0 is important when you want the string to be compatible with C-style null-terminated strings.
Memory might look like:
H e l l o , w o r l d !
48 65 6C 6C 6F ...
|
v
00
The zero byte marks the end of the string.
Reserving Memory
FASM also provides declarations for reserving space.
For example:
buffer rb 100
means:
Reserve 100 bytes.
Other declarations include:
rw
rd
rq
These are useful when creating buffers and data structures.
For example:
buffer rb 4096
creates a 4096-byte buffer.
Arithmetic
FASM supports the normal x86 arithmetic instructions.
For example:
mov rax, 20
add rax, 5
Result:
RAX = 25
Subtraction:
sub rax, 10
Increment:
inc rax
Decrement:
dec rax
Negation:
neg rax
You can also perform multiplication and division with:
mul
imul
div
idiv
These instructions require more attention because some of them use implicit registers.
Bitwise Operations
Assembly programming also involves manipulating individual bits.
Important instructions include:
and
or
xor
not
shl
shr
For example:
xor rax, rax
is a very common instruction.
It sets:
RAX = 0
because:
x XOR x = 0
Comparing Values
The cmp instruction is extremely important.
For example:
cmp rax, rbx
Conceptually, the CPU calculates:
RAX - RBX
but does not store the result.
Instead, it updates CPU flags.
Those flags can then be used by conditional jumps.
For example:
cmp rax, rbx
je equal
If the values are equal, execution jumps to:
equal:
Control Flow
Assembly programs need branches and loops.
The basic unconditional jump is:
jmp label
For example:
jmp start
start:
...
Conditional jumps include:
je
jne
jg
jge
jl
jle
ja
jb
These are based on CPU flags.
You will eventually learn the important difference between:
signed comparisons
and:
unsigned comparisons
because instructions such as jg and ja do not mean the same thing.
The Stack
The stack is one of the most important concepts in assembly.
The stack is an area of memory used for temporary data and function calls.
Two important registers are:
RSP
RBP
RSP is the stack pointer.
RBP is commonly used as a frame pointer.
You can place a value onto the stack:
push rax
and retrieve it:
pop rax
Function calls also use the stack.
For example:
call my_function
The CPU saves a return address and jumps to:
my_function:
Eventually:
ret
returns to the caller.
Understanding call, ret, push, pop, and rsp is essential for writing functions in assembly.
Functions
A function can be created manually:
my_function:
mov rax, 123
ret
You can call it:
call my_function
The general flow is:
caller
|
| call
v
my_function
|
| ret
v
caller
At this level, you begin to understand how higher-level languages implement functions underneath.
Linux System Calls
One of the most interesting parts of Linux assembly programming is using system calls directly.
For x86-64 Linux, the syscall instruction enters the kernel.
For example:
mov rax, 60
mov rdi, 0
syscall
This requests the exit system call.
The general idea is:
Your program
|
| syscall
v
Linux kernel
|
v
Operating-system service
System calls allow assembly programs to communicate directly with the operating system without necessarily going through the C standard library.
FASM Macros
One of FASM's most powerful features is its macro system.
Consider:
macro exit {
mov rax, 60
xor rdi, rdi
syscall
}
Now you can write:
exit
instead of repeatedly writing:
mov rax, 60
xor rdi, rdi
syscall
This becomes especially useful for larger assembly projects.
FASM provides several powerful macro-related mechanisms, including:
macro
match
rept
repeat
irp
irps
These allow you to build abstractions and even create small domain-specific languages inside assembly.
Why Learn FASM Macros?
At first, assembly can become repetitive.
For example, imagine repeatedly writing system-call setup code.
Macros allow you to create your own abstractions.
You could eventually create something like:
print_string message, message_length
which expands into the appropriate instructions.
The CPU still receives ordinary machine code.
The macro exists only during assembly.
Conceptually:
Your FASM source
|
v
Macros
|
v
Expanded assembly
|
v
Machine code
This is one of the reasons FASM is so interesting.
FASM Structures
FASM also allows you to define structures.
For example:
struc Person {
.age dd ?
.id dd ?
}
You can then use structure layouts to organize memory.
This becomes particularly useful when working with:
- operating-system structures
- executable formats
- network packets
- file formats
- hardware structures
- custom data structures
Instead of thinking only in individual variables, you can start thinking in terms of memory layouts.
Address Expressions
FASM supports powerful address expressions.
For example:
mov rax, [rbx + rcx*8]
This can be understood as:
address = RBX + RCX * 8
Then:
RAX = Memory[address]
This type of addressing is extremely important for arrays.
Suppose each element is 8 bytes:
array[0] -> address + 0
array[1] -> address + 8
array[2] -> address + 16
array[3] -> address + 24
Then:
mov rax, [rbx + rcx*8]
can access an element based on RCX.
FASM and Executable Formats
One of the fascinating things about assembly programming is that you eventually discover that an executable is not simply "machine code."
An executable contains structures describing:
- entry points
- sections
- memory permissions
- imported libraries
- relocation information
- program headers
- metadata
For example, Linux commonly uses:
ELF
while Windows commonly uses:
PE
FASM can construct these executable formats.
This makes FASM useful for learning not only assembly instructions, but also how executable files are structured.
What Can You Build With FASM?
FASM is not limited to tiny examples.
You can use it to build serious low-level projects.
For example:
Small programs
CLI tools
calculators
text utilities
file utilities
Systems programming
bootloaders
kernel components
runtime libraries
system utilities
Binary programming
ELF files
PE files
custom binary formats
binary parsers
Educational projects
CPU experiments
memory allocators
custom data structures
assembly implementations of algorithms
Advanced projects
operating-system components
compilers
virtual machines
debuggers
JIT systems
The difficulty varies enormously, but FASM gives you the low-level control required for these types of projects.
A Good Way to Learn FASM
Don't try to memorize hundreds of instructions immediately.
A better approach is to build your understanding in layers.
Layer 1 — CPU Fundamentals
Learn:
Registers
RAX
RBX
RCX
RDX
RSI
RDI
RSP
RBP
R8-R15
RIP
RFLAGS
Understand the difference between:
64-bit
32-bit
16-bit
8-bit
Layer 2 — Basic Instructions
Learn:
mov
add
sub
inc
dec
neg
mul
imul
div
idiv
and
or
xor
not
shl
shr
Layer 3 — Control Flow
Learn:
cmp
test
jmp
je
jne
jg
jge
jl
jle
Then learn loops.
Layer 4 — Memory
Understand:
addresses
pointers
dereferencing
memory operands
arrays
structures
address expressions
For example:
mov rax, [rbx + rcx*8]
should eventually become completely natural to you.
Layer 5 — Stack and Functions
Learn:
push
pop
call
ret
rsp
rbp
stack frames
calling conventions
This is where assembly begins to connect strongly with C.
Layer 6 — Operating System Interface
On Linux, learn:
syscall
system-call numbers
register conventions
file descriptors
read
write
open
close
exit
Then build programs without relying on a C runtime.
Layer 7 — FASM Features
After understanding the CPU itself, learn FASM-specific features:
format
include
sections/segments
constants
data definitions
structures
macros
match
repeat
rept
irp
irps
This is where you start taking advantage of FASM rather than treating it like a generic assembler.
FASM Is More Than "Writing Assembly"
The most important thing to understand is that learning FASM is not just about memorizing instructions.
You are learning several layers simultaneously:
Your Program
|
v
FASM source code
|
v
FASM assembler
|
v
Machine code
|
v
CPU / OS
|
v
Hardware
As you become more comfortable, you will start seeing how all these layers connect.
A C program such as:
int x = 10;
x++;
eventually becomes machine instructions.
An operating system eventually interacts with hardware through machine-level mechanisms.
An executable eventually becomes bytes arranged according to a binary format.
Assembly sits very close to that boundary.
Final Thoughts
Flat Assembler is an excellent choice if your goal is to understand computers at a low level.
It teaches you to think about:
- registers
- memory
- addresses
- instructions
- CPU flags
- stacks
- functions
- calling conventions
- system calls
- executable formats
- machine code
And FASM's macro system gives you something especially interesting: the ability to build powerful abstractions while remaining very close to the machine.
If you are coming from C, FASM can also change the way you understand C itself.
Instead of seeing:
function();
you begin asking:
How is the function called?
Where are the arguments?
Which registers contain them?
What happens to the stack?
Where does the return address go?
How is the return value produced?
That is where assembly becomes truly valuable.
Don't learn assembly just to write assembly. Learn it to understand what your computer is actually doing.
And if you choose FASM, you get a particularly interesting tool for exploring that world.
Top comments (0)