Skip to content
System Calls
step 1/5

Reading — step 1 of 5

Read

~3 min readSystem Calls & I/O

System Calls

Ring 3 can't touch hardware, can't map memory, can't even exit cleanly — so how does any program ever do anything? It asks. The system call is the kernel's public API: the only set of doors from user space into ring 0, every one of them installed, numbered, and guarded by the kernel itself.

The mechanics, instruction by instruction

On x86-64, a user program makes a syscall like this:

mov  rax, 1        ; syscall number  (1 = write on Linux)
mov  rdi, 1        ; arg1: fd        (stdout)
mov  rsi, msg      ; arg2: buffer
mov  rdx, 14       ; arg3: length
syscall            ; cross the boundary
; on return: rax = bytes written, or negative errno

The syscall instruction does, atomically: switch to ring 0 and jump to one fixed address — the kernel's syscall entry stub, whose address is registered in an MSR at boot. (Unlike an interrupt gate, syscall does not switch the stack pointer itself; the entry stub switches to the kernel stack manually as its first job.) Note what the user program did not choose: where execution lands. It chose only the number in rax. The kernel's entry stub then:

  1. Saves the user registers.
  2. Bounds-checks rax against the table size.
  3. Looks up syscall_table[rax] and calls that handler.
  4. Puts the result in rax, restores state, sysrets back to ring 3.

Step 3 is the heart, and it's exactly what your exercise builds: a dispatch table — register handlers under numbers, dispatch incoming calls by number, and reject unknown numbers cleanly. In Linux that rejection has a name: -ENOSYS, the return value for "no such syscall." Every OS you've used boots up and fills in this exact table.

Why a table and not just… calling functions?

Three reasons, each load-bearing:

  • Privilege: user code cannot call kernel functions directly — it can't even see kernel memory (the User bit from the paging lesson). The table lives in the kernel; the user supplies only an index.
  • Validation: every pointer argument is hostile until proven otherwise. A write(fd, buf, len) handler must verify buf actually lies in the caller's address space before touching it (Linux's copy_from_user), or user programs could trick the kernel into reading/writing kernel memory for them. The syscall boundary is a trust boundary, and the table is its checkpoint.
  • ABI stability: numbers are forever. Linux syscall 1 has meant write for decades; numbers are never reused even when calls are retired. Recompile nothing, run binaries from 2003 — because the table's meaning is a promise.

The cost of asking

A syscall is a controlled context switch into the kernel: mode switch, stack switch, register save/restore — hundreds of nanoseconds before the handler does any work, plus the same cache effects you costed in the context-switch lesson. This is why real programs batch: one write of 64 KiB beats 64K one-byte writes by three orders of magnitude, and why interfaces like io_uring exist to amortize the crossing. libc hides the raw instruction from you (printf → buffering → eventually one write), but strace ./yourprogram strips the costume: every line it prints is one row of the dispatch table being hit.

Run strace true sometime — even doing nothing takes ~20 syscalls. The table is the busiest data structure in the operating system, and after this exercise, it's one you've built.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…