!arch/arm: Use r9 as the PIC base register, and bring mknxflat in-tree - #19600
!arch/arm: Use r9 as the PIC base register, and bring mknxflat in-tree#19600casaroli wants to merge 4 commits into
Conversation
|
@patacongo — a licensing question, since you are the copyright holder. This PR imports mknxflat into the tree as tools/nxflat/. The reason is in the PR body: moving the ARM PIC base register to r9 breaks NXFLAT silently, because mknxflat bakes the register into the import thunks it generates as add ip,ip,sl, and while the tool lives outside this repository it can drift out of step with the kernel. The three files taken are mknxflat.c and arm/dyncall_skeleton.def / thumb2/dyncall_skeleton.def from the buildroot NXFLAT toolchain. Each carries a BSD-3-Clause header naming you and Cadenux LLC, and notes that the code derives from XFLAT's ldelflib. The containing repository's COPYING is GPL-2.0, so I would like that confirmed rather than assumed: is the per-file BSD-3 grant the operative licence for those three files, and are you content for them to be imported here with their headers intact? ldnxflat is deliberately not imported. It carries an explicit GPL-2.0-or-later notice and descends from elf2flt, with roughly ten copyright holders going back to 1996, so it stays where it is and NXFLAT continues to need it from buildroot. The only substantive change to the imported code is dropping the libbfd dependency, which was used solely to open the file and enumerate symbols; the tool now reads the ELF symbol table directly. The generated thunk files are byte-identical to what the current tool produces, for both ARM and Thumb-2, with and without -w. |
|
@casaroli please fix the documentation ci error. |
The documentation CI error is not related to this PR changes and is happening in any PR that touches documentation. I will investigate and try to push the fix in another PR anyway. |
|
@jerpelea PTAL |
a692868
|
Amazing piece of work @casaroli =) |
An NXFLAT module reaches the base firmware through a "thunk" file: one assembly stub per imported function, generated by mknxflat. That tool has always lived outside this repository, in the NuttX buildroot NXFLAT toolchain, so building an NXFLAT module needs a separate checkout and a separate build of a tool that links against libbfd. libbfd is why it stayed out. It is GPL, which an Apache project cannot depend on, and it is awkward to obtain besides -- a stock binutils install often ships libbfd without the libiberty it needs to link. But the dependency was never deep. mknxflat used libbfd for eight calls, all of them opening the file and walking the symbol table; it never relocates or rewrites anything. That is replaced here by reading the ELF symbol table directly, which removes the dependency outright and costs about a hundred lines. The emitted text is unchanged. The format strings live in the .def files, which are carried here byte-for-byte from upstream, and the selection rule for what becomes a thunk is the upstream one: everything undefined that is not explicitly an object. Symbol typing cannot be trusted for this -- imported functions are routinely emitted as STT_NOTYPE rather than STT_FUNC, while a weakly defined object does appear as an undefined object -- so the test is on what a symbol is not. Upstream chose the instruction set at compile time through an "arch" symlink pointing at either arm/ or thumb2/. A symlink cannot be carried in the repository, and one host binary has to serve boards of both flavours, since lpc31xx is ARM while lpc17xx, tiva, stm32f1 and rp23xx are Thumb-2. That choice becomes a runtime "-a" option. The "-f" option, which read further command line arguments from a file, is dropped; nothing in the tree used it. This commit changes no output. Against the upstream tool, for both architectures, with and without -w, over modules exercising the plain, weak and non-returning thunk paths, the generated thunk files are byte-identical. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
The tool arrived from the buildroot NXFLAT toolchain under BSD-3-Clause, jointly copyright Gregory Nutt and Cadenux, LLC. Gregory Nutt owned Cadenux and was its only developer on this code, and has agreed to the conversion, so the six files take the ASF header like the rest of the NuttX code he donated. Copyright attribution moves to NOTICE, which is where the donation put it for everything else of his in the tree. This covers only what was imported: mknxflat and the thunk skeletons it emits from. ldnxflat is the file with an elf2flt lineage, and it is not here -- it stays out of tree in buildroot, and NuttX keeps calling it as an external tool. The .def files also gain their in-tree path on the first line, which the import had left pointing at the buildroot layout. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
ARM PIC has used r10 as the base register, but the tree has never been consistent about it. Toolchain.defs gives CONFIG_BUILD_PIC -mpic-register=r9 and CONFIG_PIC -mpic-register=r10, twenty-five lines apart, and arm_initialstate.c sets REG_R9 from inline assembly under one and REG_PIC under the other, with a comment reading "Set the PIC base register (probably R10)". This settles it on r9 for all of PIC: NXFLAT, ELF PIC and CONFIG_BUILD_PIC alike. r9 is the right choice rather than an arbitrary one. It is the AAPCS platform register, the "static base", and it is what GCC itself picks for -msingle-pic-base on an EABI target; r10 is the non-EABI default. It also removes a combination that cannot build today. Stack checking adds -ffixed-r10 in armv7-m/Toolchain.defs and armv8-m/Toolchain.defs, while CONFIG_PIC adds -mpic-register=r10, and GCC rejects the pair with "unable to use 'r10' for PIC register". The comment above REG_PIC has always said the register "can be R9 if stack checking is enabled", but the definition was unconditionally REG_R10, so it would have named the wrong register even had the build succeeded. The thunk generator moves with the firmware. NXFLAT import stubs had the register baked in as "add ip,ip,sl", so a module built for r9 would load and then branch to a wild address on its first call out. The stubs now come from NXFLAT_PIC_REG in the in-tree tool, which is built only when CONFIG_NXFLAT is set, following the CONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLE precedent in tools/Unix.mk. That leaves modules built before this change, and they are the reason for the ABI marker. The NXFLAT header cannot carry a version: h_magic is written by ldnxflat, which is GPL, derived from elf2flt, and stays out of this repository, so it can never be changed in step with the loader. The import table can, because both of its ends are in-tree -- mknxflat emits it and nxflat_bindimports() reads it -- and ldnxflat passes it through untouched. So every module now imports __nxflat_abi_v2, the base firmware defines it, and a module that does not import it is refused. Making the marker a real exported symbol rather than a name the loader special-cases is what keeps it out of the build system's way: a board's symbol table picks it up exactly as it picks up printf, so mksymtab.sh and its equivalents need no change. It also gives the reverse direction a diagnosis for free -- a module built against a newer ABI than its firmware fails with "Exported symbol __nxflat_abi_v2 not found". Most of the remaining churn is boards restating a default. ARCHPICFLAGS is a "?=" default so that a board only speaks up when it differs, and twenty-six were assigning the value the default already had. MKNXFLAT gets the same treatment: thirteen boards named the same tool, and the only thing that varies is ARM versus Thumb-2, which falls out of CONFIG_ARM_THUMB. LDNXFLAT gains a default too -- it stays an out-of-tree PATH lookup, but naming it centrally fixes boards that never assigned it, where it expanded to nothing and handed make a recipe beginning "-e", whose leading dash make ate as "ignore errors". The non-ARM boards carrying -mpic-register=r10 lose it: it is an ARM-only option, reachable only through CPICFLAGS, which is only used to build NXFLAT modules, and no non-ARM board enables NXFLAT. Boards keep nothing about PIC flags any more. ARCHPICFLAGS was set by sixty-three of them and only ever fed CPICFLAGS, which is only used to build NXFLAT modules; no board outside arch/arm enables NXFLAT, so every non-ARM copy was setting a variable nothing read. Those are removed rather than moved somewhere more central, which would only make dead text look load-bearing. LDNXFLAT goes the same way as MKNXFLAT, for the same reason: thirteen boards named the same tool that Toolchain.defs now names once. One of them was not merely redundant. am67/t3-gem-o1 asked for "-mpic-register=r10 -ffixed-r10", which GCC refuses outright with "unable to use 'r10' for PIC register" -- the very combination the filter-out machinery in Toolchain.defs exists to prevent. It has survived because that board does not build NXFLAT modules, so the flags are never handed to a compiler. Renaming the register would have carried the fault forward unchanged, so the line goes. Tested on lm3s6965-ek:qemu-nxflat under QEMU, configured and built with no overrides. The nxflat example runs the errno, hello and struct modules with output identical to the same config built from master. Built with the old out-of-tree thunk generator instead, the same firmware refuses all three with ENOEXEC rather than locking up in a HardFault, which is what this change is for. mps3-an547:picostest, which is CONFIG_PIC without CONFIG_NXFLAT, builds clean and does not build the thunk generator. The .def files pick up two cosmetic changes here alongside the register: a "Dyanamic" typo that codespell rejects, and a reworded comment in each thunk_*.c. Neither appears in the emitted thunk -- both are in C comments -- so the generated text is still what the upstream tool produces, modulo the register itself. BREAKING CHANGE: ARM PIC moves from r10 to r9. An NXFLAT module built before this change has r10 baked into its import stubs and will not run against a firmware carrying it; the two cannot be mixed. The module is refused with ENOEXEC rather than branching to a wild address, by way of the __nxflat_abi_v2 marker described below. Quick fix: rebuild the module against this tree. Its source needs no change. A board that reserved r10 by hand, or that assigned ARCHPICFLAGS or MKNXFLAT to restate a default, should drop those assignments; nothing else is affected, and CONFIG_PIC without CONFIG_NXFLAT needs no action. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
mknxflat is built from tools/nxflat by the NuttX build itself, so the toolchain section no longer sends the reader to buildroot for it; only ldnxflat still comes from there. Bring the mknxflat usage text in line with the tool, note that MKNXFLAT and LDNXFLAT are supplied by the ARM Toolchain.defs, and correct the r10 references left in the PIC descriptions. Describe the module ABI marker, so that a user whose prebuilt module starts failing exec() with ENOEXEC finds out that the loader refuses a module whose import table does not name __nxflat_abi_v2, and that rebuilding the module is the fix. Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Summary
ARM PIC uses r10 as the base register, but the tree has never been consistent about it.
arch/arm/src/common/Toolchain.defsgivesCONFIG_BUILD_PIC-mpic-register=r9at line 594 andCONFIG_PIC-mpic-register=r10at line 620, andarm_initialstate.csetsREG_R9from inline assembly under one andREG_PICunder the other, with a comment that reads "Set the PIC base register (probably R10)". This settles it on r9 for all of PIC: NXFLAT, ELF PIC andCONFIG_BUILD_PICalike.r9 is the right register rather than an arbitrary one. It is the AAPCS platform register, the "static base", and it is what GCC itself selects for
-msingle-pic-baseon an EABI target; r10 is the non-EABI default.It also fixes a configuration that cannot build today. Stack checking adds
-ffixed-r10inarmv7-m/Toolchain.defs:149andarmv8-m/Toolchain.defs:168whileCONFIG_PICadds-mpic-register=r10, and GCC rejects the pair with "unable to use 'r10' for PIC register". The comment aboveREG_PIChas always said the register "can be R9 if stack checking is enabled", but the definition was unconditionallyREG_R10, so it would have named the wrong register even if the build had succeeded.Why the NXFLAT thunk generator comes with it
Moving the register on its own breaks NXFLAT, silently. Import thunks are generated by
mknxflat, which had the register baked into its template asadd ip,ip,sl, so a module compiled-mpic-register=r9reaches its data through r9 while its import stubs still add r10, and it branches to a wild address on its first call into the base firmware.mknxflatlives outside this repository, in the buildroot NXFLAT toolchain, so the kernel and the tool could drift with nothing to detect it. The first commit therefore brings it in-tree astools/nxflat/, with libbfd replaced by reading the ELF symbol table directly — libbfd was only ever used to open the file and enumerate symbols, and it is GPL, which we cannot depend on. The thunks are then generated from a singleNXFLAT_PIC_REGon every build, so the two cannot drift.That commit changes no output. Against the upstream tool, for both ARM and Thumb-2, with and without
-w, over modules exercising the plain, weak and non-returning thunk paths, the generated thunk files are byte-identical.Modules built before this change
They are refused at load rather than left to crash.
The NXFLAT header cannot carry a version:
h_magicis written byldnxflat, which is GPL, derived from elf2flt, and stays out of this repository, so it can never be changed in step with the loader. The import table can, because both of its ends are in-tree —mknxflatemits it andnxflat_bindimports()reads it — andldnxflatpasses it through untouched. Every module now imports__nxflat_abi_v2, the base firmware defines it, and a module that does not import it is refused with-ENOEXEC.Making the marker a real exported symbol rather than a name the loader special-cases keeps it out of the build system's way: a board's symbol table picks it up exactly as it picks up
printf, somksymtab.shand its equivalents need no change. It also diagnoses the reverse direction for free — a module built against a newer ABI than its firmware fails withExported symbol "__nxflat_abi_v2" not found.Impact
Warning
BREAKING CHANGE: ARM PIC moves from r10 to r9. An NXFLAT module built before this has r10 baked into its import stubs and will not run against a firmware carrying it; the loader refuses it with
-ENOEXECrather than branching to a wild address.Quick fix: rebuild the module against this tree — its source needs no change. A board that reserved r10 by hand, or that assigned
ARCHPICFLAGSorMKNXFLATto restate a default, should drop those assignments.CONFIG_PICwithoutCONFIG_NXFLATneeds no action, and configurations with neither are untouched.Most of the churn is boards restating a default.
ARCHPICFLAGSis a?=default so that a board only speaks up when it differs, and twenty-six were assigning the value the default already had; those lines are removed rather than edited.MKNXFLATgets the same treatment: thirteen boards named the same tool, and the only thing that varies is ARM versus Thumb-2, which falls out ofCONFIG_ARM_THUMB.LDNXFLATgains a default too — it stays an out-of-tree PATH lookup, but naming it centrally fixes boards that never assigned it, where it expanded to nothing and handed make a recipe beginning-e, whose leading dash make ate as "ignore errors".The non-ARM boards carrying
-mpic-register=r10lose it. It is an ARM-only option, reachable only throughCPICFLAGS, which is only used to build NXFLAT modules, and no non-ARM board enables NXFLAT; six other RISC-V boards already omitted it.mknxflatis built only whenCONFIG_NXFLATis set, following theCONFIG_BOARD_ETC_ROMFS_PASSWD_ENABLEprecedent intools/Unix.mk.Licensing
tools/nxflat/mknxflat.cand the two.deftemplates are BSD-3-Clause, copyright Gregory Nutt and Cadenux LLC, imported from the NuttX buildroot NXFLAT toolchain with their headers intact. The containing repository'sCOPYINGis GPL-2.0, but these particular files carry their own BSD-3 grant, name only Nutt and Cadenux as copyright holders, and descend from XFLAT'sldelflibrather than from elf2flt. @patacongo for confirmation.ldnxflatis deliberately not imported. It is GPL-2.0-or-later and derives from elf2flt, with roughly ten copyright holders reaching back to 1996 — it cannot be relicensed and cannot live in an Apache repository. NXFLAT continues to need it from the buildroot toolchain, exactly as today.Here is the licensing agreement mailing list archive:
https://www.mail-archive.com/dev@nuttx.apache.org/msg14863.html
How to verify this on your board
Help with this is very welcome — §1.14 asks for runtime logs from more than one real architecture, and I have only an RP2350 here.
Any board, ARM or not (two minutes). Build your usual configuration, boot it, and run
ostest. That is the whole test for most boards: the only thing that touches them is the PIC flag cleanup, which is a no-op unless the board setsARCHPICFLAGSor enables NXFLAT. Non-ARM boards lose-mpic-register=r10, which was an ARM-only option reachable only throughCPICFLAGS, andCPICFLAGSis only used to build NXFLAT modules — so nothing should change at all.If your board enables
CONFIG_NXFLAT. Build and run a module, for exampleolimex-lpc1766stk:thttpd-nxflatorapps/examples/nxflat.mknxflatnow comes fromtools/nxflatand the build makes it for you;ldnxflatis still an out-of-tree tool from buildroot, unchanged.If you have an NXFLAT module built before this change. Run it without rebuilding. It must be refused, with
and
exec()failing with-ENOEXEC(-8). A crash or a wild branch instead of that message is a bug — please report it. Rebuilding the module is the fix; its source needs no change.Build logs and
ostestoutput from any architecture are welcome and I will collect them here.Testing
Host: macOS 26.5.1 on arm64, Arm GNU Toolchain 14.2.Rel1, QEMU 11.0.3.
lm3s6965-ek:qemu-nxflatunder QEMU, configured and built with no overrides. Thenxflatexample runs the errno, hello and struct modules with output identical to the same configuration built from master:The same firmware, with the modules rebuilt by the old out-of-tree generator, refuses all three:
Before the ABI marker that same combination locked up in a HardFault with no console output, which is what the marker is for.
mps3-an547:picostest, which isCONFIG_PICwithoutCONFIG_NXFLAT, builds clean and does not build the thunk generator.tools/checkpatch.sh -fpasses on every changed C file and header.ELF PIC on hardware. Pimoroni Pico Plus 2 (RP2350, Cortex-M33),
pimoroni-pico-2-plus:nshplusCONFIG_ELF,CONFIG_PICandapps/examples/elf. The modules link PIC with r9 as their base —hellocarries 13 r9 references and 18 GOT/RELATIVE relocations — and eight of them load from the embedded ROMFS and run: errno, hello, signal, struct, mutex, pthread, task and longjmp. Heap usage returns to its exact starting value at the end of the run.structis the test that matters for the register. It reads GOT data, dereferences a pointer into.rodatain flash (0x10033aec), and calls through a function pointer, and every one of its checks passes.Two things this turned up, neither introduced by this PR.
The
signaltest reportsSIGUSR1 not received, its handler printing nothing for eitherraise()orkill(). This is not a PIC problem: the same test fails the same way on an ESP32-S3, where nothing in the build is PIC at all. It is a property of that test or of signal delivery to a loaded module, and is mentioned here only so the log above is not read as a regression.A C++ ELF module cannot link under
CONFIG_PIC:hello++3fails withundefined reference to __dso_handle, which is hidden and so cannot be left unresolved by--unresolved-symbols=ignore-in-object-files. TheCONFIG_PICbranch ofLDELFFLAGSpredates this PR and is unchanged by it. The tests above were run withCONFIG_HAVE_CXXoff.Still not covered:
mps3-an547:picostestbuilds and boots identically to master, but NSH takes no console input in that configuration under QEMU — on master as well — so no module is executed there.