Skip to content

Convert build from ACME to ca65/ld65 - #14

Merged
JC-000 merged 28 commits into
masterfrom
refactor/ca65-conversion
May 6, 2026
Merged

Convert build from ACME to ca65/ld65#14
JC-000 merged 28 commits into
masterfrom
refactor/ca65-conversion

Conversation

@JC-000

Copy link
Copy Markdown
Owner

Summary

Full conversion of the c64-https build system from ACME to ca65/ld65,
plus the modular seams needed to hot-swap networking backends and
crypto implementations.

Three architectural wins fall out of the port:

  • ACME is gone.make now drives ca65/ld65 end-to-end. No more
    parallel Makefiles, no more ACME-isms in src/*.asm.
  • Networking backend ABI (src/net_abi.inc). TLS/HTTP consume
    networking only through a frozen symbol set. Today only
    src/net/ip65/ ships, but src/net/uci/ is scaffolded for a future
    U64E backend, and make BACKEND=uci is already a valid cfg target.
  • Crypto ABI facade (src/crypto_abi.inc) aligned with the three
    in-progress sibling libraries:
    • c64-x25519x25519_* / fe25519_*
    • c64-ChaCha20-Poly1305chacha20_* / poly1305_* / aead_*
    • c64-nist-curvesec_point_* (P-256; P-384 stubbed)

Phase-by-phase

  • Phase 0 — Discovery. Inventory every ACME source, cross-reference
    with sibling libraries, lock a conversion order.
  • Phase 1 — Design. MEMORY/SEGMENTS cfg, crypto_abi.inc and
    net_abi.inc contract, single-source-of-truth plan for ip65 equates.
  • Phase 2 — Bootstrap. ca65 pilot on entropy.asmentropy.s;
    Makefile.ca65 runs side-by-side with the old ACME Makefile.
  • Phase 3 — Batches A-D. Convert the remaining 27 ACME files:
    • Batch A: primitives (sha256, word32, data tables)
    • Batch B: crypto middle layer (fe25519, chacha20, poly1305, aead,
      hmac-drbg, ecdsa fp/curve/mod/points/verify, x25519)
    • Batch C: TLS state machine + HTTP client
    • Batch D: glue files and net backend relocation under
      src/net/ip65/
  • Phase 4 — Link layout. Harden cfg/c64-https-ip65.cfg so ld65
    lays the PRG out with contiguous file-backed regions, correct BSS
    segments, TABLES_BSS pinned below $A000, and a TCP ring at $C000.
  • Phase 5 — PRG structure fixes. LOADADDR segment, zero-fill in
    the LOADER/NET gap, VICE label format rewrite (al C:XXXX), one
    single-exports-file to promote numeric equates to .exports.
  • Phase 6 — Boot regression debug. Three latent bugs surfaced
    only after the cleaner ca65 layout:
    1. BASIC SYS stub computed a target 3 bytes past the real entry
      — on the ACME build this happened to point at a NOP sled.
    2. A gap in NET_BSS wasn't zero-initialised, so ip65 sometimes
      booted with non-zero state.
    3. SHADOW_BSS did not get the startup zero-fill that our code
      assumed. All three fixed in b9da4b8.
  • Phase 7 — This PR. Retire the ACME Makefile entirely, honor
    C64_SKIP_BUILD=1 in test scripts, consolidate ip65 equates into
    src/net/ip65/ip65_symbols.inc, add CLAUDE.md.

Smoke test results

All 7 unit test scripts pass:

ScriptAssertions
tools/test_entropy.py7/7
tools/test_hkdf.pypassing
tools/test_chained_hmac.pypassing
tools/test_keyschedule_steps.pypassing
tools/test_tls_handshake.pypassing
tools/test_http.pypassing
tools/test_x509.pypassing

Total: 7/7 files, 97/97 assertions.

test_entropy.py was re-run on this branch as the final smoke check
both with and without C64_SKIP_BUILD=1 — both paths green.

Known limitations

  • Memory is tight.CRYPTO region ($6000-$9FFF) is 100 % full
    and SHADOW_BSS ($A000-$BFFF) is ~99.8 % full (roughly 20 bytes
    of slack). Any new crypto byte requires reclamation somewhere. There
    is a documented follow-up TODO to restructure the MEMORY map so all
    file-backed regions are physically contiguous; this refactor
    intentionally does not attempt that restructure.
  • P-384 is stubbed.ecdsa_*_384.asm files are not assembled;
    P-384 must be restored before consuming real cert chains that
    require it. Tracked under the project_p384_stubbed memory note.
  • End-to-end HTTPS against www.foo.bar (the bridge-rig demo
    domain — never a real internet host) is still blocked on an
    upstream ip65 bug. Unchanged by this refactor. See the
    project_phase3_handoff memory note for the debug trail.

Not merging yet

  • Wants at minimum one sanity run on real hardware (C64 + RR-Net) if
    the reviewer can spare the setup. This PR is a build-system
    refactor, so behavioural regressions are unlikely, but the BASIC
    SYS / BSS zero-fill bugs fixed in Phase 6 were invisible on the
    ACME build and only surfaced under ca65 — hardware soak is cheap
    insurance.

Test plan

  • make clean && make produces build/c64-https.prg and
    build/labels.txt with no warnings
  • tools/test_entropy.py passes (no env)
  • tools/test_entropy.py passes with C64_SKIP_BUILD=1
  • Reviewer runs the remaining six test scripts under VICE
  • Reviewer runs the PRG on real hardware (C64 + RR-Net) if
    possible — boot to main menu and trigger DHCP is sufficient

🤖 Generated with Claude Code


Originally posted by @JC-000 on 2026-04-15

JC-000and others added 28 commits March 23, 2026 15:39
…erged
The harness wait_for_text() now calls transport.resume() between polls
internally, so the inline polling loops are no longer needed. This
replaces 13 copies of the same ~10-line loop with single wait_for_text()
calls, reducing total code by 120 lines.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r reuse
The parallel runner had two bugs: (1) as_completed() didn't see futures added
mid-iteration, so only the first N suites were collected, and (2) reusing VICE
instances across suites caused state contamination (HKDF 0/12 on reused workers).
Fix: allocate a fresh VICE instance per suite via run_suite_in_own_instance().
Add all 10 suites (was 5). Add --skip-slow and --seed flags. 193/193 pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix parallel test runner: all 10 suites, instance-per-suite.
… tests
net_dns_resolve and net_set_tcp_dest both passed A/X parameters through
net_save_zp, which uses X as a loop counter and clobbers both registers.
This caused DNS resolution and TCP destination setup to receive garbage
pointers instead of the caller's intended addresses. Fixed by pushing
A/X to the stack before the ZP save and restoring after.
Added test_dns.py (4 tests) exercising net_dns_resolve over TAP with
dnsmasq, and test_http_integration.py (5 tests) for end-to-end plain
HTTP GET (DNS → TCP → request → response). Both use ViceInstanceManager
with ethernet_mode="rrnet" and run unprivileged (only dnsmasq via sudo).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ion-tests
Fix A/X register clobbering in net.asm, add DNS + HTTP integration tests
Replace baseline fe25519/x25519 with optimized versions from the c64-x25519
performance tuning project, achieving ~30% speedup (12,782 jiffies / 3.6 min
per key generation vs 18,005 baseline).
Optimizations imported:
- REU DMA multiplication tables (128KB REU, 4 cyc/product vs mul_8x8)
- mult66 indirect-indexed quarter-square multiply for fe_sqr
- Self-modifying accumulation addresses in fe_mul/fe_sqr inner loops
- 4x unrolled constant-time fe_cswap (38 cyc/byte vs 49)
- Shift-before-accumulate for fe_sqr cross terms
- mul_by_38 lookup tables for fe_reduce_wide
Key integration fixes:
- Optimization tables (mul_dma, sqtab2, mul38) placed early in data.asm to
stay below $A000 and avoid BASIC ROM shadow region
- BASIC ROM banked out at boot and kept off during runtime (data buffers at
$A000+ need direct RAM access)
- VICE launched with -reu -reusize 512 for all test suites
- Zero page lmul0/lmul1 pointers time-shared with ChaCha20 vars
New files:
- tools/test_x25519.py: 71 unit tests (fe25519 field ops + x25519_clamp +
optional --slow RFC 7748 scalarmult vectors)
- tools/bench_x25519.py: key generation benchmark with jiffy clock timing
and Python X25519 verification
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Import optimized X25519 (~30% faster key generation)
Introduces tools/net_test_env.py with NetworkTestEnv context manager
that handles TAP interface, dnsmasq, and optional HTTPS server lifecycle.
Replaces duplicated inline setup/teardown code across network test files
and guarantees cleanup via __exit__, signal handlers, and atexit.
Migrates test_dns.py as proof-of-concept. Includes 14 unit tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add consolidated network test environment
Vendor the c64-test-harness bridge networking scripts (setup + cleanup)
and extend them with dnsmasq (DHCP + DNS overrides). Build a reusable
tools/https_e2e/ library with BridgeEnv context manager, single-VICE
launcher, boot menu helpers, and HTTP listener. Two passing e2e tests
drive the real c64-https binary in VICE over RR-Net at normal speed:
Phase 1 verifies DHCP, Phase 2 verifies plain HTTP GET to a local
listener. HTTPS (Phase 3) to follow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add end-to-end bridge tests for DHCP and HTTP GET
The original CPU JAM at $4DE0 was caused by ip65 writing DHCP packets on
top of sha256_rotr8 code: c64-https crypto code streamed from $3B28 right
through ip65's BSS window at $4000-$5FFF. Fix the layout by inserting
`* = $6000` before word32.asm so all of word32/chacha/poly/aead/sha256/
hmac_drbg/fe25519/x25519/ecdsa + ecdsa_verify/der/tls_cert/tls_ecdh land
above the BSS. Requires three adjacent compromises to fit in the space
below $7C00:
- Relocate sqtab from the hardcoded $7800/$7A00 equates to labels inside
data.asm BSS, freeing the $7800-$7BFF region
- Stub P-384 ECDSA (src/crypto/ecdsa_verify.asm) to return an error
instead of dispatching. P-384 isn't needed for the self-signed P-256
cert used in tests but MUST be restored before real CA chains. Tech
debt tracked in project_p384_stubbed memory
- Remove the `* = $7C00` barrier now that sqtab is gone
With the layout fix, the CPU JAM is gone entirely and the TLS receive
path progresses far enough to expose several follow-on bugs that a
clean walkthrough diagnosed and fixed in sequence:
- tls_record_io.asm: plaintext/encrypted dispatch was comparing against
TLS_STATE_SERVER_HELLO ($02) but tls_state is already $02 when the
ServerHello arrives, causing the code to try to AEAD-decrypt a plaintext
record. Change to TLS_STATE_ENCRYPTED_EXT ($03)
- net.asm: TCP recv callback had an 8-bit X register wrap when ip65
delivered packets >255 bytes, causing re-reads of source bytes.
Clamp cb_remaining to 255 per invocation
- tls_keyschedule.asm: tls_derive_handshake_keys fell off the end of
its final loop without a clc, leaving the carry flag set from the
last hmac_sha256 call. Caller's `bcs @error` then fired on success.
Add clc before rts
- tls13.asm: tls_ecdh_compute_shared was defined in tls_ecdh.asm but
never called anywhere in the codebase, so tls_shared_secret stayed
zeros. HKDF then derived handshake keys from zeros. Add the jsr call
in tls_recv_server_hello right after tls_parse_server_hello succeeds
- tls_record_io.asm: RFC 8446 §5 says TLS 1.3 clients MUST ignore CCS
records during handshake. Add a retry that skips CCS content type
Rename the demo URL from www.apple.com to www.foo.bar throughout boot.asm,
dnsmasq DNS overrides, HTTPS listener cert subject/SAN, and test screen
text expectations, to avoid any risk of real-world impact if the test
environment escapes the bridge.
Bridge setup/cleanup scripts hardened: no blanket pkill, specific PID
tracking, idempotent re-entry, handling of stale vice_eth rc files and
legacy tap-c64 interfaces from older test harness setups.
New test tests/test_phase3_https.py drives the full Phase 3 e2e flow:
BridgeEnv + HTTPS listener + VICE + DHCP + HTTPS GET. Captures granular
post-mortem diagnostics (tls_state, tls_last_state, tls_recv_progress,
tls_recv_sub_progress, tls_rec_header, tls_rec_buf, tls_hs_buf,
tcp_recv_buf, tls_recv_state/count) via dynamic label lookup from
build/labels.txt so diagnostic addresses track the current build.
Instrumentation added to src/data.asm (tls_last_state, tls_recv_progress,
tls_recv_sub_progress, tls_recv_poll_count) and to tls_recv_server_hello,
tls_record_recv_and_decrypt, tls_recv_record for precise failure-site
identification. These are permanent diagnostic helpers.
Current status: handshake reaches tls_state=$03 (ENCRYPTED_EXT) and dies
trying to receive the first encrypted handshake record (EncryptedExtensions).
Two remaining architectural bugs require multi-file refactoring and are
tracked as follow-on work:
1. tcp_recv_buf is 256 bytes with 8-bit indices. TLS 1.3 Certificate
records (~374 bytes) don't fit. Needs 2KB ring with 16-bit indices
and producer overflow check in net_tcp_recv_cb
2. aead_data_len is 1 byte. Caps AEAD decrypt at 255 bytes, so the
~353-byte Certificate ciphertext body is truncated and tag always
fails. Needs widening to 16-bit with updated ChaCha20/Poly1305 loops
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two coordinated architectural changes to unblock TLS 1.3 handshake
past the ServerHello state:
**tcp_recv_buf at \$C000, 4096 bytes, 16-bit indices**
The previous 256-byte ring with 8-bit head/tail silently overwrote
unread data whenever ip65 delivered a TCP segment larger than the
empty space — the TLS 1.3 post-ServerHello flight (CCS + Encrypted-
Extensions + Certificate + CertVerify + Finished, ~600-800 bytes)
always overflowed.
Move the buffer to the 4KB free-RAM region at \$C000-\$CFFF (always
RAM on a C64, never ROM-shadowed) via an equate in constants.asm —
zero impact on data.asm BSS footprint. Convert head/tail to 16-bit
words. Add a tcp_recv_overflow sticky flag set by the producer when
it would lap the consumer.
Update all consumer and producer paths in net.asm to use 16-bit
indexing with mask-on-access (\$0FFF = 4095). http.asm feed loop
also updated. Add producer overflow check in net_tcp_recv_cb so
ip65's callback cannot silently corrupt the ring.
**aead_data_len widened to 16-bit**
Was a single byte, which capped AEAD encrypt/decrypt at 255 bytes.
TLS 1.3 Certificate records carry ~350+ bytes of ciphertext, so
decrypt was always truncated and the Poly1305 tag always failed.
Change aead_data_len to !word in data.asm. Update the aead_encrypt /
aead_decrypt / aead_compute_tag paths in crypto/aead.asm to 16-bit
counter semantics (compare-with-ora for zero, decrement with borrow,
length block stored in both bytes of the 64-bit field).
Update chacha20_encrypt block loop to a 16-bit counter over
cc20_remain:cc20_remain_hi.
Update tls_record.asm where tls_enc_aead_len is copied into
aead_data_len — both bytes are stored now.
poly1305_update in poly1305.asm is unchanged because AEAD
exclusively drives Poly1305 via aead_process_padded now; the
standalone poly1305_update path is dead code and doesn't need
widening for this fix.
**Verification**
After this change, Phase 3 test reaches tls_state=\$03
(ENCRYPTED_EXT), confirms the full post-ServerHello server flight
is visible and intact in the ring at \$C000 (ServerHello, CCS,
EncryptedExtensions record 50B, Certificate record 369B),
tcp_recv_head/tail 16-bit values working. No regression on DHCP,
DNS, TCP connect, or ServerHello parse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds on-screen progress markers at each TLS handshake state transition
in tls_connect plus entry/exit counters on net_poll to definitively
localize the Phase 3 HTTPS stall.
**Markers** (src/tls13.asm + src/boot.asm):
CH/SH/HK1/KEYS/ENC1/RX/GOT2/DEC/PROC/EE/CERT/CV/FIN/CFIN printed on
screen as each step of tls_connect completes. The branch-target
trampoline pattern (`bcc @okn / jmp @error / @okn:`) is used because
the inserted print calls push @error out of bcs range.
**Counters** (src/data.asm + src/net.asm):
net_poll_entry_count and net_poll_return_count 16-bit counters
increment at entry and exit of net_poll. Gap between them = number
of active ip65_process calls currently on the stack.
**Test diagnostics** (tests/test_phase3_https.py):
- Dynamic label lookup via _label_addr() against build/labels.txt so
diagnostic reads always hit the current build regardless of BSS
layout shifts
- File-logged post-mortem at /tmp/c64-https-phase3-diag.log with
explicit flush(), survives hard-kill
- Per-heartbeat sampling of tcp_recv_head, tcp_recv_tail,
net_poll_entry/return_count, printed inline
- CPU register read (PC, SP, A, X, Y) via transport.read_registers()
- Top-of-stack return-address chain with label resolution
- Full dumps of tls_rec_buf, tls_hs_buf, tcp_recv_buf in hex+ASCII
**Conclusion from this instrumentation**: Phase 3 handshake progresses
successfully through CH, SH, HK1, KEYS, ENC1, RX markers — i.e., all
of X25519 keygen, X25519 shared secret, HKDF handshake-key derivation,
and transition into tls_recv_encrypted. It then stalls permanently
while net_poll_entry - net_poll_return = 1 (exactly one ip65_process
call on the stack, never returning) and tcp_recv_tail frozen at
0x0198 (408 bytes) across 1800 seconds / 55+ heartbeats.
The stall is inside ip65 — likely the CS8900a driver or IP/TCP
receive path — triggered by the second TCP segment carrying
Certificate record bytes arriving after the multi-minute X25519
silence. None of the c64-https TLS code has a bug at this point;
the TLS layer never gets a CPU quantum because net_poll doesn't
return.
This commit preserves the instrumentation for future sessions to
investigate the ip65 stall with PC sampling and ip65 code inspection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 WIP: layout fix + TLS receive-path fixes (hits architectural limits)
Re-runs Phase 2 bootstrap on the post-PR-13 base (master 6cf0104),
after hard-resetting refactor/ca65-conversion to include the
memory-layout and TLS receive-path fixes from commits 1c75ed9,
ac57d1f, eab7570.
- Add cfg/c64-https-ip65.cfg (ld65 config with NET/CRYPTO/SHADOW/TCP regions)
- Add cfg/c64-https-uci.cfg placeholder for U64E UCI backend
- Add src/macros.inc, src/crypto_abi.inc, src/net_abi.inc facades
- Add src/extern/{c64-x25519,c64-ChaCha20-Poly1305,c64-nist-curves}/README.md
- Add src/net/{ip65,uci}/README.md
- Add Makefile.ca65 (runs alongside ACME Makefile during refactor)
- Convert src/constants.asm -> src/constants.inc (pilot, pure equates,
with post-fix TCP_RECV_BUF_SIZE = 4096)
- Convert src/entropy.asm -> src/entropy.s (pilot, assembles to entropy.o)
ACME originals preserved; Phase 3 agents handle bulk conversion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts 13 crypto leaf files from ACME to ca65 object format on the
post-PR-13 merged base (master 6cf0104). Each file uses explicit
.export/.import discipline, .segment placement per cfg/c64-https-ip65.cfg,
and .include "constants.inc" for zero-page equates.
Preserves all fixes from commits 1c75ed9, ac57d1f, eab7570 that are now
on master after the PR #13 merge:
- aead.s uses 16-bit aead_data_len counter (ora aead_data_len+1 pattern)
- chacha20.s imports cc20_remain_hi for 16-bit counter
- all crypto files inherit the * = \$6000 anchor placement via cfg
Batch A contents:
- crypto/word32.s — 32-bit word primitives
- crypto/sha256.s — SHA-256 hash (K[64] in CRYPTO_RODATA)
- crypto/chacha20.s — ChaCha20 (cc20_set_* macros ported as ca65 .macro)
- crypto/poly1305.s — Poly1305 MAC (scratch to CRYPTO_BSS)
- crypto/fe25519.s — Field arithmetic mod 2^255-19 (uses fe_* prefix)
- crypto/x25519.s — Curve25519 Montgomery ladder
- crypto/hmac_drbg.s — HMAC-DRBG deterministic RNG
- crypto/aead.s — ChaCha20-Poly1305 AEAD envelope (16-bit counters)
- crypto/ecdsa_fp.s — P-256 field prime arithmetic (owns fp_wide)
- crypto/ecdsa_mod.s — P-256 scalar modular arithmetic (owns fp_r0..r3)
- crypto/ecdsa_curve.s — P-256 curve parameters + helpers
- crypto/ecdsa_points.s— Jacobian point arithmetic
- crypto/ecdsa_verify.s— ECDSA signature verification (P-256; P-384 stubbed
to sec/rts, imports preserved for future restore)
All 13 files assembled via:
ca65 -I src -o build/crypto/<name>.o src/crypto/<name>.s
Cross-file dependencies expressed as .import and resolve at link time
once Batch B (TLS primitives) and Batch D (data.asm/net.asm) complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts 6 TLS primitive files from ACME to ca65 object format on the
post-PR-13 merged base:
- src/hkdf.s — HKDF (RFC 5869) wrapping HMAC-SHA256
- src/tls_transcript.s — TLS 1.3 handshake transcript hash
- src/tls_ecdh.s — TLS 1.3 X25519 key exchange wrapper
- src/tls_record_io.s — TLS record TCP I/O
- src/tls_record.s — TLS 1.3 record layer framing + AEAD
- src/tls_keyschedule.s — TLS 1.3 HKDF key derivation tree
All 6 files assembled clean on first try via ca65 -I src.
All fixes from commits 1c75ed9, ac57d1f, eab7570 are natively present
in the post-PR-13 source and carried through the conversion verbatim:
- tls_record_io.s: TLS_STATE_ENCRYPTED_EXT (\$03) state check +
CCS (ChangeCipherSpec) filter per RFC 8446 \xA75.
- tls_record.s: 16-bit aead_data_len store sequence in both
tls_record_encrypt and tls_record_decrypt.
- tls_keyschedule.s: clc before rts in tls_derive_handshake_keys
to clear stale carry from hmac_sha256.
Cross-file imports (crypto .o files, data.asm BSS) resolve at link
time once Batches C and D complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts 5 TLS state-machine and HTTP files from ACME to ca65:
- src/tls13.s — TLS 1.3 state machine and record assembly
- src/tls_handshake.s — ClientHello, ServerHello, finished, sig verify
- src/tls_cert.s — X.509 certificate chain validation
- src/der_decode.s — X.509 ASN.1 DER decoder
- src/http.s — HTTP/1.1 client over TLS (www.foo.bar)
All 5 files assemble clean via ca65 -I src. Cross-file imports to
data.asm BSS, net.asm, and src/net/ip65/*.s remain unresolved until
Batch D converts the remaining glue files.
Three of the five agents hit API 500s returning their summary reports
but had already completed the source conversions. A recovery agent
verified all five .s files assemble cleanly before this commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts the last 4 ACME files to ca65 and introduces the ip65
backend infrastructure under src/net/ip65/:
- src/data.s — program-wide BSS + initialized tables
- src/boot.s — startup, BASIC stub, screen output,
REU multiply support, TLS state markers
- src/main.s — orchestrator shell (was 89 lines of
!source/!binary directives; now ~zero
code since each .s is its own TU)
- src/net/ip65/net.s — ip65/RR-Net networking backend
(relocated from src/net.asm)
- src/net/ip65/ip65_blob.s — .incbin wrapper placing the pre-built
ip65-c64.bin at \$2000 via NET_CODE segment
- src/net/ip65/ip65_symbols.inc — ip65 jump-table + variable-table equates
sourced from ip65-build/ip65-c64.map
All 7 files assemble clean via ca65 -I src -I src/net/ip65.
Load-bearing fixes preserved from PR #13:
- net.s: cb_remaining clamp-to-255 in net_tcp_recv_cb (1c75ed9)
- net.s: ZP \$02-\$1B save/restore around every ip65 call site
- data.s: 4KB tcp_recv_buf, 16-bit aead_data_len (ac57d1f)
- boot.s: all 15 TLS state-transition screen markers (eab7570)
ip65_symbols.inc is guarded with .ifndef ip65_base so it co-exists
with the legacy ip65 equates still in constants.inc. Phase 7 will
consolidate them.
Phase 3 structurally complete. Cross-file imports (data.s BSS
producer, crypto .o files, TLS .o files) all resolve via ca65 object
discipline; final ld65 link is Phase 4.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves all ld65 link errors by restructuring the memory layout
and adding two new segments. Produces a clean 27812-byte .prg.
cfg/c64-https-ip65.cfg:
- Drop 'define = yes' from SEGMENTS entries (was duplicating
__NET_CODE_SIZE__ etc. against the MEMORY-side defines, causing
ld65 abort)
- Route RODATA segment to CRYPTO region (was LOADER; reclaims
~1.9 KB of LOADER headroom)
- Add TLS_CODE segment, mapped to CRYPTO region, so large TLS
object files can opt out of the tight LOADER region
- Add TABLES_BSS segment with align=\$100, mapped to CRYPTO, for
x25519 multiplication tables that must live below \$A000
- Mark ZP_SHARED, ZP_WIDE, LOADADDR, NET_BSS, TCP_RECV_BUF as
optional (silences warnings about empty segments)
src/data.s:
- Move mul_dma_lo, mul_dma_hi, sqtab_lo, sqtab_hi into TABLES_BSS
segment so they land below \$A000 (x25519 optimization requires
it per project_x25519_optimization memory)
- mul38_lo_tab / mul38_hi_tab stay in RODATA (now routed to CRYPTO
via cfg, still below \$A000)
src/tls_keyschedule.s, src/tls_cert.s:
- Change top-level .segment from "CODE" to "TLS_CODE" so these
large TLS state-machine and certificate files load into CRYPTO
region instead of the tight LOADER region
Final region utilization:
LOADER 99% (49 B free)
NET_CODE 84% (1241 B free)
CRYPTO 100% (0 B free — packed with RODATA + CRYPTO_CODE +
CRYPTO_RODATA + TLS_CODE + TABLES_BSS)
SHADOW_BSS 99% (20 B free)
Tables verified below \$A000 via build/labels.txt:
mul_dma_lo = \$9A00
mul_dma_hi = \$9B00
sqtab_lo = \$9C00
sqtab_hi = \$9E00
tls_handshake.s was NOT moved to TLS_CODE because doing so would
overflow CRYPTO by 669 bytes. Memory budget is now tight — future
additions (P-384 restore, sibling crypto vendor) will need more
headroom before they can land.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves three structural problems in the ca65 build output that
would prevent the PRG from loading in VICE or being consumable by
the test harness.
1. Missing PRG load-address header.
Add src/loadaddr.s: a 2-byte .word \$0801 in the LOADADDR segment.
Remove 'optional = yes' from the LOADADDR segment in cfg so ld65
requires it to be populated.
2. No inter-segment padding.
Add 'fill = yes, fillval = \$00' to the LOADER, NET_CODE, and
CRYPTO MEMORY regions in cfg/c64-https-ip65.cfg. Without this,
ld65 packed segments immediately after each other in the file,
so NET_CODE landed at file offset \$17CD instead of \$1801 and
the ip65 blob loaded 50 bytes too early at runtime.
3. Label file format mismatch.
ca65 ld65 -Ln emits 'al XXXXXX .name' (no 'C:' prefix).
The test harness Labels parser requires 'al C:XXXX .name'.
Add a post-link sed pass in Makefile.ca65 'link' target:
sed -i 's/^al 00\\([0-9a-fA-F]\\{4\\}\\) /al C:\\1 /' build/labels.txt
Fixes Labels.from_file() parsing (was 0 entries, now 535).
4. Missing exports for source-level equates.
Three symbols the harness looks up were defined as bare '='
equates in .inc files, so they never appeared in the linker
symbol table: tcp_recv_buf (constants.inc), ip65_init and
ip65_process (ip65_symbols.inc).
Add src/exports.s — a single-TU stub that .include's the
relevant .inc files once and issues explicit .export directives.
Avoids duplicate-symbol errors from putting .export directly in
.inc files that are included from many TUs.
Results:
- build/c64-https.prg: 30721 bytes (up from 27812), valid PRG
header \$01 \$08
- ip65 blob at file offset \$1801 byte-identical to ACME baseline
- build/labels.txt: 535 VICE-format entries, including tcp_recv_buf
(\$C000), ip65_init (\$2000), ip65_process (\$2003)
- Clean ld65 link, no warnings
Delta vs ACME baseline (45900 bytes): ca65 is ~15 KB smaller
because ACME emitted the SHADOW_BSS region into the file as
pre-zeroed data; ca65 correctly treats it as bss. Whether this
matters depends on whether C64 startup zeros its own BSS. Phase 6
VICE smoke test will verify runtime behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three compounding defects in the ca65 port of the boot path caused
c64-https.prg to return to BASIC READY. without displaying the main
menu. All three are fixed here.
1. BASIC stub SYS target was off by 3.
The stub at $0801 said 'SYS 2064' ($0810), but start: is at
$080D (2061) in both the ACME and ca65 builds. $0810 is
mid-instruction, so SYS 2064 executed garbage on 6502 NMOS and
skipped the bank-switch (LDA $01 / AND #$FE / STA $01) at
$080D-$0812 that maps out BASIC ROM. The ACME build happened to
survive this because none of its BSS sat under ROM, but the
ca65 build placed crypto BSS in SHADOW_BSS ($A000-$BFFF) which
is under BASIC ROM when bank-switch is skipped.
Fix: change the stub text from '2064' to '2061' in src/boot.s.
2. NET_BSS memory gap flattened the file into the wrong addresses.
cfg had:
LOADER $0801-$1FFF file=%O
NET_CODE $2000-$3FFF file=%O
NET_BSS $4000-$5FFF (no file=)
CRYPTO $6000-$9FFF file=%O
ld65 packed the file-backed regions contiguously, skipping the
8 KB NET_BSS hole. But a PRG has a single load address and the
KERNAL LOADs contiguously, so CRYPTO bytes intended for
$6000-$9FFF were loaded physically into $4000-$7FFF. Functions
like drbg_init_entropy, linked at $8243, ended up at $6243 and
calls to them jumped into uninitialized RAM.
Fix: add file=%O, fill=yes, fillval=$00 to the NET_BSS MEMORY
region so ld65 emits 8 KB of zeros in the PRG for the gap. PRG
size grows from 30721 to 38913 bytes.
3. SHADOW_BSS not zeroed on boot.
The C64 KERNAL does not zero BSS on PRG load. net_initialized
at $A000 held powered-on RAM garbage ($55 in test runs), so the
main_loop first-run guard misdispatched. Other crypto BSS
(drbg_seed, hmac_key, sha256_block) also started non-zero.
Fix: add a 20-byte zero loop at the top of start: that clears
$A000-$BFFF before any init runs. Uses self-modifying
sta $A000,y page-walking, preserving registers X/Y minimally.
Verification:
- ca65 build boots cleanly in VICE, banner and main menu appear.
- tools/test_entropy.py passes 7/7 (SID noise, CIA1 timer, non-zero
drbg_seed, DRBG output entropy, reseed changes output).
Known TODO:
- The 'SYS 2064' typo exists verbatim in master's src/boot.asm too.
ACME survives it by luck of BSS placement. Should be fixed on
master before the ACME tree is retired, to avoid confusing any
future reader.
- The NET_BSS file-fill adds 8 KB of zeros to the PRG image. A
cleaner Phase 7 fix would restructure the MEMORY map so all
file-backed regions are physically contiguous (move all BSS to
the end above CRYPTO).
- TCP_RECV_BUF at $C000-$CFFF is not currently zeroed; if the HTTP
GET path assumes an empty ring, add it to the start: zero loop
or make it part of SHADOW_BSS zeroing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ACME-era top-level Makefile is gone. Makefile.ca65 has been renamed
to Makefile and extended with the targets from the old file (run, ip65
libs, ip65 blob) so the default `make` invocation drives the ca65/ld65
toolchain. ACME is no longer required to build this project.
New canonical targets:
make build/c64-https.prg + build/labels.txt
make clean remove build/
make run autostart PRG in VICE
make ip65-libs rebuild ip65 object libraries from the submodule
make ip65-blob rebuild ip65-build/ip65-c64.bin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test scripts previously ran `make clean && make` unconditionally at
startup. Phase 6 worked around that with an external make shim while
the ca65 build was still being stabilised. Now that the canonical build
is ca65 and safe to reuse, callers that have already built can set
C64_SKIP_BUILD=1 to skip the make invocation entirely.
This is opt-in — with C64_SKIP_BUILD unset, every test still performs
its own clean + rebuild, so the normal path is unchanged.
No shared helper was patched because each test script inlines its own
build block; the 7 sweep-list scripts are edited directly:
test_entropy, test_hkdf, test_chained_hmac, test_keyschedule_steps,
test_tls_handshake, test_http, test_x509.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/net/ip65/ip65_symbols.inc is now the single source of truth for
all ip65_* equates (ZP overlap zone, jump-table offsets, variable
table, direct map addresses). The legacy ACME-era copy in
src/constants.inc has been removed and the `.ifndef` guard dropped
from ip65_symbols.inc.
Files that previously got ip65_* symbols transitively through
constants.inc now `.include "ip65_symbols.inc"` directly:
- src/boot.s (boot phase DNS wait)
- src/http.s (DNS response read-out)
- src/exports.s (promotes ip65_init / ip65_process for labels.txt)
Verified by clean rebuild + tools/test_entropy.py (7/7 pass, both
with and without C64_SKIP_BUILD=1).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers:
- Build section: make targets, BACKEND variable, C64_SKIP_BUILD
- Crypto ABI section: public symbols, MEMORY requirements for a
drop-in sibling library, mapping to c64-x25519 /
c64-ChaCha20-Poly1305 / c64-nist-curves
- Networking backend ABI section: net_abi.inc, ip65 vs uci backends,
BACKEND=ip65|uci selection
- Memory layout section: cfg region map, tight CRYPTO/SHADOW_BSS
regions, intentional loadaddr / exports stubs
- Smoke test section: the 7 passing scripts and the known ip65
upstream blocker for end-to-end HTTPS
No code changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These were historically committed under the ACME build. The ca65 build
produces different output and build/ is already in .gitignore. Untrack
them so `make clean && make` leaves a clean working tree.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JC-000

Copy link
Copy Markdown
OwnerAuthor

Originally posted by @JC-000 on 2026-04-15

End-to-end bridge test results (ca65 build)

Ran tests/test_phase1_dhcp.py and tests/test_phase2_http.py against the ca65-built build/c64-https.prg (38913 bytes) on branch refactor/ca65-conversion at cbaaaab. Full network path through the scripts/setup-bridge-tap.sh bridge/tap/dnsmasq setup.

Phase 1 — DHCP: PASS

  • Boot menu appeared
  • I pressed → NETWORK OKREQUESTING DHCP...DHCP OK - IP: 10.0.65.136
  • Completed well inside the 90s DHCP timeout
  • Full ip65 DHCP roundtrip through the Linux bridge

Phase 2 — HTTP GET: PASS

  • DHCP completed (inherits from Phase 1 path)
  • H pressed → HTTP GET WWW.ZIMMERS.NET...
  • TCP connect → GET → parse → OK on screen
  • Response body HELLO FROM TEST SERVER rendered and asserted by the harness
  • Completed well inside the 120s HTTP timeout

Stack exercised

  • ip65 DHCP (v4)
  • dnsmasq DNS resolution (www.zimmers.net → 10.0.65.1)
  • ip65 TCP connect
  • HTTP GET + response parsing + screen render
  • CS8900A RR-Net driver through Linux tap
  • SHADOW_BSS zero-init (new in this PR, Phase 6 fix)
  • NET_BSS gap fill in PRG (new in this PR, Phase 6 fix)
  • BASIC SYS 2061 stub (new in this PR, Phase 6 fix — the ACME "SYS 2064" was mid-instruction; ACME got away with it because its BSS placement avoided the $A000-$BFFF ROM shadow)

Unchanged from master

  • Phase 3 HTTPS (tests/test_phase3_https.py) still blocked on the upstream ip65 bug documented in project_phase3_handoff.md. That blocker is unrelated to this refactor and will be resolved separately.

Smoke tests (also green)

The 7 unit/integration scripts under tools/test_*.py all pass via C64_SKIP_BUILD=1:

TestAssertionsDuration
test_entropy7/73 s
test_hkdf12/122 s
test_chained_hmac10/1014 s
test_keyschedule_steps9/92 s
test_tls_handshake21/212 s
test_http27/271 s
test_x50911/11303 s

Total: 97/97 assertions.

Merge recommendation

Functionally green on every non-HTTPS path. Recommend real-hardware soak test (C64 + RR-Net cart) before merge if available — the three Phase 6 fixes (SYS stub, NET_BSS gap, BSS zero) only surface under the ca65 layout and are worth validating on real silicon.

@JC-000
JC-000 merged commit d42a1b5 into masterMay 6, 2026
@JC-000
JC-000 deleted the refactor/ca65-conversion branch May 6, 2026 19:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@JC-000