aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-24 12:48:37 +0200
committeruser <user@clank>2026-09-24 12:48:37 +0200
commitdc9dffb9069e32a1a0cf14883176544b234fc3e5 (patch)
tree4877daa5f01e98ac5d7e64d98c4144c94fa94a91
parentterm: write the palette attribute for every re-rendered tile, not just the cu... (diff)
downloadgbos-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.md13
-rw-r--r--include/gbos.inc25
-rw-r--r--src/fs.asm128
-rw-r--r--src/kdata.asm1
-rw-r--r--src/syscall.asm16
-rw-r--r--usr/gbos.h8
-rw-r--r--usr/libc.s20
-rw-r--r--usr/stat.h36
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
diff --git a/src/fs.asm b/src/fs.asm
index 9aeaeb7..30dfdd6 100644
--- a/src/fs.asm
+++ b/src/fs.asm
@@ -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)
diff --git a/usr/gbos.h b/usr/gbos.h
index 5fd897a..014aea4 100644
--- a/usr/gbos.h
+++ b/usr/gbos.h
@@ -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 */
diff --git a/usr/libc.s b/usr/libc.s
index fbca836..de087b6 100644
--- a/usr/libc.s
+++ b/usr/libc.s
@@ -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