diff options
| author | user <user@clank> | 2026-09-24 14:15:46 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-24 14:15:46 +0200 |
| commit | 50a56b48f22cd31868403f1fde10cf8367344e2e (patch) | |
| tree | 57c606182378dde67ce63d115ea07f98cc96b709 | |
| parent | usr: xxd - hex dump a file or stdin (diff) | |
| download | gbos-50a56b48f22cd31868403f1fde10cf8367344e2e.tar.gz gbos-50a56b48f22cd31868403f1fde10cf8367344e2e.tar.xz gbos-50a56b48f22cd31868403f1fde10cf8367344e2e.zip | |
fs: mount the builtins at /bin
The commands only existed in the kernel's program table, so the only way to
find out what the machine could do was to already know (or read the source).
They are now a directory in the tree:
/# ls / /# xxd -l 16 /bin/uname
bin 0000: cd 6b 43 06 00 0e 00 f7 .kC.....
readme 0008: 18 fe 47 0e 03 f7 c9 21 ..G....!
/# ls -l /bin | head -2
- 1 16384 worker
/bin is synthetic, not disk: dir_find resolves "bin" in the root to BIN_INO
and names inside it through the NameTable, so `ls /bin` lists exactly what the
shell can run - one table, one truth, no second list to keep in sync. The root
listing carries "bin" as a synthetic first entry so it is discoverable by
walking the tree.
A program image is inode BIN_FILE|progid: it stats as a 16 KiB file (a program
IS its ROM bank) and reads through rom_getb, which maps that bank, takes the
byte, and maps the CALLER'S text bank back before returning - the caller is
itself executing from $4000, so leaving the wrong bank mapped would return
into another program's code. Verified byte-for-byte against the host's view of
the blob. The tree is read-only: create/remove/write inside /bin all fail.
xxd grows -l (stop after N bytes), because a program image is 2048 dump lines
- more than a pipe's temp file can hold, let alone the screen.
Two traps worth remembering, both found the hard way here: `and BIN_FILE`
destroys A, so the inode must be reloaded before inode_ptr (missing it once
made ls -l stat inode 0 and call every file an empty non-directory); and ls
now copies its directory argument to the stack first, because stat()'s request
block is a static living at $A000 - on top of the command line it was reading.
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | docs/internals.md | 11 | ||||
| -rw-r--r-- | include/gbos.inc | 7 | ||||
| -rw-r--r-- | src/fs.asm | 237 | ||||
| -rw-r--r-- | src/syscall.asm | 2 | ||||
| -rw-r--r-- | usr/ls.c | 20 | ||||
| -rw-r--r-- | usr/xxd.c | 22 |
7 files changed, 293 insertions, 10 deletions
@@ -47,6 +47,10 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).* directories, per-process cwd — survives power-off (files up to 3 KiB). `ls -l` and `df` report sizes and free space; `poweroff` is the only safe way to stop the machine (the battery save is written on a clean exit). +- **`/bin` in the tree**: the builtins are not files on the disk, but they + show up as one anyway — `ls /bin` enumerates the kernel's own command + table, `cd /bin` works, and `xxd -l 16 /bin/uname` dumps the program's ROM + bank. Read-only, since it is ROM. - **A C toolchain**: userland is plain C compiled with SDCC (`sm83`); ~45 tools ship in `usr/` (`cat`, `wc`, `head`, `tail`, `grep`, `cp`, `mv`, `tee`, `more`, `ls`, `df`, `xxd`, `sleep`, `uptime`, …) — `help` lists them all on diff --git a/docs/internals.md b/docs/internals.md index f9c5899..f630025 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -241,6 +241,17 @@ blocks 4.. data blocks - **`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. +- **`/bin` is synthetic.** It is not on the disk: `dir_find` resolves "bin" in + the root to `BIN_INO`, and names inside it through the kernel's own + NameTable, so `ls /bin` shows exactly what the shell can run - one table, + one truth. A program image is inode `BIN_FILE|progid`, `stat`s as a 16 KiB + file (a program IS its ROM bank) and reads through `rom_getb`, which maps + that bank, takes the byte, and maps the CALLER'S text bank back before + returning - the caller is itself running from `$4000`. The whole tree is + read-only: creating, removing or writing inside `/bin` fails. + NB every `and BIN_FILE` test destroys A, so the inode has to be reloaded + before `inode_ptr`; forgetting that once made `ls -l` stat inode 0 and + report every file as an empty non-directory. - **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. diff --git a/include/gbos.inc b/include/gbos.inc index 1af3fc8..32418b6 100644 --- a/include/gbos.inc +++ b/include/gbos.inc @@ -235,6 +235,13 @@ DEF BLK_DATA0 EQU 4 ; first data block DEF FS_VERSION EQU 3 ; bumped: dirs now carry "."/".." entries DEF NINODES EQU 32 DEF ROOT_INO EQU 1 +; /bin is synthetic: a directory made of the kernel's program table rather +; than of disk blocks, so the builtins can be found (ls /bin), stat'd and read +; (xxd /bin/echo) like files. Both values are outside the real inode range +; (1..NINODES), so anything holding an inode number can tell them apart. +DEF BIN_INO EQU $F0 ; the /bin directory itself +DEF BIN_FILE EQU $80 ; BIN_FILE|progid = one program image +DEF BIN_SIZE EQU $4000 ; a program is exactly its 16 KiB ROM bank DEF NDIRECT EQU 12 ; direct block pointers per inode -> files <= 3 KiB ; (4..15: the whole tail of the 16-byte inode. It ; was 8, and bytes 12-15 were unused - but nothing @@ -242,6 +242,31 @@ dir_write: ; dir_find - dir inode in wFsDir, name in wFsNameBuf -> A = inode (0 if none) ; ----------------------------------------------------------------------------- dir_find:: + ; /bin is synthetic. Inside it, names come from the kernel's NameTable; + ; in the root, the name "bin" resolves to it. Everything else is disk. + ld a, [wFsDir] + cp BIN_INO + jr z, .inbin + cp ROOT_INO + jr nz, .disk + ld hl, .binname + ld de, wFsNameBuf + call fs_streq + jr nz, .disk + ld a, BIN_INO + ret +.inbin + ld de, wFsNameBuf + call sys_lookup ; A = program id, or $FF + cp $FF + jr z, .nobin + or BIN_FILE ; -> the synthetic inode for that program + ret +.nobin + xor a + ret +.binname: db "bin", 0 +.disk call dir_load ld c, 0 .l @@ -264,10 +289,27 @@ dir_find:: ld a, [hl] ret +; fs_streq(HL, DE) -> Z if the two NUL-terminated strings match. +fs_streq: + ld a, [de] + cp [hl] + ret nz + or a + ret z + inc hl + inc de + jr fs_streq + ; ----------------------------------------------------------------------------- ; dir_add - dir in wFsDir, child in wFsInode, name in wFsNameBuf -> A = 1/0 ; ----------------------------------------------------------------------------- dir_add:: + ld a, [wFsDir] ; /bin is read-only: it is ROM + cp BIN_INO + jr nz, .notbin + xor a + ret +.notbin call dir_load ld c, 0 .l @@ -676,10 +718,18 @@ sys_open:: call copy_path ; path -> wFsPath call resolve_parent ; A = parent dir inode, wFsNameBuf = final name or a - jr z, .fail + jp z, .fail ld [wFsDir], a call dir_find ; A = target inode or 0 ld [wFsInode], a + cp BIN_INO + jp z, .isdir ; /bin: a directory, not a file + and BIN_FILE + jp nz, .romfile ; a program image: read-only, no inode on disk + ld a, [wFsDir] + cp BIN_INO + jp z, .fail ; creating inside /bin: it is ROM + ld a, [wFsInode] or a jr z, .notdir ; doesn't exist yet: no type to check call inode_ptr @@ -728,6 +778,11 @@ sys_open:: ld a, [wFsMode] cp O_APPEND jr z, .allocof ; append: keep the data, seek to the end below +.romfile + ld a, [wFsMode] + or a + jr nz, .fail ; O_WRITE / O_APPEND on a program image + jp .allocof .trunc ld a, [wFsInode] call file_truncate @@ -850,6 +905,9 @@ sys_getb:: ld e, a ld a, [hl] ld d, a ; DE = pos + ld a, [wFsInode] + and BIN_FILE + jp nz, rom_getb ; /bin/<prog>: the bytes are in ROM, not on disk ; pos < size ? ld a, [wFsInode] call inode_ptr @@ -911,6 +969,125 @@ sys_getb:: scf ret +binname: db "bin", 0 + +; list_copy_name - HL = NUL-terminated name -> copy it to the caller's buffer +; (wFsOFPtr) and return A = 1. Shared by the /bin and root-"bin" list paths. +list_copy_name: + ld a, [wFsOFPtr] + ld e, a + ld a, [wFsOFPtr+1] + ld d, a + ld b, 15 +.cp + ld a, [hl+] + ld [de], a + inc de + or a + jr z, .done + dec b + jr nz, .cp + xor a + ld [de], a +.done + ld a, 1 + ret + +; bin_list - the index-th name from the kernel's NameTable, so `ls /bin` shows +; exactly what the shell can run: one table, one truth. +bin_list: + ld hl, NameTable + ld a, [wFsSlot] + ld c, a ; entries still to skip +.scan + ld a, [hl] + or a + jr z, .none ; end of table: no such index + ld a, c + or a + jp z, list_copy_name ; this is the one + dec c +.skip + ld a, [hl+] + or a + jr nz, .skip ; past the name + inc hl ; past the program id + jr .scan +.none + xor a + ret + +; ============================================================================= +; rom_getb - read one byte of a /bin program image. DE = position, wFsInode = +; BIN_FILE|progid, wFsOFPtr = the open-file entry. Returns like sys_getb: +; A = byte with CF clear, or CF set at EOF. +; +; A program image IS its 16 KiB ROM bank, so this maps that bank into +; $4000-$7FFF, takes the byte, and maps the CALLER'S text bank back before +; returning - the caller is itself running from $4000, so leaving the wrong +; bank mapped would return into another program's code. +; ============================================================================= +rom_getb: + ld a, d ; pos >= BIN_SIZE ($4000) -> EOF + cp HIGH(BIN_SIZE) + jr c, .inrange + scf + ret +.inrange + push de + ld a, [wFsInode] + and $7F ; program id + add a + add a ; *4: ProgramTable entry = bank(2) + entry(2) + ld hl, ProgramTable + add l + ld l, a + ld a, h + adc 0 + ld h, a + ld a, [hl+] + ld b, a ; bank lo + ld a, [hl] + ld c, a ; bank hi + ld a, b + ld [MBC5_ROMB_LO], a + ld a, c + ld [MBC5_ROMB_HI], a + pop de + push de + ld a, d + add $40 ; HL = $4000 + pos + ld h, a + ld l, e + ld a, [hl] + ld [wFsByte], a + ; put the caller's own text bank back + ld a, [wCurProc] + add PROC_ROMB + ld l, a + ld a, [wCurProc+1] + adc 0 + ld h, a + ld a, [hl+] + ld [MBC5_ROMB_LO], a + ld a, [hl] + ld [MBC5_ROMB_HI], a + pop de + inc de ; pos++ + ld a, [wFsOFPtr] + add OF_POS + ld l, a + ld a, [wFsOFPtr+1] + adc 0 + ld h, a + ld a, e + ld [hl+], a + ld a, d + ld [hl], a + ld a, [wFsByte] + and a ; CF = 0 + ret + ; ============================================================================= ; sys_putb(B = fd, E = byte) ; ============================================================================= @@ -1030,6 +1207,21 @@ sys_list:: ld [wFsSlot], a ; wanted index ld a, [wListDir] ld [wFsDir], a + cp BIN_INO + jp z, bin_list ; /bin comes from the program table + cp ROOT_INO + jr nz, .ondisk + ; the root carries a synthetic "bin" as its first entry, so the builtins + ; can be found by walking the tree instead of by knowing they are there + ld a, [wFsSlot] + or a + jr nz, .shift + ld hl, binname + jp list_copy_name +.shift + dec a + ld [wFsSlot], a ; the disk entries follow it +.ondisk call dir_load ld c, 0 ; entry cursor ld b, 0 ; used counter @@ -1175,10 +1367,16 @@ sys_chdir:: or a jr z, .fail ld [wFsInode], a + cp BIN_INO + jr z, .setcwd ; /bin: a directory, just not one on disk + and BIN_FILE + jr nz, .fail ; a program image is not a directory + ld a, [wFsInode] call inode_ptr ld a, [hl] ; I_TYPE cp IT_DIR jr nz, .fail +.setcwd ld a, [wCurProc] add PROC_CWD ld l, a @@ -1217,6 +1415,11 @@ sys_stat:: or a jr z, .fail ld [wFsInode], a + cp BIN_INO + jr z, .statbindir + and BIN_FILE + jr nz, .statbinfile + ld a, [wFsInode] ; the AND above clobbered it call inode_ptr ; HL = &inode: type, nlink, size lo, size hi ld a, [wFsStatReq] add SR_TYPE @@ -1235,6 +1438,32 @@ sys_stat:: ld [de], a ; SR_INO follows SR_SIZE xor a ret +.statbindir + ld hl, .bindir + jr .statfixed +.statbinfile + ld hl, .binfile +.statfixed + ; type, nlink, size lo, size hi from a fixed template, then the inode + ld a, [wFsStatReq] + add SR_TYPE + ld e, a + ld a, [wFsStatReq+1] + adc 0 + ld d, a + ld b, 4 +.cpfx + ld a, [hl+] + ld [de], a + inc de + dec b + jr nz, .cpfx + ld a, [wFsInode] + ld [de], a + xor a + ret +.bindir: db IT_DIR, 1, 0, 0 +.binfile: db IT_FILE, 1, LOW(BIN_SIZE), HIGH(BIN_SIZE) .fail ld a, $FF ret @@ -1297,10 +1526,16 @@ sys_opendir:: or a jr z, .fail ld [wFsInode], a + cp BIN_INO + jr z, .setdir ; /bin: listed from the program table + and BIN_FILE + jr nz, .fail + ld a, [wFsInode] call inode_ptr ld a, [hl] cp IT_DIR jr nz, .fail +.setdir ld a, [wFsInode] ld [wListDir], a xor a diff --git a/src/syscall.asm b/src/syscall.asm index 9e5ca33..b548328 100644 --- a/src/syscall.asm +++ b/src/syscall.asm @@ -203,7 +203,7 @@ sys_setout: ; ----------------------------------------------------------------------------- ; sys_lookup(DE = name) -> A = program id, or $FF if not a known command. ; ----------------------------------------------------------------------------- -sys_lookup: +sys_lookup:: ld h, d ld l, e ; HL = query name ld de, NameTable @@ -21,15 +21,25 @@ void main(void) { struct gstat st; unsigned char argc = argv_parse(argv, 4); unsigned char lng = hasflag(argv, argc, 'l'); - unsigned char i, j, k; - char *dir = 0; + char dir[32]; + unsigned char i, j, k, havedir = 0; + /* Copy the directory argument onto the stack before anything else: argv + points into the command line at $A000, and gstat()'s request block is a + static - which lives at $A000 too. The first stat would otherwise eat + the argument, and every later entry would stat a garbage path. */ for (i = 0; i < argc; i++) if (argv[i][0] != '-') { - dir = argv[i]; + for (j = 0; argv[i][j] && j < 31; j++) dir[j] = argv[i][j]; + dir[j] = 0; + havedir = 1; break; } - if (opendir(dir ? dir : ".")) { + if (!havedir) { + dir[0] = '.'; + dir[1] = 0; + } + if (opendir(dir)) { puts("ls: no such directory"); nl(); sexit(1); @@ -44,7 +54,7 @@ void main(void) { } /* stat wants a path, and flist only hands back the leaf name */ j = 0; - if (dir) { + if (havedir && !(dir[0] == '.' && dir[1] == 0)) { for (k = 0; dir[k] && j < 30; k++) path[j++] = dir[k]; if (j && path[j - 1] != '/') path[j++] = '/'; } @@ -6,10 +6,22 @@ * line and make the dump unreadable. -c overrides it anyway. * * There is no seek syscall, so this streams from the start - which is also - * why there is no -s (skip). */ + * why there is no -s (skip). -l stops after N bytes, which is how you look at + * a program image: /bin/<prog> is a whole 16 KiB ROM bank, and dumping all of + * it is 2048 lines (and more than a pipe's temp file can hold). */ #define MAXC 16 +/* atou() is 8-bit; -l wants up to 16 KiB */ +static unsigned int atou16(const char *s) { + unsigned int n = 0; + while (*s >= '0' && *s <= '9') { + n = n * 10 + (unsigned char)(*s - '0'); + s++; + } + return n; +} + static char hexd(unsigned char v) { return (char)(v < 10 ? '0' + v : 'a' + (v - 10)); } static void hex2(unsigned char v) { putc(hexd((unsigned char)(v >> 4))); @@ -25,6 +37,8 @@ void main(void) { unsigned char buf[MAXC]; unsigned char argc = argv_parse(argv, 8); char *cv = optval(argv, argc, 'c'); + char *lv = optval(argv, argc, 'l'); + unsigned int limit = 0; /* 0 = no limit */ unsigned char cols = 8, fd = NOFD, n = 0, i; unsigned int off = 0; char *fname = 0; @@ -34,9 +48,10 @@ void main(void) { cols = atou(cv); if (cols == 0 || cols > MAXC) cols = 8; } + if (lv) limit = atou16(lv); for (i = 0; i < argc; i++) { if (argv[i][0] == '-') continue; - if (cv == argv[i]) continue; + if (cv == argv[i] || lv == argv[i]) continue; fname = argv[i]; break; } @@ -55,7 +70,8 @@ void main(void) { } for (;;) { - if (fd == NOFD) { + if (limit && off + n >= limit) ch = -1; /* -l: stop after N bytes */ + else if (fd == NOFD) { char c = readc(); ch = (c == EOF) ? -1 : c; } else ch = fgetc(fd); |
