Files
Sprinter-SDCC/runtime/crt0.s
T
snark13 8490288d79 libc/crt0: возврат из main завершает программу по-настоящему
Два бага одного пути завершения, оба видны только на железе.

1. ЗВУК ПРОДОЛЖАЛСЯ ПОСЛЕ ВЫХОДА.  cbl_close() закрывает СЕССИЮ, но не
гасит железо: bit7 порта 0x004E держит gfx_wait_vsync ради бита луча,
поэтому порт оставался включённым ("немой" режим), и CBL крутил свои 256
слотов уже под шеллом — тихо ровно до первой чужой записи в порт данных,
а дальше она зацикливалась.  Новый cbl_shutdown() гасит bit7 независимо
от держателей и центрует ЦАП обычного COVOX; pop_shutdown зовёт его
последним действием, а _cbl_open_raw регистрирует в atexit его, а не
cbl_close.

2. ЦЕПОЧКА atexit НЕ ВЫПОЛНЯЛАСЬ ПРИ ВОЗВРАТЕ ИЗ main.  crt0 уходил прямо
в ESTEX EXIT, то есть нарушал контракт C (возврат из main = exit(status)).
Молча терялись не только гашение звука и снятие vsync-ссылки, но и
_fclosall: буферизованная запись в файлы пропадала, если программа не
звала exit() явно.  Теперь crt0 после main дёргает _atexit_hook.

Косвенность обязательна: прямая ссылка crt0 на разматыватель притащила бы
его и стек хендлеров в КАЖДУЮ программу.  Указатель живёт в отдельном
data-модуле (два байта _DATA, ни байта кода), ставит его сам atexit() при
первой регистрации — нет регистраций, нет и кода.  Тот же приём, что у
_irq_cbl_hook.

Цена замерена: +14 Б всем программам (блок в crt0) и +64 Б тем
одиннадцати, что реально регистрируют хендлеры (CBL, файловые через
_fclosall, irqtest, gfx_dbuf, solidt) — у них раньше эти хендлеры были
мёртвым кодом.  Эталоны обновлены (кроме atlas: его +434 Б не отсюда,
замерен тот же и без этих правок).

Проверено в MAME: старт и звук как были, выход по F10 возвращает в шелл
чисто (текстовый режим восстановлен, зависания нет), повторный запуск
работает.  Пункт 1 проверяется только на железе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSYUpuaQpKr48kBav2iiV4
2026-09-02 14:15:48 +03:00

392 lines
12 KiB
ArmAsm

