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:
- Basic knowledge of assembly language (NASM syntax)
- Understanding of x64 registers (RAX, RDI, RSI, etc.)
- NASM assembler and ld linker installed
- Familiarity with C programming helps but isn’t required
sudo apt install nasm
On Fedora/RHEL: sudo yum install nasmObjective
We’ll build a Linux ELF64 binary that uses basic syscalls to print a message on the screen. Along the way, we’ll learn:
- How to find syscall numbers
- How to set up registers for syscall invocation
- Why programs segfault without proper exit handling
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:
| |
The basic write syscall is number 1. Now let’s check the manual to understand its arguments:
| |

As we can see, the write syscall has three arguments:
ssize_t write(int fd, const void *buf, size_t count);
- fd - File descriptor (0=stdin, 1=stdout, 2=stderr)
- buf - Pointer to the data to write
- count - Number of bytes to write
Syscall Register Conventions
Reading the syscall manual, we learn how to set up x64 registers to invoke syscalls:

Register mapping for x64 syscalls:
- RAX - Syscall number
- RDI - First argument
- RSI - Second argument
- RDX - Third argument
- R10 - Fourth argument
- R8 - Fifth argument
- R9 - Sixth argument
Writing the Assembly Code
For our program, we’ll:
- Use file descriptor 1 (stdout) to print to the screen
- Point RSI to our message string “Hack The Planet”
- Set RDX to 16 (the length of our message including newline)
| |
equ $-msg directive calculates the length by subtracting the starting address of msg from the current position $. This automatically gives us the string length.Compiling the Code
Let’s compile and link this assembly code:
| |
Flags explained:
-felf64- Output format is 64-bit ELF (Executable and Linkable Format)ld- Links the object file into an executable binary
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:

Adding the Exit Syscall
Let’s find the exit syscall number:
| |
The exit syscall is number 60. Let’s check its manual page:
| |

There’s only one argument: int status (the exit code, like 0 for success or non-zero for errors).
| |
Let’s add the exit syscall to our program. We’ll use exit status 1 for testing:
| |
Now compile again:
| |
After running ./syscall-001.bin, we can verify the exit status is 1 as we designed:

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:


Syscall Execution Flow
Here’s how syscalls work under the hood:


The process:
- User program sets up registers (RAX for syscall number, RDI/RSI/RDX for arguments)
syscallinstruction triggers a context switch to kernel mode- Kernel validates arguments and executes the requested operation
- Kernel returns control to userland with result in RAX
- Program continues execution
Key Takeaways
- Syscalls are the interface between userland programs and the kernel
- x64 syscall numbers are defined in
/usr/include/x86_64-linux-gnu/asm/unistd_64.h - Register convention: RAX=syscall number, RDI/RSI/RDX/R10/R8/R9 for arguments
- Always call exit to terminate programs gracefully (syscall 60 on x64)
- strace is essential for understanding and debugging syscall behavior
- No C library needed - you can interact directly with the kernel
Practice Exercises
- Modify the program to write to stderr (file descriptor 2) instead of stdout
- Create a program that uses the
readsyscall (number 0) to get user input - Write a program that opens a file using the
opensyscall (number 2) - Add error checking by examining the return value in RAX after each syscall
- Use strace on system utilities like
lsand identify the syscalls they make
Further Reading
- Debugging with strace - Learn to trace and debug syscalls
- Getting Started with GDB - Debug your assembly programs
- GCC Compiling Cheat Sheet - Compilation and linking reference
- Linux Syscall Reference - Complete syscall documentation
- x64 ABI Documentation - Official x86-64 calling conventions