danos/docs/syscall.md

9.8 KiB

System Calls

System calls (syscalls) are the bridge between your programs and the operating system's restricted core (kernel).

Status: danos has real user processes. User programs enter the kernel via the syscall instruction (STAR/LSTAR/SFMASK set per core; the entry stub in isr.s does the swapgs + kernel-stack switch and reuses the interrupt dispatcher); the int 0x80 gate is kept alongside as a minimal test path. The live table is system/abi.zig (private, renumberable — see vdso.md for the public boundary): process lifecycle + threads, memory (mmap/dma/shared-memory), synchronous + async IPC with capability passing, device access, time, the tagged-log diagnostics (debug_write with a level, klog_read/klog_status), and filesystem NAMING (fs_resolve/fs_node/ fs_mount/fs_unmount — the kernel VFS root routes paths and serves the read-only /system initrd mount; file DATA stays with userspace filesystem servers over the vfs-protocol, docs/vfs-protocol.md).

The Mechanism of a Syscall

A system call follows a highly orchestrated, 6-step sequence to ensure hardware safety and process security:

  1. Setting the Arguments: The program places a unique ID corresponding to the requested service (e.g., (sys_write)) into a specific CPU register (like RAX in x86_64) along with its required parameters.
  2. Executing the Trap: The program triggers a special CPU instruction, such as syscall or int 0x80. This acts as a software interrupt.
  3. Mode Switch: The CPU atomically flips its execution privilege from unprivileged User Mode (Ring 3) to the highly privileged Kernel Mode (Ring 0).
  4. Lookup and Execution: The kernel looks up the syscall ID in a dispatch table and runs the designated internal routine to perform the actual work (like fetching data from the hard drive).
  5. Returning the Status: The kernel places the result of the operation—or an error code—back into the RAX register.
  6. Return to User Mode: The kernel executes a return instruction (such as sysret), the CPU shifts back to User Mode, and your program continues executing.

Approaches

There are two approaches to system calls, stable public ABI and unstable private ABI. A public ABI uses a standard defined set of numbers. It makes it easy to guess, easy to write compilers and tools for. private ABI tend to change the numbers to obscure the numbers, preventing attackers from bypassing libc using CPU instructions directly. Private ABIs force users to use a library like libSystem, which the kernel can inject the syscall numbers. An alternative solution is to just have 1 system number but make it generic by the address to a struct in memory.

Practical minimum primitives (Monolithic kernel)

Category Primitive Detail
lifecyle spawn / exec Loads a binary from storage into memory and begins execution.
lifecyle exit Terminates the current process and frees its memory back to the kernel.
I/O read Requests data from a hardware device or file descriptor into user memory.
I/O write Pushes data from user memory out to a device or file descriptor (like a screen).
Memory brk / mmap Requests the kernel to allocate more physical or virtual memory pages to the process.
Control ioctl A catch-all "escape hatch" call to send hardware-specific commands to device drivers.

The Microkernel Minimum Set

Everything else---includingread(),write(),malloc(), andfork()---will run in user space as servers (e.g., a VFS server, a memory manager server) that threads communicate with using these three calls:

  1. IPC_Call(endpoint, message_buffer)(Synchronous Send + Receive)
    • **What it does:**The calling thread sends a message block to a service endpoint and immediately blocks (sleeps) until that service processes the request and sends a reply back.
    • **Why it's minimal:**CombiningSendandReceiveinto a single atomic atomic system call eliminates the need for separate tracking and prevents a massive amount of context-switching overhead. This is the foundation of high-performance microkernels likeseL4and L4.[1,2]
  2. IPC_ReplyWait(endpoint, reply_buffer)(Respond + Wait for Next)
    • **What it does:**Used strictly by your background user-space servers (like your disk driver or filesystem). It sends a reply to the last client that called it, and immediately puts the server to sleep until the next request arrives.[1]
  3. Yield()/Thread_Ctrl()
    • **What it does:**Allows a thread to voluntarily give up its CPU time slice, or allows a root task to spawn/kill threads.
  4. ipc_send(endpoint, message_buffer)(Asynchronous Send)
    • **What it does:**Posts a small payload to an endpoint's bounded queue and returns without blocking — no rendezvous, no reply. The receiver picks it up through the same IPC_ReplyWait, as a buffered message. It is the async counterpart of IPC_Call, for one-to-many broadcasts where a synchronous rendezvous would let one dead or slow receiver hang the sender. The input service — keyboard-event fan-out — is its first user. A full queue drops the oldest message (a buffered message is discrete data, unlike a coalescing interrupt notification).

Hardware Implementation: x86_64 vs. aarch64

Because you are targeting both platforms, you must design a cleanArchitecture Abstraction Layer (AAL). Each architecture uses completely different assembly instructions, CPU registers, and privilege levels to jump from user space (Ring 3 / EL0) to kernel space (Ring 0 / EL1).[1,2,3,4]

Here is how you will map your bare minimum system calls on both architectures:

1. x86_64 Implementation

On 64-bit Intel and AMD processors, you ignore the oldint 0x80software interrupts. Instead, you use the high-performancesyscallandsysretinstructions.[1]

  • **The Trap:**The user-space program executes thesyscallinstruction.[1]
  • **The Registers:**The hardware automatically moves the instruction pointer, but you must pass your arguments in specific registers. A common microkernel convention mimics the System V AMD64 ABI:
    • rax: System Call ID (e.g.,0for IPC_Call,1for IPC_ReplyWait)
    • rdi: Argument 1 (Endpoint ID / Destination)
    • rsi: Argument 2 (Pointer to the message payload buffer)
    • rdx: Argument 3 (Size of the message)[1]
  • **Kernel Setup:**Your kernel must configure the Model Specific Registers (MSRs)---specificallyIA32_STARandIA32_LSTAR---during boot to point to your kernel's system call entry assembly code.[1,2]

2. aarch64 (ARM 64-bit) Implementation

On ARMv8-A and ARMv9-A architectures, privilege levels are called Exception Levels. User space runs atEL0, and your microkernel runs atEL1.[1,2,3,4]

  • **The Trap:**The user-space program executes thesvc #0(Supervisor Call) instruction.[1]
  • **The Registers:**ARM provides a clean, plentiful register set. You typically pass your arguments in the standard parameter registers:
    • x0: System Call ID
    • x1: Argument 1 (Endpoint ID / Destination)
    • x2: Argument 2 (Pointer to message payload buffer)
    • x3: Argument 3 (Size of the message)[1,2]
  • **Kernel Setup:**Your kernel must set up an Exception Vector Table and write its base address to theVBAR_EL1register. Whensvcis executed, the CPU jumps to the synchronous exception offset in that table.[1,2,3]

Managing the Payload Challenge

Because it is a microkernel, performance lives or dies by how fast yourIPC_Callcan move data from Client to Server. You have two minimal choices for handling themessage_bufferpointer:[1]

  • **The Copy Method (Simplest to start):**Your kernel pauses the client, reads the data from the client's memory space, switches page tables to the server, and copies the data into the server's buffer.
  • **The Shared Memory Method (Fastest):**The kernel sets up a temporary, shared virtual memory page between the client and server. The client writes to it, callssyscall/svc, and the server reads it instantly without the kernel copying any bytes