92 lines
9.6 KiB
Markdown
92 lines
9.6 KiB
Markdown
# 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 (M3). 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
|
|
> current call set is still a placeholder — `0 = exit(code)`, `1 = ping`,
|
|
> `2 = write(ptr, len)`, `3 = sleep(ms)` (see `system/kernel/process.zig`); the
|
|
> handler dispatches on whether the caller is a scheduled process (its own address
|
|
> space) or a borrowed test thread. The microkernel set below (IPC_Call /
|
|
> IPC_ReplyWait / Yield) replaces it once a second user server exists.
|
|
|
|
## 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---including`read()`,`write()`,`malloc()`, and`fork()`---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:**Combining*Send*and*Receive*into 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 like[seL4](https://sel4.systems/)and L4.[[1](https://en.wikipedia.org/wiki/L4_microkernel_family),[2](https://www.microchip.com/en-us/products/microprocessors/64-bit-mpus/pic64hx/ecosystem)]
|
|
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](https://news.ycombinator.com/item?id=33078441)]
|
|
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](input.md) — 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 clean**Architecture 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](https://android.googlesource.com/kernel/common/+/84d3e59750bbd/arch/arm64/Kconfig),[2](https://blog.codingconfessions.com/p/making-system-calls-in-x86-64-assembly),[3](https://alex.dzyoba.com/blog/os-segmentation/),[4](https://dev.to/ripan030/linux-kernel-interrupt-handling-part-2-fe1)]
|
|
|
|
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 old`int 0x80`software interrupts. Instead, you use the high-performance`syscall`and`sysret`instructions.[[1](https://alex.dzyoba.com/blog/os-segmentation/)]
|
|
|
|
- **The Trap:**The user-space program executes the`syscall`instruction.[[1](https://namastedev.com/blog/kernel-vs-user-space-2/)]
|
|
- **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.,`0`for IPC_Call,`1`for 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](https://dev.to/kaamkiya/hello-world-in-assembly-x86-64-2kb8)]
|
|
- **Kernel Setup:**Your kernel must configure the Model Specific Registers (MSRs)---specifically`IA32_STAR`and`IA32_LSTAR`---during boot to point to your kernel's system call entry assembly code.[[1](https://nfil.dev/kernel/rust/coding/rust-kernel-to-userspace-and-back/),[2](https://johannst.github.io/notes/arch/x86_64.html)]
|
|
|
|
2\. aarch64 (ARM 64-bit) Implementation
|
|
|
|
On ARMv8-A and ARMv9-A architectures, privilege levels are called Exception Levels. User space runs at**EL0**, and your microkernel runs at**EL1**.[[1](https://community.nxp.com/pwmxy87654/attachments/pwmxy87654/imx-processors/183079/1/AN12212.pdf),[2](https://developer.arm.com/-/media/Arm%20Developer%20Community/PDF/Learn%20the%20Architecture/Exception%20model.pdf?revision=a62f2bf2-b08a-4a4f-8cbe-38c67ddf4434),[3](https://people.kernel.org/linusw/),[4](https://drewdevault.com/blog/Helios-aarch64/)]
|
|
|
|
- **The Trap:**The user-space program executes the`svc #0`(Supervisor Call) instruction.[[1](https://hackmd.io/@xlYUTygoRkyuQQlwXuWDWQ/SJWQuIsIZe)]
|
|
- **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](https://github.com/lelegard/arm-cpusysregs),[2](https://medium.com/@vincentcorbee/http-server-in-arm64-assembly-apple-silicon-m1-077a55bbe9ca)]
|
|
- **Kernel Setup:**Your kernel must set up an Exception Vector Table and write its base address to the`VBAR_EL1`register. When`svc`is executed, the CPU jumps to the synchronous exception offset in that table.[[1](https://dev.to/ripan030/linux-kernel-interrupt-handling-part-2-fe1),[2](https://eastrivervillage.com/blog/archive/2018/06/),[3](https://www.wadixtech.com/blog/armv8-a-exception-levels-el0-to-el3)]
|
|
|
|
* * * * *
|
|
|
|
Managing the Payload Challenge
|
|
|
|
Because it is a microkernel, performance lives or dies by how fast your`IPC_Call`can move data from Client to Server. You have two minimal choices for handling the`message_buffer`pointer:[[1](https://anazimzada2020.medium.com/microkernel-architectural-pattern-5e4e9184170e)]
|
|
|
|
- **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, calls`syscall`/`svc`, and the server reads it instantly without the kernel copying any bytes |