From 057dd615ba9a1b3f4f752f437ada172ffa82ea0a Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 6 Jul 2026 16:09:29 +0300 Subject: [PATCH] =?UTF-8?q?libc:=20=D0=BA=D0=B2=D0=B8=D1=80=D0=BA=D0=B8=20?= =?UTF-8?q?DSS=20=E2=80=94=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80=D0=B0=D1=82=20?= =?UTF-8?q?WRITE=20=D0=B8=20=D0=BB=D0=B8=D0=BC=D0=B8=D1=82=20=D0=BC=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=BF=D1=83=D0=BB=D1=8F=D1=82=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B2;=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20fdmax/fbench?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ESTEX WRITE ($14) на успехе возвращает DE=0, а НЕ счётчик записанного (вопреки докам; solid-c в своём fflush тоже отключил сравнение по счётчику) — write() теперь судит по CF/A: CF=0&A=0 → n, CF=0&A!=0 → ENOSPC/-1 - DSS выдаёт 8 манипуляторов (fd 2..9; fd 1 держит шелл под запущенный exe), а 9-й OPEN не возвращает 06h — ВЕШАЕТ систему; предохранитель _fd_guard: счётчик в open()/close(), отказ EMFILE без захода в DSS - tests/fdmax — эмпирика лимита (8 хендлов, затем EMFILE=6); tests/fbench — бенчмарк буферизации (floor 512-байтными read, оценка небуферизованного по 1-байтным, fgetc/fgets/fputc) - filetest расширен: raw-probe возврата write, сценарий r+ (чтение-запись-чтение с инвалидацией буфера), ungetc, fprintf Co-Authored-By: Claude Fable 5 --- Makefile | 3 +- libc/io/_fd_count.c | 8 ++++ libc/io/_fd_guard.h | 22 ++++++++++ libc/io/close.c | 30 ++++++++++++++ libc/io/open.c | 81 +++++++++++++++---------------------- libc/io/write.c | 53 ++++++++++++++++++++++++ tests/fbench/Makefile | 8 ++++ tests/fbench/fbench.c | 85 +++++++++++++++++++++++++++++++++++++++ tests/fdmax/Makefile | 8 ++++ tests/fdmax/fdmax.c | 57 ++++++++++++++++++++++++++ tests/filetest/filetest.c | 64 +++++++++++++++++++++++++---- 11 files changed, 361 insertions(+), 58 deletions(-) create mode 100644 libc/io/_fd_count.c create mode 100644 libc/io/_fd_guard.h create mode 100644 libc/io/close.c create mode 100644 libc/io/write.c create mode 100644 tests/fbench/Makefile create mode 100644 tests/fbench/fbench.c create mode 100644 tests/fdmax/Makefile create mode 100644 tests/fdmax/fdmax.c diff --git a/Makefile b/Makefile index 8ed0fd3..2804e3b 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,8 @@ # Small libc-feature tests (one program per .c-language feature or libc API). TESTS := hello banked bankedbg strtest cat seek malloc mem_test argv errno \ rt_test openenv ls conio attrprob timedir mouse banklocl stdlib \ - assrtest ptime stattest filetest gfx_demo gfx_d16 gfx_text gfx_mous + assrtest ptime stattest filetest fdmax fbench \ + gfx_demo gfx_d16 gfx_text gfx_mous # Larger end-user applications under examples/. APPS := mdview diff --git a/libc/io/_fd_count.c b/libc/io/_fd_count.c new file mode 100644 index 0000000..1ca0d9f --- /dev/null +++ b/libc/io/_fd_count.c @@ -0,0 +1,8 @@ +/* + * _fd_count — счётчик открытых манипуляторов DSS (только данные, + * см. _fd_guard.h). Пишут open() и close(). + */ + +#include "_fd_guard.h" + +uint8_t _fd_open_count; diff --git a/libc/io/_fd_guard.h b/libc/io/_fd_guard.h new file mode 100644 index 0000000..d18ef5e --- /dev/null +++ b/libc/io/_fd_guard.h @@ -0,0 +1,22 @@ +/* + * _fd_guard.h — предохранитель от исчерпания манипуляторов DSS + * (НЕ публичный заголовок). + * + * Факт (MAME, DSS 1.71.57, тест fdmax 2026-07-06): DSS выдаёт максимум + * 8 манипуляторов (fd 2..9), а 9-й OPEN не возвращает ошибку 06h + * "Too many open files" — он ВЕШАЕТ систему. Поэтому libc считает + * открытые манипуляторы сама и отказывает с EMFILE, не доводя до DSS. + * + * Счётчик в _fd_count.c: инкремент в open() (creat идёт через него), + * декремент в close() на успешном пути. + */ +#ifndef _FD_GUARD_H +#define _FD_GUARD_H + +#include + +#define _FD_OPEN_MAX 8 + +extern uint8_t _fd_open_count; + +#endif diff --git a/libc/io/close.c b/libc/io/close.c new file mode 100644 index 0000000..4f0aa02 --- /dev/null +++ b/libc/io/close.c @@ -0,0 +1,30 @@ +/* + * close — закрыть файловый хендл ESTEX CLOSE ($12). + * Возвращает 0 или -1 + errno. На успехе декрементирует счётчик + * открытых манипуляторов (_fd_guard.h — защита от зависания DSS). + */ + +#include +#include "_fd_guard.h" + +int close(int fd) __naked +{ + (void)fd; + __asm + push ix + ld a, l + ld c, #0x12 ; ESTEX CLOSE + rst #0x10 + pop ix + jr c, _oclose_err + ;; успех — минус один открытый манипулятор + ld hl, #__fd_open_count + dec (hl) + ld de, #0 + ret + _oclose_err: + call __errno_set + ld de, #-1 + ret + __endasm; +} diff --git a/libc/io/open.c b/libc/io/open.c index 2babaf3..838b1a6 100644 --- a/libc/io/open.c +++ b/libc/io/open.c @@ -1,33 +1,33 @@ /* - * open / creat / close — ESTEX file-handle primitives. + * open — открыть/создать файл по POSIX-флагам. * - * The interesting part is the open() flag state machine. We expose POSIX - * flag bits (O_RDONLY/O_WRONLY/O_RDWR + O_CREAT/O_EXCL/O_TRUNC/O_APPEND) - * and dispatch onto the three ESTEX entry points: + * Машина состояний флагов: экспонируем POSIX-биты (O_RDONLY/O_WRONLY/ + * O_RDWR + O_CREAT/O_EXCL/O_TRUNC/O_APPEND) и диспетчеризуем на три + * входа ESTEX: * - * $11 OPEN — open existing file - * $0A CREATE — create (truncate if exists), return new handle - * $0B CREATE_NEW — create only if file does not exist + * $11 OPEN — открыть существующий файл + * $0A CREATE — создать (усечь, если существует), вернуть хендл + * $0B CREATE_NEW — создать, только если файла нет * - * Three private __naked wrappers do the raw RST 10h calls; the public - * open() / creat() / close() are plain C orchestrators. - * - * IX is saved across every RST 10h. Failures set errno and return -1. + * Три приватных __naked-обёртки делают сырые RST 10h; публичный open() + * — обычный C-оркестратор. IX сохраняется через каждый RST. + * При ошибке errno установлен, возврат -1. */ #include #include #include +#include "_fd_guard.h" -/* ---- raw ESTEX wrappers ---------------------------------------------- */ +/* ---- сырые ESTEX-обёртки ---------------------------------------------- */ -/* ESTEX $11 OPEN: A=mode (1=R, 2=W, 0=R/W), HL=path → A=handle, CF=err. */ +/* ESTEX $11 OPEN: A=режим (1=R, 2=W, 0=R/W), HL=путь → A=хендл, CF=err. */ static int _estex_open_raw(const char *path, int posix_mode) __naked { (void)path; (void)posix_mode; __asm push ix - ;; HL = path, DE = posix_mode. Translate to ESTEX numbering. + ;; HL = путь, DE = posix_mode. Перевести в нумерацию ESTEX. ld a, e and a, #0x03 ld c, #1 @@ -53,14 +53,14 @@ static int _estex_open_raw(const char *path, int posix_mode) __naked __endasm; } -/* ESTEX $0A CREATE: A=attr, HL=path → A=handle, CF=err. - * Truncates an existing file. */ +/* ESTEX $0A CREATE: A=атрибут, HL=путь → A=хендл, CF=err. + * Усекает существующий файл. */ static int _estex_create_raw(const char *path) __naked { (void)path; __asm push ix - xor a, a ; A = 0 (normal attribute) + xor a, a ; A = 0 (обычный атрибут) ld c, #0x0A rst #0x10 pop ix @@ -75,8 +75,8 @@ static int _estex_create_raw(const char *path) __naked __endasm; } -/* ESTEX $0B CREATE_NEW: A=attr, HL=path → A=handle, CF=err. - * Fails (errno=EEXIST) if file already exists. */ +/* ESTEX $0B CREATE_NEW: A=атрибут, HL=путь → A=хендл, CF=err. + * Падает (errno=EEXIST), если файл уже существует. */ static int _estex_create_new_raw(const char *path) __naked { (void)path; @@ -97,21 +97,28 @@ static int _estex_create_new_raw(const char *path) __naked __endasm; } -/* ---- public surface --------------------------------------------------- */ +/* ---- публичная поверхность --------------------------------------------- */ int open(const char *path, int flags) { int fd; + /* Предохранитель: 9-й OPEN вешает DSS (см. _fd_guard.h) — + * отказать самим, не доводя до syscall. */ + if (_fd_open_count >= _FD_OPEN_MAX) { + errno = EMFILE; + return -1; + } + if (flags & O_CREAT) { if (flags & O_EXCL) { - /* Must not already exist. */ + /* Не должен существовать. */ fd = _estex_create_new_raw(path); } else if (flags & O_TRUNC) { - /* Always create or truncate. */ + /* Всегда создать или усечь. */ fd = _estex_create_raw(path); } else { - /* Open if it exists, otherwise create. */ + /* Открыть, если есть, иначе создать. */ fd = _estex_open_raw(path, flags); if (fd < 0 && errno == ENOENT) { fd = _estex_create_raw(path); @@ -124,35 +131,11 @@ int open(const char *path, int flags) if (fd < 0) { return -1; } + _fd_open_count++; if (flags & O_APPEND) { - /* Position at end of file so future writes append. */ + /* Встать на конец файла — дальнейшие записи дописывают. */ (void)lseek(fd, 0L, SEEK_END); } return fd; } - -int creat(const char *path, int mode) -{ - (void)mode; /* Sprinter has no per-file permission bits */ - return open(path, O_WRONLY | O_CREAT | O_TRUNC); -} - -int close(int fd) __naked -{ - (void)fd; - __asm - push ix - ld a, l - ld c, #0x12 - rst #0x10 - pop ix - jr c, _oclose_err - ld de, #0 - ret - _oclose_err: - call __errno_set - ld de, #-1 - ret - __endasm; -} diff --git a/libc/io/write.c b/libc/io/write.c new file mode 100644 index 0000000..82cf4cc --- /dev/null +++ b/libc/io/write.c @@ -0,0 +1,53 @@ +/* + * write — блочная запись в файловый хендл ESTEX. + * + * ESTEX WRITE ($14): A=handle, HL=буфер, DE=число байт + * → CF=err с кодом в A. + * + * КВИРК (подтверждён в MAME, DSS 1.71.57, 2026-07-06): вопреки докам + * («DE = реальное количество записанных байт») на успехе DE = 0. + * Достоверный признак полной записи — A = 0 при CF = 0 (как у READ, + * где A=0 «всё», A=FFh «меньше»); solid-c в своём fflush так же + * закомментировал сравнение по DE и проверяет A. Поэтому: + * CF=1 → errno из A, возврат -1; + * CF=0, A=0 → всё записано, возврат n (запрошенного); + * CF=0, A!=0 → частичная запись (диск полон), реальный счётчик + * неизвестен → errno=ENOSPC, возврат -1. + * + * ABI-паттерн идентичен read() — см. read.c. + */ + +#include + +int write(int fd, const void *buf, size_t n) __naked +{ + (void)fd; (void)buf; (void)n; + __asm + pop iy ; IY = адрес возврата + pop bc ; BC = n (стековый аргумент) + ld a, l ; A = handle + ex de, hl ; HL = buf + ld d, b + ld e, c ; DE = n + push bc ; n нужен после RST (всё клобберится) + push ix + push iy ; сохранить адрес возврата через RST + ld c, #0x14 ; ESTEX WRITE + rst #0x10 + pop iy + pop ix + pop bc ; BC = n + jr c, _write_err + or a, a ; A=0 — всё записано (DE недостоверен) + jr nz, _write_short + ld d, b + ld e, c ; вернуть запрошенное n + jp (iy) + _write_short: + ld a, #10 ; ENOSPC (DSS 0Ah No free space) + _write_err: + call __errno_set + ld de, #-1 + jp (iy) + __endasm; +} diff --git a/tests/fbench/Makefile b/tests/fbench/Makefile new file mode 100644 index 0000000..2fe9ab7 --- /dev/null +++ b/tests/fbench/Makefile @@ -0,0 +1,8 @@ +# Build fbench.exe — бенчмарк буферизованного FILE* по BIG.TXT. +# BIG.TXT (100 КБ) нужен тесту — кладём на дискету и при одиночном +# `make run` (берём из tests/seek). + +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := fbench +EXTRA_DATA := ../seek/big.txt +include $(PROJ_ROOT)/app.mk diff --git a/tests/fbench/fbench.c b/tests/fbench/fbench.c new file mode 100644 index 0000000..1963bc1 --- /dev/null +++ b/tests/fbench/fbench.c @@ -0,0 +1,85 @@ +/* + * fbench — бенчмарк буферизации FILE* (вариант B+) с опорными замерами. + * + * 0a. read() блоками по 512 — «пол»: чистая скорость диск+syscall, + * ровно те же вызовы, что делает буферный слой изнутри; + * 0b. read() по 1 байту (16 КБ) — имитация НЕбуферизованного fgetc, + * ×6.25 даёт оценку на весь файл; + * 1. fgetc по всему файлу (102400 байт); + * 2. fgets построчно; + * 3. fputc 32 КБ (буфер: ~64 write-syscall'а вместо 32768). + * + * Интерпретация: если (1) ≈ (0a) — буферизация упирается в диск и + * работает идеально; выигрыш против небуферизованного = (0b)×6.25 − (1). + * Время — time(), секундная гранулярность. На дискете доминирует FDD; + * для чистого замера скопировать fbench.exe + big.txt на HDD (C:). + */ + +#include +#include +#include +#include + +static char blk[512]; +static char line[128]; + +int main(void) +{ + long n; + int c, fd, r; + time_t t0, t1; + + /* ----- 0a. Пол: блочный read по 512 ----- */ + fd = open("BIG.TXT", O_RDONLY); + if (fd < 0) { puts("BIG.TXT not found"); return 1; } + t0 = time(NULL); + n = 0; + while ((r = read(fd, blk, 512)) > 0) n += r; + t1 = time(NULL); + printf("read512: %ld bytes, %ld s (floor)\n", n, (long)(t1 - t0)); + close(fd); + + /* ----- 0b. Имитация небуферизованного fgetc: read по 1 байту ----- */ + fd = open("BIG.TXT", O_RDONLY); + t0 = time(NULL); + for (n = 0; n < 16384L; n++) { + if (read(fd, blk, 1) != 1) break; + } + t1 = time(NULL); + printf("read1 : %ld bytes, %ld s (x6.25 = unbuffered est.)\n", + n, (long)(t1 - t0)); + close(fd); + + /* ----- 1. fgetc по всему файлу ----- */ + FILE *fp = fopen("BIG.TXT", "r"); + if (!fp) { puts("fopen failed"); return 1; } + t0 = time(NULL); + n = 0; + while ((c = fgetc(fp)) != EOF) n++; + t1 = time(NULL); + printf("fgetc : %ld bytes, %ld s\n", n, (long)(t1 - t0)); + + /* ----- 2. fgets построчно ----- */ + rewind(fp); + t0 = time(NULL); + n = 0; + while (fgets(line, sizeof(line), fp)) n++; + t1 = time(NULL); + printf("fgets : %ld lines, %ld s\n", n, (long)(t1 - t0)); + fclose(fp); + + /* ----- 3. fputc 32 КБ ----- */ + fp = fopen("FBENCH.TMP", "w"); + if (!fp) { puts("cannot create FBENCH.TMP"); return 1; } + t0 = time(NULL); + for (n = 0; n < 32768L; n++) { + if (fputc('x', fp) == EOF) { puts("fputc error"); return 1; } + } + if (fclose(fp)) { puts("fclose error"); return 1; } + t1 = time(NULL); + printf("fputc : 32768 bytes, %ld s\n", (long)(t1 - t0)); + unlink("FBENCH.TMP"); + + puts("fbench done."); + return 0; +} diff --git a/tests/fdmax/Makefile b/tests/fdmax/Makefile new file mode 100644 index 0000000..6b18f6c --- /dev/null +++ b/tests/fdmax/Makefile @@ -0,0 +1,8 @@ +# Build fdmax.exe — проверка лимита открытых файлов DSS/FILE-таблицы. +# TEST.TXT нужен тесту как открываемый файл — кладём на дискету +# и при одиночном `make run` (берём из tests/cat). + +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := fdmax +EXTRA_DATA := ../cat/test.txt +include $(PROJ_ROOT)/app.mk diff --git a/tests/fdmax/fdmax.c b/tests/fdmax/fdmax.c new file mode 100644 index 0000000..59f3e2f --- /dev/null +++ b/tests/fdmax/fdmax.c @@ -0,0 +1,57 @@ +/* + * fdmax — эмпирическая проверка лимита одновременно открытых файлов. + * + * ФАКТ (прогон 2026-07-06, DSS 1.71.57): DSS выдал 8 манипуляторов + * (fd 2..9; fd 1 держит шелл DSS под запущенный .exe), а 9-й OPEN + * НЕ вернул ошибку — ПОВЕСИЛ систему. После этого в libc/io/open.c + * добавлен предохранитель _fd_guard (отказ EMFILE на 9-м open без + * захода в DSS), и этот тест снова безопасен. + * + * 1) Низкий уровень: open("TEST.TXT") в цикле — ожидание: 8 хендлов, + * затем EMFILE=6 от предохранителя libc (не от DSS!). + * 2) Высокий уровень: fopen до NULL — таблица OPEN_MAX (8), EMFILE. + * + * Требует TEST.TXT на диске (без него обе фазы дадут ENOENT=3). + */ + +#include +#include +#include +#include +#include + +#define TRY_MAX 32 + +int main(void) +{ + int fds[TRY_MAX]; + int n = 0; + + /* ----- 1. Голые манипуляторы DSS ----- */ + errno = 0; + while (n < TRY_MAX) { + int fd = open("TEST.TXT", O_RDONLY); + if (fd < 0) break; + fds[n++] = fd; + printf("open #%d -> fd=%d\n", n, fd); + } + printf("low-level: %d handles, then errno=%d (%s)\n", + n, errno, strerror(errno)); + for (int i = 0; i < n; i++) close(fds[i]); + + /* ----- 2. fopen / таблица FILE ----- */ + FILE *fps[TRY_MAX]; + int m = 0; + errno = 0; + while (m < TRY_MAX) { + FILE *fp = fopen("TEST.TXT", "r"); + if (!fp) break; + fps[m++] = fp; + } + printf("fopen: %d streams (OPEN_MAX=%d), then errno=%d (%s)\n", + m, OPEN_MAX, errno, strerror(errno)); + for (int i = 0; i < m; i++) fclose(fps[i]); + + puts("fdmax done."); + return 0; +} diff --git a/tests/filetest/filetest.c b/tests/filetest/filetest.c index 7decf15..0a1b125 100644 --- a/tests/filetest/filetest.c +++ b/tests/filetest/filetest.c @@ -1,22 +1,41 @@ /* - * filetest — exercise minimal FILE * stream API (fopen/fclose/fputs/ - * fgets/fread/fwrite/fseek/ftell/feof + stdin/stdout/stderr). + * filetest — exercise the buffered FILE * stream API (variant B+). * * Steps: - * 1. fopen("FILE.TXT","w"), fputs+fputc lines, fclose. + * 0. RAW probe: open/write/close — печатает ГОЛЫЙ возврат write() + * (диагностика: что реально возвращает ESTEX WRITE в DE). + * 1. fopen("FILE.TXT","w"), fputs+fputc lines, fclose (с errno). * 2. fopen("FILE.TXT","r"), fgets each line, fclose. - * 3. fopen("r"), fread block, fseek, ftell, feof, fread again. - * 4. fputs to stdout (sanity check console pseudo-stream). + * 3. fopen("r"), fread block, fseek, ftell, feof. + * 4. fputs to stdout/stderr (console pseudo-streams). + * 5. "r+" mix: fgetc×10 → fputc×10 → fseek(0) → контроль, что + * чтение видит записанное (инвалидация буфера); ungetc; fprintf. */ #include #include +#include +#include #include int main(void) { const char *path = "FILE.TXT"; + /* ----- 0. RAW write() return probe ----- */ + { + int fd = open("WRT.TMP", O_WRONLY | O_CREAT | O_TRUNC); + if (fd >= 0) { + errno = 0; + int w = write(fd, "12345", 5); + printf(" raw write(5) -> %d, errno=%d\n", w, errno); + close(fd); + unlink("WRT.TMP"); + } else { + printf(" raw probe: open failed errno=%d\n", errno); + } + } + /* ----- 1. Write via fputs + fputc ----- */ { FILE *fp = fopen(path, "w"); @@ -24,11 +43,14 @@ int main(void) printf("fopen(w) failed: errno=%d (%s)\n", errno, strerror(errno)); return 1; } - fputs("Hello from FILE*\n", fp); - fputs("Second line via fputs\n", fp); + int r1 = fputs("Hello from FILE*\n", fp); + int r2 = fputs("Second line via fputs\n", fp); + if (r1 || r2) printf(" fputs ret: %d %d\n", r1, r2); for (int i = '0'; i <= '9'; i++) fputc(i, fp); fputc('\n', fp); - if (fclose(fp) != 0) puts("fclose: error"); + errno = 0; + if (fclose(fp) != 0) + printf("fclose: error, errno=%d (%s)\n", errno, strerror(errno)); } /* ----- 2. Read via fgets ----- */ @@ -69,6 +91,32 @@ int main(void) fputs(" fputs to stderr works\n", stderr); fputc('!', stdout); fputc('\n', stdout); + /* ----- 5. "r+": чтение→запись→чтение + ungetc + fprintf ----- */ + { + FILE *fp = fopen(path, "r+"); + if (!fp) { puts("fopen(r+) failed"); return 1; } + for (int i = 0; i < 10; i++) fgetc(fp); /* поз. 10, readahead жив */ + for (int i = 0; i < 10; i++) fputc('X', fp); /* sync: запись на 10..19 */ + fseek(fp, 0L, SEEK_SET); + char buf[24]; + size_t got = fread(buf, 1, 20, fp); + int ok = (got == 20); + for (int i = 10; i < 20; i++) if (buf[i] != 'X') ok = 0; + printf(" r+ mix: %s (got=%u)\n", ok ? "OK" : "FAIL", (unsigned)got); + + /* ungetc: прочитать байт, вернуть, прочитать снова */ + int c1 = fgetc(fp); + ungetc(c1, fp); + int c2 = fgetc(fp); + printf(" ungetc: %s, ftell=%ld\n", c1 == c2 ? "OK" : "FAIL", ftell(fp)); + + /* fprintf в файл */ + fseek(fp, 0L, SEEK_END); + int n = fprintf(fp, "num=%d hex=%04x\n", 42, 0xBEEF); + printf(" fprintf -> %d chars\n", n); + if (fclose(fp)) puts(" r+ fclose: error"); + } + puts("filetest done."); return 0; }