Reading — step 1 of 5
Read
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:
- Saves the user registers.
- Bounds-checks
raxagainst the table size. - Looks up
syscall_table[rax]and calls that handler. - 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 verifybufactually lies in the caller's address space before touching it (Linux'scopy_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
writefor 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…