Article :: Using the Linux Syscalls in Assembly

Introduction

This hands-on tutorial teaches you how to write assembly programs that invoke Linux system calls directly, bypassing the C library and interacting with the kernel at the lowest level.

Overview

When a program needs to interact with the operating system; to write to a file, allocate memory, or exit gracefully; it makes a system call (syscall). Most programmers use these indirectly through C library wrappers, but understanding how to invoke syscalls directly from assembly gives you deep insight into how programs actually work.

In this tutorial, we’ll build a simple x64 Linux program that prints “Hack The Planet” to the screen using raw syscalls, with no C library dependency.

Prerequisites

Before starting, you should have:

Objective

We’ll build a Linux ELF64 binary that uses basic syscalls to print a message on the screen. Along the way, we’ll learn:

Finding Syscall Numbers

Every syscall in Linux has a unique number. For x64, these are defined in a header file. Let’s find the write syscall:

1
2
3
4
5
6
7
cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep write
#define __NR_write 1
#define __NR_pwrite64 18
#define __NR_writev 20
#define __NR_pwritev 296
#define __NR_process_vm_writev 311
#define __NR_pwritev2 328

The basic write syscall is number 1. Now let’s check the manual to understand its arguments:

1
man 2 write
Write syscall manual page

As we can see, the write syscall has three arguments:

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

  1. fd - File descriptor (0=stdin, 1=stdout, 2=stderr)
  2. buf - Pointer to the data to write
  3. count - Number of bytes to write

Syscall Register Conventions

Reading the syscall manual, we learn how to set up x64 registers to invoke syscalls:

Syscall register setup

Register mapping for x64 syscalls:

  1. RAX - Syscall number
  2. RDI - First argument
  3. RSI - Second argument
  4. RDX - Third argument
  5. R10 - Fourth argument
  6. R8 - Fifth argument
  7. R9 - Sixth argument

Writing the Assembly Code

For our program, we’ll:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
global _start

section .text

_start:
;
; Setting up the registers to print the message
; using the "write" syscall
;
  mov rax, 1          ; syscall number for write
    mov rdi, 1          ; file descriptor 1 = stdout
    mov rsi, msg        ; pointer to message
    mov rdx, length     ; message length
    syscall             ; invoke kernel

section .data
    msg: db 'Hack The Planet',0xa    ; message with newline
    length: equ $-msg                 ; calculate length

Compiling the Code

Let’s compile and link this assembly code:

1
2
nasm -felf64 syscall-001.nasm -o syscall-001.o
ld syscall-001.o -o syscall-001.bin

Flags explained:

Once we run our program with ./syscall-001.bin, we can see that our message “Hack The Planet” is printed, but we get a Segmentation Fault error:

Program execution with segfault

Adding the Exit Syscall

Let’s find the exit syscall number:

1
2
3
cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep exit
#define __NR_exit 60
#define __NR_exit_group 231

The exit syscall is number 60. Let’s check its manual page:

1
man 2 exit
Exit syscall manual page

There’s only one argument: int status (the exit code, like 0 for success or non-zero for errors).

1
void _exit(int status);

Let’s add the exit syscall to our program. We’ll use exit status 1 for testing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
global _start

section .text

_start:
;
; Setting up the registers to print the message
; using the "write" syscall
;
  mov rax, 1          ; syscall number for write
    mov rdi, 1          ; file descriptor 1 = stdout
    mov rsi, msg        ; pointer to message
    mov rdx, length     ; message length
    syscall             ; invoke kernel

;
; Setting up the registers to call the "exit" syscall
; and exit normally with return 1
; Note: if we comment this the process will finish with
; Segmentation Fault error
;
  mov rax, 60         ; syscall number for exit
    mov rdi, 1          ; exit status code
    syscall             ; invoke kernel

section .data
    msg: db 'Hack The Planet',0xa
    length: equ $-msg

Now compile again:

1
2
nasm -felf64 syscall-001.nasm -o syscall-001.o
ld syscall-001.o -o syscall-001.bin

After running ./syscall-001.bin, we can verify the exit status is 1 as we designed:

Program execution with exit status 1

Perfect! No segfault. The program exits cleanly.

Analyzing with strace

If we use strace to analyze this ELF64 binary, we see interesting details. When we run the binary, the shell uses execve with our binary as an argument along with environment variables:

Strace output showing execve
Execve syscall details

Syscall Execution Flow

Here’s how syscalls work under the hood:

Syscall execution flow diagram
Syscall execution details

The process:

  1. User program sets up registers (RAX for syscall number, RDI/RSI/RDX for arguments)
  2. syscall instruction triggers a context switch to kernel mode
  3. Kernel validates arguments and executes the requested operation
  4. Kernel returns control to userland with result in RAX
  5. Program continues execution

Key Takeaways

Practice Exercises

Further Reading