;; ---------------------------------------------------------------------- ;; 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 ;; Linker-emitted symbols (resolved at link time). .globl s__INITIALIZER .globl l__INITIALIZER .globl s__INITIALIZED .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 ;; 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 _DATA _estex_startup_ix:: .ds 2 _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 — copy _INITIALIZER -> _INITIALIZED, then zero _BSS. ;; ========================================================================= .area _GSINIT gsinit:: ld bc, #l__INITIALIZER ld a, b or a, c jr Z, gsinit_bss ld de, #s__INITIALIZED ld hl, #s__INITIALIZER ldir gsinit_bss: ld bc, #l__BSS ld a, b or a, c jr Z, gsinit_done ld hl, #s__BSS ld (hl), #0 dec bc ld a, b or a, c jr Z, gsinit_done ld d, h ld e, l inc de ldir gsinit_done: .area _GSFINAL ret