Skip to main content
  1. Posts/

Getting Started with NASM Assembly

·14 mins
Programming Reverse Engineering
Table of Contents

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

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.

raxeaxaxalTo provide the system call number
To provide the function return value
Caller-save register
rcxecxcxcl4th function parameter
Caller-save register
rdxedxdxdl3rd function parameter
Caller-save register
rbxebxbxblCallee-save register
rsiesisisil2nd function parameter
Caller-save register
Source pointer for string instructions
rdiedididil1st function parameter
Caller-save register
rspespspsplSlack pointer (top element)
Caller-save register
rbpebpbpbplStack Base pointer
Callee-save register
r8r8dr8wr8b5th function parameter
Caller-save register
r9r9dr9wr9b6th function parameter
Caller-save register
r10r10dr10wr10bCaller-save register
r11r11dr11wr11bCaller-save register
r12r12dr12wr12bCallee-save register
r13r13dr13wr13bCallee-save register
r14r14dr14wr14bCallee-save register
r15r15dr15wr15bCallee-save register
ripeipNext 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

mov dest, srcCopy the value of src into dest


syscallCall 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 codeSend 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 labelAllow calling a defined label (i.e. function - might be coming from another file)
push itemPush an item into the stack
pull itemPull an item from the stack into a register

Arithmetic Operations

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

Jumps and Conditions

jmp locationJump to the location (can be a register or a label)
test reg, constBitwise compare a register and a constant. jz or jnz must follow
jz labelJump to label if bits were not equal to 0
jnz labelJump to label if bits were equal to 0
cmp x, yCompare x and y. Must be followed by jn, jne, jg, jge, ji, or jil
je labelJump to label if x is equal to y
jne labelJump to label if x is different from y
jg labelJump to label if x is greater than y
ji labelJump to label if x is smaller than y
jge labelJump to label if x is greater or equal to y
jil labelJump 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 TypeSuffixData AssignationSize (bits)
Double wordddresd32
Quad worddqresq64
Ten bytesdtrest80
Octo Worddoreso128
Y Worddyresy256
Z Worddzresz512

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 

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

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.

user@vm1:/tmp/test$ nasm -f elf64 main.S && nasm -f elf64 function.S
user@vm1:/tmp/test$ gcc -o a.out -no-pie main.o function.o
user@vm1:/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.

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:

user@vm1:/tmp/test$ nasm -f elf64 example.S
user@vm1:/tmp/test$ ld -o a.out example.o

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.