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?
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
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:
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:
|RAX||System call number|
Perfect, we now have everything we need to write a program to display a character string on the screen.
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.