libc: квирки DSS — возврат WRITE и лимит манипуляторов; тесты fdmax/fbench

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 16:09:29 +03:00
parent 48d552bf3a
commit 057dd615ba
11 changed files with 361 additions and 58 deletions
+2 -1
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
/*
* _fd_count — счётчик открытых манипуляторов DSS (только данные,
* см. _fd_guard.h). Пишут open() и close().
*/
#include "_fd_guard.h"
uint8_t _fd_open_count;
+22
View File
@@ -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 <stdint.h>
#define _FD_OPEN_MAX 8
extern uint8_t _fd_open_count;
#endif
+30
View File
@@ -0,0 +1,30 @@
/*
* close — закрыть файловый хендл ESTEX CLOSE ($12).
* Возвращает 0 или -1 + errno. На успехе декрементирует счётчик
* открытых манипуляторов (_fd_guard.h — защита от зависания DSS).
*/
#include <unistd.h>
#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;
}
+32 -49
View File
@@ -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 <fcntl.h>
#include <unistd.h>
#include <errno.h>
#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;
}
+53
View File
@@ -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 <unistd.h>
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;
}
+8
View File
@@ -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
+85
View File
@@ -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 <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
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;
}
+8
View File
@@ -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
+57
View File
@@ -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 <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#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;
}
+56 -8
View File
@@ -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 <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
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;
}