diff options
| author | user <user@clank> | 2026-09-24 12:48:37 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-24 12:48:37 +0200 |
| commit | dc9dffb9069e32a1a0cf14883176544b234fc3e5 (patch) | |
| tree | 4877daa5f01e98ac5d7e64d98c4144c94fa94a91 | |
| parent | term: write the palette attribute for every re-rendered tile, not just the cu... (diff) | |
| download | gbos-dc9dffb9069e32a1a0cf14883176544b234fc3e5.tar.gz gbos-dc9dffb9069e32a1a0cf14883176544b234fc3e5.tar.xz gbos-dc9dffb9069e32a1a0cf14883176544b234fc3e5.zip | |
kernel: stat/fsstat/isatty syscalls, and O_APPEND
Four things userland had no way to ask for:
- SYS_STAT: type, link count, size and inode for a path. The trap needs HL
for its dispatch table, so a call taking both a path and an output buffer
passes a request block - the shape SYS_NET already established. usr/stat.h
wraps it, header-only like sock.h, because the block must live in the
caller's RAM.
- SYS_FSSTAT: blocks/inodes, total and free. The bitmaps are already cached
write-through in fs_bmbuf, so this is the popcount boot_fs already prints.
- SYS_ISATTY: is this process's stdin the console? The shell needs it to tell
an interactive session from a script.
- O_APPEND: open a file positioned at EOF. There is no seek syscall, so this
is the only way to add to a file - without it nothing running ON the Game
Boy can build a multi-line file, shell scripts included.
Together these back ls -l, df, and `>>`.
| -rw-r--r-- | docs/internals.md | 13 | ||||
| -rw-r--r-- | include/gbos.inc | 25 | ||||
| -rw-r--r-- | src/fs.asm | 128 | ||||
| -rw-r--r-- | src/kdata.asm | 1 | ||||
| -rw-r--r-- | src/syscall.asm | 16 | ||||
| -rw-r--r-- | usr/gbos.h | 8 | ||||
| -rw-r--r-- | usr/libc.s | 20 | ||||
| -rw-r--r-- | usr/stat.h | 36 |
8 files changed, 242 insertions, 5 deletions
diff --git a/docs/internals.md b/docs/internals.md index 3e27bd2..f9c5899 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -234,6 +234,13 @@ blocks 4.. data blocks the damage once something else touched the neighbouring inode. - **One-block data cache.** Two files open at once thrash it: `cp` of a 3 KiB file alternates read/write blocks per byte and takes ~25s. Correct, slow. +- **Open modes** are read / write / append. There is no seek syscall, so + `O_APPEND` (open at EOF) is the only way to add to a file - which is what + makes `>>`, and therefore building a multi-line script on the device, + possible at all. +- **`sys_stat` / `sys_fsstat`** back `ls -l` and `df`. The syscall trap needs + HL for its dispatch table, so a call taking both a path and an output + buffer passes a request block, the shape SYS_NET already uses. - **Paths + per-process cwd.** `open`/`mkdir`/`chdir`/`remove`/`ls` take paths (absolute `/a/b` or relative `a/b`); each process has a cwd inode (`PROC_CWD`, inherited on fork). Shell builtins: `cd`, and a cwd-aware prompt. @@ -271,6 +278,12 @@ gbos programs are written in **C**, compiled with **SDCC** (the `sm83` port): Arguments: the shell leaves `"cmd\0args\0"` at `$A000`; the child inherits it through `fork`, `getargs()` returns the raw string, `argv_parse()` tokenizes. +**Scripts.** `sh` is the same program interactively and as a script runner: it +asks `isatty()` (SYS_ISATTY: is stdin the console?) and, when stdin is a file, +drops the prompt and the echo. EOF ends that shell - only pid 1 on the console +treats EOF as "power the machine off", which is why a script used to shut the +Game Boy down when it reached the end. init runs `/rc` at boot if it exists. + **Toolchain gotchas found the hard way:** SDCC's `--asm=rgbds` mode mis-orders instructions (emits `ld [hl],a` before the `ld hl,sp+0` that sets the pointer) — so we use the native asxxxx path and INCBIN the blob instead. String diff --git a/include/gbos.inc b/include/gbos.inc index eabdcec..76981da 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -123,6 +123,9 @@ DEF PROC_SIZE RB 0 DEF KILL_CODE EQU 137 ; exit status of a killed process (128 + SIGKILL) +DEF O_READ EQU 0 +DEF O_WRITE EQU 1 +DEF O_APPEND EQU 2 ; write, keeping what is there (pos starts at EOF) DEF STD_CONSOLE EQU $FF ; ----------------------------------------------------------------------------- @@ -165,7 +168,20 @@ DEF SYS_NET EQU 33 ; socket layer (DE=&netreq -> A=resu DEF SYS_SLEEP EQU 34 ; cooperative delay (B = 1/64-second units) DEF SYS_POLLCON EQU 35 ; poll console ring (link-injected -> A=byte or 0) DEF SYS_UPTIME EQU 36 ; copy the 64 Hz tick counter (4B LE) to [DE] -DEF SYS_MAX EQU 37 +DEF SYS_STAT EQU 37 ; stat a path (DE=&statreq -> A=0/$FF) +DEF SYS_FSSTAT EQU 38 ; filesystem totals (DE=buf[4] -> A=0) +DEF SYS_ISATTY EQU 39 ; is stdin the console? (-> A=1/0) +DEF SYS_MAX EQU 40 + +; ---- SYS_STAT request block (see fs.asm sys_stat) --------------------------- +; The syscall trap needs HL for its dispatch table, so a call that takes both a +; path and an output buffer passes a request block, like SYS_NET does. +DEF SR_PATH EQU 0 ; 2: pointer to the path (in) +DEF SR_TYPE EQU 2 ; 1: IT_FILE / IT_DIR (out) +DEF SR_NLINK EQU 3 ; 1 (out) +DEF SR_SIZE EQU 4 ; 2: bytes, little-endian(out) +DEF SR_INO EQU 6 ; 1: inode number (out) +DEF SR_SIZE_ EQU 7 ; sizeof(statreq) ; ---- socket layer (SYS_NET) -------------------------------------------------- ; netreq struct (in caller RAM; DE points to it): @@ -307,5 +323,12 @@ DEF PROG_CP EQU 35 DEF PROG_NC EQU 36 DEF PROG_HTTPD EQU 37 DEF PROG_IRCD EQU 38 +DEF PROG_DF EQU 39 +DEF PROG_MV EQU 40 +DEF PROG_SLEEP EQU 41 +DEF PROG_TEE EQU 42 +DEF PROG_TAIL EQU 43 +DEF PROG_HELP EQU 44 +DEF PROG_POWEROFF EQU 45 ENDC @@ -698,7 +698,7 @@ sys_open:: .write ld a, [wFsInode] or a - jr nz, .trunc + jr nz, .exists ; --- create --- call alloc_inode or a @@ -724,6 +724,10 @@ sys_open:: or a jr z, .fail jr .allocof +.exists + ld a, [wFsMode] + cp O_APPEND + jr z, .allocof ; append: keep the data, seek to the end below .trunc ld a, [wFsInode] call file_truncate @@ -755,6 +759,34 @@ sys_open:: xor a ld [hl+], a ; pos lo ld [hl], a ; pos hi + ld a, [wFsMode] + cp O_APPEND + jr nz, .ofdone + ; append: start writing at the end. With no seek syscall this is the only + ; way to add to a file - and without it nothing on the Game Boy can build + ; a multi-line file, shell scripts included. + push bc + dec hl ; HL = &pos lo + push hl + ld a, [wFsInode] + call inode_ptr + ld a, l + add I_SIZE + ld l, a + ld a, h + adc 0 + ld h, a + ld a, [hl+] + ld c, a + ld a, [hl] + ld b, a ; BC = size + pop hl + ld a, c + ld [hl+], a + ld a, b + ld [hl], a ; pos = size + pop bc +.ofdone ld a, c ret @@ -1162,6 +1194,100 @@ sys_chdir:: ret ; ============================================================================= +; sys_stat(DE = &statreq) -> A = 0 ok / $FF no such path +; +; req[SR_PATH] is a pointer to the path; the type, link count, size and inode +; come back in the same block. (The syscall trap needs HL for its dispatch +; table, so a call with both a path and an output buffer uses a request block - +; the shape SYS_NET already established.) +; ============================================================================= +sys_stat:: + ld a, e + ld [wFsStatReq], a + ld a, d + ld [wFsStatReq+1], a + ld h, d + ld l, e + ld a, [hl+] ; DE = req.path + ld e, a + ld a, [hl] + ld d, a + call copy_path + call resolve ; A = inode or 0 (handles "/", "." and paths) + or a + jr z, .fail + ld [wFsInode], a + call inode_ptr ; HL = &inode: type, nlink, size lo, size hi + ld a, [wFsStatReq] + add SR_TYPE + ld e, a + ld a, [wFsStatReq+1] + adc 0 + ld d, a ; DE = &req[SR_TYPE] + ld b, 4 +.cpst + ld a, [hl+] + ld [de], a + inc de + dec b + jr nz, .cpst + ld a, [wFsInode] + ld [de], a ; SR_INO follows SR_SIZE + xor a + ret +.fail + ld a, $FF + ret + +; ============================================================================= +; sys_fsstat(DE = buf[4]) -> buf = { blocks total, blocks free, +; inodes total, inodes free } +; The bitmaps are already cached in fs_bmbuf (write-through), so this is just +; a popcount - the same one boot_fs prints at startup. +; ============================================================================= +sys_fsstat:: + ld a, FS_NBLOCKS + ld [de], a + inc de + ld hl, fs_bmbuf + BM_BLOCKS + ld b, FS_NBLOCKS / 8 + call bm_free + ld a, c + ld [de], a + inc de + ld a, NINODES + ld [de], a + inc de + ld hl, fs_bmbuf + BM_INODES + ld b, NINODES / 8 + call bm_free + ld a, c + ld [de], a + xor a + ret + +; bm_free - HL = bitmap, B = bytes -> C = count of CLEAR bits (= free units). +; Preserves DE (the caller is walking its output buffer with it). +bm_free: + ld c, 0 +.byte + ld a, [hl+] + cpl ; set bits = free + push de + ld e, 8 +.bit + rra + jr nc, .used + inc c +.used + dec e + jr nz, .bit + pop de + dec b + jr nz, .byte + ret + +; ============================================================================= ; sys_opendir(DE = path) -> A = 0 ok / $FF ; sets the directory ls enumerates. ; Empty path or "." means the current directory. ; ============================================================================= diff --git a/src/kdata.asm b/src/kdata.asm index 7a6aab9..b6c2b68 100644 --- a/src/kdata.asm +++ b/src/kdata.asm @@ -53,6 +53,7 @@ wFsDir:: DS 1 ; directory inode for dir_* ops wFsDirBlk:: DS 1 ; that directory's data block (for write-back) wFsCur:: DS 1 ; current inode during path resolution wFsPath:: DS 40 ; kernel-side copy of a path argument +wFsStatReq:: DS 2 ; sys_stat: the caller's request block wFsSlashPtr::DS 2 ; last '/' seen while splitting a path wListDir:: DS 1 ; directory sys_list enumerates wKChar:: DS 1 ; scratch for KPutc diff --git a/src/syscall.asm b/src/syscall.asm index 0ed9f85..9e5ca33 100644 --- a/src/syscall.asm +++ b/src/syscall.asm @@ -71,6 +71,9 @@ SyscallTable: dw sys_sleep ; 34 SLEEP dw sys_pollcon ; 35 POLLCON dw sys_uptime ; 36 UPTIME + dw sys_stat ; 37 STAT + dw sys_fsstat ; 38 FSSTAT + dw sys_isatty ; 39 ISATTY ; ----------------------------------------------------------------------------- sys_nosys: @@ -332,6 +335,19 @@ sys_sleep: ret ; ----------------------------------------------------------------------------- +; sys_isatty() -> A = 1 if this process's stdin is the console, 0 if it is a +; file or a pipe. The shell uses it to tell an interactive session from a +; script (`sh < file`): no prompt, no echo, and EOF is just the end. +; ----------------------------------------------------------------------------- +sys_isatty: + call CurStdin + cp STD_CONSOLE + ld a, 0 + ret nz + inc a + 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) @@ -20,6 +20,9 @@ unsigned char pollin(void); /* poll OSK for a typed char, or 0 */ unsigned char pollcon(void); /* poll console ring (link-injected input, filled by net pumps), or 0 */ unsigned char sys_net(void *req); /* socket layer trap (see sock.h) */ +unsigned char sys_stat(void *req); /* stat trap (see stat.h) */ +unsigned char sys_fsstat(void *buf); /* free blocks/inodes (see stat.h) */ +unsigned char isatty(void); /* 1 if stdin is the console */ void gsleep(unsigned char units); /* delay units*(1/64)s (raw trap) */ void gticks(unsigned char buf[4]); /* 64 Hz uptime ticks, 32-bit LE */ void msleep(unsigned int ms); /* delay ~ms milliseconds */ @@ -42,8 +45,9 @@ unsigned char mkdir(const char *path); unsigned char chdir(const char *path); unsigned char opendir(const char *path); void poweroff(void); /* clean emulator shutdown (saves) */ -#define O_READ 0 -#define O_WRITE 1 +#define O_READ 0 +#define O_WRITE 1 +#define O_APPEND 2 /* write at the end, keeping the existing contents */ #define NOFD 0xFF #define EISDIR 0xFE /* open() failed: path is a directory */ @@ -1,7 +1,7 @@ .module libc .globl _writes, _putc, _puts, _nl, _strlen, _readc, _sexit, _getpid, _getargs .globl _open, _close, _fgetc, _fputc, _flist, _fremove, _pipe, _ssend, _srecv, _srecv_nb, _pollin, _pollcon - .globl _sys_net, _gsleep, _gticks + .globl _sys_net, _gsleep, _gticks, _sys_stat, _sys_fsstat, _isatty ;; SDCC sm83 sdcccall(1): 1st arg -> A (byte) or DE (pointer); ret A / BC. ;; rst $30 (the trap) clobbers BC/DE/HL; SDCC treats them caller-saved, so ;; wrappers need not preserve them - but we compute cleanly regardless. @@ -318,3 +318,21 @@ _opendir: _poweroff: .db 0xED 1$: jr 1$ + + ;; unsigned char sys_stat(void *req /*DE*/) -> A (see fs.asm sys_stat) +_sys_stat: + ld c, #37 ; SYS_STAT + rst #0x30 + ret + + ;; unsigned char sys_fsstat(void *buf /*DE*/) -> A +_sys_fsstat: + ld c, #38 ; SYS_FSSTAT + rst #0x30 + ret + + ;; unsigned char isatty(void) -> A (1 = stdin is the console) +_isatty: + ld c, #39 ; SYS_ISATTY + rst #0x30 + ret diff --git a/usr/stat.h b/usr/stat.h new file mode 100644 index 0000000..10acc54 --- /dev/null +++ b/usr/stat.h @@ -0,0 +1,36 @@ +#ifndef STAT_H +#define STAT_H +/* stat.h - file metadata and filesystem totals. Header-only (one copy per + program), like sock.h: the request block has to live in the caller's RAM. + The kernel trap needs HL for its dispatch table, so a call with both a path + and an output buffer passes a request block rather than two pointers. */ +#include "gbos.h" + +#define ST_FILE 1 +#define ST_DIR 2 + +struct gstat { + unsigned char type; /* ST_FILE / ST_DIR */ + unsigned char nlink; + unsigned int size; /* bytes */ + unsigned char ino; +}; + +static unsigned char _sreq[7]; /* path ptr, then type/nlink/size/ino */ + +/* -> 1 on success, 0 if the path does not exist */ +static unsigned char gstat(const char *path, struct gstat *st) { + _sreq[0] = (unsigned char)(unsigned int)path; + _sreq[1] = (unsigned char)(((unsigned int)path) >> 8); + if (sys_stat(_sreq) != 0) return 0; + st->type = _sreq[2]; + st->nlink = _sreq[3]; + st->size = (unsigned int)_sreq[4] | ((unsigned int)_sreq[5] << 8); + st->ino = _sreq[6]; + return 1; +} + +/* out[] = { blocks total, blocks free, inodes total, inodes free } */ +static void gfsstat(unsigned char *out) { sys_fsstat(out); } + +#endif |
