size: our own calls pass arguments in registers, and the gate that keeps it honest - #121
Merged
Merged
Conversation
tinic
force-pushed
the
size/regparm
branch
3 times, most recently
from
September 30, 2026 20:47
9bab2f8 to
0fe65b4
Compare
…eps it honest
The m68k backend can pass arguments in registers instead of on the stack, and
-mregparm=3 is now what every drawer builds with. It is per CLASS: N integer
registers d0..d(N-1) and N pointer registers a0..a(N-1), six slots at N=3, and
a class that runs out spills into what the other left -- so "d0/d1/d2" is the
wrong model, and a callee assuming it returns a wrong number, not a crash.
Measured against the same tree built with -DAMINETXDUO_REGPARM=0, so the flag
is the only difference, on m68k-amigaos-gcc 16.2.0b with -flto and the shipping
flags, in LOADED bytes -- CODE + DATA + BSS, what LoadSeg has to find room for
-- and in file bytes. From a CLEAN tree, which the numbers this message first
carried were not; see the note below the census.
bsdsocket.library loaded 353,404 -> 329,464 file 370,880 -> 346,608
(code 343,420 -> 319,480; DATA and BSS unchanged)
minimal 231,136 -> 215,244 micro 198,240 -> 184,884
anxnet.device loaded 43,584 -> 40,144 file 45,704 -> 42,228
so the two images a machine keeps resident come to -27,380 loaded. Over every
image BOTH arms link: 137 images 7,917,004 -> 7,707,384 loaded (-209,620,
-2.65%) and 6,772,936 -> 6,560,528 file (-212,408, -3.14%), 123 minimal
-93,460 (-2.11%), 122 micro -87,364 (-2.05%). Five test images grew by 4 to 20
bytes (tests/perf/chipscreen +20, tests/perf/n68kmv +16, tests/tools/AamProbe,
ResolveBreak and PtrProbe +4 each); nothing that ships did, and no image is
present in one arm only.
An uncommitted edit is not size-neutral in this tree, and that is not a
footnote: cmake/AmiNetXDuoGitStamp.cmake appends -dirty to the version hash
whenever `git status --porcelain` is non-empty, and the hash is inside every
image. It is 0 to 8 bytes per image, and not the same 0 to 8 in both arms:
-dirty added 8 bytes to the rp0 default drawer and 4 to the rp3 one, which
moved the rp3-rp0 delta by 4. That is the "+-4 byte spread" earlier size work
here put down to LTO relinking. It is why the figures above differ from the
ones this message first carried, and why every number in it was re-measured
with the tree committed. The same measurement is the proof that the two source
changes below cost nothing: two clean arms, compared byte for byte across their
CODE hunks, differ in exactly 7 bytes -- the short hash inside that string.
Nothing a program can call changed. This is how our functions call each
other, not how they are called from outside: every LVO is entered through a
register the NDK's own headers already pin, and libgcc is compiled with the
same flag by the same compiler. A differential over bsdsocket.library's
foreign calls reads 207 direct calls at regparm 0 and 201 at regparm 3, and
the only sites that push in neither arm are libgcc's own internal jsr.
What the compiler does not build is pinned where it is declared:
* main(), which crt0.o and src/tools/tool_startup.S both call by pushing
argv and then argc. Pinned by a force-included header rather than
-Dmain=..., because CMake writes flags into the make recipe as raw text:
the parentheses would reach /bin/sh unquoted and the compiler would never
run. The macro is function-like because 16 files here take main's
address, and it is 87 of the 130 main-bearing images that link crt0.o.
* the assembly routines in src/net68k and src/crypto68k, which read 4(sp),
8(sp), 12(sp): AMIGA_ASM_ARGS on the declaration, and on the function
pointer types that store one.
* libc.a, amiga.lib and every other archive this tree links.
* the operating system's entry points -- hooks, interrupt servers, task
entries, SetFunction patches, RawDoFmt putChProc -- none of which take
arguments, or whose registers the headers pin by name.
src/net68k/n68k_memcpy_hook.c is the one place where nothing is pinned and the
convention holds by name recognition instead: the compiler calls memcpy BY NAME
with the arguments on the stack whatever the caller looks like, and a
definition of memcpy reads them off the stack for the same reason and not
because the file says so. <string.h> makes that stated -- this toolchain
declares `__stdargs void *memcpy(...)', the same __attribute__((__stkparm__))
this tree spells AMIGA_ASM_ARGS -- so a later -fno-builtin cannot flip the
definition to d0/a0/a1 while the callers go on pushing. It costs no bytes, per
the clean-tree comparison above. And tests/fuzz/CMakeLists.txt puts
${CMAKE_SOURCE_DIR}/include on _fuzz_tls_crypto_inc, because
src/crypto68k/crypto68k.h now includes <aminetxduo/asm_abi.h> for that same
pin, and fuzz_tls_crypto is the one target that compiles the header without
include/ on its path. It exists only where sizeof(void*) == 4, so neither the
64-bit host nor any cross arm could see it: CI found it, in host32 and
sanitize32, and both are green on this tip.
A call across one of those with the wrong convention gets its pointer from the
wrong register and returns a wrong number instead of trapping, so the gate
goes with this rather than beside it -- and the gate is not a warning about
`()', it is the language. m68k-amigaos-gcc 16.2 compiles as C23
(__STDC_VERSION__ 202311L, no -std on the command line), where `()' MEANS
`(void)': f(a, b) through an unprototyped declaration is "too many arguments
to function", a hard error for a direct call and for a call through a function
pointer alike. include/aminetxduo/asm_main.h #errors unless that holds, so a
later -std, or a compiler that drops C23, cannot reopen the hole silently.
-Wstrict-prototypes was tried for that and removed, because it is the wrong
tool twice over. It cannot fire where the hazard is: the m68k arm's C23 makes
the call an error first. And where it does fire it cannot be satisfied -- a
full host build (gcc 14, C17) reports 26 diagnostics from three sites, none of
them ours to change: the NDK's own `VOID (*putChProc)()' in exec_protos.h,
which forces the cast at src/bsdsocket/loghook.c:163; the NDK-impersonating
src/netdev/test/shim/exec/interrupts.h; and the vendored
third_party/netxduo/nx_secure/inc/nx_secure_tls_api.h.
What remains is -Wmissing-prototypes with -Werror=implicit-function-
declaration, which close the same hole from the other side, and the gate is
global: the hidden `amiga' preset every drawing preset inherits sets
CMAKE_PROJECT_INCLUDE to cmake/ci-warnings.cmake, so one of this project's own
drawers cannot be configured without it. The exemptions are rules, not a file
list: all of third_party/ is vendored verbatim, tests/atf/ holds FreeBSD's
netinet tests byte for byte under their own header, and the host tests under
src/<component>/test/ forge exec.library on purpose. -Wno-cast-function-type
used to be among them and is gone as dead: main replaced all five hook casts
with union punning (4607a0c), no shipping source ever cast h_Entry -- src/
only reads it -- and a full host build with the escape removed reports 0.
Checked on this tree, 2026-09-30: the host tier builds all 434 targets with 0
errors and 0 warnings of any of the four kinds, and ctest passes 493 of 493;
the 32-bit host tier builds and passes 24 of 24; the emulator tier is green on
3af0a0c -- both arms, every test -- and nothing this tree has that 3af0a0c
does not can reach an image: CHANGELOG.md, the include proved byte-neutral
above, an include directory in tests/fuzz that only a 32-bit host build uses,
and the two host test files of 6a39528, which this is rebased over. That
rebase is CI's doing: the sanitizer stage ran those two tests for the first
time on this branch and LeakSanitizer caught their fixtures being kept, which
main had already fixed one commit earlier.
Build consequence, and it is the one that bites: an image must come from one
configuration. CMake does not treat the flags file as a prerequisite of the
object, so a build/ directory made before this change has to have its objects
rebuilt, not relinked -- a relink would mix both conventions in one image.
Co-Authored-By: Claude Code <noreply@anthropic.com>
This was referenced Oct 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
-mregparm=3for the m68k build: our own functions pass their first arguments inregisters instead of on the stack, and the build gate that keeps that honest.
For review, not for merge. It touches how every image in the tree is
compiled, so it needs the CI matrix and the emulator tier green on this exact
tip before anyone decides anything. I am opening it because a branch push runs
no CI at all —
ci.ymltriggers onpushtomain,pull_request, andworkflow_dispatch— so codex's "no CI run for this tip" cannot be answered anyother way.
Single commit, rebased onto
6a395286.Measurement
Against the same tree built with
-DAMINETXDUO_REGPARM=0, so the flag is theonly difference. m68k-amigaos-gcc 16.2.0b,
-flto, shipping flags, from aclean tree. In loaded bytes — CODE + DATA + BSS, what
LoadSeghas tofind room for — and in file bytes:
bsdsocket.library(default)bsdsocket.library(minimal)bsdsocket.library(micro)anxnet.deviceThe two images a machine keeps resident come to −27,380 loaded. Over every
image both arms link: 137 images 7,917,004 → 7,707,384 loaded (−209,620, −2.65%)
and 6,772,936 → 6,560,528 file (−212,408, −3.14%); 123 minimal −93,460
(−2.11%); 122 micro −87,364 (−2.05%). Five test images grew by 4–20 bytes
(
tests/perf/chipscreen+20,tests/perf/n68kmv+16,tests/tools/AamProbe,ResolveBreak,PtrProbe+4 each); nothing that ships did, and no image ispresent in one arm only.
Every figure here is from a clean tree, and the ones this PR first carried
were not.
cmake/AmiNetXDuoGitStamp.cmakeappends-dirtyto the versionhash whenever
git status --porcelainis non-empty, and that hash is insideevery image: an uncommitted edit is 0–8 image bytes, and not the same 0–8 in
both arms —
-dirtyadded 8 bytes to the rp0 default drawer and 4 to the rp3one, moving the delta by 4. That is the "±4 byte spread" earlier size work on
this tree put down to LTO relinking. It is also the proof that the two source
changes below cost nothing: two clean arms compared byte for byte across their
CODE hunks differ in exactly 7 bytes, the short hash inside that string.
ABI
Nothing a program can call changed. Every LVO is entered through a register the
NDK's own headers already pin, and libgcc is compiled with the same flag by the
same compiler. A differential over
bsdsocket.library's foreign calls reads 207direct calls at regparm 0 and 201 at regparm 3, and the only sites that push in
neither arm are libgcc's own internal
jsr.What the compiler does not build is pinned where it is declared:
main(), whichcrt0.oandsrc/tools/tool_startup.Sboth call by pushingargv then argc. Pinned by a force-included header (
include/aminetxduo/asm_main.h)rather than
-Dmain=..., because CMake writes flags into the make recipe asraw text and the parentheses would reach
/bin/shunquoted. The macro isfunction-like because 16 files here take
main's address.src/net68kandsrc/crypto68k, which read4(sp),8(sp),12(sp):AMIGA_ASM_ARGSon the declaration and on the functionpointer types that store one.
libc.a,amiga.liband every other archive this tree links.SetFunctionpatches,
RawDoFmtputChProc— none of which take arguments, or whoseregisters the headers pin by name.
src/net68k/n68k_memcpy_hook.cis the one place where nothing is pinned and theconvention holds by name recognition instead: under
-mregparm=3the compilercalls
memcpyby name with the arguments on the stack whatever the callerlooks like, and a definition of
memcpyreads them off the stack for the samereason — not because the file says so.
<string.h>makes it stated: thistoolchain declares
__stdargs void *memcpy(...), the same__attribute__((__stkparm__))this tree spellsAMIGA_ASM_ARGS, so a later-fno-builtincannot flip the definition tod0/a0/a1while the callers go onpushing. It costs no bytes, per the clean-tree comparison above.
The gate
A call across one of those with the wrong convention gets its pointer from the
wrong register and returns a wrong number instead of trapping, so the gate ships
with this rather than beside it — and the gate is not a warning about
(), it isthe language.
-mregparm=Nis per class: N integer registers d0..d(N−1) and N pointerregisters a0..a(N−1), six slots at N=3, and a class that runs out spills into
what the other left. "d0/d1/d2" is the wrong model.
m68k-amigaos-gcc 16.2 compiles as C23 (
__STDC_VERSION__ 202311L, no-stdonthe command line), where
()means(void):f(a, b)through anunprototyped declaration is "too many arguments to function" — a hard error for
a direct call and for a call through a function pointer alike.
include/aminetxduo/asm_main.h#errors unless that holds, so a later-std, ora compiler that drops C23, cannot reopen the hole silently.
-Wstrict-prototypeswas tried for that and removed, because it is the wrongtool twice over: it cannot fire where the hazard is (the m68k arm's C23 makes the
call an error first), and where it does fire it cannot be satisfied — a full host
build (gcc 14, C17) reports 26 diagnostics from three sites, none of them ours to
change: the NDK's own
VOID (*putChProc)()inexec_protos.h, which forces thecast at
src/bsdsocket/loghook.c:163; the NDK-impersonatingsrc/netdev/test/shim/exec/interrupts.h; and the vendoredthird_party/netxduo/nx_secure/inc/nx_secure_tls_api.h.What remains is
-Wmissing-prototypeswith-Werror=implicit-function-declaration,which close the same hole from the other side, and the gate is global: the hidden
amigapreset every drawing preset inherits setsCMAKE_PROJECT_INCLUDEtocmake/ci-warnings.cmake, so one of this project's own drawers cannot beconfigured without it. The exemptions are rules, not a file list: all of
third_party/is vendored verbatim,tests/atf/holds FreeBSD's netinet testsbyte for byte under their own header, and the host tests under
src/<component>/test/forgeexec.libraryon purpose.-Wno-cast-function-typeused to be among them and is gone as dead:mainreplaced all five hook casts with union punning (
4607a0c6), no shipping sourceever cast
h_Entry—src/only reads it — and a full host build with the escaperemoved reports 0.
Verified on this tip
ctest 493 of 493 passed.
the one real break in the first push —
src/crypto68k/crypto68k.hnow includes<aminetxduo/asm_abi.h>for the pin, andfuzz_tls_cryptois the single targetthat compiles that header without
include/on its path. The target exists onlywhere
sizeof(void*) == 4, so no 64-bit host or cross arm could see it.tests/fuzz/CMakeLists.txtnow lists${CMAKE_SOURCE_DIR}/include.default/minimal/micro× regparm 0/3), all imageslink; the numbers above are from these six.
3af0a0cd— both arms, every test (bracket 247checks, mbuf_bpf 172, IoSumDrill 776, soak 96, ipv6 85, lifecycle 20,
KernelStop, ram_driver 38),
EMURC=0, with thegreenarm skipped by design.Nothing this tip has beyond that commit can reach an image:
CHANGELOG.md,the
memcpyinclude proved byte-neutral above, an include directory intests/fuzzthat only a 32-bit host build reaches (no emulator arm buildsthat target), and main's
6a395286, two host test fixtures — a rebase CIasked for, since the sanitizer stage ran those two tests here for the first
time and LeakSanitizer caught their fixtures being kept.
🤖 Generated with Claude Code