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
_start:
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
, andrdx
are called registers, andmov
,syscall
andxor
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#
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)
Instructions#
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 |
Functions#
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:
.bss
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 definedmsg
just before, then we can know that the length ofmsg
will be the distance in bytes obtained bycurrent 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
while:
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)
end:
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
main:
mov r10, rdi ; We save argv into r10
mov r11, 0 ; We initialize r11 to 0 and will use it as a loop counter
loop:
; 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
syscall
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.
Resources#
- Netwide Assembler (Wikipedia)
- x86 calling conventions (Wikipedia)
- x64 Cheat Sheet (Doeppner - brown.edu)
- Notes on x86-64 programming (filliatr - lri.fr)
- The Netwide Assembler: NASM (NASM documentation)
- NASM Intel x86 Assembly Language Cheat Sheet (Bencode.net)
- IntelĀ® 64 and IA-32 Architectures Software Developer’s Manual (Intel)
- NASM Assembly Language Tutorials (asmtutor.com)
- NASM Tutorial (lmu.edu - ray)
- Linux System Call Table for x86_64 (blog.rchapman.org)
- NASM Manual - Local labels (tortall.net)
Credits#
- Cover photo by Markus Spiske on Unsplash