Content of The Series
In this series of tutorials we will look into how to implement multithreading techniques in x86_64 Assembly from scratch without using the standard library.
Part 1: Hello World! from Scratch
Writing a simple program to print to the standard output from two different processes.
Part 2: Threads
Implementing a functional way of creating and waiting for threads.
Part 3: Shared Memory
Implementing memory managment functions and showing how we can share memory between threads.
Part 4: Mutexes
Implementing a mutex to control memory access between threads.
Prerequisites
To follow along with this tutorial you wil need a basic knowledge in assembly. You will also need a linux environment as we will implement everything from scratch using only the kernel system calls.
Tools
In this tutorial I'm using the GNU assembler gas
with intel syntax
along the the GNU linker.
Using other assemblers like MASM
or NASM
should be fine, just make sure to review the differences in syntax between the code shown in this series of tutorials with the code you intend to write, take a look at this link.
Hello, World! from scratch
Since we won't be linking against the standard library (Linking with -nostdlib
) we will not have access to commonly used functions like printf
or malloc
and will have to implement them from scratch using the x86_64 linux system calls.
Project structure
- Project Folder
|
|-- lib/ # The directory for our reusable code
| |
| |-- util.asm # utilities (print, malloc, etc..)
| |....
|
|-- hello_world/
|
| -- main.asm # Our code for Part 1
Print function
Now we will need to implement a simple function to print to the standard output STDOUT
using the write system call.
Our print
function will take 2 arguments: ptr
and len
ptr
: The address of the string (passed in register rdi
)
len
: The length of the string (passed in register rsi
)
# lib/util.asm
.intel_syntax
.section .text
# print function that takes (ptr, len)
# as arguments (rdi, rsi)
.global print
print:
mov %rdx, %rsi # move the length to rdx
mov %rsi, %rdi # move the pointer (rdi) to rsi
mov %rax, 0x01 # write syscall on x64 Linux
mov %rdi, 0x01 # STDOUT file descriptor
syscall
ret
This function that we defined above takes 2 arguments using the rdi
and rsi
registers then rearrange the registers to call the write
syscall on the STDOUT
file descriptor.
Now lets assemble it with the following command:
$ as lib/util.asm -o lib/util.o
Now we should have an object file lib/util.o
that we will link against to have access to our utility functions.
fork
System Call
Throughout this series we will be making heavy use of the fork system call which basically tell the OS to create a child process of the current process, this child process is an exact copy of the parent and it will start executing from the next instruction of the parent.
Writing the Code
# hello_world/main.asm
.intel_syntax
.global _start
.section .text
.extern print # Use our print function
_start:
# Call the 'fork' syscall
mov %rax, 0x39 # fork syscall on x64 Linux
syscall
cmp %rax, 0 # 'fork' will return 0 to the child process
je _child
_parent:
# Print 'Hello from parent!'
lea %rdi, [%rip + msg1]
mov %rsi, OFFSET msg1len
call print
jmp _exit
_child:
# Print 'Hello from child!'
lea %rdi, [%rip + msg2]
mov %rsi, OFFSET msg2len
call print
_exit:
# Call the 'exit' syscall
mov %rax, 0x3c # exit syscall on x64 Linux
mov %rdi, 0x0 # Exit code
syscall
.section .data
msg1:
.ascii "Hello from parent!\n"
msg1len = . - msg1
msg2:
.ascii "Hello from child!\n"
msg2len = . - msg2
In the code above we have 2 different functions: _parent
which will print Hello from parent!
and exit, and function _child
which will print Hello from child!
and exit. On the main function _start
we will make a fork
system call which will create a copy of the current process and store a value in the register rax
, this value will be 0
on the child process and will be the PID of the child process on the parent, so we will make use of this information. We will compare the value of register rax
to zero, if it is zero we will jump to the _child
function, if not, we will continue to execute the _parent
function.
Assembling and Linking
We will assemble the code and link it with util.o
object file containing the print
function without linking the standard library using the following commads:
$ as hello_world/main.asm -o hello_world/hello_world.o
$ ld hello_world/hello_world.o lib/util.o -o hello_world/hello_world.elf -nostdlib
If everything goes well we should have an executable hello_world/hello_world.elf
and if we execute it we should see the output:
$ ./hello_world/hello_world.elf
Hello from parent!
Hello from child!
$
Great! we have successfully written an x86_64 assembly program to execute 2 deifferent code pieces from two different threads. Next, we will look into how to implement a more functional solution to make it easy create and wait for threads.
The code for this tutorial is available in this repository. The code repository will be updated with every new part of the series.
Top comments (0)