Getting Started with NASM Assembly

The Assembly language (ASM), is the lowest-level programming language one can find. It is very close to the CPU code instructions, and therefore, there is a multitude of assembly languages, each designed for specific computer architecture. ASM is used in various situations, such as for performance-sensitive programs, system's boot code, or reverse-engineer programs.

In today's article, we will study NASM ("Netwide Assembler"), which is an ASM language for Intel x86 architecture. It can be used to write 16-bit, 32-bit, and 64-bit programs and is considered one of the most popular assemblers for Linux (although it can also be used with Mac and Windows).

This article will go through most of the things you need to know to be able to start coding in NASM or to understand NASM code if you want to reverse engineer programs. Although some things will change depending on the operating system for which you are coding, the general concepts stay the same so this tutorial can still be of use to you, even if you are not planning to use Linux.

As you can guess, assembly is quite a long and complex topic, so it is not possible to cover 100% of the things. If you want to know more about NASM, you can refer to this documentation.

NASM Program Structure

First, let's study an actual piece of code written in NASM. The snipped below presents a "Hello World" code, and will be used to introduce some main concepts, and how NASM code is structured. Some of the notions will be discussed more in-depth in the following parts.

global _start

section .text
	mov rax, 1		; Set the function to call (1 is write)
	mov rdi, 1		; Set the first argument of write (fd 1)
	mov rsi, msg		; Set the 2nd argument, the text to write
	mov rdx, msg.len	; Set the 3rd argument, lengh to write
	syscall			; Call write w/ previously defined things

	mov rax, 60		; Set the function (syscall) exit (id 60)
	xor rdi, rdi		; Set the exit return to be 0
        syscall			; Execute the syscall exit

section .data
msg: db	"Hello, world!",10	; Assign "Hello, world!\n" to the var msg
.len: equ $ - msg		; Assign len(msg) to msg.len
Hello World in NASM x86_64 (Linux)

First, we can observe that the code is divided into sections. There are only two on our example (.text and .data), but there are a couple more that you can use (note that this will change depending on the target OS: for example, Windows would use .code instead of .text).

  • .text is the section where you will write your ASM code. rax, rdi, rsi, and rdx are called registers, and mov, syscall and xor are instructions. We will go through them in the next part
  • .data allows you to statically allocate initialized global and static objects for the duration of the program execution
  • .bss allows you to reserve space for uninitialized global and static objects
  • .rodata is the same as .data with the difference that the variables declared here will be read-only (i.e. constant)

The next things to stand out are the _start and msg elements. They are what is called labels. _start is the equivalent of the main function in a higher-level language like C or C++. This is where the program will start its execution. Alternatively, it is also possible to use labels to define functions and as points to jump to (a bit like goto in C - more on that later). Labels are also used to define variables, like msg in our example; we will see more about these later as well.

One thing you will notice under the msg is the .len. A label that begins with a period is walled a local label and will be associated with the previous non-local label. In our example, it will be called as msg.len.

The code is probably explicit enough but ; is used to put comments: everything put after it will not be interpreted.


Registers are storage locations kept inside of the processor, which make them very fast. There are 17 of them, and some have a specific usage attribution (for example pass arguments to a function). Technically, all of them can be modified at will (except rip), even if there are some conventions stating how it should be done.

