Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Syscall Base

Any OS has to have syscalls — there’s no two ways about it. All of the basic operations we need are syscalls: write, read, open, close. They’re the interface between user programs and the kernel, and without them no program can do anything useful.

On x86 systems, syscalls traditionally use interrupt 0x80 (Linux style). The caller puts a syscall number in eax, arguments in ebx, ecx and edx, then fires int 0x80. The kernel catches it via IDT entry 128, reads the registers, and dispatches to the right handler. We can’t implement full syscalls yet — that needs processes and KFS_5 — but we can build the base infrastructure now so it’s ready to wire up later.

The three stubs we’ll want:

  • sys_write (0) — write to screen
  • sys_read (1) — read from keyboard
  • sys_exit (2) — terminate process

The Syscall Table

The syscall system is structurally very similar to the signal system — a fixed array of function pointers indexed by number, a register function, and a dispatcher. The key difference is what the index means: signal table indices are interrupt numbers, syscall table indices are syscall numbers read from eax.

#![allow(unused)]
fn main() {
type SyscallHandler = fn(frame: &InterruptFrame);
static mut SYSCALL_TABLE: [Option<SyscallHandler>; 3] = [None; 3];
}

Initialisation registers the stubs in the syscall table, then registers the syscall dispatcher itself with the signal system at interrupt 0x80:

#![allow(unused)]
fn main() {
pub fn init() {
    register(0, handlers::sys_write);
    register(1, handlers::sys_read);
    register(2, handlers::sys_exit);
    signals::register(0x80, syscall_handler); // hook into IDT via signal system
}
}

Notice two register functions — signals::register puts the dispatcher into the SIGNAL_TABLE at slot 128, and our own register puts individual syscall handlers into the SYSCALL_TABLE:

#![allow(unused)]
fn main() {
pub fn register(syscall_num: usize, handler: SyscallHandler) {
    unsafe {
        SYSCALL_TABLE[syscall_num] = Some(handler);
    }
}
}

The Syscall Dispatcher

When int 0x80 fires, the signal system calls syscall_handler with the full InterruptFrame. The dispatcher reads eax to get the syscall number and looks up the right handler:

#![allow(unused)]
fn main() {
pub fn syscall_handler(_interrupt: u32, frame: &InterruptFrame) {
    let syscall_num = frame.eax as usize;
    if syscall_num >= 3 {
        println!("Unknown syscall: {}", syscall_num);
        return;
    }
    unsafe {
        if let Some(handler) = SYSCALL_TABLE[syscall_num] {
            handler(frame);
        }
    }
}
}

The bounds check is important — without it, a caller passing an invalid eax would cause an out-of-bounds panic. Unknown syscalls just print an error and return.

The Stub Handlers

For now the handlers are stubs. sys_write is the most fleshed out — it reads its arguments from ebx, ecx, edx and prints them:

#![allow(unused)]
fn main() {
pub fn sys_write(frame: &InterruptFrame) {
    println!("{} {} {}", frame.ebx, frame.ecx, frame.edx);
    // TODO: return value via eax — needs mutable frame (KFS_5)
}

pub fn sys_read(frame: &InterruptFrame) {
    // TODO: KFS_5 — read from keyboard buffer into user buffer
}

pub fn sys_exit(frame: &InterruptFrame) {
    // TODO: KFS_5 — terminate the calling process
}
}

The commented out return value is worth noting. In a real syscall, the return value goes back in eax. But our InterruptFrame is immutable — we’d need to either make it mutable or use a different mechanism to write back to the caller’s registers. That’s a KFS_5 problem.

Testing

Wired up a sys call command in NPS that constructs a test frame and fires syscall 0:

sys call
Testing syscall 0 (sys_write)...
1 2 3

ebx=1, ecx=2, edx=3 came through correctly. The dispatch table is working.