If you're still here, you want to learn more about the 64-bit assembler. But before we can begin the practice, we still miss some indispensable notions.

1. Anatomy of a NASM code

In order to be able to write a code in NASM, one has to understand the structure of the code.
When creating a program in C, we indicate the point of entry of the program to our compiler GCC thanks to the main function.

As explained in the previous section, the TEXT section contains the code of our program and the DATA section contains the initialized variables.
So when writing ASM code, to improve its visibility it is interesting to specify the sections.

Finally, how to tell the kernel where is the entry point of our program?
It's very simple, just write "global _start". This statement tells the kernel that the function "_ start" will be our entry point.

Here is an example of a structure:

global _start

section .text
_start:
    ; put your code here

section .data
    ; put your variable here

We now know what a NASM program looks like, but how do variables get to our functions?
Where are the system functions defined?

2. Syscall

System calls are methods of taking advantage of the operating system to perform actions.
They avoid having to recreate the functions from scratch to interact with system components like:

  • Display data on the screen
  • Write on disk
  • Etc.

These calls are a simple interface from the "userland" mode to the "kerneland".
But where are his system calls defined?
On my operating system (Void Linux), they are defined here:

$ head /usr/include/asm/unistd_64.h

#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H 1

#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
[...]

How to invoke a system call? ? To invoke a system call, there is an assembly instruction:

  • syscall

I recommend reading these articles if you want to deepen the internal workings of the system calls: Anatomy of a system call part 1 and Anatomy of a system call part 2

3. Passing arguments

This is the last necessary point before the realization of our first program. Indeed, you have to know how to pass arguments to system functions to call them correctly.
It must be remembered that for the 64-bit assembler, the first 6 parameters are passed by register and the rest are placed on the Stack. While for the x86 assembler, all the parameters are placed on the Stack.
So here's how to set the parameters:

Register Value
RAX System call number
RDI 1st argument
RSI 2nd argument
RDX 3rd argument
RCX 4th argument
R8 5th argument
R9 6th argument

Perfect, we now have everything we need to write a program to display a character string on the screen.

4. HelloWorld.s

To display a string, we will use the function: write. To know how this method works, we will look at the manual (man 2 write):

NAME
       write - write to a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);

DESCRIPTION
[...]

When we look at how the function works, we realize thate :

  • The first argument is a file descriptor (1 for stdout)
  • The second argument is a pointer to a string
  • the third argument is the number of characters to display.

Which translates to the following assembler code:

global _start   ; define entrypoint
section .text
_start:
    mov rax, 0x1    ; syscall number for write 
    mov rdi, 0x1    ; int fd 
    mov rsi, msg    ; const void* buf 
    mov rdx, mlen   ; size_t count
    syscall

    mov rax, 0x3c ; syscall number for exit 
    mov rdi, 0x1  ; int status
    syscall

section .data
    msg: db "Hello World!",0xa, 0xd
    mlen: equ $-msg

For the compilation here is the command:

$ nasm hello.s -f elf64
$ ld hello.o -o hello

Congratulations, you have just created your first assembly program :)

I let you watch with your favorite disassembler the operation of the program, the state of the registers, ect.

5. Reference

  1. NASM documentation
    man nasm
    nasm reference
  2. GDB Tutorial
    GDB Tutorial