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 screensys_read(1) — read from keyboardsys_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.