; ============================================================================= ; 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_kill ; 9 KILL 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 dw sys_putc ; 16 PUTC dw sys_setin ; 17 SETIN dw sys_setout ; 18 SETOUT dw sys_lookup ; 19 LOOKUP dw sys_ps ; 20 PS dw sys_progname ; 21 PROGNAME dw sys_reap ; 22 REAP dw sys_bfill ; 23 BFILL (debug) dw sys_bpeek ; 24 BPEEK (debug) ; ----------------------------------------------------------------------------- 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 .loop ld a, [de] push de push bc ld e, a call KPutc ; route the byte to this process's stdout pop bc pop de inc de dec b jr nz, .loop ret ; ----------------------------------------------------------------------------- ; CurStdin / CurStdout -> A = current process PROC_STDIN / PROC_STDOUT ; ----------------------------------------------------------------------------- CurStdin: ld a, [wCurProc] add PROC_STDIN ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, [hl] ret CurStdout: ld a, [wCurProc] add PROC_STDOUT ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, [hl] ret ; ----------------------------------------------------------------------------- ; KGetc -> A = byte, CF set on EOF. Reads this process's stdin (console/file). ; ----------------------------------------------------------------------------- KGetc:: call CurStdin cp STD_CONSOLE jr z, .console ld b, a jp sys_getb ; B=fd -> A=byte, CF=EOF .console .poll ld a, $80 ld [rSC], a ld a, [rSC] bit 7, a jr z, .got call SchedYield jr .poll .got ld a, [rSB] and a ; CF = 0 ret ; ----------------------------------------------------------------------------- ; KPutc(E = byte) - write to this process's stdout (console/file). ; ----------------------------------------------------------------------------- KPutc:: ld a, e ld [wKChar], a call CurStdout cp STD_CONSOLE jr z, .console ld b, a ; fd ld a, [wKChar] ld e, a jp sys_putb ; B=fd, E=byte .console ld a, [wKChar] ld [rSB], a ld a, $81 ld [rSC], a ret ; ----------------------------------------------------------------------------- ; sys_putc(E = byte), sys_setin/sys_setout(B = fd) ; ----------------------------------------------------------------------------- sys_putc: call KPutc ret sys_setin: ld a, [wCurProc] add PROC_STDIN ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, b ld [hl], a ret sys_setout: ld a, [wCurProc] add PROC_STDOUT ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, b ld [hl], a ret ; ----------------------------------------------------------------------------- ; sys_lookup(DE = name) -> A = program id, or $FF if not a known command. ; ----------------------------------------------------------------------------- sys_lookup: ld h, d ld l, e ; HL = query name ld de, NameTable .l ld a, [de] or a jr z, .none push hl push de call StrEqual ; HL=query vs DE=table name -> Z if equal pop de pop hl jr z, .match call SkipName ; DE -> NUL inc de ; -> id inc de ; -> next entry jr .l .match call SkipName inc de ; -> id ld a, [de] ret .none ld a, $FF 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: call KGetc ; A=byte, CF=EOF (file); console EOF is $04 ret nc ld a, $04 ; file EOF -> EOT 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 ; ============================================================================= ; sys_kill(B = pid) -> A = 0 ok / $FF fail. pid 1 (init/shell) is immortal. ; Terminates the target: mark zombie, reparent its children to init, wake its ; parent if blocked in wait. If we killed ourselves, schedule away forever. ; ============================================================================= sys_kill:: ld a, b cp INIT_PID jr z, .fail ; init is immortal call FindPcbByPid ; A=pid -> DE=&PCB, CF if not found (B preserved=pid) jr c, .fail ld a, [de] ; state cp PS_FREE jr z, .fail cp PS_ZOMBIE jr z, .fail ; already dead ; mark ZOMBIE ld a, PS_ZOMBIE ld [de], a ; PROC_EXIT = KILL_CODE ld a, e add PROC_EXIT ld l, a ld a, d adc 0 ld h, a ld a, KILL_CODE ld [hl], a ; victim's parent pid ld a, e add PROC_PARENT ld l, a ld a, d adc 0 ld h, a ld a, [hl] ld [wKillParent], a ; reparent victim's children to init (B still = victim pid) ld a, b ld [wExitMyPid], a call ReparentToInit ; wake victim's parent if it is blocked in wait() ld a, [wKillParent] call FindPcbByPid jr c, .maybe_self ld a, [de] cp PS_BLOCKED jr nz, .maybe_self ld a, PS_READY ld [de], a .maybe_self ; if the victim is the current process, don't return to it ld a, [wCurProc] add PROC_PID ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, [wExitMyPid] cp [hl] jr nz, .ok .dead call SchedYield jr .dead .ok xor a ret .fail ld a, $FF ret ; ============================================================================= ; sys_ps(B = slot, DE = buf[3]) -> A = 1 if used (buf = pid,state,prog), else 0 ; ============================================================================= sys_ps:: ld a, e ld [wPsBuf], a ld a, d ld [wPsBuf+1], a ld a, b cp MAX_PROCS jr nc, .no call PcbPtr ; DE = &PCB[slot] ld a, [de] ; state cp PS_FREE jr z, .no ld a, [wPsBuf] ld l, a ld a, [wPsBuf+1] ld h, a ; HL = buf push de ; PCB ld a, e add PROC_PID ld e, a ld a, d adc 0 ld d, a ld a, [de] ; pid ld [hl+], a pop de push de ld a, [de] ; state ld [hl+], a pop de ld a, e add PROC_PROG ld e, a ld a, d adc 0 ld d, a ld a, [de] ; prog id ld [hl], a ld a, 1 ret .no xor a ret ; ============================================================================= ; sys_progname(B = id, DE = namebuf) - copy the command name for a program id. ; ============================================================================= sys_progname:: ld hl, NameTable .l ld a, [hl] or a jr z, .none ld a, l ld [wPnStart], a ld a, h ld [wPnStart+1], a .sk ld a, [hl+] or a jr nz, .sk ; HL now at the id byte ld a, [hl] cp b jr z, .match inc hl ; skip id -> next entry jr .l .match ld a, [wPnStart] ld l, a ld a, [wPnStart+1] ld h, a ld b, 8 .cp ld a, [hl+] ld [de], a inc de or a jr z, .done dec b jr nz, .cp .done ret .none ld a, $3F ; '?' ld [de], a inc de xor a ld [de], a ret ; ============================================================================= ; sys_reap() -> A = pid of a reaped zombie child, or 0 (never blocks). ; Used by the shell to clean up finished background jobs. ; ============================================================================= sys_reap:: ld a, [wCurProc] add PROC_PID ld l, a ld a, [wCurProc+1] adc 0 ld h, a ld a, [hl] ld [wWaitMyPid], a ld c, 0 .scan ld a, c call PcbPtr ; DE = &PCB[c] ld a, [de] cp PS_ZOMBIE jr nz, .next 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 ; reap: pid, free bank, free slot ld h, d ld l, e inc hl ; PROC_PID ld a, [hl] ld [wWaitRetPid], a ld a, e add PROC_RAMB ld l, a ld a, d adc 0 ld h, a ld a, [hl] call FreeRamBank ld a, PS_FREE ld [de], a ld a, [wWaitRetPid] ret .next inc c ld a, c cp MAX_PROCS jr c, .scan xor a ret