; ============================================================================= ; syscall.asm - the trap entry, dispatch table, and handlers. ; ; ABI: user loads C = syscall number, args in DE/B/HL, then `rst $30`. ; return value in A. ($30 vector jp's here - see boot.asm) ; ============================================================================= INCLUDE "include/gbos.inc" SECTION "syscall", ROM0 ; ----------------------------------------------------------------------------- SyscallTrap:: ld a, c cp SYS_MAX jr nc, .bad ; index the table WITHOUT touching DE/B (those are syscall args). ld hl, SyscallTable ld a, c add a ; A = C*2 (word entries; C < SYS_MAX) add l ld l, a jr nc, .nocarry inc h .nocarry ld a, [hl+] ld h, [hl] ld l, a jp hl ; tail-call; handler's `ret` returns to user .bad ld a, $FF ; ENOSYS-ish ret ; ----------------------------------------------------------------------------- ; Dispatch table (indexed by syscall number) ; ----------------------------------------------------------------------------- SyscallTable: dw sys_exit ; 0 dw sys_fork ; 1 FORK dw sys_read ; 2 READ dw sys_write ; 3 WRITE dw sys_open ; 4 OPEN dw sys_close ; 5 CLOSE dw sys_exec ; 6 EXEC dw sys_wait ; 7 WAIT dw sys_getpid ; 8 GETPID dw sys_nosys ; 9 KILL (TODO) dw sys_nosys ; 10 BRK (TODO) dw sys_yield ; 11 YIELD dw sys_getb ; 12 GETB dw sys_putb ; 13 PUTB dw sys_list ; 14 LIST dw sys_remove ; 15 REMOVE ; ----------------------------------------------------------------------------- sys_nosys: ld a, $FF ret ; ----------------------------------------------------------------------------- ; sys_write(fd=B, buf=DE, len=B?) -- scaffold: fd ignored, DE=buf, B=len. ; Writes to the serial console. Returns A = bytes written. ; ----------------------------------------------------------------------------- sys_write: ld a, b or a ret z ; len 0 ld c, b ; C = remaining .loop ld a, [de] call PutChar inc de dec c jr nz, .loop ld a, b ; return len ret ; ----------------------------------------------------------------------------- ; sys_read() -> A = one input byte from the console (blocking). ; Uses an external-clock serial transfer as a clean RX: it completes only when ; the host has a byte (no stdout echo). While waiting we yield so other procs ; run. Preserves BC/DE/HL (the context switch saves/restores them). ; ----------------------------------------------------------------------------- sys_read: .poll ld a, $80 ld [rSC], a ; request a byte (external clock) ld a, [rSC] bit 7, a jr z, .got ; bit7 clear => transfer completed call SchedYield ; nothing yet: let others run, then retry jr .poll .got ld a, [rSB] ret ; ----------------------------------------------------------------------------- ; sys_getpid() -> A = current pid ; ----------------------------------------------------------------------------- sys_getpid: ld a, [wCurProc] add PROC_PID ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, [hl] ret ; ----------------------------------------------------------------------------- ; sys_yield() ; ----------------------------------------------------------------------------- sys_yield: call SchedYield ret ; ----------------------------------------------------------------------------- ; sys_exit(code=B) -- become a zombie awaiting reap; never returns. ; - store exit code, set PS_ZOMBIE ; - reparent any children to init (so they can still be waited on) ; - wake our parent if it is blocked in wait() ; - schedule away forever (RAM bank + PCB slot are freed by the reaper) ; ----------------------------------------------------------------------------- sys_exit: ; PROC_STATE = PS_ZOMBIE ld a, [wCurProc] ld l, a ld a, [wCurProc+1] ld h, a ld a, PS_ZOMBIE ld [hl], a ; PROC_EXIT = B ld a, [wCurProc] add PROC_EXIT ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, b ld [hl], a ; stash my pid + parent pid ld a, [wCurProc] add PROC_PID ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, [hl] ld [wExitMyPid], a ld a, [wCurProc] add PROC_PARENT ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, [hl] ld [wExitParentPid], a ; hand any children to init call ReparentToInit ; wake our parent if it's blocked in wait() ld a, [wExitParentPid] call FindPcbByPid ; DE=&parent PCB, CF if gone jr c, .gone ld a, [de] cp PS_BLOCKED jr nz, .gone ld a, PS_READY ld [de], a .gone ; never run this process again .dead call SchedYield jr .dead ; ----------------------------------------------------------------------------- ; sys_wait() -- reap a zombie child. ; out: A = child pid, B = exit code; or A = $FF if we have no children. ; Blocks (PS_BLOCKED) until a child becomes a zombie. ; ----------------------------------------------------------------------------- sys_wait: ld a, [wCurProc] add PROC_PID ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, [hl] ld [wWaitMyPid], a .retry ld c, 0 ; slot cursor ld b, 0 ; "any child exists" flag .scan ld a, c call PcbPtr ; DE=&PCB[c] (preserves BC) ld a, [de] cp PS_FREE jr z, .next ; parent == my pid ? ld h, d ld l, e ld a, l add PROC_PARENT ld l, a ld a, h adc 0 ld h, a ld a, [wWaitMyPid] cp [hl] jr nz, .next ld b, 1 ; we have at least one child ld a, [de] ; state (DE still = base) cp PS_ZOMBIE jr z, .reap .next inc c ld a, c cp MAX_PROCS jr c, .scan ; finished scan: any children? ld a, b or a jr z, .nochild ; children exist but none are zombies: block and yield, then retry ld a, [wCurProc] ld l, a ld a, [wCurProc+1] ld h, a ld a, PS_BLOCKED ld [hl], a call SchedYield ld a, [wCurProc] ld l, a ld a, [wCurProc+1] ld h, a ld a, PS_READY ld [hl], a jr .retry .reap ; DE = &zombie child PCB ld h, d ld l, e inc hl ; ->PROC_PID ld a, [hl] ld [wWaitRetPid], a ld a, e add PROC_EXIT ld l, a ld a, d adc 0 ld h, a ld a, [hl] ld [wWaitRetCode], a ld a, e add PROC_RAMB ld l, a ld a, d adc 0 ld h, a ld a, [hl] call FreeRamBank ; recycle the child's cart-RAM bank ld a, PS_FREE ld [de], a ; free the PCB slot ld a, [wWaitRetCode] ld b, a ld a, [wWaitRetPid] ret .nochild ld a, $FF ret