;; ----------------------------------------------------------------------
;; crt0.s — Sprinter ESTEX C runtime startup, with argv parsing.
;;
;; What's special vs crt0_minimal.s:
;; After gsinit and before calling main, we read the ESTEX command-line
;; from the startup prefix (IX+0 = length, IX+1.. = ASCIIZ bytes) and
;; tokenize it in place into a static argv[] table. argc → HL, argv → DE
;; when we then `call _main`, which matches SDCC's __sdcccall(1) two-arg
;; ABI. Programs declared as `int main(void)` simply ignore the input
;; registers — no harm done — so this is the default crt0.
;;
;; Entry contract (per ESTEX EXEC):
;; IX -> startup prefix:
;; IX-3 = open file handle (only if EXE header.loader > 0)
;; IX-2 = memory block id (released automatically on EXIT)
;; IX-1 = current process level
;; IX+0 = command-line length (1 byte)
;; IX+1 = command-line bytes (ASCIIZ, up to 127 chars)
;;
;; Memory map: see compiler_approach memory file.
;; ----------------------------------------------------------------------
.module crt0
.globl _main
.globl __atexit_hook
;; Linker-emitted symbols (resolved at link time).
.globl s__INITIALIZER
.globl l__INITIALIZER
.globl s__INITIALIZED
.globl s__DATA
.globl l__DATA
.globl s__BSS
.globl l__BSS
;; Tunables — match the constants in argv parsing below.
ARGV_MAX_ARGS = 16 ; including argv[0]
ARGV_BUF_BYTES = 128 ; cmdline buffer size (ESTEX max + 1)
;; ___sdcc_heap_end (upper bound of the malloc free-list) lives in a
;; separate per-program file (runtime/heap_top.s) so it can be regenerated
;; with a custom value without re-assembling crt0. The default is set in
;; that file (0xBB00); sprinter-cc may emit a different value when
;; `--stack-size N` is given.
;; =========================================================================
;; AREA ORDERING — emitted up-front so the linker walks them in this order.
;; =========================================================================
.area _HOME
.area _CODE
.area _INITIALIZER
.area _GSINIT
.area _GSFINAL
.area _DATA
.area _INITIALIZED
.area _BSEG
.area _BSS
.area _HEAP
;; =========================================================================
;; Entry point — first instruction in _CODE, hence at --code-loc (0x4100).
;; =========================================================================
.area _CODE
_start::
ld sp, #0xBFFE
ld (_estex_startup_ix), ix ; save IX prefix pointer
call gsinit
.ifdef DEBUG_RT
;; tiny mode never self-allocates a W2 page (CODE+DATA both live in W2,
;; which DSS itself maps for us). Publish 0 to the diagnostic flag —
;; must run AFTER gsinit since BSS is zeroed there and we want a
;; deterministic value visible to user code from main() onward.
xor a
ld (_w2_self_allocated), a
.endif
;; Parse the ESTEX command-line into argv[]; populates _argc, _argv.
call parse_argv
;; Replace argv[0] (empty string placeholder) with the basename of the
;; running .EXE. Safe to skip if ESTEX APPINFO fails.
call get_progname
;; Load argc/argv per SDCC __sdcccall(1): arg1 → HL, arg2 → DE.
ld hl, (_argc)
ld de, (_argv)
call _main
;; Returning from main is exit(status) per the C standard, so the
;; atexit chain must run here too. The call is INDIRECT on purpose:
;; atexit() fills __atexit_hook on its first registration, so programs
;; that never register anything keep it NULL and link neither the
;; unwinder nor the handler stack (see libc/sys/_atexit_hook.c).
push de ; DE = int return of main
ld hl, (__atexit_hook)
ld a, h
or a, l
jr Z, atexit_done
ld de, #atexit_done
push de ; return address: Z80 has no `call (hl)`
jp (hl)
atexit_done:
pop de
;; SDCC's int return → DE. Low byte is the exit code.
ld a, e
ld b, a
ld c, #0x41 ; ESTEX EXIT
rst #0x10
;; Should not return; halt loop just in case.
1$: halt
jr 1$
;; =========================================================================
;; parse_argv — tokenize ESTEX cmdline into argv[].
;;
;; The prefix: (IX+0) = length; (IX+1...) = ASCIIZ bytes.
;; We copy up to ARGV_BUF_BYTES-1 chars into our own buffer (so we can
;; overwrite separators with NUL), strip leading whitespace (DSS quirk),
;; then walk through tokens. argv[0] is set to an empty string because
;; ESTEX doesn't pass the program name in the prefix.
;; =========================================================================
.area _CODE
parse_argv::
;; Copy cmdline body into argv_buf, NUL-terminate.
ld hl, (_estex_startup_ix)
ld a, (hl) ; A = cmdline length
cp #ARGV_BUF_BYTES
jr c, len_ok
ld a, #ARGV_BUF_BYTES-1
len_ok:
ld c, a
ld b, #0 ; BC = number of bytes to copy
inc hl ; HL = body start (= IX+1)
ld de, #argv_buf
ld a, b
or a, c
jr Z, after_copy
ldir ; DE = one past last copied byte
after_copy:
xor a
ld (de), a ; NUL-terminate
;; Skip leading whitespace.
ld hl, #argv_buf
strip_ws:
ld a, (hl)
or a
jr Z, no_args
cp #0x20 ; ' '
jr Z, strip_more
cp #0x09 ; tab
jr nz, have_first_token
strip_more:
inc hl
jr strip_ws
no_args:
;; Only argv[0] = "" — no real arguments.
ld hl, #empty_str
ld (argv_array), hl
ld hl, #0
ld (argv_array+2), hl ; argv[1] = NULL
ld hl, #1
ld (_argc), hl
ld hl, #argv_array
ld (_argv), hl
ret
have_first_token:
;; HL points at start of first real argument.
;; argv[0] = empty string (placeholder for program name).
ld de, #empty_str
ld (argv_array), de
;; B = current argc (start at 1). Use B because HL/DE are busy.
ld b, #1
token_loop:
;; Bound check: argc < ARGV_MAX_ARGS
ld a, b
cp #ARGV_MAX_ARGS
jr nc, tokens_done
;; argv[argc] = HL. Compute slot address = argv_array + argc*2.
push hl
ld a, b
add a, a ; A = argc * 2
ld e, a
ld d, #0
ld hl, #argv_array
add hl, de ; HL = &argv[argc]
pop de ; DE = current token pointer
ld (hl), e
inc hl
ld (hl), d
ex de, hl ; HL = token pointer again
inc b ; argc++
;; Advance past token (non-whitespace).
walk_token:
ld a, (hl)
or a
jr Z, tokens_done
cp #0x20
jr Z, end_of_token
cp #0x09
jr Z, end_of_token
inc hl
jr walk_token
end_of_token:
;; Replace separator with NUL.
xor a
ld (hl), a
inc hl
;; Skip extra whitespace.
skip_ws:
ld a, (hl)
or a
jr Z, tokens_done
cp #0x20
jr Z, skip_more
cp #0x09
jr Z, skip_more
jr token_loop
skip_more:
inc hl
jr skip_ws
tokens_done:
;; argv[argc] = NULL terminator.
ld a, b
add a, a
ld e, a
ld d, #0
ld hl, #argv_array
add hl, de
ld (hl), #0
inc hl
ld (hl), #0
;; Publish argc (16-bit, zero-extended from B).
ld a, b
ld (_argc), a
xor a
ld (_argc+1), a
;; Publish argv = &argv_array[0]
ld hl, #argv_array
ld (_argv), hl
ret
;; The empty string used as argv[0] placeholder — lives in code.
empty_str:
.db 0
;; =========================================================================
;; get_progname — fetch the running program's full path via ESTEX APPINFO
;; (subfn 2, $47) and set argv[0] to the basename portion.
;;
;; ESTEX APPINFO: A=err, C=$47, B=subfn, HL=buf → A=err, CF=1 on error.
;; subfn 2 = full app path, e.g. "A:\PROGRAMS\ARGV_TES.EXE"
;;
;; On error or when no separator is found, we leave argv[0] as the empty
;; string placeholder that parse_argv set.
;; =========================================================================
.area _CODE
get_progname::
push ix ; ESTEX clobbers IX
ld hl, #progname_buf
ld b, #2 ; subfn 2 = app_path
ld c, #0x47
rst #0x10
pop ix
jr c, gpn_skip ; APPINFO failed → keep empty argv[0]
;; First make sure the buffer is null-terminated (it should already be).
;; Then scan forward to find the end-of-string, then scan backwards to
;; the last directory separator ('\' or ':').
ld hl, #progname_buf
gpn_find_end:
ld a, (hl)
or a, a
jr Z, gpn_at_end
inc hl
jr gpn_find_end
gpn_at_end:
;; HL points at the NUL. Walk backwards looking for '\' or ':'. Stop
;; if we reach the start of the buffer — then the whole path is the
;; basename.
ld de, #progname_buf
gpn_scan_back:
ld a, h
cp a, d
jr nz, gpn_dec
ld a, l
cp a, e
jr Z, gpn_set ; reached buffer start
gpn_dec:
dec hl
ld a, (hl)
cp #0x5C ; backslash
jr Z, gpn_after_sep
cp #0x3A ; colon (drive separator)
jr Z, gpn_after_sep
jr gpn_scan_back
gpn_after_sep:
inc hl ; skip past the separator
gpn_set:
ld (argv_array), hl ; argv[0] = basename
gpn_skip:
ret
;; =========================================================================
;; Runtime data (HOME's _DATA / _BSS)
;; =========================================================================
.area _CODE
;; Written BEFORE gsinit runs (gsinit zeroes _DATA), so these live in
;; _CODE — RAM on the Sprinter, always mapped.
_estex_startup_ix::
.ds 2
.area _DATA
_argc::
.ds 2
_argv::
.ds 2
.ifdef DEBUG_RT
;; Runtime diagnostic: 0 = no extra W2 page was self-allocated by crt0
;; (DSS gave us what we needed); 1 = crt0 had to allocate W2 itself.
;; Only present when sprinter-cc is invoked with --debug.
_w2_self_allocated::
.ds 1
.endif
.area _BSS
argv_buf:
.ds ARGV_BUF_BYTES
argv_array:
.ds (ARGV_MAX_ARGS + 1) * 2 ; +1 for trailing NULL pointer
progname_buf:
.ds 128 ; ESTEX APPINFO app_path target
;; =========================================================================
;; gsinit — zero _DATA + _BSS, then copy _INITIALIZER -> _INITIALIZED.
;; _DATA is zeroed because C guarantees zero-valued statics — this lets
;; code drop explicit `= 0` initializers that burn _INITIALIZER bytes.
;; =========================================================================
.area _GSINIT
.area _CODE
;; zero_area — fill BC bytes at HL with 0 (BC may be zero).
zero_area:
ld a, b
or a, c
ret Z
ld (hl), #0
dec bc
ld a, b
or a, c
ret Z
ld d, h
ld e, l
inc de
ldir
ret
.area _GSINIT
gsinit::
ld hl, #s__DATA
ld bc, #l__DATA
call zero_area
ld hl, #s__BSS
ld bc, #l__BSS
call zero_area
ld bc, #l__INITIALIZER
ld a, b
or a, c
jr Z, gsinit_done
ld de, #s__INITIALIZED
ld hl, #s__INITIALIZER
ldir
gsinit_done:
.area _GSFINAL
ret