Files
Sprinter-SDCC/runtime/crt0_small.s
T
snark13 961cfb786d toolchain: gsinit зануляет _DATA (C-семантика статиков) + sprinter-cc --max-allocs
Все четыре crt0 (default/small/minimal/banked): gsinit теперь зануляет
_DATA и _BSS через общий zero_area, затем копирует _INITIALIZER.
Явные `= 0` у глобалов/статиков больше не нужны (они жгли байты
_INITIALIZER в образе). crt0-приватные переменные, записываемые ДО
gsinit (_estex_startup_ix и др.), перенесены из _DATA в _CODE (RAM).

sprinter-cc: новая опция --max-allocs N → SDCC --max-allocs-per-node
(агрессивнее аллокация регистров, меньше/быстрее код ценой времени
компиляции).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 21:15:42 +03:00

412 lines
11 KiB
ArmAsm

;; ----------------------------------------------------------------------
;; crt0_small.s — Sprinter ESTEX C runtime startup for SMALL memory mode.
;;
;; Layout (small mode):
;; CODE at 0x4100 (W1)
;; DATA chained after code by the linker (W1, possibly spills into W2)
;; STACK at 0xBFFE (top of W2, valid after W2 page is mapped)
;; HEAP from end-of-BSS up to ___sdcc_heap_end (default 0xBB00)
;;
;; Size auto-detect:
;; For programs < ~14 KB, DSS allocates exactly ONE 16 KB page (W1). W2
;; is "page #FF" (read=0xFF, writes ignored) and we must allocate &
;; map a page ourselves. For programs that already span > 16 KB (the
;; linker emitted bytes above 0x7FFF), DSS allocates TWO pages on its
;; own — W2 already RAM — and we MUST NOT call SETWIN2, or we'd swap
;; the DSS-loaded code page out of W2.
;;
;; We auto-detect by writing a test byte at 0xBFFF (well above stack
;; pushes) and reading back. Match → W2 is RAM, skip allocation.
;; Mismatch → W2 is page #FF, allocate via ESTEX $3D + $3A.
;;
;; Chicken-and-egg:
;; DSS sets SP from the EXE header (0xBFFE) — in unmapped W2 for small
;; programs. Pushes silently fail; first `call` returns to 0xFFFF and
;; crashes. Boot stack lives in HOME (W1, real RAM) until W2 is ready.
;;
;; Why ESTEX $3A SETWIN2 not BIOS $C4 + OUT (0xC2):
;; BIOS calls require SP in W2 (0x8000..0xBFFF) — exactly what we don't
;; have yet. SETWIN2 is ESTEX, works with W1 stack.
;;
;; Entry contract — same as crt0.s.
;; ----------------------------------------------------------------------
.module crt0_small
.globl _main
;; 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 — mirror crt0.s.
ARGV_MAX_ARGS = 16
ARGV_BUF_BYTES = 128
BOOT_STACK_BYTES = 32 ; enough for 2-3 nested RST 10h/RST 8 calls
;; =========================================================================
;; AREA ORDERING.
;; =========================================================================
.area _HOME
.area _CODE
.area _INITIALIZER
.area _GSINIT
.area _GSFINAL
.area _DATA
.area _INITIALIZED
.area _BSEG
.area _BSS
.area _HEAP
;; =========================================================================
;; Entry point — must be FIRST in _CODE so it lands at --code-loc (0x4100).
;; W2 is unmapped at this instant — no `call` allowed until we point SP at
;; boot_stack_top (in W1, declared just below).
;; =========================================================================
.area _CODE
_start::
;; Switch to the boot stack in W1. The DSS-set SP=0xBFFE may point
;; into unmapped W2 — pushing there silently fails and the first call
;; would return to 0xFFFF. Boot stack lives in HOME (W1, real RAM).
ld sp, #boot_stack_top
;; The DSS startup-prefix pointer is in IX. Stash it on the boot
;; stack — _estex_startup_ix is in W2 which may not be RAM yet.
push ix
;; ----- Step 1: detect whether W2 is already mapped. -----------------
;; Port 0xC2 is the W2 page register (0x82/A2/C2/E2 = W0/W1/W2/W3).
;; Reading it returns the page number currently selected for W2; page
;; 0xFF is the "unmapped" sentinel (read=0xFF / write-ignored). A
;; non-0xFF value means DSS already allocated and mapped a real RAM
;; page for W2 — this happens for programs whose HOME image extends
;; past 0x7FFF (DSS allocates as many pages as the image size demands).
in a, (#0xC2)
cp #0xFF
jr nz, w2_already_mapped
;; ----- Step 2a: W2 unmapped → allocate one page and map it. --------
;; ESTEX $3D GETMEM: B = npages → A = block id, CF = err.
ld b, #1
ld c, #0x3D
rst #0x10
jr c, w2_alloc_fail
;; A = block id of the freshly-allocated 1-page block.
;; ESTEX $3A SETWIN2: A = block id, B = page idx → maps to W2. This
;; is an ESTEX (RST 10h) call, not BIOS (RST 8) — BIOS would require
;; SP in W2, which is exactly what we don't have yet.
ld b, #0 ; page 0 of the 1-page block
ld c, #0x3A
rst #0x10
jr c, w2_alloc_fail
.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
;; ----- Step 3: switch to the canonical SP at the top of W2. --------
;; Pop IX back from the boot stack first.
pop ix
ld sp, #0xBFFE
.ifdef DEBUG_RT
;; Save the self-alloc flag across gsinit (which clobbers BC/DE/HL and
;; zeroes _DATA/_BSS). SP is in W2 now, push is safe.
push af
.endif
;; ----- Step 5: save IX prefix in its canonical slot (_CODE, W1). ----
ld (_estex_startup_ix), ix
;; ----- Step 6: standard gsinit + argv flow (identical to crt0.s). --
call gsinit
.ifdef DEBUG_RT
pop af
ld (_w2_self_allocated), a
.endif
call parse_argv
call get_progname
ld hl, (_argc)
ld de, (_argv)
call _main
;; SDCC int return in DE. Low byte = exit code.
ld a, e
ld b, a
ld c, #0x41 ; ESTEX EXIT
rst #0x10
1$: halt
jr 1$
;; -------------------------------------------------------------------------
;; W2 allocation failed — there's not much we can do without DATA/BSS.
;; Try to abort via ESTEX EXIT with a non-zero code (no return).
;; -------------------------------------------------------------------------
w2_alloc_fail:
ld b, #0xFE ; arbitrary failure code
ld c, #0x41 ; ESTEX EXIT
rst #0x10
2$: halt
jr 2$
;; =========================================================================
;; Boot stack — reserved in HOME (W1, real RAM). Used only during the
;; first few instructions of _start, before W2 is mapped. After we switch
;; SP to 0xBFFE the boot stack memory is just dead bytes in HOME.
;; =========================================================================
boot_stack:
.ds BOOT_STACK_BYTES
boot_stack_top:
;; =========================================================================
;; parse_argv / get_progname / runtime data / gsinit — same as crt0.s.
;; Kept inline rather than factored out to keep crt0 build self-contained.
;; =========================================================================
.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
.area _CODE
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 — same layout as crt0.s. Lives in DATA/BSS which the
;; linker places in W2 (--data-loc 0x8000).
;; =========================================================================
.area _CODE
;; _estex_startup_ix is written BEFORE gsinit runs (gsinit zeroes _DATA),
;; so it lives in _CODE — RAM on the Sprinter, always mapped.
_estex_startup_ix::
.ds 2
.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: 0 = DSS already mapped W2 for us (program > 16 KB,
;; or some other case); 1 = crt0 had to allocate and map W2 itself via
;; ESTEX $3D + $3A. Only present when sprinter-cc is invoked with --debug.
_w2_self_allocated::
.ds 1
.endif
;; =========================================================================
;; gsinit — zero _DATA + _BSS, then copy _INITIALIZER -> _INITIALIZED.
;; Identical to crt0.s; runs AFTER W2 is mapped so the writes land.
;; _DATA is zeroed because C guarantees zero-valued statics — initializing
;; `static uint8_t x;` here lets code drop explicit `= 0` initializers
;; that would otherwise burn _INITIALIZER bytes in the EXE image.
;; =========================================================================
.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