DEV Community

Ahmed A. Elkhalifa
Ahmed A. Elkhalifa

Posted on • Updated on

x64 Assembly: Multithreading from Scratch Part 1: Hello World!

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

Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
$ 
Enter fullscreen mode Exit fullscreen mode

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)