Showing posts with label Assembly Language. Show all posts
Showing posts with label Assembly Language. Show all posts

Monday, August 17, 2026

NASM Assembly Language Tutorial: A Beginner’s Guide to x86-64 Assembly Programming

NASM Assembly Language provides a direct way to understand how software interacts with a computer's processor. Unlike high-level languages such as Python, Java, or C++, assembly language exposes important concepts such as CPU registers, memory addresses, processor instructions, the stack, and program control flow.
NASM stands for Netwide Assembler and is a popular assembler for the x86 and x86-64 architectures. It uses Intel-style syntax and supports multiple operating systems and object-file formats.
In this guide, we will explore the fundamentals of NASM assembly programming, including registers, instructions, memory addressing, functions, stack operations, and 32-bit versus 64-bit assembly.
What Is NASM?
NASM is an assembler that converts assembly-language source code into machine-readable object code.
The basic process is:
Assembly Source → NASM → Object File → Linker → Executable
For example:
mov rax, 10
This instruction places the value 10 into the RAX register. NASM converts the instruction into the corresponding machine-code representation that the processor can execute.
NASM primarily targets x86 and x86-64 processors and is widely used for low-level programming and computer architecture education.
NASM vs Assembly Language
NASM and assembly language are related, but they are not the same thing.
Assembly language refers to low-level programming languages designed around a processor's instruction set. NASM is a particular assembler that implements x86 assembly using Intel-style syntax.
Other assemblers include MASM and GNU Assembler (GAS).
A typical NASM instruction looks like:
mov rax, rbx add rax, 5
The destination normally appears first. The above instructions can be understood as:
RAX = RBX
RAX = RAX + 5
Basic Structure of a NASM Program
A NASM program is commonly divided into three sections:
section .data
; initialized data
section .bss
; reserved storage
section .text
; program instructions
The .text section normally contains executable instructions.
The .data section contains initialized data such as strings and constants.
For example:
message db "Hello, NASM!", 10
The .bss section is used to reserve storage that does not require initial values:
buffer resb 256
This reserves 256 bytes for the buffer.
A Simple NASM Program
A minimal Linux x86-64 program can look like this:
section .text
global _start
_start: mov rax, 60 xor rdi, rdi syscall
Here, global _start makes the entry point available to the linker. The value 60 is placed in RAX as the Linux exit system-call number, RDI is cleared, and syscall invokes the operating system.
System-call conventions depend on the operating system and processor architecture.
Understanding CPU Registers
Registers are small and extremely fast storage locations inside the CPU.
Common x86-64 registers include:
RAX
RBX
RCX
RDX
RSI
RDI
RBP
RSP
R8–R15
Many registers also have smaller versions.
For example:
RAX → 64-bit
EAX → 32-bit
AX → 16-bit
AL → 8-bit
Understanding register sizes is essential when learning x86-64 assembly.
Important NASM Instructions
Some of the most useful instructions for beginners are:
MOV — Move data
ADD — Add values
SUB — Subtract values
INC — Increment
DEC — Decrement
IMUL — Multiplication
DIV — Division
AND — Bitwise AND
OR — Bitwise OR
XOR — Bitwise XOR
CMP — Compare values
JMP — Unconditional jump
CALL — Call a function
RET — Return from a function
PUSH — Put data on the stack
POP — Retrieve data from the stack
LEA — Calculate an effective address
For example:
mov rax, 20 add rax, 10 sub rax, 5
After these instructions, RAX contains 25.
Comparisons and Conditional Jumps
Programs need to make decisions, and assembly uses comparison and jump instructions to implement conditions.
For example:
cmp rax, rbx je equal
CMP updates processor flags based on the comparison. JE then jumps to the equal label when the values are equal.
Other commonly used conditional jumps include:
JE — Jump if equal
JNE — Jump if not equal
JG — Jump if greater, signed
JL — Jump if less, signed
JGE — Jump if greater than or equal
JLE — Jump if less than or equal
JA — Jump if above, unsigned
JB — Jump if below, unsigned
The distinction between signed and unsigned comparisons is an important part of x86 assembly programming.
Memory Addressing in NASM
Understanding memory is one of the most important parts of assembly language.
Consider:
mov rax, [number]
The square brackets indicate that the instruction is accessing a memory location.
NASM also supports more complex addressing:
mov rax, [rbx + rcx*8]
This represents an address calculated using a base register, an index register, and a scale.
This type of addressing is especially useful when working with arrays and pointers.
Arrays in NASM
Suppose we define an array of 64-bit values:
numbers: dq 10, 20, 30, 40, 50
Each value occupies 8 bytes.
Therefore:
Element 0 → offset 0
Element 1 → offset 8
Element 2 → offset 16
Element 3 → offset 24
The third element can therefore be accessed using:
mov rax, [numbers + 16]
This demonstrates how high-level array indexing ultimately becomes address calculation at the machine level.
What Does LEA Do?
LEA stands for Load Effective Address.
For example:
lea rax, [rbx + rcx*4]
calculates the effective address represented by the expression and places the result in RAX.
LEA is particularly useful for pointer calculations and certain arithmetic operations.
Understanding the Stack
The stack is an important area of memory used during program execution.
It is commonly involved in:
• Function calls
• Local storage
• Saved registers
• Temporary values
• Return addresses
Two fundamental instructions are PUSH and POP.
For example:
mov rax, 100 push rax pop rbx
The value stored in RAX is pushed onto the stack and later retrieved into RBX.
Two important stack-related registers are RSP, the Stack Pointer, and RBP, which is traditionally used as a frame or base pointer.
Functions Using CALL and RET
NASM programs can be divided into functions or procedures.
For example:
add_numbers: mov rax, rdi add rax, rsi ret
On the common Unix x86-64 calling convention, the first two integer arguments are passed through RDI and RSI, while the return value is placed in RAX.
Conceptually, the function performs the same operation as:
long add_numbers(long a, long b) { return a + b; }
This is a useful way to understand how high-level programming languages eventually interact with processor instructions.
32-Bit vs 64-Bit Assembly
NASM supports both 32-bit and 64-bit x86 programming.
32-bit programs commonly use registers such as:
EAX
EBX
ECX
EDX
64-bit programs use:
RAX
RBX
RCX
RDX
and additional registers such as R8 through R15.
64-bit programming also introduces different calling conventions, larger pointers, additional registers, and different ABI requirements.
For example, Unix x86-64 commonly uses RDI, RSI, RDX, RCX, R8, and R9 for the first six integer arguments. Windows x64 follows a different calling convention.
Therefore, assembly code that interacts with C or C++ must follow the appropriate platform ABI.
How to Assemble NASM Code
On a Linux x86-64 system, a NASM source file can be assembled using:
nasm -f elf64 program.asm -o program.o
The object file can then be linked:
ld program.o -o program
And executed:
./program
The -f option specifies the output format. NASM supports several formats, including ELF, COFF, Mach-O, Win32, Win64, and raw binary.
Where Is NASM Used?
Although most modern applications are written using high-level languages, NASM remains useful in several areas:
• Operating-system development
• Bootloader development
• Reverse engineering
• Cybersecurity
• Computer architecture
• Performance-oriented programming
• Compiler research
• Low-level debugging
• C and C++ interoperability
You do not need to write an entire application in assembly to benefit from learning it.
Even understanding a small section of compiler-generated assembly can provide a much better understanding of how software works internally.
NASM and Cybersecurity
Assembly knowledge is particularly useful in cybersecurity.
Concepts such as buffer overflows, stack corruption, return addresses, calling conventions, binary analysis, and reverse engineering become easier to understand when you know how registers, memory, and instructions work.
When examining a compiled program using a debugger or disassembler, understanding x86 assembly allows you to see what the processor is actually executing.
Is NASM Still Worth Learning?
Yes.
Modern compilers are extremely capable, and most developers do not need to write large amounts of assembly manually. However, NASM remains valuable for anyone interested in computer architecture, operating systems, cybersecurity, reverse engineering, compilers, or systems programming.
Learning assembly also improves your understanding of languages such as C and C++ because you begin to see how high-level statements are translated into lower-level operations.
Conclusion
NASM assembly language provides a practical way to understand what happens beneath the abstractions of high-level programming.
Start with the fundamentals:
MOV, ADD, SUB, CMP, JMP, PUSH, POP, CALL and RET.
Then move on to registers, memory addressing, pointers, stack frames, calling conventions, and system calls.
The best way to learn NASM is through experimentation. Write small programs, assemble them, run them under a debugger, inspect the registers and memory, and connect each instruction with what the CPU is actually doing.
That is where assembly language becomes more than a collection of unfamiliar instructions—it becomes a way to understand how software really works.
If you are exploring computer science, programming, cybersecurity, or technical project topics, you can also find related resources at ProjectAssignments.