ARM64 OS Handbook
🔍

Chapter 14: System Calls

What You Will Learn in This Chapter
  • What system calls are and how they work
  • How the SVC instruction triggers a system call
  • How to pass arguments and return values
  • How to implement a system call dispatch table
  • How to write system call handlers
  • How user-space programs make system calls

14.1 What is a System Call?

A system call is the mechanism by which a user-space program (EL0) requests a service from the kernel (EL1). System calls are the only way for user code to perform privileged operations like reading files, allocating memory, or sending network data.

Without system calls, user programs cannot access hardware or kernel memory. This is by design: it prevents buggy or malicious programs from crashing the system.

14.2 The SVC Instruction

On ARM64, system calls are triggered by the SVC (Supervisor Call) instruction. When a user program executes SVC #n:

  1. The CPU saves the return address in ELR_EL1
  2. The CPU saves the processor state in SPSR_EL1
  3. The CPU sets ESR_EL1.EC to 0x15 (SVC from AArch64)
  4. The CPU transitions to EL1 and jumps to the synchronous exception vector
  5. The kernel examines ESR_EL1 to determine it is an SVC
  6. The kernel reads the syscall number from x0 and arguments from x1-x6
  7. The kernel calls the appropriate handler
  8. The kernel puts the return value in x0
  9. The kernel executes ERET to return to user space

14.3 System Call Convention

Our kernel follows a simple convention for system calls:

RegisterUsage
x0System call number
x1Argument 1
x2Argument 2
x3Argument 3
x4Argument 4
x5Argument 5
x0 (return)Return value (0 = success, negative = error)

The SVC immediate value (#n) is not used on ARM64 Linux convention either; we put the syscall number in x0 to keep it simple.

14.4 System Call Numbers

We define our system call numbers in a header:

#define SYS_exit     0
#define SYS_write    1
#define SYS_read     2
#define SYS_open     3
#define SYS_close    4
#define SYS_fork     5
#define SYS_sbrk     6  /* allocate heap memory */
#define SYS_sleep    7
#define SYS_getpid   8
#define SYS_uptime   9

14.5 The System Call Handler

/* Called from the synchronous exception handler when EC=0x15 */
void handle_syscall(void) {
    uint64_t nr, ret;

    /* Read syscall number and arguments from registers */
    /* These were saved on the stack by the exception handler */
    __asm__("str x0, %0" : "=m" (nr));  /* syscall number from saved context */
    /* In practice, the exception handler passes a pointer to the saved regs */

    if (nr >= MAX_SYSCALLS || syscall_table[nr] == NULL) {
        ret = -1;  /* unknown syscall */
    } else {
        ret = syscall_table[nr](arg1, arg2, arg3, arg4, arg5);
    }

    /* Store return value in x0 (in the saved context) */
    /* When ERET restores context, x0 will contain the return value */
}

The dispatch table is an array of function pointers:

typedef long (*syscall_fn)(long, long, long, long, long);

static long sys_exit(long code, long a2, long a3, long a4, long a5) {
    /* Terminate the current process */
    process_exit(current_process, code);
    return 0;  /* never reaches here */
}

static long sys_write(long fd, long buf, long count, long a4, long a5) {
    /* Write to a file descriptor */
    return do_write((int)fd, (const void *)buf, (size_t)count);
}

syscall_fn syscall_table[MAX_SYSCALLS] = {
    [SYS_exit]   = sys_exit,
    [SYS_write]  = sys_write,
    [SYS_read]   = sys_read,
    [SYS_open]   = sys_open,
    [SYS_close]  = sys_close,
    /* ... */
};

14.6 User-Space Side

User-space programs need a way to make system calls. In a real OS, the C library (libc) provides wrapper functions. Since we have no libc, we write inline assembly wrappers or a minimal user-space header:

/* user/syscall.h */
static inline long syscall(long nr, long a1, long a2, long a3,
                           long a4, long a5) {
    long ret;
    register long r0 __asm__("x0") = nr;
    register long r1 __asm__("x1") = a1;
    register long r2 __asm__("x2") = a2;
    register long r3 __asm__("x3") = a3;
    register long r4 __asm__("x4") = a4;
    register long r5 __asm__("x5") = a5;

    __asm__ volatile(
        "svc #0\n"
        : "=r" (r0)
        : "r" (r0), "r" (r1), "r" (r2), "r" (r3), "r" (r4), "r" (r5)
        : "memory"
    );
    return r0;
}

static inline long write(int fd, const void *buf, unsigned long count) {
    return syscall(SYS_write, fd, (long)buf, count, 0, 0);
}

14.7 System Call Flow (Complete)

  1. User program calls write(1, "Hello", 5)
  2. The write wrapper puts SYS_write in x0, arguments in x1-x5, executes SVC
  3. CPU transitions to EL1, saves context, jumps to sync vector
  4. Handler reads ESR_EL1, sees EC=0x15, calls handle_syscall
  5. handle_syscall indexes syscall_table, calls sys_write
  6. sys_write calls do_write which talks to the UART driver
  7. Return value goes in x0 (in saved context)
  8. Exception handler restores context, ERET to user space
  9. User program reads "Hello" written to output

14.8 Our Implementation

Our kernel implements system calls using this architecture:

  • vectors.S: synchronous exception vector calls sync_handler
  • entry.S: saves context, calls C dispatcher
  • syscall.c: handles the SVC exception, dispatches to table
  • syscall.h: syscall numbers and C prototypes for handlers
  • user/syscall.h: user-space header with syscall wrappers
/* In syscall.c */
void syscall_handler(struct saved_context *ctx) {
    uint64_t nr = ctx->x0;
    if (nr >= MAX_SYSCALLS || !syscall_table[nr]) {
        ctx->x0 = -1;
        return;
    }
    ctx->x0 = syscall_table[nr](ctx->x1, ctx->x2, ctx->x3, ctx->x4, ctx->x5);
}

The saved_context struct is the 34-register save we created in Chapter 11. By modifying ctx->x0 before restoring context, the return value is automatically placed in x0 when ERET executes.

14.9 Exercises

Exercise 1: Add a Syscall

Add a system call SYS_uptime that returns the number of timer ticks since boot. Add the handler and test it from user space.

Exercise 2: Syscall with String Argument

Write a syscall that takes a user-space string pointer and prints it. The kernel must copy the string from user space to kernel space safely.

14.10 Summary

System calls are the interface between user space and kernel space. The SVC instruction triggers a synchronous exception that transitions from EL0 to EL1. The kernel dispatches to the correct handler based on the syscall number in x0, performs the requested operation, and returns the result in x0. On return via ERET, execution resumes in user space with the return value available. This mechanism is the foundation for all user-space interaction with the operating system.