64-bit 32-bit 16-bit 8-bit Comment
rax eax ax al To provide the system call number
To provide the function return value
Caller-save register
rcx ecx cx cl 4th function parameter
Caller-save register
rdx edx dx dl 3rd function parameter
Caller-save register
rbx ebx bx bl Callee-save register
rsi esi si sil 2nd function parameter
Caller-save register
Source pointer for string instructions
rdi edi di dil 1st function parameter
Caller-save register
rsp esp sp spl Slack pointer (top element)
Caller-save register
rbp ebp bp bpl Stack Base pointer
Callee-save register
r8 r8d r8w r8b 5th function parameter
Caller-save register
r9 r9d r9w r9b 6th function parameter
Caller-save register
r10 r10d r10w r10b Caller-save register
r11 r11d r11w r11b Caller-save register
r12 r12d r12w r12b Callee-save register
r13 r13d r13w r13b Callee-save register
r14 r14d r14w r14b Callee-save register
r15 r15d r15w r15b Callee-save register
rip eip Next instruction to be executed (rip can't be accessed directly by the programmer)

You will notice that there are only 6 registers that can be used to pass parameters to a function. They allow passing integers or pointers only. To pass parameters larger than 64-bit, or to pass more than 6, they should be pushed into the stack, with the first argument being on the top.

You will also notice that there are two types of register: "Callee-saved" and "Caller-saved". This is actually a convention rather than something strict, but what it means is that:

  • Caller-saved (volatile) registers are meant to be general-purpose and to hold temporary information. They can be rewritten by any subroutine
  • Callee-saved (non-volatile) registers are meant to gold long-lived values and should be preserved across calls. i.e.: a function is supposed to back them up in the stack at its beginning and to restore them from there at the end (if the function wants to use these registers)


The next important concept in NASM is the instructions, which are basically keywords allowing us to tell the computer what to do. This part aims to list the main ones.

Moving Data

Instruction Effect
mov dest, src Copy the value of src into dest


Instruction Effect
syscall Call a function
See the code in the first part or the 'Run ASM Code and more Complex Files Structure' part for more details on how to use it. This page lists the code and arguments to use for common functions
int code Send an interrupt signal. Can be another way to do syscall
See the Linux example in Wikipedia. The function calls for Linux x32 are defined in sys/syscall.h
call label Allow calling a defined label (i.e. function - might be coming from another file)
push item Push an item into the stack
pull item Pull an item from the stack into a register

Arithmetic Operations

Instruction Effect
inc dest dest = dest + 1
dec dest dest = dest - 1
add dest, src dest = dest + src
sub dest, src dest = dest - src
shr dest, k dest = dest >> k
shl dest, k dest = dest << k
xor dest, src dest = dest ^ src
shl dest, src dest = dest & src
shl dest, src dest = dest | src

Jumps and Conditions

Instruction Effect
jmp location Jump to the location (can be a register or a label)
test reg, const Bitwise compare a register and a constant. jz or jnz must follow
jz label Jump to label if bits were not equal to 0
jnz label Jump to label if bits were equal to 0
cmp x, y Compare x and y. Must be followed by jn, jne, jg, jge, ji, or jil
je label Jump to label if x is equal to y
jne label Jump to label if x is different from y
jg label Jump to label if x is greater than y
ji label Jump to label if x is smaller than y
jge label Jump to label if x is greater or equal to y
jil label Jump to label if x is small or equal to y

For more examples, you can have a look at this cheat sheet which is very useful.

Data Type

In the example at the beginning of this article, we had the following line of code: msg: db "Hello, world!,10", and we explained that this was a variable msg getting attributed as Hello, world!\n. db here is the type of variable. Unlike a higher level language where you may have int to store numbers, char to store a single character, ... types in NASM are just used to say how much space your data will take. The available types are listed in the following table.

Data Type Suffix Data Assignation Size (bits)
Byte db resb 8
Word dw resw 16
Double word dd resd 32
Quad word dq resq 64
Ten bytes dt rest 80
Octo Word do reso 128
Y Word dy resy 256
Z Word dz resz 512

In our previous example, we can see that we are using a db suffix, which works because a char is 8 bits big.

One thing that we haven't seen before is the data assignation column. This is used in the .bss section to be able to define how much space we want to keep. For example:

buffer:         resb    64              ; reserve 64 bytes 
wordvar:        resw    1               ; reserve a word 
realarray:      resq    10              ; array of ten reals 
Extract from the NASM documentation

Note that there are multiple ways to write values in NASM. The following code demonstrates various ways that can be used. Notice that when writing on bases other than 10, the values will have a suffix (e.g. h for hexadecimal, b for binary, ...) or a prefix (e.g. 0x for hexadecimal, 0o for octal, ...). See part 3.4.1 of the documentation for more details.

db    0x55                ; just the byte 0x55 
db    0x55,0x56,0x57      ; three bytes in succession 
db    'a',0x55            ; character constants are OK 
db    'hello',13,10,'$'   ; so are string constants 
dw    0x1234              ; 0x34 0x12 
dw    'a'                 ; 0x61 0x00 (it is just a number) 
dw    'ab'                ; 0x61 0x62 (character constant) 
dw    'abc'               ; 0x61 0x62 0x63 0x00 (string) 
dd    0x12345678          ; 0x78 0x56 0x34 0x12 
dd    1.234567e20         ; floating-point constant 
dq    0x123456789abcdef0  ; eight byte constant 
dq    1.234567e20         ; double-precision float 
dt    1.234567e20         ; extended-precision float

mov     ax,200            ; decimal 
mov     ax,0200           ; still decimal 
mov     ax,0200d          ; explicitly decimal 
mov     ax,0d200          ; also decimal 
mov     ax,0c8h           ; hex 
mov     ax,$0c8           ; hex again: the 0 is required 
mov     ax,0xc8           ; hex yet again 
mov     ax,0hc8           ; still hex 
mov     ax,310q           ; octal 
mov     ax,310o           ; octal again 
mov     ax,0o310          ; octal yet again 
mov     ax,0q310          ; octal yet again 
mov     ax,11001000b      ; binary 
mov     ax,1100_1000b     ; same binary constant 
mov     ax,1100_1000y     ; same binary constant once more 
mov     ax,0b1100_1000    ; same binary constant yet again 
mov     ax,0y1100_1000    ; same binary constant yet again
Usage of values in NASM (source: official documentation)

Another important concept in NASM is the effective address: an operand to an instruction that references memory. The syntax will be an expression contained in brackets. The following code snippet demonstrates some examples of how to use it:

; Accessing a variable
msg              ; The msg variable address
byte[msg]        ; The value of the first byte of the variable msg
byte[msg + 1]  	 ; The value of the second byte of the msg variable
word[msg]        ; The value of the first two bytes of the msg variable

; Various operations
cmp BYTE [rdi], 0h ; Check if the first byte of rdi is 0h

One final thing I want to discuss in this part is the .len: equ $ - msg portion of our first example:

  • equ is used to define a symbol to be a constant value. When used, it will always be to attribute a label. The definition is absolute and can't be changed later
  • The $ is the address of the current position. since we defined msg just before, then we can know that the length of msg will be the distance in bytes obtained by current address - address of msg

Run ASM Code and more Complex Files Structure

To finish this article, we will write a short program in ASM. It will get the program's argument, and print them and their size. For the sake of the example, we will write a strlen function in a different file from the main one.

global my_strlen:function	; We declare the label as a global function

section .text

my_strlen:			; This is the function we will call later
  xor rax, rax			; We set the return value as 0

  cmp BYTE[rdi], 0h		; If this is the end of the string (\0)
  je end			; Then we jump to the label end
  inc rax			; We increment the return value by one
  inc rdi			; We continue to the next char in the string
  jmp while			; We jump to the label while (start of the loop)
  ret				; We reached the end of the string, return rax

If you have read everything until there, nothing in the my_strlen function should be surprising you. One thing that wasn't mentioned before is that you need to declare your label as a global function if you want to be able to call it from another file. Let's jump to the main part of the program.

global main

; We use the extern keyword to be able to use the functions defined outside of the file
extern printf
extern my_strlen

section .text
  mov r10, rdi		; We save argv into r10
  mov r11, 0		; We initialize r11 to 0 and will use it as a loop counter

  ; rsi is the address of the first argv
  ; We use counter * 8 to be able to get the address of argv[r11]
  mov r12, qword [rsi + r11 * 8]

  ; We call our my_strlen function and give argv[r11] as an argument
  ; It will return the result in rax
  mov rdi, r12
  call my_strlen

  ; The prinf call will overwrite some registers, we save them in the stack
  push r10
  push r11
  push r12
  push rsi

  ; We set the printf arguments, and call the function
  mov rdi, printf_format
  mov rsi, r11
  mov rdx, r12
  mov rcx, rax
  mov rax, 0 ; We need to set this to 0 or the program will segfault
  call printf

  ; Once we called printf, we restore the registers from the stack
  pop r10
  pop r11
  pop r12
  pop rsi

  ; We increase our counter and check that r11 < argc. If so, we jump to the loop label
  inc r11
  cmp r10, r11
  jg loop

  ; We call exit(0)
  mov rax, 60
  xor rdi, rdi

section .data
; The string we will pass to printf as required by the prototype
; Unless write in the first example, we won't give printf the numbers of characters to write, so we need to string to end with '\0'
printf_format: db "The argument number %d ('%s') is %d characters long.",10,0

We can then compile our program as follows, and see that the output is what is expected.

[email protected]:/tmp/test$ nasm -f elf64 main.S && nasm -f elf64 function.S
[email protected]:/tmp/test$ gcc -o a.out -no-pie main.o function.o
[email protected]:/tmp/test$ ./a.out 1234 123 12345
The argument number 0 ('./a.out') is 7 characters long.
The argument number 1 ('1234') is 4 characters long.
The argument number 2 ('123') is 3 characters long.
The argument number 3 ('12345') is 5 characters long.
Compile our NASM code using GCC, and test it

This time again, you shouldn't be too surprised by the content of the file, except for one thing. We mentioned before that NASM programs should start with _start, but our program here is starting with main. The reason is that we use gcc for the linking, and it will generate the _start himself before calling the main function.

If we wanted to use _start, we could have compiled with ld. The problem is that it is less convenient when using external functions like printf (see this for more information). If we wanted to compile our first example, it would however be simple to do with ld:

[email protected]:/tmp/test$ nasm -f elf64 example.S
[email protected]:/tmp/test$ ld -o a.out example.o
Compile NASM code using ld

One final thing you could be wondering relating to the compilation is why we are using -no-pie with GCC. The reason is that on Ubuntu, GCC will generate Position Independent Executables (PIE), which our current code is not compatible with. The -no-pie argument tells GCC to not generate a PIE executable, but we also have the option of doing call printf wrt ..plt in the code, which would make our code Position independent.