Files
Sprinter-SDCC/runtime/crt0_banked.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

515 lines
14 KiB
ArmAsm

;; ----------------------------------------------------------------------
;; crt0_banked.s — Sprinter ESTEX C runtime startup with bank loading.
;;
;; Same entry contract as crt0.s, plus the EXE was packaged with
;; `loader > 0` so the file handle survives in (IX-3).
;;
;; Sequence:
;; 1. Boot stack in W1 (CODE area). We can't yet touch W2 because
;; small images (<16 KB) get only the W1 page from DSS — W2 reads
;; as 0xFF and ignores writes. This mirrors crt0_small.s.
;; 2. Detect via IN A,(#0xC2) whether DSS already mapped W2. If not,
;; ESTEX $3D GETMEM + $3A SETWIN2 maps a fresh page (BIOS $C4 + OUT
;; would require SP in W2, which we don't have yet).
;; 3. SP = 0xBFFE (now W2 is real RAM)
;; 4. Stash file handle + ESTEX block id from prefix
;; 5. GETMEM(_n_banks) → block id, then for each bank i in 1..n:
;; BIOS_EMM_GETPAGE → phys page → _bank_pages[i]
;; OUT (#0xE2), phys ; map into W3
;; ESTEX READ 16384 bytes → 0xC000 ; load bank from file
;; 6. CLOSE file
;; 7. Standard gsinit + call _main + ESTEX EXIT
;;
;; _n_banks is generated by sprinter-cc from the highest --bank number.
;; It is emitted as const because we read it before gsinit; a normal
;; initialized variable would still contain 0 at this point.
;; ----------------------------------------------------------------------
.module crt0_banked
.globl _main
.globl __atexit_hook
.globl _n_banks
.globl _bank_pages
.globl s__INITIALIZER
.globl l__INITIALIZER
.globl s__INITIALIZED
.globl s__DATA
.globl l__DATA
.globl s__BSS
.globl l__BSS
;; argv parsing tunables — mirror crt0.s.
ARGV_MAX_ARGS = 16
ARGV_BUF_BYTES = 128
BOOT_STACK_BYTES = 32 ; enough for a few ESTEX RST 10h calls
;; ----- Banking-window parameters ----------------------------------------
;; Choose between W3 (default = HUGE mode) and W1 (BIG mode, prepended
;; BANK_W1 = 1 at assembly time by sprinter-cc). These constants steer
;; the bank-loading IO port and the address where each bank lives in
;; the program's virtual memory once mapped in.
.ifdef BANK_W1
BANK_PORT = 0xA2 ; W1 page register
BANK_LOAD_ADDR = 0x4000 ; banks at 0x4000-0x7FFF when mapped
.else
BANK_PORT = 0xE2 ; W3 page register
BANK_LOAD_ADDR = 0xC000 ; banks at 0xC000-0xFFFF when mapped
.endif
;; ----- Area declaration 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::
;; ----- Step 1: switch to W1 boot stack (W2 may be unmapped). -------
ld sp, #boot_stack_top
push ix ; stash IX in W1 (W2 not yet safe)
;; ----- Step 2: detect / allocate W2. ------------------------------
;; Port 0xC2 = W2 page register. 0xFF means DSS hasn't mapped W2
;; (program ≤ 16 KB). Any other value means W2 is real RAM and we
;; must NOT replace it (DSS may have stored bytes there).
in a, (#0xC2)
cp #0xFF
jr nz, w2_already_mapped
;; ESTEX $3D GETMEM — alloc 1 page; A = block id, CF = err
ld b, #1
ld c, #0x3D
rst #0x10
jp c, abort_fault
;; ESTEX $3A SETWIN2 — A = block id, B = page idx → map into W2
ld b, #0
ld c, #0x3A
rst #0x10
jp c, abort_fault
.ifdef DEBUG_RT
ld a, #1 ; flag: we self-allocated W2
jr w2_join
.endif
w2_already_mapped:
.ifdef DEBUG_RT
xor a ; flag: DSS already gave us W2
w2_join:
.endif
pop ix ; restore IX
ld sp, #0xBFFE ; canonical stack top (W2)
.ifdef DEBUG_RT
push af ; preserve flag across gsinit/bank load
.endif
;; Stash startup prefix info from IX (asxxxx syntax: disp(ix)).
ld a, -3 (ix)
ld (_estex_file_handle), a
ld a, -2 (ix)
ld (_estex_block_id), a
ld (_estex_startup_ix), ix
.ifdef W3_RESIDENT
;; --w3 в HUGE: резидентный код окна W3 (0xC000) — часть HOME-образа,
;; замаплен DSS. Загрузка банков ниже подменяет W3 своими страницами;
;; запомним физ-страницу резидента, чтобы вернуть её дефолтом после
;; цикла — тогда прямые вызовы в резидентный W3-код снова корректны.
;; (Трамплин bank.s сам сохраняет/восстанавливает W3 на каждый __banked
;; вызов, поэтому дефолтная страница обязана быть резидентной.)
in a, (#0xE2)
ld (_w3_resident_page), a
.endif
;; ---- Allocate _n_banks pages via ESTEX GETMEM ----
ld a, (_n_banks)
or a, a
jr Z, skip_bank_load ; nothing to allocate
ld b, a
ld c, #0x3D ; ESTEX GETMEM
rst #0x10
jp c, abort_fault ; CF=1 → out of memory
ld (_bank_block_id), a ; remember block id
;; ---- For each bank i in 0..n-1 ----
ld b, #0 ; B = page index inside block
load_bank_loop:
ld a, (_n_banks)
cp b
jr Z, load_bank_done ; B == n_banks → finished
;; --- Get physical page for slot B from BIOS EMM ---
push bc ; save loop counter
ld a, (_bank_block_id)
ld c, #0xC4 ; BIOS EMM_GETPAGE
rst #0x08
pop bc
jp c, abort_fault
;; A = physical page. Store in _bank_pages[B+1].
push af ; save A (the phys page)
push bc
ld c, b
inc c ; C = B+1 (1-based index)
ld b, #0
ld hl, #_bank_pages
add hl, bc
pop bc
pop af
ld (hl), a ; _bank_pages[B+1] = phys
;; Map this page into the banking window (W1 for BIG, W3 for HUGE).
out (#BANK_PORT), a
;; ESTEX READ: A=handle, HL=buf, DE=count, C=0x13
push bc ; save loop counter across RST
ld a, (_estex_file_handle)
ld hl, #BANK_LOAD_ADDR
ld de, #0x4000 ; 16384 bytes (one full bank)
ld c, #0x13 ; ESTEX READ
rst #0x10
pop bc
jp c, abort_fault
inc b
jr load_bank_loop
load_bank_done:
;; Close the executable file — banks are in RAM now.
ld a, (_estex_file_handle)
ld c, #0x12 ; ESTEX CLOSE
rst #0x10
;; Ignore CF from close.
.ifdef W3_RESIDENT
;; Вернуть резидентную страницу W3 дефолтом (цикл выше оставил в W3
;; последний банк). После этого прямые call'ы в резидентный код 0xC0xx
;; корректны, а трамплины будут сохранять/возвращать именно её.
ld a, (_w3_resident_page)
out (#0xE2), a
.endif
skip_bank_load:
;; ---- Standard SDCC init path ----
call gsinit
.ifdef DEBUG_RT
pop af
ld (_w2_self_allocated), a
.endif
;; Parse cmdline into argv[] (BSS now zeroed by gsinit), then fix
;; argv[0] to the .EXE basename via APPINFO. Both routines fail
;; gracefully (argv[0] stays empty if APPINFO returns CF=1).
call parse_argv
call get_progname
;; SDCC __sdcccall(1): main(argc, argv) → 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
;; main returned: int return is in DE per SDCC 4.5 __sdcccall(1).
ld a, e
jr exit_with_a
_exit::
;; Userspace _exit(int code) — code in HL (single-arg ABI).
ld a, l
exit_with_a:
ld b, a
ld c, #0x41 ; ESTEX EXIT
rst #0x10
1$: halt
jr 1$
abort_fault:
;; Bail out with a sentinel exit code so failures are visible in the
;; emulator / shell.
ld b, #0xFE
ld c, #0x41
rst #0x10
1$: halt
jr 1$
;; =========================================================================
;; parse_argv / get_progname — identical copy of crt0.s. When we build
;; libsprinter.lib these two routines will be factored into argv.s and
;; linked once for all crt0 variants.
;; =========================================================================
.area _CODE
parse_argv::
ld hl, (_estex_startup_ix)
ld a, (hl)
cp #ARGV_BUF_BYTES
jr c, len_ok
ld a, #ARGV_BUF_BYTES-1
len_ok:
ld c, a
ld b, #0
inc hl
ld de, #argv_buf
ld a, b
or a, c
jr Z, after_copy
ldir
after_copy:
xor a
ld (de), a
ld hl, #argv_buf
strip_ws:
ld a, (hl)
or a
jr Z, no_args
cp #0x20
jr Z, strip_more
cp #0x09
jr nz, have_first_token
strip_more:
inc hl
jr strip_ws
no_args:
ld hl, #empty_str
ld (argv_array), hl
ld hl, #0
ld (argv_array+2), hl
ld hl, #1
ld (_argc), hl
ld hl, #argv_array
ld (_argv), hl
ret
have_first_token:
ld de, #empty_str
ld (argv_array), de
ld b, #1
token_loop:
ld a, b
cp #ARGV_MAX_ARGS
jr nc, tokens_done
push hl
ld a, b
add a, a
ld e, a
ld d, #0
ld hl, #argv_array
add hl, de
pop de
ld (hl), e
inc hl
ld (hl), d
ex de, hl
inc b
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:
xor a
ld (hl), a
inc hl
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:
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
ld a, b
ld (_argc), a
xor a
ld (_argc+1), a
ld hl, #argv_array
ld (_argv), hl
ret
empty_str:
.db 0
;; =========================================================================
;; Boot stack — reserved in _CODE (W1, real RAM). Used only during the
;; first few instructions of _start, before W2 is known-mapped. After we
;; switch SP to 0xBFFE this memory is just dead bytes in HOME.
;; =========================================================================
boot_stack:
.ds BOOT_STACK_BYTES
boot_stack_top:
get_progname::
push ix
ld hl, #progname_buf
ld b, #2
ld c, #0x47
rst #0x10
pop ix
jr c, gpn_skip
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:
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
gpn_dec:
dec hl
ld a, (hl)
cp #0x5C
jr Z, gpn_after_sep
cp #0x3A
jr Z, gpn_after_sep
jr gpn_scan_back
gpn_after_sep:
inc hl
gpn_set:
ld (argv_array), hl
gpn_skip:
ret
;; =========================================================================
;; Runtime data
;; =========================================================================
.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
_estex_file_handle::
.ds 1
_estex_block_id::
.ds 1
_bank_block_id::
.ds 1
.ifdef W3_RESIDENT
_w3_resident_page:
.ds 1
.endif
.area _DATA
_argc::
.ds 2
_argv::
.ds 2
.area _BSS
argv_buf:
.ds ARGV_BUF_BYTES
argv_array:
.ds (ARGV_MAX_ARGS + 1) * 2
progname_buf:
.ds 128
.ifdef DEBUG_RT
;; Runtime diagnostic — same semantics as in crt0.s / crt0_small.s.
_w2_self_allocated::
.ds 1
.endif
;; =========================================================================
;; gsinit — same as crt0.s (zero _DATA + _BSS, copy INITIALIZER)
;; =========================================================================
.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::
;; --- Zero _DATA + _BSS (C guarantees zero-valued statics) ---
ld hl, #s__DATA
ld bc, #l__DATA
call zero_area
ld hl, #s__BSS
ld bc, #l__BSS
call zero_area
;; --- Copy _INITIALIZER -> _INITIALIZED if non-empty ---
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