diff --git a/.codespellrc b/.codespellrc index 14770824d32..3a1f6d04a97 100644 --- a/.codespellrc +++ b/.codespellrc @@ -47,6 +47,7 @@ ignore-words-list = mot, mis, nexted, + nneeded, numer, nwe, oen, diff --git a/Documentation/applications/examples/fdpicxip/index.rst b/Documentation/applications/examples/fdpicxip/index.rst new file mode 100644 index 00000000000..a8b5c822810 --- /dev/null +++ b/Documentation/applications/examples/fdpicxip/index.rst @@ -0,0 +1,81 @@ +================================================ +``fdpicxip`` FDPIC Executed In Place from XIPFS +================================================ + +Writes FDPIC modules into a :doc:`XIPFS ` volume +at run time, the way a download would, and runs them. Their text is executed +directly out of flash and shared between concurrent instances; each instance +gets its own data. + +This is the FDPIC counterpart of :doc:`nxflatxip <../nxflatxip/index>`, kept +separate from it because the two module formats have little in common beyond +running in place, and one program demonstrating both would demonstrate +neither clearly. What FDPIC adds over NXFLAT is shared libraries and C++ with +global constructors — see :doc:`/components/fdpic`. + +It demonstrates; it does not assert. The assertions live in the ``fdpic`` and +``reject`` sections of :doc:`the xipfs test suite `. + +Usage:: + + fdpicxip [qsort | solib | cxx | jmprel] + +``qsort`` + The simplest case the loader has: one self-contained module, no library + behind it, run as two concurrent instances. This is what runs with no + argument:: + + nsh> fdpicxip + === FDPIC module executed in place from xipfs === + + staged qsorter (2768 bytes) + extent: block 98 x1, size 2768, flash addr 0x10162000 + + spawning two concurrent instances... + + [while running] pins on the shared text = 2 + + [inst 1] 11 21 31 41 51 61 71 81 + [inst 2] 12 22 32 42 52 62 72 82 + + [after exit] pins on the shared text = 0 + + One copy of the text is mapped in flash and pinned once per instance, which + is what stops the defragmenter relocating code that is executing. The two + rows differ because each instance has its own data; shared data would show + up as interleaved or corrupted output. + +``solib`` + Adds a shared library, so two objects are mapped and each instance gets its + own copy of *both* objects' data. If the library's state were shared, the + two totals would interleave instead of each reaching ``seed * 3``. + +``cxx`` + The same in C++, which additionally requires each object's global + constructors to have run, in dependency order, before ``main``, and its + destructors to run on unload with that instance's own state. + +``jmprel`` + A module whose imports are all bound through ``DT_JMPREL`` rather than + ``DT_REL``. A loader that ignored that table would load the module happily + and fault only when it first called out. + +The modules are embedded in the image as C headers. Their sources are in +``apps/examples/fdpicxip/modules``, along with the makefile that rebuilds +every header from them:: + + make -C apps/examples/fdpicxip/modules regen NUTTX_DIR=/path/to/nuttx + +That is never part of the application build, because linking a module needs +``arm-uclinuxfdpiceabi`` binutils, which building NuttX does not. See +:ref:`fdpic_tooling`. + +Configuration +============= + +``CONFIG_EXAMPLES_FDPICXIP`` + Enable the example. Depends on ``CONFIG_FS_XIPFS``, ``CONFIG_FDPIC`` and + ``CONFIG_LIBC_EXECFUNCS``. + +``CONFIG_EXAMPLES_FDPICXIP_MOUNTPT`` + Where the xipfs volume is mounted. Defaults to ``/mnt/xipfs``. diff --git a/Documentation/applications/testing/xipfs/index.rst b/Documentation/applications/testing/xipfs/index.rst index 7b68753c03f..3511a3525bb 100644 --- a/Documentation/applications/testing/xipfs/index.rst +++ b/Documentation/applications/testing/xipfs/index.rst @@ -79,6 +79,20 @@ Sections power loss mid-program leaves behind. These need ``CONFIG_FS_XIPFS_FAULT_INJECT``. +``fdpic`` + That a module staged into the volume loads and runs from flash: global + constructors in dependency order, per-instance data, manufactured + descriptors, both relocation tables, the GOT fallback for a library with no + PLT, and every firmware entry point that has to resolve a module callback. + Each module reports its own invariants through its exit status. Needs + :doc:`CONFIG_FDPIC `. + +``reject`` + That the loader refuses a module it cannot honour rather than loading + something broken -- a mutated header, an absent dependency, a missing + import, more ``DT_NEEDED`` entries than the walk will follow. Needs + ``CONFIG_FDPIC``. + Configuration ============= diff --git a/Documentation/components/fdpic.rst b/Documentation/components/fdpic.rst new file mode 100644 index 00000000000..aab37b1860b --- /dev/null +++ b/Documentation/components/fdpic.rst @@ -0,0 +1,490 @@ +============= +FDPIC Modules +============= + +FDPIC modules are ELF shared objects that execute **in place** out of +memory-mapped flash. The read-only segment is mapped where it already sits on +the media and never copied to RAM; only the writable segment is copied, once +per running instance. Shared libraries work, and so does C++ with global +constructors. + +The loader is ``binfmt/fdpic.c``, enabled by ``CONFIG_FDPIC``. Modules are +built out of tree with the tooling in ``tools/fdpic``. + +How it works +============ + +An FDPIC object has two ``PT_LOAD`` segments that may be placed independently +of one another. Nothing in the code depends on the distance between them, +which is what lets the loader leave the read-only segment in flash and put the +writable one wherever RAM is available. + +Code reaches its own data through a base register -- **r9** on ARM -- holding +the address of that object's GOT. A function pointer is therefore not enough +to call a function: the callee needs its data base too. FDPIC represents a +function pointer as a two-word **descriptor**: + +=========== ============================================================== +Word Contents +=========== ============================================================== +``entry`` code address, including its Thumb bit +``got`` data base to install in r9 before branching +=========== ============================================================== + +Building those descriptors is most of what the loader does. Because each one +names its own base, two objects can be in use at once with separate writable +segments, and a pointer handed to the firmware carries everything needed to +call back into the module later. + +A module links against nothing. libc and everything else are undefined +imports, resolved at load time against other loaded objects first and the +firmware's exported symbol table second. + +Load sequence +------------- + +``binfmt/fdpic.c``, in order: + +#. Parse the ELF and program headers and check the FDPIC marker. Anything + else is declined quietly so another binfmt handler can try. + +#. Read ``PT_DYNAMIC`` from the file before either segment is placed: the + relocation count bounds the descriptor pool the writable allocation needs. + +#. Pin the read-only segment on the media with ``XIPFSIOC_PIN``. The returned + pointer *is* the execution address. The pin stops the filesystem + relocating code that is executing. + +#. Allocate the writable segment, copy ``.data`` from flash, zero ``.bss``. + +#. Walk ``DT_NEEDED`` and load each shared library the same way, depth capped + by ``FDPIC_MAX_DEPTH``. + +#. Apply relocations for every object, from both ``DT_REL`` and ``DT_JMPREL``. + +#. Run ``DT_INIT_ARRAY`` for every object, dependencies first. + +#. Hand the scheduler a ``struct dspace_s`` holding the module's GOT, which + ``up_initial_state()`` installs into r9. + +On unload the loader runs ``DT_FINI_ARRAY`` -- module first, then its +libraries -- while their writable segments still exist, drops the pin, and +frees the allocation. The pin release does not depend on the module calling +``munmap``: ``mm_map_destroy()`` invokes each mapping's callback during group +release, so a module that faults or is killed still drops its pin. + +Differences from NXFLAT and ELF PIC +=================================== + +All three run position-independent code from flash on a target with no MMU, +and all three give several instances of one module a shared ``.text`` with +private ``.data``. They differ in what a *pointer* can express and in what +the toolchain has to provide. + +========================= ============== ============== ============ +Property NXFLAT ELF PIC (XIP) FDPIC +========================= ============== ============== ============ +Format NuttX only ELF ELF +Extra build tools yes none linker only +Base register r10 r10 r9 +Data base per task task object +Shared libraries no no yes +Foreign-thread callback no no yes +``.rodata`` in flash linker script automatic automatic +Cores Cortex-M3/M4 CONFIG_PIC Thumb-2 +========================= ============== ============== ============ + +**NXFLAT** is a NuttX-specific format. A module imports symbols from the base +firmware but cannot export any, so shared libraries are not possible, and the +build needs ``mknxflat`` to generate a thunk, ``ldnxflat`` to link, and one of +the ``binfmt/libnxflat`` linker scripts to place the sections -- with which +script depending on how the compiler addresses ``.rodata``. + +**ELF PIC** needs no extra tools at all. With ``CONFIG_PIC`` the loader +allocates the writable sections separately and, when the filesystem answers +``FIOC_XIPBASE``, leaves the read-only ones on the media +(``libs/libc/elf/elf_load.c``); ``mps3-an547:picostest`` runs ``ostest`` that +way. Two limits follow from having one base register per task. A shared +object is loaded as a single allocation, because "the relative positions +between text and data must be maintained due to references to the GOT", so its +text cannot stay in flash. And ``tcb->dspace`` is installed into ``REG_PIC`` +once per task, so every object in a task shares one data base and there is no +``DT_NEEDED`` walk to load a second one. + +**FDPIC** pays for its descriptors with an ``arm-uclinuxfdpiceabi`` linker, and +gets back the two things a single register cannot express: + +* **A module can be called back on a thread it never created.** A task or + pthread the module starts inherits its data base through + ``nxtask_dup_dspace()``, so a register is enough there. A work-queue worker + is not: it was created at boot and carries no module base. A descriptor + supplies one, which is how ``SIGEV_THREAD`` notifications from + ``timer_create()`` and ``mq_notify()`` reach module code. + +* **Two objects hold distinct data bases at once.** A module and the + libraries it imports each have their own writable segment and still call one + another, so two instances of a module share a library's text while each gets + its own copy of the library's data. + +Requirements +============ + +**An ARM Thumb-2 core.** The boundary is the instruction set, not the core +profile: GCC rejects FDPIC in Thumb-1 mode. + +========================= ========================== ===== +Core Architecture FDPIC +========================= ========================== ===== +Cortex-M3 / M4 / M7 ARMv7-M / ARMv7E-M yes +Cortex-M33 ARMv8-M Mainline yes +Cortex-M0 / M0+ / M23 ARMv6-M / ARMv8-M Baseline no +========================= ========================== ===== + +If the toolchain's multilib list includes Thumb-1 variants, the Thumb-1 error +appears even when the target is a Thumb-2 core. Configure the multilib set to +exclude them. + +RISC-V has no FDPIC ABI -- the psABI addendum is an unmerged RFC and no +``EI_OSABI`` value is assigned -- so a RISC-V target cannot use this loader. + +**Flash that is memory mapped and executable**, exposed by a filesystem that +answers ``BIOC_XIPBASE``: :doc:`XIPFS ` or ROMFS. + +**The linker.** A stock ``arm-none-eabi`` GCC compiles correct FDPIC objects +for both C and C++. What it cannot do is link them: ``arm-none-eabi-ld`` is +configured with the ``armelf`` emulation alone, and rather than failing it +marks the output ``UNIX - System V``, which the loader refuses at run time. +Only ``arm-uclinuxfdpiceabi`` carries ``armelf_linux_fdpiceabi``. + +No distribution packages it, so build it -- binutils alone, about a minute:: + + tools/fdpic/build-binutils.sh ~/fdpic + export PATH=~/fdpic/toolchain/bin:$PATH + +An FDPIC GCC is not needed and nothing here uses one. + +GNU ld's ARM FDPIC support has open defects, notably a failure on +``R_ARM_GOTOFFFUNCDESC`` against a hidden function symbol (binutils PR31407, +PR31408, PR31409). + +Configuration +------------- + +``CONFIG_FDPIC`` selects ``BINFMT_LOADABLE`` and ``PIC``, and depends on +``CONFIG_ARCH_ARM``. A working configuration also needs a symbol table for +modules to import from and a filesystem that can expose its media:: + + CONFIG_FDPIC=y + CONFIG_LIBC_EXECFUNCS=y + CONFIG_EXECFUNCS_HAVE_SYMTAB=y + CONFIG_EXECFUNCS_SYSTEM_SYMTAB=y + CONFIG_FS_XIPFS=y + +The options under ``if FDPIC``: + +===================== =========== ===================================== +Option Default Meaning +===================== =========== ===================================== +``FDPIC_LIBPATH`` /mnt/xipfs Where ``DT_NEEDED`` objects are found +``FDPIC_STACKSIZE`` 2048 Stack for a task made from a module +``FDPIC_ALLOW_COPY`` n Copy text to RAM when the media + cannot be mapped +===================== =========== ===================================== + +``FDPIC_ALLOW_COPY`` is for bring-up. Left off, a module that cannot execute +in place fails to load instead of silently costing the RAM this mechanism +exists to save. + +**The base firmware must reserve r9.** It is not enough for the module to be +well behaved: a firmware routine calling back into module code arrives with +the module's GOT in r9 only if the compiler was never free to allocate that +register elsewhere. ``arch/arm/src/common/Toolchain.defs`` adds +``--fixed-r9`` to ``ARCHCPUFLAGS`` under ``CONFIG_FDPIC`` -- not to ``CFLAGS``, +which most board ``Make.defs`` files assign with ``:=`` afterwards and would +discard. A lost flag is silent, so check it arrived:: + + make V=1 2>&1 | grep -m1 fixed-r9 + +.. _fdpic_tooling: + +Building a module +================= + +A whole module is three lines of makefile beside the source. Taking +``apps/examples/fdpicxip/modules/qsorter.c`` as the source:: + + MODULE = qsorter + SRCS = qsorter.c + + include /path/to/nuttx/tools/fdpic/nuttx-fdpic.mk + +:: + + make NUTTX_DIR=/path/to/nuttx + CC qsorter.c + LD qsorter.fdpic + OK qsorter.fdpic: FDPIC, entry 0x2a1, 4 imports resolved + +``NUTTX_DIR`` must be a configured, **built** tree: the compile needs its +headers and the verification step needs ``libs/libc/exec_symtab.c``, which the +build generates. + +C++ sources go in ``CXXSRCS``. ``ARM_TOOLCHAIN=`` and ``FDPIC_TOOLCHAIN=`` +override the two toolchain prefixes, and ``BINDNOW=`` empties the default +``-z now``. ``make`` will run the verification step alone against a stale +``.fdpic`` and report ``OK``, so ``make clean`` when changing toolchains. + +The commands it runs are:: + + arm-none-eabi-gcc -mcpu=cortex-m33 -mthumb -mfdpic -fPIC -Os \ + -fno-builtin -I$NUTTX/include -D__NuttX__ -c qsorter.c -o qsorter.o + + arm-uclinuxfdpiceabi-ld -m armelf_linux_fdpiceabi -shared -z now \ + -e main -o qsorter.fdpic qsorter.o + +Four flags carry weight: + +* **-mfdpic** is stated rather than assumed, so a mis-set toolchain fails + loudly instead of producing a plain ELF the loader refuses. + +* **-fPIC** is not implied by ``-mfdpic`` on the bare-metal target, and + without it the link emits ``TEXTREL``. Text relocations cannot work + against text executed from read-only flash. + +* **-shared** preserves the ``R_ARM_FUNCDESC_VALUE`` relocations for imported + symbols. A PIE link with ``--unresolved-symbols=ignore-all`` appears to + work but degrades every import to ``R_ARM_NONE``, and the module branches to + zero on its first call into the firmware. + +* **-m armelf_linux_fdpiceabi** is required: this linker supports four + emulations and will not guess. + +Verifying imports +----------------- + +A module links with ``-shared``, so undefined symbols are permitted: importing +something the firmware does not export links cleanly and fails on the target +as a bare ``-ENOENT`` naming no symbol. ``fdpic-verify.sh`` runs as part of the +default target and turns that into a build failure:: + + FAIL imports the firmware does not export: + this_symbol_does_not_exist + +The export list comes from ``CONFIG_EXECFUNCS_SYSTEM_SYMTAB``, which generates +``libs/libc/exec_symtab.c``. Most entries there sit behind +``#if defined(CONFIG_...)`` guards, so the file has to go through the +preprocessor before its names mean anything -- read textually it offers every +symbol that *could* be exported rather than the ones that were:: + + tools/fdpic/nuttx-exports.sh /path/to/nuttx # the list + make exports NUTTX_DIR=/path/to/nuttx # the count + +Shared libraries +---------------- + +A library is built the same way with a soname and no entry point, and the +module names it on the link line:: + + arm-uclinuxfdpiceabi-ld ... -shared -soname libcounter.so \ + -e 0 -o libcounter.so libcounter.o + + arm-uclinuxfdpiceabi-ld ... -shared -e main \ + -o user.fdpic user.o libcounter.so + +At run time the library must be present at ``CONFIG_FDPIC_LIBPATH`` under the +name in the module's ``DT_NEEDED`` entry. Each object gets its own GOT and its +own writable data, so two modules using one library see independent library +state while sharing one copy of its code in flash. + +A leaf library -- one that calls nothing outside itself -- has no +``DT_PLTGOT``, because with no external calls there is no PLT. It still has a +GOT, and the loader falls back to ``PT_DYNAMIC`` vaddr plus memsz. + +C++ modules +----------- + +Three compile flags are not optional: + +* ``-fno-use-cxa-atexit``. The default registers each static object's + destructor with ``__cxa_atexit(dtor, obj, &__dso_handle)``, and + ``__dso_handle`` comes from ``crtbegin``, which ``-nostartfiles`` excludes; + the link fails outright. Turning it off also puts destructors in + ``.fini_array``, which is what the loader walks on unload. + +* ``-fno-exceptions -fno-rtti``. Both need ``libsupc++``, which a module + linking ``-nostdlib`` cannot reach. + +C++ is compiled with ``arm-none-eabi-g++`` and linked by the FDPIC linker, +exactly as C is. + +``DT_INIT_ARRAY`` runs after relocations and before the module is entered, +``DT_FINI_ARRAY`` on unload, both per object and in dependency order. +Constructors run in the task that called the loader, not the task that will +run the module, so a constructor sees the loading task's identity. + +Getting this wrong is quiet: a loader that ignores ``DT_INIT_ARRAY`` still +loads a C++ module, resolves every symbol and runs it, with every global left +zero. + +Calling back into a module +========================== + +A module's function pointer is the address of a descriptor in its **writable +segment**. Firmware that stores one and later branches to it jumps into RAM +data. ``fdpic_callback()`` resolves such a pointer to a code address, +deciding whether it is a descriptor by testing r9: a module task runs with its +GOT there, a kernel task with zero. + +It is applied at every entry point a module can reach today: ``qsort``, +``bsearch``, ``pthread_create``, ``signal``, ``sigaction``, +``task_create``/``task_create_with_stack``, ``task_spawn``, ``pthread_once``, +``scandir`` (its filter), and ``mq_notify``/``timer_create`` with +``SIGEV_THREAD``. + +A new entry point that takes a module callback must resolve it too, under +three rules: + +* **Resolve once, in the innermost common routine.** Resolving twice treats a + code address as a descriptor. ``qsort`` recurses, so its wrapper resolves + and the recursive body does not; ``signal()`` does not resolve because + ``nxsig_action()`` does it for both paths; ``scandir`` resolves its filter + but not the comparison function it hands to ``qsort``. + +* **Exclude sentinel values by hand.** ``fdpic_callback()`` declines to + dereference NULL and nothing else. ``sigaction`` excludes ``SIG_IGN``, + ``SIG_DFL``, ``SIG_HOLD`` and ``SIG_ERR`` -- the integers 0, 1, 2 and -1. + +* **A callback on a shared thread needs its base installed.** A + ``SIGEV_THREAD`` notification runs on a work-queue worker with no module + base, so resolving the entry is not enough. Capture the base at + registration with ``fdpic_base()``, in the module's context, and install it + around the call with ``fdpic_invoke()``. + +Everywhere else the callback runs in a task that inherited the module's +D-Space, so only the code address needs resolving. + +Reference +========= + +Object layout +------------- + +The FDPIC marker is ``e_ident[EI_OSABI] == ELFOSABI_ARM_FDPIC`` (65), which +``readelf -h`` shows as "OS/ABI: ARM FDPIC". It is *not* in ``e_flags``, +which read an ordinary ``0x5000000, Version5 EABI``. Validate the OS/ABI byte +and nothing else. + +A linked module has the shape execute-in-place needs with no linker script:: + + LOAD vaddr 0x00000000 R E .text .rodata .hash .dynsym .dynstr + LOAD vaddr 0x000012b4 RW .dynamic .got .data .bss + DYNAMIC DT_PLTGOT -> .got + +``.rodata`` lands in the RX segment on its own, resolved PC-relative or +GOT-indirect. That matters: in the RW segment it would be copied to RAM with +``.data`` and most of the saving would evaporate, silently and with everything +still working. + +``DT_PLTGOT`` gives the GOT base, which is the value r9 must hold. + +Relocations +----------- + +The static link resolves ``R_ARM_GOT_BREL`` and ``R_ARM_GOTFUNCDESC`` into the +GOT already, so only three types carry work into a linked module. +``R_ARM_NONE`` is accepted and skipped. + +``R_ARM_RELATIVE`` + An address needing its segment's base added. + +``R_ARM_FUNCDESC_VALUE`` + A descriptor the linker has laid out, for the loader to fill in with + {resolved code, GOT base}. This is what a *call* to an imported function + produces, and where the firmware's symbol table is consulted. + +``R_ARM_FUNCDESC`` + A pointer to a descriptor that does not exist yet, which the loader + manufactures from a pool behind the writable segment. This is what *taking + the address* of an imported function produces -- a different thing from + calling one, and both can appear for the same symbol. Unlike NXFLAT, a + pointer to a ``static`` function needs no workaround. + +Constants, from binutils ``include/elf/arm.h`` and now in +``arch/arm/include/elf.h``: ``R_ARM_GOTFUNCDESC`` 161, +``R_ARM_GOTOFFFUNCDESC`` 162, ``R_ARM_FUNCDESC`` 163, +``R_ARM_FUNCDESC_VALUE`` 164. + +Relocation tables +----------------- + +Which table an imported function's descriptor lands in is the linker's +decision. With ``-z now`` imports stay in ``DT_REL``; without it they go to +``DT_JMPREL`` and a module may have no ``DT_REL`` at all. + +The loader binds **both**, eagerly. There is no lazy resolver, so an unwalked +table would not be deferred work -- the descriptors would stay unrelocated +until the module branched through one. Nothing is lost by binding early: a +module carries a handful of relocations. + +The two are not walked identically. In ``DT_REL`` the word being overwritten +is the addend and is added to the resolved symbol value. In ``DT_JMPREL`` it +is the lazy-binding bootstrap -- the address of the entry's own PLT stub, with +a GOT half of ``-1`` -- and the binder overwrites the descriptor outright. + +``DT_PLTREL`` must say ``REL``. Nothing reads ``RELA``, whose entries are +twelve bytes rather than eight, so an object declaring that layout is refused +rather than misread. + +``.rofixup`` is skipped. It is the self-relocation list a *static* +executable's ``crt0`` walks to find its own GOT; a module links ``-shared +-nostartfiles``, so no ``crt0`` runs, and the section holds only the GOT +self-pointer that the loader supplies from r9 instead. Everything else +load-dependent is in ``.rel.dyn``/``.rel.plt``, and a PIC object cannot keep an +absolute pointer in its read-only segment. + +binfmt integration +------------------ + +The loader registers a ``struct binfmt_s``, so modules load through the +standard dispatch alongside ELF and NXFLAT. Two details of that contract are +easy to get wrong: + +* ``binp->picbase`` is a ``struct dspace_s *``, not an address. + ``up_initial_state()`` dereferences ``->region``, so a bare GOT puts a kernel + address in r9 and panics. + +* ``binp->stacksize`` must be set, or ``binfmt_execmodule`` rejects the load + with ``-EINVAL``. + +``binfmt`` reports any loader error as ``-ENOENT``, because at the dispatch +layer "refused it" and "not my format" are the same answer. The reason +reaches the syslog only, so do not read ``-ENOENT`` from ``exec`` as "file not +found". + +Testing +======= + +``apps/testing/fs/xipfs`` stages modules out of the filesystem and asserts on +them. ``xipfs_test fdpic`` runs that part in about twenty seconds, and +``xipfs_test reject`` the loader's refusal paths. + +It covers what fails silently: constructors in dependency order, per-instance +data, manufactured descriptors, both relocation tables, the GOT fallback for a +library with no PLT, and every callback entry point. Each check has been +verified against a deliberate one-line breakage of the loader. + +**A run that ends without its summary line is a failure, not a hang.** These +tests call into modules whose relocations must be correct; when they are not, +the module branches to an unrelocated address and the HardFault takes the +system down mid-test, because on Cortex-M a HardFault is system-wide rather +than per-task. + +The module sources are in ``apps/examples/fdpicxip/modules``: ``libshape.cpp`` +for the init/fini arrays, ``funcdesc.c`` for the descriptor pool, ``lazymod.c`` +for ``DT_JMPREL``, ``libcounter.c`` for a leaf library with no ``DT_PLTGOT``. +An opt-in ``make regen`` there rebuilds the headers both apps embed; it is +never part of the application build, so the tree stays buildable with a plain +toolchain. + +``apps/examples/fdpicxip`` is a runnable demonstration of the same modules -- +see :doc:`/applications/examples/fdpicxip/index`. diff --git a/Documentation/components/index.rst b/Documentation/components/index.rst index fe60af7b985..7b041920dce 100644 --- a/Documentation/components/index.rst +++ b/Documentation/components/index.rst @@ -14,6 +14,7 @@ case, you can head to the :doc:`reference <../reference/index>`. binfmt.rst concurrency/index.rst drivers/index.rst + fdpic.rst nxflat.rst nxgraphics/index.rst paging.rst diff --git a/Documentation/platforms/arm/rp23xx/boards/pimoroni-pico-2-plus/index.rst b/Documentation/platforms/arm/rp23xx/boards/pimoroni-pico-2-plus/index.rst index 4dc1f70f3bc..db8844887b6 100644 --- a/Documentation/platforms/arm/rp23xx/boards/pimoroni-pico-2-plus/index.rst +++ b/Documentation/platforms/arm/rp23xx/boards/pimoroni-pico-2-plus/index.rst @@ -177,6 +177,14 @@ xipfs XIPFS mounted on the on-board flash, with the ``xipfs`` command and the XIPFS test suite. +xipfs-fdpic +----------- + +Same as ``xipfs``, plus the FDPIC module loader, the FDPIC half of the test +suite and the ``fdpicxip`` demo. The modules it runs are prebuilt blobs, so +this configuration builds with a plain toolchain; rebuilding them from source +needs an ``arm-uclinuxfdpiceabi`` toolchain. + xipfs-nxflat ------------ diff --git a/arch/arm/include/armv7-m/irq.h b/arch/arm/include/armv7-m/irq.h index 1f37bb6fbb5..69d63984d56 100644 --- a/arch/arm/include/armv7-m/irq.h +++ b/arch/arm/include/armv7-m/irq.h @@ -182,9 +182,16 @@ /* The PIC register is usually R10. It can be R9 is stack checking is enabled * or if the user changes it with -mpic-register on the GCC command line. + * + * The ARM FDPIC ABI mandates R9, and GCC hardcodes it for -mfdpic, so an + * FDPIC build has no choice in the matter. */ -#define REG_PIC REG_R10 +#ifdef CONFIG_FDPIC +# define REG_PIC REG_R9 +#else +# define REG_PIC REG_R10 +#endif /* CONTROL register */ diff --git a/arch/arm/include/armv8-m/irq.h b/arch/arm/include/armv8-m/irq.h index 3d4290eeb7f..938e4c57283 100644 --- a/arch/arm/include/armv8-m/irq.h +++ b/arch/arm/include/armv8-m/irq.h @@ -188,9 +188,16 @@ /* The PIC register is usually R10. It can be R9 is stack checking is enabled * or if the user changes it with -mpic-register on the GCC command line. + * + * The ARM FDPIC ABI mandates R9, and GCC hardcodes it for -mfdpic, so an + * FDPIC build has no choice in the matter. */ -#define REG_PIC REG_R10 +#ifdef CONFIG_FDPIC +# define REG_PIC REG_R9 +#else +# define REG_PIC REG_R10 +#endif /* CONTROL register */ diff --git a/arch/arm/include/elf.h b/arch/arm/include/elf.h index e5c2780472c..44e3550d391 100644 --- a/arch/arm/include/elf.h +++ b/arch/arm/include/elf.h @@ -206,6 +206,24 @@ #define R_ARM_THM_TLS_DESCSEQ16 129 /* Thumb16 */ #define R_ARM_THM_TLS_DESCSEQ32 130 /* Thumb32 */ +/* FDPIC relocations. + * + * Under the FDPIC ABI each PT_LOAD segment is placed independently, so a + * function pointer cannot be a bare code address: it has to carry the data + * base its callee will need. A "function descriptor" is that pair, and + * these relocations are how the loader is asked to build and reference + * them. Values are from the ARM FDPIC ABI as implemented by binutils + * (include/elf/arm.h). + */ + +#define R_ARM_GOTFUNCDESC 161 /* Data GOT entry holding a descriptor */ +#define R_ARM_GOTOFFFUNCDESC 162 /* Data GOT-relative descriptor */ +#define R_ARM_FUNCDESC 163 /* Data Address of a descriptor */ +#define R_ARM_FUNCDESC_VALUE 164 /* Data The descriptor itself: {code, GOT} */ +#define R_ARM_TLS_GD32_FDPIC 165 /* Data */ +#define R_ARM_TLS_LDM32_FDPIC 166 /* Data */ +#define R_ARM_TLS_IE32_FDPIC 167 /* Data */ + /* Processor specific values for the Phdr p_type field. */ #define PT_ARM_EXIDX (PT_LOPROC + 1) /* ARM unwind segment. */ diff --git a/arch/arm/src/common/Toolchain.defs b/arch/arm/src/common/Toolchain.defs index c535bde05aa..66d5147233e 100644 --- a/arch/arm/src/common/Toolchain.defs +++ b/arch/arm/src/common/Toolchain.defs @@ -612,11 +612,42 @@ CELFFLAGS = $(filter-out --fixed-r10,$(CFLAGS)) -fvisibility=hidden \ CXXELFFLAGS = $(filter-out --fixed-r10,$(CXXFLAGS)) -fvisibility=hidden \ -mlong-calls +ifeq ($(CONFIG_FDPIC),y) + # An FDPIC module reaches its own data through r9, and that register has + # to survive a call into the base firmware. A base firmware routine that + # calls back into module code -- qsort() with a module comparison + # function is the standard case -- must arrive there with the module's + # GOT still in r9, which it will not if the compiler was free to allocate + # r9 here. + # + # This goes in ARCHCPUFLAGS rather than CFLAGS because almost every board + # Make.defs assigns + # + # CFLAGS := $(ARCHCFLAGS) $(ARCHOPTIMIZATION) $(ARCHCPUFLAGS) ... + # + # with ':=' after including this file, which discards anything added to + # CFLAGS here but re-expands ARCHCPUFLAGS. The failure is silent and the + # symptom is remote from the cause: the build succeeds and only a callback + # from firmware into module code misbehaves, reading its data through a + # register the firmware has since reused. + # + # CPICFLAGS derives from CFLAGS, so this reaches module compiles too. + # That is harmless: GCC only rejects --fixed-rN when it names the same + # register as -mpic-register, and PIC modules use r10. + + ARCHCPUFLAGS += --fixed-r9 +endif + ifeq ($(CONFIG_PIC),y) +ifneq ($(CONFIG_FDPIC),y) # ARCHCFLAGS, not CFLAGS: board Make.defs reassign CFLAGS with ':=' # after including this file, which would discard the flag. + # + # FDPIC reserves r9 above instead; reserving both would cost a register + # for nothing. ARCHCFLAGS += --fixed-r10 +endif CELFFLAGS += $(PICFLAGS) -mpic-register=r10 CXXELFFLAGS += $(PICFLAGS) -mpic-register=r10 diff --git a/binfmt/CMakeLists.txt b/binfmt/CMakeLists.txt index 1af4cd3042b..451eba1fb74 100644 --- a/binfmt/CMakeLists.txt +++ b/binfmt/CMakeLists.txt @@ -61,6 +61,10 @@ if(CONFIG_NXFLAT) list(APPEND SRCS nxflat.c) endif() +if(CONFIG_FDPIC) + list(APPEND SRCS fdpic.c) +endif() + # Builtin application interfaces if(CONFIG_BUILTIN) diff --git a/binfmt/Kconfig b/binfmt/Kconfig index 93844898da0..ec5aa6f1ad6 100644 --- a/binfmt/Kconfig +++ b/binfmt/Kconfig @@ -46,6 +46,55 @@ if NXFLAT source "binfmt/libnxflat/Kconfig" endif +config FDPIC + bool "Enable the FDPIC ELF Binary Format" + default n + select BINFMT_LOADABLE + select PIC + depends on ARCH_ARM + ---help--- + Enable support for FDPIC ELF modules. + + An FDPIC module has independently placeable read-only and writable + segments, so its text can be mapped and executed directly out of + flash while only the writable segment is copied to RAM -- once per + running instance. This requires a filesystem that can expose the + media, such as XIPFS or ROMFS, and an arm-uclinuxfdpiceabi + toolchain to build the modules. + + The base firmware must reserve the FDPIC register (r9 on ARM) or + callbacks from the base firmware into module code will fail. + +if FDPIC + +config FDPIC_LIBPATH + string "Directory searched for shared libraries" + default "/mnt/xipfs" + ---help--- + Where the loader looks for the objects a module names in its + DT_NEEDED entries. For execute-in-place this should be a + filesystem that can expose its media, so a library's code is + mapped rather than copied. + +config FDPIC_STACKSIZE + int "FDPIC module stack size" + default 2048 + ---help--- + Size of the stack given to a task created from an FDPIC module. + +config FDPIC_ALLOW_COPY + bool "Allow copying text to RAM when execute-in-place is impossible" + default n + ---help--- + By default a module whose text cannot be mapped directly onto the + media fails to load, because copying it to RAM silently forfeits + the memory saving that FDPIC was chosen for. + + Enable this during board bring-up to keep modules loading from a + filesystem that does not support execute-in-place. + +endif # FDPIC + config ELF bool "Enable the ELF Binary Format" default n diff --git a/binfmt/Makefile b/binfmt/Makefile index 6ebd09632f3..9c170c4cddb 100644 --- a/binfmt/Makefile +++ b/binfmt/Makefile @@ -58,6 +58,10 @@ ifeq ($(CONFIG_NXFLAT),y) CSRCS += nxflat.c endif +ifeq ($(CONFIG_FDPIC),y) +CSRCS += fdpic.c +endif + # Add configured binary modules include libnxflat/Make.defs diff --git a/binfmt/binfmt.h b/binfmt/binfmt.h index c362bf041c6..73fa5a36c42 100644 --- a/binfmt/binfmt.h +++ b/binfmt/binfmt.h @@ -327,6 +327,38 @@ int nxflat_initialize(void); void nxflat_uninitialize(void); #endif +#ifdef CONFIG_FDPIC +/**************************************************************************** + * Name: fdpic_initialize + * + * Description: + * FDPIC support is built unconditionally. However, in order to + * use this binary format, this function must be called during system + * initialization in order to register the FDPIC binary format. + * + * Returned Value: + * This is a NuttX internal function so it follows the convention that + * 0 (OK) is returned on success and a negated errno is returned on + * failure. + * + ****************************************************************************/ + +int fdpic_initialize(void); + +/**************************************************************************** + * Name: fdpic_uninitialize + * + * Description: + * Unregister the FDPIC binary loader + * + * Returned Value: + * None + * + ****************************************************************************/ + +void fdpic_uninitialize(void); +#endif + #undef EXTERN #if defined(__cplusplus) } diff --git a/binfmt/binfmt_initialize.c b/binfmt/binfmt_initialize.c index ef110e898ba..bad2ec08d52 100644 --- a/binfmt/binfmt_initialize.c +++ b/binfmt/binfmt_initialize.c @@ -74,6 +74,14 @@ void binfmt_initialize(void) } #endif +#ifdef CONFIG_FDPIC + ret = fdpic_initialize(); + if (ret < 0) + { + berr("ERROR: fdpic_initialize failed: %d\n", ret); + } +#endif + UNUSED(ret); } diff --git a/binfmt/fdpic.c b/binfmt/fdpic.c new file mode 100644 index 00000000000..24286cb31ee --- /dev/null +++ b/binfmt/fdpic.c @@ -0,0 +1,1546 @@ +/**************************************************************************** + * binfmt/fdpic.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Loader for FDPIC ELF modules, for executing code in place out of flash. + * + * An FDPIC module has two loadable segments that may be placed completely + * independently of one another. That is the property this loader exists to + * exploit: the read-only segment is *mapped* rather than copied, so on a + * filesystem that can hand out a direct pointer to the media -- xipfs, or + * ROMFS -- a module's text and rodata are executed and read where they + * already sit in flash, and never occupy RAM. Only the writable segment is + * copied, once per running instance. + * + * Because the two segments are unrelated at run time, a function's address + * is not enough to call it: the callee also needs its data base. FDPIC + * therefore represents a function pointer as a *descriptor*, the pair + * {entry, GOT}, and the caller installs the GOT half into the FDPIC + * register before branching. Building those descriptors is most of what + * this loader does. + * + * The relocation set is small because the static link has already folded + * the GOT-relative work away. Only three kinds survive into the module: + * + * R_ARM_RELATIVE an address that needs its segment's base added + * R_ARM_FUNCDESC_VALUE a descriptor to fill in, usually for an import + * R_ARM_FUNCDESC a pointer to a descriptor the loader must supply + * + * A module's '.rofixup' section is deliberately ignored: it exists for a + * static executable's crt0, which a module never runs, and holds only the + * GOT self-pointer that this loader supplies from the register instead. + * + * The base firmware must be built with the FDPIC register reserved + * (-ffixed-r9 on ARM); a well behaved module is not enough, because a kernel + * routine calling back into module code has to arrive with the module's GOT + * still in the register. + * + * Documentation/components/fdpic.rst has the reasoning behind all of this in + * full, including why ignoring '.rofixup' is correct rather than tolerated. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef CONFIG_FDPIC + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#ifndef ELFOSABI_ARM_FDPIC +# define ELFOSABI_ARM_FDPIC 65 +#endif + +/* The Thumb bit lives in the low bit of a code address. Descriptors and + * entry points must keep it or the core faults on the branch. + */ + +#define FDPIC_THUMB_BIT 1 + +/* Calling into a module from the loader -- which is what running its + * constructors amounts to -- needs the object's data base installed in the + * FDPIC register first, and that takes a little assembly. Where it cannot + * be done, a module carrying constructors is refused rather than run with + * whatever the register happened to hold. + */ + +#if defined(CONFIG_ARCH_ARM) && defined(__thumb__) +# define FDPIC_HAVE_CALLFN 1 +#endif + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static int fdpic_loadbinary(FAR struct binary_s *binp, + FAR const char *filename, + FAR const struct symtab_s *exports, + int nexports); +static int fdpic_unloadbinary(FAR struct binary_s *binp); + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static struct binfmt_s g_fdpicbinfmt = +{ + NULL, /* next */ + fdpic_loadbinary, /* load */ + fdpic_unloadbinary, /* unload */ +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: fdpic_read + * + * Description: + * Read from the module file at an absolute offset. + * + ****************************************************************************/ + +static int fdpic_read(FAR struct fdpic_loadinfo_s *loadinfo, + FAR void *buffer, size_t nbytes, off_t offset) +{ + FAR uint8_t *dest = buffer; + ssize_t nread; + + if (file_seek(&loadinfo->file, offset, SEEK_SET) < 0) + { + return -EIO; + } + + while (nbytes > 0) + { + nread = file_read(&loadinfo->file, dest, nbytes); + if (nread < 0) + { + if (nread == -EINTR) + { + continue; + } + + return (int)nread; + } + + if (nread == 0) + { + return -ENODATA; + } + + dest += nread; + nbytes -= nread; + } + + return OK; +} + +/**************************************************************************** + * Name: fdpic_addr + * + * Description: + * Translate a link-time virtual address into the address the segment + * actually landed at. This is the whole of the FDPIC "load map": two + * segments, each with its own independent bias. + * + * Returned Value: + * The run-time address, or 0 if the address belongs to neither segment. + * + ****************************************************************************/ + +static uintptr_t fdpic_addr(FAR struct fdpic_loadinfo_s *loadinfo, + uintptr_t vaddr) +{ + if (vaddr >= loadinfo->textvaddr && + vaddr < loadinfo->textvaddr + loadinfo->textsize) + { + return loadinfo->textaddr + (vaddr - loadinfo->textvaddr); + } + + if (vaddr >= loadinfo->datavaddr && + vaddr < loadinfo->datavaddr + loadinfo->datamemsz) + { + return loadinfo->dataaddr + (vaddr - loadinfo->datavaddr); + } + + return 0; +} + +/**************************************************************************** + * Name: fdpic_verify + * + * Description: + * Check that this really is an FDPIC module for this machine. + * + * The FDPIC marker is the OS/ABI byte, not e_flags: a module's e_flags + * are an unremarkable EABI version, so testing them would reject every + * valid module. + * + ****************************************************************************/ + +static int fdpic_verify(FAR const Elf32_Ehdr *ehdr) +{ + if (memcmp(ehdr->e_ident, ELFMAG, EI_MAGIC_SIZE) != 0) + { + return -ENOEXEC; + } + + if (ehdr->e_ident[EI_CLASS] != ELFCLASS32 || + ehdr->e_ident[EI_DATA] != ELFDATA2LSB) + { + berr("ERROR: Not a little-endian 32-bit object\n"); + return -ENOEXEC; + } + + if (ehdr->e_ident[EI_OSABI] != ELFOSABI_ARM_FDPIC) + { + /* Not FDPIC. Quietly decline so another binfmt handler can try. */ + + return -ENOEXEC; + } + + if (ehdr->e_type != ET_DYN) + { + berr("ERROR: FDPIC module must be ET_DYN, got %u\n", ehdr->e_type); + return -ENOEXEC; + } + + if (ehdr->e_machine != EM_ARM) + { + berr("ERROR: Not an ARM module (e_machine=%u)\n", ehdr->e_machine); + return -ENOEXEC; + } + + return OK; +} + +/**************************************************************************** + * Name: fdpic_readdynamic + * + * Description: + * Pull the handful of dynamic tags the loader needs out of PT_DYNAMIC. + * + * This is read straight from the file rather than from a loaded segment, + * because it has to be known before the writable segment can be sized: + * the relocation count bounds how many descriptors might be requested. + * + ****************************************************************************/ + +static int fdpic_readdynamic(FAR struct fdpic_loadinfo_s *loadinfo, + FAR const Elf32_Phdr *phdr) +{ + FAR Elf32_Dyn *dyn; + uint32_t pltreltype = DT_REL; + size_t ndyn; + size_t i; + int ret; + + loadinfo->nneeded = 0; + + if (phdr->p_filesz == 0 || phdr->p_filesz > 4096) + { + return -ENOEXEC; + } + + dyn = kmm_malloc(phdr->p_filesz); + if (dyn == NULL) + { + return -ENOMEM; + } + + ret = fdpic_read(loadinfo, dyn, phdr->p_filesz, phdr->p_offset); + if (ret < 0) + { + kmm_free(dyn); + return ret; + } + + ndyn = phdr->p_filesz / sizeof(Elf32_Dyn); + + for (i = 0; i < ndyn && dyn[i].d_tag != DT_NULL; i++) + { + switch (dyn[i].d_tag) + { + case DT_REL: + loadinfo->relvaddr = dyn[i].d_un.d_ptr; + break; + + case DT_RELSZ: + loadinfo->relsize = dyn[i].d_un.d_val; + break; + + case DT_SYMTAB: + loadinfo->symtabvaddr = dyn[i].d_un.d_ptr; + break; + + case DT_STRTAB: + loadinfo->strtabvaddr = dyn[i].d_un.d_ptr; + break; + + case DT_PLTGOT: + loadinfo->gotaddr = dyn[i].d_un.d_ptr; + break; + + case DT_JMPREL: + loadinfo->pltrelvaddr = dyn[i].d_un.d_ptr; + break; + + case DT_PLTRELSZ: + loadinfo->pltrelsize = dyn[i].d_un.d_val; + break; + + case DT_PLTREL: + pltreltype = dyn[i].d_un.d_val; + break; + + case DT_INIT_ARRAY: + loadinfo->initvaddr = dyn[i].d_un.d_ptr; + break; + + case DT_INIT_ARRAYSZ: + loadinfo->initsize = dyn[i].d_un.d_val; + break; + + case DT_FINI_ARRAY: + loadinfo->finivaddr = dyn[i].d_un.d_ptr; + break; + + case DT_FINI_ARRAYSZ: + loadinfo->finisize = dyn[i].d_un.d_val; + break; + + case DT_NEEDED: + /* An offset into DT_STRTAB. The string itself lives in the + * read-only segment, which is not mapped yet, so only the + * offset is recorded here. + */ + + if (loadinfo->nneeded < FDPIC_MAX_NEEDED) + { + loadinfo->needed[loadinfo->nneeded++] = dyn[i].d_un.d_val; + } + else + { + berr("ERROR: More than %d DT_NEEDED entries\n", + FDPIC_MAX_NEEDED); + kmm_free(dyn); + return -ENOEXEC; + } + break; + + default: + break; + } + } + + kmm_free(dyn); + + /* DT_PLTREL says which format the PLT table is in. Nothing anywhere in + * this loader reads RELA, so an object claiming that layout must be + * refused rather than walked as if it were REL: the entries are 12 bytes, + * not 8, and misreading them would apply garbage relocations. + */ + + if (loadinfo->pltrelsize != 0 && pltreltype != DT_REL) + { + berr("ERROR: %s has RELA PLT relocations, which are not supported\n", + loadinfo->name); + return -ENOEXEC; + } + + if (loadinfo->gotaddr == 0) + { + /* DT_PLTGOT is only emitted when the object has a PLT, which a leaf + * library calling nothing external does not. It still has a GOT -- + * its own data is reached through it, so the FDPIC register has to + * point at it -- and the linker places that GOT immediately after + * the dynamic section. + */ + + loadinfo->gotaddr = phdr->p_vaddr + phdr->p_memsz; + + binfo("fdpic: %s has no DT_PLTGOT, taking GOT at %08lx\n", + loadinfo->name, (unsigned long)loadinfo->gotaddr); + } + + return OK; +} + +/**************************************************************************** + * Name: fdpic_load + * + * Description: + * Place both segments: map the read-only one, copy the writable one. + * + ****************************************************************************/ + +static int fdpic_load(FAR struct fdpic_loadinfo_s *loadinfo) +{ + FAR Elf32_Phdr *phdrs; + FAR Elf32_Phdr *text = NULL; + FAR Elf32_Phdr *data = NULL; + FAR Elf32_Phdr *dynamic = NULL; + uintptr_t mapped = 0; + size_t phdrsize; + size_t i; + int ret; + + if (loadinfo->ehdr.e_phnum == 0 || + loadinfo->ehdr.e_phentsize != sizeof(Elf32_Phdr)) + { + return -ENOEXEC; + } + + phdrsize = (size_t)loadinfo->ehdr.e_phnum * sizeof(Elf32_Phdr); + phdrs = kmm_malloc(phdrsize); + if (phdrs == NULL) + { + return -ENOMEM; + } + + ret = fdpic_read(loadinfo, phdrs, phdrsize, loadinfo->ehdr.e_phoff); + if (ret < 0) + { + goto errout_with_phdrs; + } + + for (i = 0; i < loadinfo->ehdr.e_phnum; i++) + { + if (phdrs[i].p_type == PT_DYNAMIC) + { + dynamic = &phdrs[i]; + } + else if (phdrs[i].p_type == PT_LOAD) + { + if ((phdrs[i].p_flags & PF_W) != 0) + { + data = &phdrs[i]; + } + else + { + text = &phdrs[i]; + } + } + } + + if (text == NULL || data == NULL || dynamic == NULL) + { + berr("ERROR: Expected one RX and one RW PT_LOAD plus PT_DYNAMIC\n"); + ret = -ENOEXEC; + goto errout_with_phdrs; + } + + loadinfo->textvaddr = text->p_vaddr; + loadinfo->textsize = text->p_memsz; + loadinfo->datavaddr = data->p_vaddr; + loadinfo->datafilesz = data->p_filesz; + loadinfo->datamemsz = data->p_memsz; + + ret = fdpic_readdynamic(loadinfo, dynamic); + if (ret < 0) + { + goto errout_with_phdrs; + } + + /* Map the read-only segment. + * + * MAP_XIP_STRICT is the point of the exercise: it tells the filesystem + * to resolve the mapping onto the media or fail, rather than quietly + * copying the file into RAM. A silent copy would still work, which is + * exactly why it must be refused -- it would cost the RAM this loader + * exists to save, and nothing would report it. + */ + + ret = file_ioctl(&loadinfo->file, XIPFSIOC_PIN, + (unsigned long)(uintptr_t)&mapped); + if (ret >= 0) + { + /* The pin is held until unload, which happens when the spawned task + * exits. That is a different task from the one running the loader, + * so this deliberately does not go through mmap: an mm_map entry + * would be recorded against whoever called the loader and would + * never be released by the module's own exit. + */ + + loadinfo->textaddr = (uintptr_t)mapped + text->p_offset; + loadinfo->textmapped = true; + } +#ifdef CONFIG_FDPIC_ALLOW_COPY + else + { + /* Bring-up escape hatch: keep working on a filesystem that cannot + * expose its media, at the cost of the RAM saving. + */ + + fwarn("fdpic: cannot execute in place (%d), copying text to RAM\n", + ret); + + loadinfo->textaddr = (uintptr_t)kmm_malloc(text->p_memsz); + if (loadinfo->textaddr == 0) + { + ret = -ENOMEM; + goto errout_with_phdrs; + } + + ret = fdpic_read(loadinfo, (FAR void *)loadinfo->textaddr, + text->p_filesz, text->p_offset); + if (ret < 0) + { + goto errout_with_text; + } + } +#else + else + { + berr("ERROR: Module cannot be executed in place: %d\n", ret); + goto errout_with_phdrs; + } +#endif + + /* Allocate the writable segment, with room after it for any descriptors + * the relocations ask us to manufacture. Bounding that by the total + * relocation count costs a few bytes and avoids a second pass over the + * relocation table. + * + * Both tables have to be counted. An R_ARM_FUNCDESC in the PLT table + * draws from the same pool, so sizing this from DT_RELSZ alone would let + * a module carrying DT_JMPREL entries run off the end of the allocation + * and corrupt the heap. + */ + + loadinfo->ndesc = (loadinfo->relsize + loadinfo->pltrelsize) / + sizeof(Elf32_Rel); + loadinfo->dataalloc = loadinfo->datamemsz + + (size_t)loadinfo->ndesc * + sizeof(struct fdpic_desc_s); + + loadinfo->dataaddr = (uintptr_t)kumm_zalloc(loadinfo->dataalloc); + if (loadinfo->dataaddr == 0) + { + ret = -ENOMEM; + goto errout_with_text; + } + + loadinfo->descpool = loadinfo->dataaddr + loadinfo->datamemsz; + + ret = fdpic_read(loadinfo, (FAR void *)loadinfo->dataaddr, + loadinfo->datafilesz, data->p_offset); + if (ret < 0) + { + goto errout_with_data; + } + + /* Everything past p_filesz is .bss, already zeroed by kumm_zalloc */ + + loadinfo->gotaddr = fdpic_addr(loadinfo, loadinfo->gotaddr); + loadinfo->entry = fdpic_addr(loadinfo, + loadinfo->ehdr.e_entry & ~FDPIC_THUMB_BIT); + + if (loadinfo->gotaddr == 0 || loadinfo->entry == 0) + { + berr("ERROR: GOT or entry point outside any segment\n"); + ret = -ENOEXEC; + goto errout_with_data; + } + + loadinfo->entry |= (loadinfo->ehdr.e_entry & FDPIC_THUMB_BIT); + + binfo("fdpic: text %08lx (%s) data %08lx got %08lx entry %08lx\n", + (unsigned long)loadinfo->textaddr, + loadinfo->textmapped ? "in place" : "copied", + (unsigned long)loadinfo->dataaddr, + (unsigned long)loadinfo->gotaddr, + (unsigned long)loadinfo->entry); + + kmm_free(phdrs); + return OK; + +errout_with_data: + kumm_free((FAR void *)loadinfo->dataaddr); + loadinfo->dataaddr = 0; + +errout_with_text: + if (loadinfo->textmapped) + { + file_ioctl(&loadinfo->file, XIPFSIOC_UNPIN, 0); + } + else if (loadinfo->textaddr != 0) + { + kmm_free((FAR void *)loadinfo->textaddr); + } + + loadinfo->textaddr = 0; + +errout_with_phdrs: + kmm_free(phdrs); + return ret; +} + +/**************************************************************************** + * Name: fdpic_release + * + * Description: + * Release every object of a load. Dropping a text pin is what lets a + * compacting filesystem move those blocks again, now that nothing is + * executing from them. + * + ****************************************************************************/ + +static void fdpic_release(FAR struct fdpic_loadinfo_s *head) +{ + FAR struct fdpic_loadinfo_s *obj; + FAR struct fdpic_loadinfo_s *next; + + for (obj = head; obj != NULL; obj = next) + { + next = obj->flink; + + if (obj->dataaddr != 0) + { + kumm_free((FAR void *)obj->dataaddr); + } + + if (obj->textmapped) + { + file_ioctl(&obj->file, XIPFSIOC_UNPIN, 0); + } + else if (obj->textaddr != 0) + { + kmm_free((FAR void *)obj->textaddr); + } + + file_close(&obj->file); + kmm_free(obj); + } +} + +/**************************************************************************** + * Name: fdpic_loadobject + * + * Description: + * Open one object, place its segments, and append it to the load's list. + * + ****************************************************************************/ + +static int fdpic_loadobject(FAR const char *path, FAR const char *name, + FAR struct fdpic_loadinfo_s **head, + FAR struct fdpic_loadinfo_s **out) +{ + FAR struct fdpic_loadinfo_s *obj; + FAR struct fdpic_loadinfo_s *tail; + int ret; + + obj = kmm_zalloc(sizeof(struct fdpic_loadinfo_s)); + if (obj == NULL) + { + return -ENOMEM; + } + + strlcpy(obj->name, name, sizeof(obj->name)); + + ret = file_open(&obj->file, path, O_RDONLY); + if (ret < 0) + { + /* Not an error worth shouting about: binfmt offers every candidate + * name to every loader, including builtin command names that are + * not files at all. + */ + + binfo("fdpic: cannot open %s: %d\n", path, ret); + kmm_free(obj); + return ret; + } + + ret = fdpic_read(obj, &obj->ehdr, sizeof(Elf32_Ehdr), 0); + if (ret >= 0) + { + ret = fdpic_verify(&obj->ehdr); + } + + if (ret >= 0) + { + ret = fdpic_load(obj); + } + + if (ret < 0) + { + file_close(&obj->file); + kmm_free(obj); + return ret; + } + + /* Append, so the module stays first and dependencies follow it */ + + if (*head == NULL) + { + *head = obj; + } + else + { + for (tail = *head; tail->flink != NULL; tail = tail->flink) + { + } + + tail->flink = obj; + } + + if (out != NULL) + { + *out = obj; + } + + return OK; +} + +/**************************************************************************** + * Name: fdpic_loaddepends + * + * Description: + * Load whatever this object names in DT_NEEDED. + * + * The names live in the string table inside the read-only segment, so + * this can only run once that segment is mapped. Anything already + * loaded for this module is reused rather than loaded twice. + * + * That reuse is also what makes a dependency cycle harmless: an object is + * on the list before its own dependencies are walked, so a library naming + * something already loaded finds it and stops. What the list cannot + * bound is a chain of distinct names, which is why the depth is capped + * explicitly. + * + ****************************************************************************/ + +static int fdpic_loaddepends(FAR struct fdpic_loadinfo_s *obj, + FAR struct fdpic_loadinfo_s **head, + int depth) +{ + FAR const char *strtab; + FAR struct fdpic_loadinfo_s *o; + FAR struct fdpic_loadinfo_s *dep; + char path[128]; + bool seen; + int ret; + int i; + + if (obj->nneeded == 0) + { + return OK; + } + + if (depth >= FDPIC_MAX_DEPTH) + { + berr("ERROR: %s exceeds the %d level DT_NEEDED depth limit\n", + obj->name, FDPIC_MAX_DEPTH); + return -ELOOP; + } + + strtab = (FAR const char *)fdpic_addr(obj, obj->strtabvaddr); + if (strtab == NULL) + { + return -ENOEXEC; + } + + for (i = 0; i < obj->nneeded; i++) + { + FAR const char *name = &strtab[obj->needed[i]]; + + seen = false; + for (o = *head; o != NULL; o = o->flink) + { + if (strcmp(o->name, name) == 0) + { + seen = true; + break; + } + } + + if (seen) + { + continue; + } + + snprintf(path, sizeof(path), "%s/%s", CONFIG_FDPIC_LIBPATH, name); + + binfo("fdpic: %s needs %s\n", obj->name, name); + + ret = fdpic_loadobject(path, name, head, &dep); + if (ret < 0) + { + berr("ERROR: Cannot load %s needed by %s: %d\n", + name, obj->name, ret); + return ret; + } + + /* Libraries may depend on libraries */ + + ret = fdpic_loaddepends(dep, head, depth + 1); + if (ret < 0) + { + return ret; + } + } + + return OK; +} + +/**************************************************************************** + * Name: fdpic_symvalue + * + * Description: + * Resolve one relocation's symbol. A symbol defined inside the module is + * translated through the load map; an undefined one is an import and is + * looked up in the symbol table the base firmware exports. + * + ****************************************************************************/ + +static int fdpic_symvalue(FAR struct fdpic_loadinfo_s *obj, + FAR struct fdpic_loadinfo_s *head, + uint32_t symidx, + FAR const struct symtab_s *exports, int nexports, + FAR uintptr_t *value, + FAR struct fdpic_loadinfo_s **owner) +{ + FAR const Elf32_Sym *symtab; + FAR const Elf32_Sym *sym; + FAR const char *strtab; + FAR const char *name; + FAR const struct symtab_s *found; + FAR struct fdpic_loadinfo_s *o; + + symtab = (FAR const Elf32_Sym *)fdpic_addr(obj, obj->symtabvaddr); + strtab = (FAR const char *)fdpic_addr(obj, obj->strtabvaddr); + + if (symtab == NULL || strtab == NULL) + { + return -ENOEXEC; + } + + sym = &symtab[symidx]; + name = &strtab[sym->st_name]; + + if (sym->st_shndx != SHN_UNDEF) + { + /* Defined right here */ + + *value = fdpic_addr(obj, sym->st_value & ~FDPIC_THUMB_BIT); + if (*value == 0) + { + berr("ERROR: '%s' outside any segment of %s\n", name, obj->name); + return -ENOEXEC; + } + + *value |= (sym->st_value & FDPIC_THUMB_BIT); + *owner = obj; + return OK; + } + + /* Undefined here. Try the other objects in this load before falling + * back to the firmware, so a module linked against a library binds to + * the library rather than to a same-named firmware symbol. + */ + + for (o = head; o != NULL; o = o->flink) + { + FAR const Elf32_Sym *osym; + FAR const Elf32_Sym *otab; + FAR const char *ostr; + uint32_t n; + uint32_t i; + + if (o == obj || o->symtabvaddr == 0) + { + continue; + } + + otab = (FAR const Elf32_Sym *)fdpic_addr(o, o->symtabvaddr); + ostr = (FAR const char *)fdpic_addr(o, o->strtabvaddr); + if (otab == NULL || ostr == NULL) + { + continue; + } + + /* The dynamic symbol table runs from its start to the string table, + * which the linker places immediately after it. + */ + + n = (o->strtabvaddr - o->symtabvaddr) / sizeof(Elf32_Sym); + + for (i = 0; i < n; i++) + { + osym = &otab[i]; + + if (osym->st_shndx == SHN_UNDEF || osym->st_name == 0) + { + continue; + } + + if (strcmp(&ostr[osym->st_name], name) != 0) + { + continue; + } + + *value = fdpic_addr(o, osym->st_value & ~FDPIC_THUMB_BIT); + if (*value == 0) + { + continue; + } + + *value |= (osym->st_value & FDPIC_THUMB_BIT); + *owner = o; + + binfo("fdpic: '%s' resolved in %s\n", name, o->name); + return OK; + } + } + + /* Imported from the base firmware */ + + if (exports == NULL) + { + berr("ERROR: '%s' imported but no symbol table was provided\n", name); + return -ENOENT; + } + + found = symtab_findbyname(exports, name, nexports); + if (found == NULL) + { + berr("ERROR: Imported symbol '%s' not found\n", name); + return -ENOENT; + } + + *value = (uintptr_t)found->sym_value; + *owner = NULL; /* Firmware: no GOT of its own */ + return OK; +} + +/**************************************************************************** + * Name: fdpic_reltable + * + * Description: + * Apply one REL relocation table. + * + * Input Parameters: + * pltrel - This is the DT_JMPREL table, so the words being overwritten + * are not addends. See the addend handling below. + * + ****************************************************************************/ + +static int fdpic_reltable(FAR struct fdpic_loadinfo_s *loadinfo, + FAR struct fdpic_loadinfo_s *head, + FAR const struct symtab_s *exports, int nexports, + uintptr_t relvaddr, size_t relsize, + bool pltrel, FAR const char *what) +{ + FAR const Elf32_Rel *rels; + size_t nrels; + size_t i; + int ret; + + if (relsize == 0) + { + return OK; + } + + rels = (FAR const Elf32_Rel *)fdpic_addr(loadinfo, relvaddr); + if (rels == NULL) + { + berr("ERROR: %s of %s is outside any segment\n", what, loadinfo->name); + return -ENOEXEC; + } + + nrels = relsize / sizeof(Elf32_Rel); + + for (i = 0; i < nrels; i++) + { + uint32_t type = ELF32_R_TYPE(rels[i].r_info); + uint32_t symidx = ELF32_R_SYM(rels[i].r_info); + FAR struct fdpic_loadinfo_s *owner = NULL; + FAR uint32_t *where; + uintptr_t value; + + where = (FAR uint32_t *)fdpic_addr(loadinfo, rels[i].r_offset); + if (where == NULL) + { + berr("ERROR: Relocation %zu targets %08lx, outside any segment\n", + i, (unsigned long)rels[i].r_offset); + return -ENOEXEC; + } + + /* Everything relocated must land in the writable segment. A module + * that asked us to patch its text could not be executed in place by + * a second instance, so refuse rather than silently give up sharing. + */ + + if ((uintptr_t)where < loadinfo->dataaddr || + (uintptr_t)where >= loadinfo->dataaddr + loadinfo->dataalloc) + { + berr("ERROR: Relocation %zu would write outside the RW segment\n", + i); + return -ENOEXEC; + } + + switch (type) + { + case R_ARM_NONE: + break; + + case R_ARM_RELATIVE: + { + /* REL format: the addend is the link-time address already + * sitting at the target. + */ + + uintptr_t addr = fdpic_addr(loadinfo, *where); + + if (addr == 0) + { + berr("ERROR: RELATIVE target %08lx outside any segment\n", + (unsigned long)*where); + return -ENOEXEC; + } + + *where = (uint32_t)addr; + } + break; + + case R_ARM_FUNCDESC_VALUE: + { + /* The target *is* the descriptor: two words, entry then the + * data base to install before branching. + * + * The GOT written here is this module's own, including for + * imported functions. That is deliberate and is what makes + * a callback work: when the base firmware's qsort() calls + * back into the module's comparison function, the module + * needs its own data base in the FDPIC register. + */ + + FAR struct fdpic_desc_s *desc = + (FAR struct fdpic_desc_s *)where; + uint32_t addend; + + ret = fdpic_symvalue(loadinfo, head, symidx, exports, + nexports, &value, &owner); + if (ret < 0) + { + return ret; + } + + /* REL format keeps the addend in place, in the word that is + * about to become the entry point. It matters: a static + * function is referenced through its *section* symbol, whose + * value is the section base, with the offset -- and the + * Thumb bit -- carried entirely by the addend. Dropping it + * yields an even address and the core faults trying to + * execute it as ARM code. + * + * Except in the DT_JMPREL table, where that word is not an + * addend at all. A lazy descriptor is pre-loaded with the + * address of its PLT resolution stub and a GOT half of -1, + * for a resolver to overwrite on first call. Adding that + * stub address to the symbol value produces an arbitrary + * address, and the module faults on its first call exactly as + * if the relocation had never been applied -- which is a + * remarkably good imitation of the bug this table support was + * added to fix. An eager binder overwrites the descriptor + * outright. + */ + + addend = pltrel ? 0 : desc->entry; + + desc->entry = value + addend; + + /* The GOT half must be the *defining* object's, not this + * one's. Calling into a shared library installs that + * library's data base, which is how it reaches its own + * globals -- and is the entire reason FDPIC exists. A + * firmware symbol has no GOT, so leave this object's in + * place: the firmware ignores the register, and anything it + * calls back into belongs to this object. + */ + + desc->got = (owner != NULL) ? owner->gotaddr + : loadinfo->gotaddr; + } + break; + + case R_ARM_FUNCDESC: + { + /* A pointer to a descriptor, which the loader has to supply. + * Carve one out of the pool reserved behind the writable + * segment. + */ + + FAR struct fdpic_desc_s *desc; + + if (loadinfo->usedesc >= loadinfo->ndesc) + { + berr("ERROR: Out of function descriptors\n"); + return -ENOMEM; + } + + ret = fdpic_symvalue(loadinfo, head, symidx, exports, + nexports, &value, &owner); + if (ret < 0) + { + return ret; + } + + desc = (FAR struct fdpic_desc_s *)loadinfo->descpool + + loadinfo->usedesc++; + + desc->entry = value + (pltrel ? 0 : *where); /* as above */ + desc->got = (owner != NULL) ? owner->gotaddr + : loadinfo->gotaddr; + + *where = (uint32_t)(uintptr_t)desc; + } + break; + + default: + berr("ERROR: Unsupported relocation type %" PRIu32 "\n", type); + return -ENOSYS; + } + } + + binfo("fdpic: %s: applied %zu %s relocations, %u descriptors created\n", + loadinfo->name, nrels, what, loadinfo->usedesc); + + return OK; +} + +/**************************************************************************** + * Name: fdpic_bind + * + * Description: + * Apply the module's dynamic relocations. + * + * Both relocation tables are walked, and both eagerly. Which one an + * imported function's descriptor lands in is purely a linker decision -- + * -z now puts it in DT_REL, and without it the same entry goes to + * DT_JMPREL for a lazy resolver to fill in later. There is no resolver + * here, so a table left unwalked is not deferred work, it is a descriptor + * that stays unrelocated until the module branches through it. Binding + * both is what makes the layout stop mattering. + * + * Nothing is lost by binding the PLT table early: a module carries a + * handful of relocations, so there is no load time worth deferring. + * + * The two tables are not handled identically, though. A lazy descriptor + * arrives pre-loaded with its resolution stub rather than with an addend, + * so the PLT table is bound with that word ignored. + * + ****************************************************************************/ + +static int fdpic_bind(FAR struct fdpic_loadinfo_s *loadinfo, + FAR struct fdpic_loadinfo_s *head, + FAR const struct symtab_s *exports, int nexports) +{ + int ret; + + ret = fdpic_reltable(loadinfo, head, exports, nexports, + loadinfo->relvaddr, loadinfo->relsize, + false, "DT_REL"); + if (ret < 0) + { + return ret; + } + + return fdpic_reltable(loadinfo, head, exports, nexports, + loadinfo->pltrelvaddr, loadinfo->pltrelsize, + true, "DT_JMPREL"); +} + +/**************************************************************************** + * Name: fdpic_callfn + * + * Description: + * Call a function in a loaded object with that object's data base in the + * FDPIC register. + * + * A module's own code normally runs with the register already correct, + * because the scheduler installs it from the task's D-Space on every + * switch. Constructors do not get that: they run here, in whichever task + * called the loader, before the module's task exists. So the register has + * to be installed by hand around the call, and the function entered + * directly rather than through a descriptor. + * + * The base firmware is built with the register reserved, so nothing of the + * loader's own is being displaced; saving and restoring it covers the case + * where the loader was itself called from module code. Being preempted + * in the middle is harmless -- the register is part of the saved context, + * so it travels with whichever task is holding it. + * + ****************************************************************************/ + +#ifdef FDPIC_HAVE_CALLFN +static void fdpic_callfn(uintptr_t entry, uintptr_t got) +{ + __asm__ __volatile__ + ( + "mov r4, r9\n" /* Save whatever the caller had there */ + "mov r9, %1\n" /* This object's data base */ + "blx %0\n" + "mov r9, r4\n" + : + : "r" (entry), "r" (got) + : "r0", "r1", "r2", "r3", "r4", "r12", "lr", "cc", "memory" + ); +} +#endif + +/**************************************************************************** + * Name: fdpic_callarray + * + * Description: + * Run one of the DT_INIT_ARRAY / DT_FINI_ARRAY tables of an object. + * + * The array lives in the writable segment and each entry carries an + * R_ARM_RELATIVE relocation, so by the time this runs the entries are + * already run-time code addresses with their Thumb bit intact. Nothing + * further has to be translated. + * + * Input Parameters: + * reverse - Walk the array backwards. Destructors run in the opposite + * order to constructors, which is the only reason the two + * directions exist. + * + ****************************************************************************/ + +static int fdpic_callarray(FAR struct fdpic_loadinfo_s *obj, + uintptr_t vaddr, size_t size, bool reverse, + FAR const char *what) +{ + FAR const uintptr_t *array; +#ifdef FDPIC_HAVE_CALLFN + size_t n; + size_t i; +#endif + + if (vaddr == 0 || size == 0) + { + return OK; + } + + array = (FAR const uintptr_t *)fdpic_addr(obj, vaddr); + if (array == NULL) + { + berr("ERROR: %s of %s is outside any segment\n", what, obj->name); + return -ENOEXEC; + } + +#ifndef FDPIC_HAVE_CALLFN + berr("ERROR: %s has a %s, which this architecture cannot run\n", + obj->name, what); + return -ENOSYS; +#else + n = size / sizeof(uintptr_t); + + binfo("fdpic: %s: %zu entries in %s\n", obj->name, n, what); + + for (i = 0; i < n; i++) + { + uintptr_t entry = array[reverse ? n - 1 - i : i]; + uintptr_t code = entry & ~FDPIC_THUMB_BIT; + + /* 0 and ~0 are the conventional "no function here" fillers */ + + if (entry == 0 || entry == (uintptr_t)-1) + { + continue; + } + + /* The entry must point into this object's text. Left unchecked, a + * relocation that did not happen -- the failure this loader is most + * prone to -- becomes a branch to an arbitrary address, which on this + * class of target means a HardFault with no console and no clue. + */ + + if (code < obj->textaddr || code >= obj->textaddr + obj->textsize) + { + berr("ERROR: %s entry %zu of %s is %08lx, outside its text\n", + what, i, obj->name, (unsigned long)entry); + return -ENOEXEC; + } + + fdpic_callfn(entry, obj->gotaddr); + } + + return OK; +#endif +} + +/**************************************************************************** + * Name: fdpic_runinit + * + * Description: + * Run every object's constructors, dependencies first. + * + * An object joins the list before the DT_NEEDED walk appends its own + * dependencies, so a library always sits behind whatever needed it. + * Walking the list backwards therefore constructs a library before the + * object that uses it, which is the ordering that matters: a module's + * constructor may well touch a library object that has to exist already. + * + ****************************************************************************/ + +static int fdpic_runinit(FAR struct fdpic_loadinfo_s *head) +{ + FAR struct fdpic_loadinfo_s *done = NULL; + FAR struct fdpic_loadinfo_s *obj; + int ret; + + for (; ; ) + { + /* The object just ahead of the one done last time. The list is + * singly linked and short -- a handful of entries at most -- so + * rescanning it beats carrying a back pointer around. + */ + + for (obj = head; obj != NULL && obj->flink != done; obj = obj->flink) + { + } + + if (obj == NULL) + { + return OK; + } + + ret = fdpic_callarray(obj, obj->initvaddr, obj->initsize, false, + "DT_INIT_ARRAY"); + if (ret < 0) + { + return ret; + } + + done = obj; + } +} + +/**************************************************************************** + * Name: fdpic_runfini + * + * Description: + * Run every object's destructors, in the mirror of the construction + * order: the module first, then the libraries it was built on. + * + * Errors are not propagated. This runs on the teardown path, where there + * is nothing left to abandon and reporting a failure would only turn an + * incomplete teardown into a leaked one. + * + ****************************************************************************/ + +static void fdpic_runfini(FAR struct fdpic_loadinfo_s *head) +{ + FAR struct fdpic_loadinfo_s *obj; + + for (obj = head; obj != NULL; obj = obj->flink) + { + fdpic_callarray(obj, obj->finivaddr, obj->finisize, true, + "DT_FINI_ARRAY"); + } +} + +/**************************************************************************** + * Name: fdpic_loadbinary + ****************************************************************************/ + +static int fdpic_loadbinary(FAR struct binary_s *binp, + FAR const char *filename, + FAR const struct symtab_s *exports, + int nexports) +{ + FAR struct fdpic_loadinfo_s *head = NULL; + FAR struct fdpic_loadinfo_s *main_obj = NULL; + FAR struct fdpic_loadinfo_s *obj; + FAR const char *base; + int ret; + + binfo("Loading FDPIC module: %s\n", filename); + + base = strrchr(filename, '/'); + base = (base != NULL) ? base + 1 : filename; + + ret = fdpic_loadobject(filename, base, &head, &main_obj); + if (ret < 0) + { + return ret; + } + + /* Pull in whatever it needs before binding, so that cross-object + * references have something to resolve against. + */ + + ret = fdpic_loaddepends(main_obj, &head, 0); + if (ret < 0) + { + goto errout; + } + + /* Bind every object, not just the module. A library has relocations of + * its own, and its imports resolve against the same set. + */ + + for (obj = head; obj != NULL; obj = obj->flink) + { + ret = fdpic_bind(obj, head, exports, nexports); + if (ret < 0) + { + berr("ERROR: Failed to bind %s: %d\n", obj->name, ret); + goto errout; + } + } + + /* Constructors, now that every object can reach its own data. + * + * fdpic_release() can undo a load but cannot undo a constructor, so this + * sits ahead of the D-Space allocation rather than after it: the only way + * left to discard a load with constructors already run is a constructor + * list that fails partway through, and such a module is unusable anyway. + * Destructors are not run for the entries that did succeed -- a table this + * loader has just found malformed is not one to branch into on the way + * out. + */ + + ret = fdpic_runinit(head); + if (ret < 0) + { + berr("ERROR: Constructors of %s failed: %d\n", main_obj->name, ret); + goto errout; + } + + binp->entrypt = (main_t)main_obj->entry; + binp->mapsize = 0; + binp->stacksize = CONFIG_FDPIC_STACKSIZE; + binp->unload = fdpic_unloadbinary; + +#ifdef CONFIG_PIC + /* The scheduler expects a reference counted container here, not a bare + * address: up_initial_state() installs dspace->region into the FDPIC + * register. Threads of the task share it via crefs, and + * sched_releasetcb() frees the container -- but not the region, which is + * this loader's to release. + * + * The module's own GOT goes in. A call into a library switches the + * register to that library's GOT via the descriptor, and switches back + * on return. + */ + + main_obj->dspace = kmm_malloc(sizeof(struct dspace_s)); + if (main_obj->dspace == NULL) + { + ret = -ENOMEM; + goto errout; + } + + main_obj->dspace->crefs = 1; + main_obj->dspace->region = (FAR uint8_t *)main_obj->gotaddr; + + binp->picbase = main_obj->dspace; +#endif + + binp->mapped = head; + return OK; + +errout: + fdpic_release(head); + return ret; +} + +/**************************************************************************** + * Name: fdpic_unloadbinary + * + * Description: + * Release the module. Destructors run first, while the writable segment + * they operate on is still there. Unmapping the text then drops the + * filesystem's pin on it, which is what allows a compacting filesystem to + * move those blocks again once no instance is executing from them. + * + ****************************************************************************/ + +static int fdpic_unloadbinary(FAR struct binary_s *binp) +{ + fdpic_runfini(binp->mapped); + fdpic_release(binp->mapped); + binp->mapped = NULL; + return OK; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: fdpic_initialize + ****************************************************************************/ + +int fdpic_initialize(void) +{ + int ret; + + ret = register_binfmt(&g_fdpicbinfmt); + if (ret < 0) + { + berr("ERROR: Failed to register FDPIC binfmt: %d\n", ret); + } + + return ret; +} + +/**************************************************************************** + * Name: fdpic_uninitialize + ****************************************************************************/ + +void fdpic_uninitialize(void) +{ + unregister_binfmt(&g_fdpicbinfmt); +} + +#endif /* CONFIG_FDPIC */ diff --git a/boards/arm/rp23xx/pimoroni-pico-2-plus/configs/xipfs-fdpic/defconfig b/boards/arm/rp23xx/pimoroni-pico-2-plus/configs/xipfs-fdpic/defconfig new file mode 100644 index 00000000000..196ca05bc9b --- /dev/null +++ b/boards/arm/rp23xx/pimoroni-pico-2-plus/configs/xipfs-fdpic/defconfig @@ -0,0 +1,58 @@ +# +# This file is autogenerated: PLEASE DO NOT EDIT IT. +# +# You can use "make menuconfig" to make any modifications to the installed .config file. +# You can then do "make savedefconfig" to generate a new defconfig file that includes your +# modifications. +# +# CONFIG_NSH_ARGCAT is not set +# CONFIG_NSH_CMDOPT_HEXDUMP is not set +# CONFIG_NSH_DISABLE_DATE is not set +# CONFIG_NSH_DISABLE_LOSMART is not set +# CONFIG_STANDARD_SERIAL is not set +CONFIG_ARCH="arm" +CONFIG_ARCH_BOARD="pimoroni-pico-2-plus" +CONFIG_ARCH_BOARD_COMMON=y +CONFIG_ARCH_BOARD_PIMORONI_PICO_2_PLUS=y +CONFIG_ARCH_CHIP="rp23xx" +CONFIG_ARCH_CHIP_RP23XX=y +CONFIG_ARCH_RAMVECTORS=y +CONFIG_ARCH_STACKDUMP=y +CONFIG_BOARDCTL_RESET=y +CONFIG_BOARD_LOOPSPERMSEC=10450 +CONFIG_BUILTIN=y +CONFIG_DEBUG_FULLOPT=y +CONFIG_DEBUG_SYMBOLS=y +CONFIG_EXAMPLES_FDPICXIP=y +CONFIG_EXAMPLES_HELLO=y +CONFIG_EXECFUNCS_HAVE_SYMTAB=y +CONFIG_EXECFUNCS_SYSTEM_SYMTAB=y +CONFIG_FDPIC=y +CONFIG_FS_PROCFS=y +CONFIG_FS_PROCFS_REGISTER=y +CONFIG_FS_XIPFS=y +CONFIG_FS_XIPFS_FAULT_INJECT=y +CONFIG_INIT_ENTRYPOINT="nsh_main" +CONFIG_INIT_STACKSIZE=16384 +CONFIG_LIBC_EXECFUNCS=y +CONFIG_MTD=y +CONFIG_NFILE_DESCRIPTORS_PER_BLOCK=6 +CONFIG_NSH_BUILTIN_APPS=y +CONFIG_NSH_READLINE=y +CONFIG_RAM_SIZE=532480 +CONFIG_RAM_START=0x20000000 +CONFIG_READLINE_CMD_HISTORY=y +CONFIG_RP23XX_FLASH_MTD=y +CONFIG_RR_INTERVAL=200 +CONFIG_SCHED_HPWORK=y +CONFIG_SCHED_WAITPID=y +CONFIG_SIG_EVTHREAD=y +CONFIG_START_DAY=9 +CONFIG_START_MONTH=2 +CONFIG_START_YEAR=2021 +CONFIG_SYSLOG_CONSOLE=y +CONFIG_SYSTEM_NSH=y +CONFIG_SYSTEM_XIPFS=y +CONFIG_TESTING_FS_XIPFS=y +CONFIG_TESTING_FS_XIPFS_MTD="/dev/rpflash" +CONFIG_UART0_SERIAL_CONSOLE=y diff --git a/include/elf.h b/include/elf.h index a3d6dc8f927..922ba930155 100644 --- a/include/elf.h +++ b/include/elf.h @@ -278,6 +278,10 @@ #define DT_TEXTREL 22 /* d_un=ignored */ #define DT_JMPREL 23 /* d_un=d_ptr */ #define DT_BINDNOW 24 /* d_un=ignored */ +#define DT_INIT_ARRAY 25 /* d_un=d_ptr */ +#define DT_FINI_ARRAY 26 /* d_un=d_ptr */ +#define DT_INIT_ARRAYSZ 27 /* d_un=d_val */ +#define DT_FINI_ARRAYSZ 28 /* d_un=d_val */ #define DT_LOPROC 0x70000000 /* d_un=unspecified */ #define DT_HIPROC 0x7fffffff /* d_un= unspecified */ diff --git a/include/nuttx/binfmt/fdpic.h b/include/nuttx/binfmt/fdpic.h new file mode 100644 index 00000000000..79b4cdf265f --- /dev/null +++ b/include/nuttx/binfmt/fdpic.h @@ -0,0 +1,296 @@ +/**************************************************************************** + * include/nuttx/binfmt/fdpic.h + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +#ifndef __INCLUDE_NUTTX_BINFMT_FDPIC_H +#define __INCLUDE_NUTTX_BINFMT_FDPIC_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include + +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* A function descriptor is the pair that makes FDPIC work: a callee cannot + * find its own data from its code address alone, because the two segments + * are placed independently, so every function pointer carries the data base + * to install alongside the entry point. + */ + +#define FDPIC_DESC_WORDS 2 + +/* Shared libraries a single object may name in DT_NEEDED */ + +#define FDPIC_MAX_NEEDED 8 + +/* How deep a chain of libraries depending on libraries may go. + * + * A cycle cannot run away -- an object joins the load's list before its own + * dependencies are walked, so coming back around finds it already there -- + * but a chain of distinct names has nothing to stop it. The walk recurses + * once per link with a path buffer on each frame, so without a bound a + * malformed module set overflows the stack of whichever task called the + * loader rather than being rejected. + */ + +#define FDPIC_MAX_DEPTH 8 + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +struct fdpic_desc_s +{ + uintptr_t entry; /* Address of the code */ + uintptr_t got; /* Data/GOT base to install in the FDPIC register */ +}; + +/* One loaded object: the module itself, or a shared library it needs. + * + * A load produces a list of these -- the module first, then whatever its + * DT_NEEDED entries pulled in. Text is mapped and therefore shared between + * every instance; the writable segment is private to this load, so two + * tasks using the same library each get their own copy of its data, which + * is the behaviour a library with state has to have. + */ + +struct fdpic_loadinfo_s +{ + FAR struct fdpic_loadinfo_s *flink; /* Next object in this load */ + char name[32]; /* For diagnostics and DT_NEEDED */ + + struct file file; /* The module being loaded */ + Elf32_Ehdr ehdr; /* Copy of the ELF header */ + + /* Read-only segment. This is mapped, not copied: on a filesystem that + * can expose the media directly this pointer is the flash address and + * the text is never in RAM at all. + */ + + uintptr_t textaddr; /* Where the RX segment actually is */ + uintptr_t textvaddr; /* p_vaddr it was linked at */ + size_t textsize; + bool textmapped; /* True if it came from mmap */ + + /* Read-write segment. Always a private RAM copy, one per instance. */ + + uintptr_t dataaddr; /* Where the RW segment was placed */ + uintptr_t datavaddr; /* p_vaddr it was linked at */ + size_t datafilesz; /* Bytes to read from the file */ + size_t datamemsz; /* Including .bss */ + size_t dataalloc; /* Total allocation incl. descriptors */ + + uintptr_t gotaddr; /* Runtime address of the GOT */ + uintptr_t entry; /* Runtime entry point */ + + /* Reference counted container the scheduler installs into the FDPIC + * register on every context switch. Allocated here; freed by + * sched_releasetcb() once the last thread using it is gone. + */ + + FAR struct dspace_s *dspace; + + /* Pool used to satisfy R_ARM_FUNCDESC, which asks the loader to + * manufacture a descriptor and hand back its address. + */ + + uintptr_t descpool; + uint16_t ndesc; /* Capacity */ + uint16_t usedesc; /* Next free slot */ + + /* Dynamic information, all as link-time virtual addresses */ + + /* DT_NEEDED entries, as offsets into DT_STRTAB. Resolved to names once + * the read-only segment holding the string table is mapped. + */ + + uint32_t needed[FDPIC_MAX_NEEDED]; + uint8_t nneeded; + + uintptr_t relvaddr; + size_t relsize; + + /* The PLT relocation table. Separate from DT_REL only by where the + * linker chose to put an entry: without -z now the imported function + * descriptors land here instead, and the loader binds both tables + * eagerly rather than requiring one particular layout. + */ + + uintptr_t pltrelvaddr; + size_t pltrelsize; + + /* Constructors and destructors. Both arrays live in the writable + * segment and their entries arrive as link-time addresses carrying an + * R_ARM_RELATIVE relocation, so they are real code addresses -- Thumb + * bit included -- only after fdpic_bind() has run. + */ + + uintptr_t initvaddr; + size_t initsize; + uintptr_t finivaddr; + size_t finisize; + + uintptr_t symtabvaddr; + uintptr_t strtabvaddr; +}; + +/**************************************************************************** + * Inline Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: fdpic_callback + * + * Description: + * Resolve a function pointer that arrived from a caller which may be an + * FDPIC module. + * + * This exists because the two sides disagree about what a function + * pointer *is*. Base firmware is not built FDPIC, so to it a function + * pointer is a code address and it simply branches to it. An FDPIC + * module passes the address of a two-word descriptor instead. A base + * firmware routine that takes a callback -- qsort() and bsearch() are the + * ones that matter -- therefore branches straight into the module's data + * segment and faults. + * + * The caller is identified by the FDPIC register: a module task runs with + * its GOT there, and a plain kernel task runs with zero, because + * up_initial_state() only installs a value when the task has a D-Space. + * + * Only the entry point is taken from the descriptor. The data base is + * already correct in the register: the base firmware is built with that + * register reserved, so the module's GOT survives the call in. + * + * Input Parameters: + * fn - The pointer as it was received. + * + * Returned Value: + * An address that can be branched to directly. + * + ****************************************************************************/ + +#if defined(CONFIG_FDPIC) && defined(__thumb__) +static inline FAR void *fdpic_callback(FAR void *fn) +{ + uintptr_t base; + + __asm__ __volatile__ ("mov %0, r9" : "=r"(base)); + + if (base != 0 && fn != NULL) + { + return (FAR void *)((FAR struct fdpic_desc_s *)fn)->entry; + } + + return fn; +} + +/**************************************************************************** + * Name: fdpic_base + * + * Description: + * The FDPIC data base of the calling context, taken from the FDPIC + * register. Non-zero means the caller is an FDPIC module; zero means base + * firmware. This is the same test fdpic_callback() makes, exposed for a + * caller that must decide whether a pointer it was handed is a descriptor + * before it stores it somewhere the register will no longer be correct -- + * a callback that will run on a work-queue thread, for instance. + * + ****************************************************************************/ + +static inline uintptr_t fdpic_base(void) +{ + uintptr_t base; + + __asm__ __volatile__ ("mov %0, r9" : "=r"(base)); + + return base; +} + +/**************************************************************************** + * Name: fdpic_invoke + * + * Description: + * Call a resolved module entry point with the module's data base installed + * in the FDPIC register, and restore the caller's base afterwards. + * + * This is for the one case the register cannot already be correct: a + * callback that a module registered but that runs on a shared thread -- + * the signal-notification work queue -- which carries no module's base. + * The base is captured at registration (fdpic_base(), in the module's + * context) and installed here around the call. Everywhere else the + * callback runs in a task that inherited the module's data space and only + * the entry point has to be resolved; there fdpic_callback() is enough. + * + * A context switch or interrupt during the call is safe: the FDPIC + * register is REG_PIC in the saved register context, so it is preserved + * and restored across a switch, and base firmware is built with it + * reserved so no interrupt handler disturbs it. + * + * Input Parameters: + * entry - The code address to enter (already resolved from the + * descriptor). + * arg - The single word argument, passed in r0. + * got - The module data base to install in the FDPIC register. + * + ****************************************************************************/ + +static inline void fdpic_invoke(uintptr_t entry, uintptr_t arg, + uintptr_t got) +{ + register uintptr_t r0v __asm__ ("r0") = arg; + + /* arg is pinned in r0 (the first argument and the call's scratch), so the + * asm needs registers only for entry and got -- kept deliberately few so + * the allocator has room on builds that reserve a frame pointer. r9 is + * saved on the stack rather than in a scratch register; r4 rides along + * only to keep the push 8-byte aligned and is restored untouched. + */ + + __asm__ __volatile__ + ( + "push {r4, r9}\n" /* Save the caller's FDPIC register */ + "mov r9, %[got]\n" /* Install the module's data base */ + "blx %[entry]\n" /* Enter the module */ + "pop {r4, r9}\n" /* Restore the caller's FDPIC register */ + : "+r" (r0v) + : [entry] "r" (entry), [got] "r" (got) + : "r1", "r2", "r3", "r12", "lr", "cc", "memory" + ); +} +#else +# define fdpic_callback(fn) (fn) +# define fdpic_base() (0) +# define fdpic_invoke(entry, arg, got) \ + ((void)(got), (((CODE void (*)(uintptr_t))(uintptr_t)(entry))(arg))) +#endif + +#endif /* __INCLUDE_NUTTX_BINFMT_FDPIC_H */ diff --git a/include/nuttx/signal.h b/include/nuttx/signal.h index 79fa22b39d5..46b3a0e703f 100644 --- a/include/nuttx/signal.h +++ b/include/nuttx/signal.h @@ -68,6 +68,11 @@ struct sigwork_s struct work_s work; /* Work queue structure */ union sigval value; /* Data passed with notification */ sigev_notify_function_t func; /* Notification function */ +#ifdef CONFIG_FDPIC + uintptr_t got; /* FDPIC data base of a module callback, or + * zero. Captured at registration, installed + * around the call on the worker thread. */ +#endif }; #ifdef __cplusplus diff --git a/libs/libc/dirent/lib_scandir.c b/libs/libc/dirent/lib_scandir.c index c734ff1d243..f55862a460a 100644 --- a/libs/libc/dirent/lib_scandir.c +++ b/libs/libc/dirent/lib_scandir.c @@ -31,6 +31,10 @@ #include #include +#ifdef CONFIG_FDPIC +# include +#endif + #include "libc.h" /* The scandir() function is not appropriate for use within the kernel in its @@ -91,6 +95,19 @@ int scandir(FAR const char *path, FAR struct dirent ***namelist, * the original errno value to be able to restore it in case of success. */ +#ifdef CONFIG_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolve the filter, which is called from the loop below. + * + * compar is deliberately NOT resolved here. It is handed to qsort(), + * whose public entry point resolves it, and resolving it twice would + * treat an already-resolved code address as a descriptor. + */ + + filter = (CODE int (*)(FAR const struct dirent *)) + fdpic_callback((FAR void *)filter); +#endif + errsv = get_errno(); dirp = opendir(path); diff --git a/libs/libc/pthread/pthread_create.c b/libs/libc/pthread/pthread_create.c index 6c87140361c..e0b734be18b 100644 --- a/libs/libc/pthread/pthread_create.c +++ b/libs/libc/pthread/pthread_create.c @@ -30,6 +30,10 @@ #include +#ifdef CONFIG_FDPIC +# include +#endif + /**************************************************************************** * Private Functions ****************************************************************************/ @@ -88,6 +92,20 @@ static void pthread_startup(pthread_startroutine_t entry, int pthread_create(FAR pthread_t *thread, FAR const pthread_attr_t *attr, pthread_startroutine_t pthread_entry, pthread_addr_t arg) { +#ifdef CONFIG_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolve it here, once, in the public entry point. + * + * The new thread inherits the creator's D-Space -- nxtask_dup_dspace() + * runs before up_initial_state() installs it in the FDPIC register -- so + * it starts with the module's own data base already in place, and needs + * only the code address. + */ + + pthread_entry = (pthread_startroutine_t) + fdpic_callback((FAR void *)pthread_entry); +#endif + return nx_pthread_create(pthread_startup, thread, attr, pthread_entry, arg); } diff --git a/libs/libc/pthread/pthread_once.c b/libs/libc/pthread/pthread_once.c index ccd854b8788..ba2b87a73fe 100644 --- a/libs/libc/pthread/pthread_once.c +++ b/libs/libc/pthread/pthread_once.c @@ -33,6 +33,10 @@ #include #include +#ifdef CONFIG_FDPIC +# include +#endif + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -73,6 +77,21 @@ int pthread_once(FAR pthread_once_t *once_control, return EINVAL; } +#ifdef CONFIG_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolve it here, in the public entry point. + * + * init_routine() runs on this thread, so the module's data base is + * already in the FDPIC register and only the code address is needed. The + * resolved value is a local copy and is never stored, so a later call + * through the same once_control resolves the caller's descriptor afresh + * rather than re-resolving a code address. + */ + + init_routine = (CODE void (*)(void)) + fdpic_callback((FAR void *)init_routine); +#endif + if (!once_control->done) { pthread_mutex_lock(&once_control->mutex); diff --git a/libs/libc/signal/sig_signal.c b/libs/libc/signal/sig_signal.c index 8ed40cf2ef2..bf19d765ea0 100644 --- a/libs/libc/signal/sig_signal.c +++ b/libs/libc/signal/sig_signal.c @@ -71,6 +71,14 @@ _sa_handler_t signal(int signo, _sa_handler_t func) DEBUGASSERT(func != SIG_ERR && func != SIG_HOLD); + /* An FDPIC module passes the address of a function descriptor rather than + * a code address, but it is not resolved here: sigaction() then + * nxsig_action() resolves the handler in the innermost common code, which + * covers both this path and a module that calls sigaction() directly. + * Resolving here as well would resolve it twice and branch through a code + * address as if it were a descriptor. + */ + /* Initialize the sigaction structure */ act.sa_handler = func; diff --git a/libs/libc/stdlib/lib_bsearch.c b/libs/libc/stdlib/lib_bsearch.c index a4e3047bf90..141c16ef12a 100644 --- a/libs/libc/stdlib/lib_bsearch.c +++ b/libs/libc/stdlib/lib_bsearch.c @@ -37,6 +37,10 @@ ****************************************************************************/ #include + +#ifdef CONFIG_FDPIC +# include +#endif #include /**************************************************************************** @@ -114,6 +118,13 @@ FAR void *bsearch(FAR const void *key, FAR const void *base, size_t nel, DEBUGASSERT(base != NULL || nel == 0); DEBUGASSERT(compar != NULL); +#ifdef CONFIG_FDPIC + /* See qsort(): an FDPIC caller passes a descriptor, not a code address */ + + compar = (CODE int (*)(FAR const void *, FAR const void *)) + fdpic_callback((FAR void *)compar); +#endif + for (lim = nel, lower = (const char *)base; lim != 0; lim >>= 1) { middle = lower + (lim >> 1) * width; diff --git a/libs/libc/stdlib/lib_qsort.c b/libs/libc/stdlib/lib_qsort.c index 5646452388f..3965058bdf2 100644 --- a/libs/libc/stdlib/lib_qsort.c +++ b/libs/libc/stdlib/lib_qsort.c @@ -45,6 +45,10 @@ #include #include +#ifdef CONFIG_FDPIC +# include +#endif + /**************************************************************************** * Pre-processor Definitions ****************************************************************************/ @@ -156,8 +160,9 @@ static inline FAR char *med3(FAR char *a, FAR char *b, FAR char *c, * ****************************************************************************/ -void qsort(FAR void *base, size_t nel, size_t width, - CODE int(*compar)(FAR const void *, FAR const void *)) +static void qsort_internal(FAR void *base, size_t nel, size_t width, + CODE int(*compar)(FAR const void *, + FAR const void *)) { FAR char *pa; FAR char *pb; @@ -277,7 +282,7 @@ loop: if ((r = pb - pa) > width) { - qsort(base, r / width, width, compar); + qsort_internal(base, r / width, width, compar); } if ((r = pd - pc) > width) @@ -289,3 +294,31 @@ loop: goto loop; } } + +/**************************************************************************** + * Name: qsort + * + * Description: + * Public entry point. Resolves the comparison function once and then + * hands an ordinary pointer to the implementation. + * + * The split matters: qsort_internal() recurses, and resolving on every + * entry would treat an already-resolved code address as a descriptor the + * second time round and branch somewhere meaningless. + * + ****************************************************************************/ + +void qsort(FAR void *base, size_t nel, size_t width, + CODE int(*compar)(FAR const void *, FAR const void *)) +{ +#ifdef CONFIG_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a + * code address. + */ + + compar = (CODE int (*)(FAR const void *, FAR const void *)) + fdpic_callback((FAR void *)compar); +#endif + + qsort_internal(base, nel, width, compar); +} diff --git a/sched/mqueue/mq_notify.c b/sched/mqueue/mq_notify.c index ea1f35fc279..0b403aac856 100644 --- a/sched/mqueue/mq_notify.c +++ b/sched/mqueue/mq_notify.c @@ -34,6 +34,10 @@ #include #include +#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD) +# include +#endif + #include "sched/sched.h" #include "mqueue/mqueue.h" @@ -156,6 +160,19 @@ int mq_notify(mqd_t mqdes, FAR const struct sigevent *notification) sizeof(struct sigevent)); msgq->ntpid = rtcb->pid; + +#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD) + /* If a module registered a SIGEV_THREAD callback, capture its data + * base now, while this runs in the module's context. The callback + * fires later on a work-queue worker that has no base of its own; + * nxsig_notification() reads this to install it around the call. + */ + + msgq->ntwork.got = + (fdpic_base() != 0 && + (notification->sigev_notify & SIGEV_THREAD) != 0) ? + fdpic_base() : 0; +#endif } } diff --git a/sched/signal/sig_action.c b/sched/signal/sig_action.c index ab37db83d26..0bb28cea125 100644 --- a/sched/signal/sig_action.c +++ b/sched/signal/sig_action.c @@ -38,6 +38,10 @@ #include #include +#ifdef CONFIG_FDPIC +# include +#endif + #include "sched/sched.h" #include "group/group.h" #include "signal/signal.h" @@ -326,6 +330,27 @@ int nxsig_action(int signo, FAR const struct sigaction *act, handler = act->sa_handler; +#ifdef CONFIG_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolve it here, in the innermost common code, so a module + * that calls sigaction() directly is covered as well as one that goes + * through signal(), and each exactly once -- signal() passes its argument + * through unresolved for that reason. + * + * The dispositions have to be excluded by hand. SIG_ERR, SIG_IGN, + * SIG_DFL and SIG_HOLD are the small integers -1, 0, 1 and 2 rather than + * addresses, and fdpic_callback() only declines to dereference NULL. + * sa_handler and sa_sigaction are a union, so this one resolution serves + * both handler forms. + */ + + if (handler != SIG_ERR && handler != SIG_IGN && handler != SIG_DFL && + handler != SIG_HOLD) + { + handler = (_sa_handler_t)fdpic_callback((FAR void *)handler); + } +#endif + #ifdef CONFIG_SIG_DEFAULT /* If the caller is setting the handler to SIG_DFL, then we need to * replace this with the correct, internal default signal action handler. diff --git a/sched/signal/sig_notification.c b/sched/signal/sig_notification.c index 86909faf62b..1f22bc51222 100644 --- a/sched/signal/sig_notification.c +++ b/sched/signal/sig_notification.c @@ -34,6 +34,10 @@ #include +#ifdef CONFIG_FDPIC +# include +#endif + #include "sched/sched.h" #include "signal/signal.h" @@ -70,7 +74,24 @@ static void nxsig_notification_worker(FAR void *arg) /* Perform the callback */ - work->func(work->value); +#ifdef CONFIG_FDPIC + /* A module's callback runs here on a shared worker thread, which does not + * carry the module's data base. Install the base captured at + * registration around the call so the callback can reach its own globals; + * work->func has already been resolved to the code address. A non-module + * callback has a zero base and is called directly. + */ + + if (work->got != 0) + { + fdpic_invoke((uintptr_t)work->func, (uintptr_t)work->value.sival_ptr, + work->got); + } + else +#endif + { + work->func(work->value); + } } #endif /* CONFIG_SIG_EVTHREAD */ @@ -157,6 +178,22 @@ int nxsig_notification(pid_t pid, FAR struct sigevent *event, work->value = event->sigev_value; work->func = event->sigev_notify_function; +#ifdef CONFIG_FDPIC + /* When the callback is a module's, work->got was set at registration + * to the module's data base (this runs at send or expiry time, whose + * context is not the module's, so it cannot be read here). The + * callback is a descriptor: resolve it to the code address now -- + * reading the descriptor is just a memory access and needs no base -- + * and the worker installs the base around the call. + */ + + if (work->got != 0) + { + work->func = (sigev_notify_function_t) + ((FAR struct fdpic_desc_s *)event->sigev_notify_function)->entry; + } +#endif + /* Then queue the work */ return work_queue(SIG_EVTHREAD_WORK, &work->work, diff --git a/sched/task/task_create.c b/sched/task/task_create.c index 4530a742491..a517dcf0962 100644 --- a/sched/task/task_create.c +++ b/sched/task/task_create.c @@ -37,6 +37,10 @@ #include #include +#ifdef CONFIG_FDPIC +# include +#endif + #include "sched/sched.h" #include "group/group.h" #include "task/task.h" @@ -202,8 +206,23 @@ int task_create_with_stack(FAR const char *name, int priority, FAR void *stack_addr, int stack_size, main_t entry, FAR char * const argv[]) { - int ret = nxtask_create(name, priority, stack_addr, - stack_size, entry, argv, NULL); + int ret; + +#ifdef CONFIG_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolving it here covers task_create() too, which is a plain + * forwarder -- and covers it exactly once, which matters: resolving twice + * would treat an already-resolved code address as a descriptor. + * + * The new task inherits the creator's D-Space, so it starts with the + * module's own data base installed and needs only the code address. + */ + + entry = (main_t)fdpic_callback((FAR void *)entry); +#endif + + ret = nxtask_create(name, priority, stack_addr, + stack_size, entry, argv, NULL); if (ret < 0) { set_errno(-ret); diff --git a/sched/task/task_spawn.c b/sched/task/task_spawn.c index a29db7f893c..4252b6b577e 100644 --- a/sched/task/task_spawn.c +++ b/sched/task/task_spawn.c @@ -38,6 +38,10 @@ #include #include +#ifdef CONFIG_FDPIC +# include +#endif + #include "sched/sched.h" #include "group/group.h" #include "task/spawn.h" @@ -335,6 +339,17 @@ int task_spawn(FAR const char *name, main_t entry, pid_t pid = INVALID_PROCESS_ID; int ret; +#ifdef CONFIG_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolve it here, once, in the public entry point. + * + * The new task inherits the creator's D-Space, so it starts with the + * module's own data base installed and needs only the code address. + */ + + entry = (main_t)fdpic_callback((FAR void *)entry); +#endif + sinfo("name=%s entry=%p file_actions=%p attr=%p argv=%p\n", name, entry, file_actions, attr, argv); diff --git a/sched/timer/timer_create.c b/sched/timer/timer_create.c index 1811f3e68af..69799381f71 100644 --- a/sched/timer/timer_create.c +++ b/sched/timer/timer_create.c @@ -37,6 +37,10 @@ #include #include +#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD) +# include +#endif + #include "sched/sched.h" #include "timer/timer.h" @@ -196,6 +200,19 @@ int timer_create(clockid_t clockid, FAR struct sigevent *evp, /* Yes, copy the entire struct sigevent content */ memcpy(&ret->pt_event, evp, sizeof(struct sigevent)); + +#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD) + /* If a module registered a SIGEV_THREAD callback, capture its + * data base now, while this runs in the module's context. The + * callback fires later on a work-queue worker with no base of + * its own; nxsig_notification() installs this around the call. + */ + + ret->pt_work.got = + (fdpic_base() != 0 && + (evp->sigev_notify & SIGEV_THREAD) != 0) ? + fdpic_base() : 0; +#endif } else { diff --git a/tools/fdpic/README.md b/tools/fdpic/README.md new file mode 100644 index 00000000000..3e1f84413c8 --- /dev/null +++ b/tools/fdpic/README.md @@ -0,0 +1,61 @@ +# FDPIC module build tooling + +Everything needed to build an FDPIC module out of tree: a module is an ELF +shared object whose read-only segment the target maps straight out of flash +and executes in place, while its writable segment is copied to RAM once per +running instance. It links against nothing -- libc and everything else are +imported from the firmware's exported symbol table at load time. + +The loader that consumes these is `binfmt/fdpic.c`, enabled by `CONFIG_FDPIC`. +The full description of the format, the toolchain and the load-time contract +is in `Documentation/components/fdpic.rst`. + +## Contents + +| File | Purpose | +| --- | --- | +| `nuttx-fdpic.mk` | the module build itself; include it from a two-line makefile | +| `fdpic-verify.sh` | checks a built module's imports resolve against the firmware | +| `nuttx-exports.sh` | turns `libs/libc/exec_symtab.c` into a symbol list | +| `fdpic-embed.py` | turns a built module into a C header, for carrying one in an image | +| `build-binutils.sh` | builds the `arm-uclinuxfdpiceabi` binutils, the one from-source dependency | + +## Building a module + +A whole module is three lines of makefile beside the source. Taking +`apps/examples/fdpicxip/modules/qsorter.c`, which is a module in its own +right, as the source: + + MODULE = qsorter + SRCS = qsorter.c + + include /path/to/nuttx/tools/fdpic/nuttx-fdpic.mk + +Then: + + make NUTTX_DIR=/path/to/nuttx + CC qsorter.c + LD qsorter.fdpic + OK qsorter.fdpic: FDPIC, entry 0x2a1, 4 imports resolved + +`NUTTX_DIR` has to be a configured, built tree: the compile needs its headers +and the verify step needs the export table generated into +`libs/libc/exec_symtab.c`. + +Two toolchains are involved. The stock `arm-none-eabi` compiler does the +compiling -- it emits perfectly good FDPIC objects for both C and C++ -- and +`arm-uclinuxfdpiceabi` **binutils** does the linking, because +`arm-none-eabi-ld` cannot produce an FDPIC object at all. So the from-source +dependency is binutils alone, which `build-binutils.sh` builds in about a +minute. + +Verification runs as part of the default target on purpose: a module that +imports a symbol the firmware does not export links perfectly happily and +fails only once it is on the target, as a bare `-ENOENT` that names nothing. + +## Modules carried inside an image + +`fdpic-embed.py` exists for apps that have to load a module before there is any +way to put files on the target, so they embed one and write it out at run +time. `apps/examples/fdpicxip/modules/` uses it that way, and is the worked +example of driving this tooling for several modules at once. diff --git a/tools/fdpic/build-binutils.sh b/tools/fdpic/build-binutils.sh new file mode 100755 index 00000000000..28e3277441b --- /dev/null +++ b/tools/fdpic/build-binutils.sh @@ -0,0 +1,80 @@ +#!/bin/bash +############################################################################ +# tools/fdpic/build-binutils.sh +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +# Build arm-uclinuxfdpiceabi binutils -- the only part of the module +# toolchain that has to be built from source. +# +# The compiling is done by the stock Arm bare-metal toolchain, which emits +# correct FDPIC objects for both C and C++. What it cannot do is *link* +# them: arm-none-eabi-ld is configured with the `armelf` emulation alone, so +# it produces an object marked "UNIX - System V" that the loader refuses. +# The FDPIC linker carries armelf_linux_fdpiceabi, and that is the whole of +# the gap. +# +# So this builds binutils and nothing else: about a minute, roughly 23 MB. +# An FDPIC GCC is not needed for any of this. +# +# Usage: build-binutils.sh [install-prefix] +# +# Then add /bin to PATH. + +set -e + +WORK="${1:?usage: build-binutils.sh [prefix]}" +PREFIX="${2:-$WORK/toolchain}" +TARGET=arm-uclinuxfdpiceabi +BINUTILS=binutils-2.43 +J="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)" + +mkdir -p "$WORK/src" "$WORK/build" + +cd "$WORK/src" +[ -d "$BINUTILS" ] || { + curl -fL -O "https://ftp.gnu.org/gnu/binutils/$BINUTILS.tar.xz" + tar xf "$BINUTILS.tar.xz" +} + +rm -rf "$WORK/build/binutils" +mkdir -p "$WORK/build/binutils" +cd "$WORK/build/binutils" + +# --with-system-zlib because the bundled copy does not compile against the +# macOS SDK headers. Harmless elsewhere. + +"$WORK/src/$BINUTILS/configure" \ + --target="$TARGET" \ + --prefix="$PREFIX" \ + --disable-nls \ + --disable-werror \ + --with-system-zlib + +make -j"$J" +make install + +echo +echo "Installed to $PREFIX/bin" +echo +"$PREFIX/bin/$TARGET-ld" -V | head -8 +echo +echo "armelf_linux_fdpiceabi in the list above is the one that matters." +echo "Add to PATH: export PATH=$PREFIX/bin:\$PATH" diff --git a/tools/fdpic/fdpic-embed.py b/tools/fdpic/fdpic-embed.py new file mode 100755 index 00000000000..a82d140218b --- /dev/null +++ b/tools/fdpic/fdpic-embed.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +############################################################################ +# tools/fdpic/fdpic-embed.py +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +# +# fdpic-embed.py -- turn a built module into a C header the firmware can carry. +# +# The demo apps have to load a module before there is any way to put files +# on the target, so they embed one and write it to the filesystem at run +# time. This generates that header. +# +# fdpic-embed.py libshape.so g_libshape > libshape_bin.h +# +# The second argument is the symbol base: the array is and its +# length is _len. + +import os +import sys + +LICENSE = """\ +/**************************************************************************** + * %s + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ +""" + + +def main(argv): + if len(argv) not in (3, 4): + sys.stderr.write("usage: fdpic-embed.py [header]\n") + return 1 + + path = argv[1] + symbol = argv[2] + + # Pass the header's own path relative to the repository root + # ("apps/testing/fs/xipfs/foo_bin.h") as the third argument when the + # result is committed: that is what nxstyle wants on line 2, and the + # module's basename -- the default -- fails the check. + + header = argv[3] if len(argv) > 3 else os.path.basename(path) + + with open(path, "rb") as f: + blob = f.read() + + out = sys.stdout + out.write(LICENSE % header) + out.write( + "\n/* Generated from %s -- do not edit.\n *\n" + " * An FDPIC module, embedded so the demo has something to " + "load without\n" + " * needing a filesystem populated from the host first.\n */\n" + % os.path.basename(path) + ) + out.write( + "\n/*****************************************************" + "***********************\n" + " * Public Data\n" + " ****************************************************" + "************************/\n\n" + ) + + # static, because this is a header that defines data. More than one app + # embeds the same module, and with external linkage the two copies + # collide at link time as soon as both are enabled. + + out.write("static const unsigned char %s[] =\n{\n" % symbol) + for i in range(0, len(blob), 12): + row = ", ".join("0x%02x" % b for b in blob[i : i + 12]) + out.write(" %s%s\n" % (row, "," if i + 12 < len(blob) else "")) + out.write("};\n\n") + out.write("static const unsigned int %s_len = %d;\n" % (symbol, len(blob))) + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tools/fdpic/fdpic-verify.sh b/tools/fdpic/fdpic-verify.sh new file mode 100755 index 00000000000..204caaf6ede --- /dev/null +++ b/tools/fdpic/fdpic-verify.sh @@ -0,0 +1,99 @@ +#!/bin/sh +############################################################################ +# tools/fdpic/fdpic-verify.sh +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +# Check that a built module is loadable before it ever reaches the target. +# +# Two things go wrong quietly: +# - the object is not actually FDPIC (wrong toolchain, or -shared omitted), +# which the loader rejects with a generic -ENOEXEC; +# - it imports a symbol the firmware does not export, which the loader +# reports as -ENOENT with no indication of which symbol. +# +# Usage: fdpic-verify.sh [exports-file] [lib.so ...] +# +# Any shared libraries the module links against are passed after the +# exports file; the symbols they define count as satisfied, exactly as they +# will at load time when the loader resolves DT_NEEDED. + +set -e +MOD="${1:?usage: fdpic-verify.sh [exports-file] [libs...]}" +EXPORTS="$2" +shift 2 2>/dev/null || shift $# +LIBS="$*" +READELF="${READELF:-arm-uclinuxfdpiceabi-readelf}" + +fail=0 + +osabi=$("$READELF" -h "$MOD" | sed -n 's/.*OS\/ABI:[[:space:]]*//p') +case "$osabi" in + *"ARM FDPIC"*) ;; + *) + echo "FAIL not an FDPIC object (OS/ABI: ${osabi:-unknown})" + echo " check the toolchain is arm-uclinuxfdpiceabi and that" + echo " the link used -Wl,-shared" + fail=1 + ;; +esac + +etype=$("$READELF" -h "$MOD" | sed -n 's/.*Type:[[:space:]]*\([A-Z]*\).*/\1/p') +[ "$etype" = "DYN" ] || { echo "FAIL e_type is $etype, expected DYN"; fail=1; } + +nload=$("$READELF" -lW "$MOD" | grep -c '^ LOAD' || true) +[ "$nload" -ge 2 ] || { + echo "FAIL expected 2 LOAD segments (RX + RW), found $nload"; fail=1; } + +# Undefined dynamic symbols are the module's imports +imports=$("$READELF" --dyn-syms -W "$MOD" \ + | awk '$7 == "UND" && $8 != "" { print $8 }' | sort -u) + +if [ -n "$EXPORTS" ] && [ -f "$EXPORTS" ]; then + tmp=$(mktemp) + avail=$(mktemp) + echo "$imports" > "$tmp" + cat "$EXPORTS" > "$avail" + + # Symbols the linked libraries define are resolved at load time + for lib in $LIBS; do + [ -f "$lib" ] || continue + "$READELF" --dyn-syms -W "$lib" \ + | awk '$7 != "UND" && $4 != "SECTION" && $8 != "" { print $8 }' \ + >> "$avail" + done + + sort -u "$avail" -o "$avail" + missing=$(comm -23 "$tmp" "$avail" || true) + rm -f "$tmp" "$avail" + if [ -n "$missing" ]; then + echo "FAIL imports the firmware does not export:" + echo "$missing" | sed 's/^/ /' + fail=1 + fi +fi + +if [ "$fail" -eq 0 ]; then + entry=$("$READELF" -h "$MOD" | sed -n 's/.*Entry point address:[[:space:]]*//p') + nimp=$(echo "$imports" | grep -c . || true) + echo "OK $(basename "$MOD"): FDPIC, entry $entry, $nimp imports resolved" +fi + +exit $fail diff --git a/tools/fdpic/nuttx-exports.sh b/tools/fdpic/nuttx-exports.sh new file mode 100755 index 00000000000..b09d91ddca4 --- /dev/null +++ b/tools/fdpic/nuttx-exports.sh @@ -0,0 +1,102 @@ +#!/bin/sh +############################################################################ +# tools/fdpic/nuttx-exports.sh +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +# Print the symbols the firmware exports to modules, one per line. +# +# A module links with -shared, so undefined symbols are permitted and the +# link succeeds even for a symbol the firmware does not provide. The +# failure then surfaces at load time as a bare -ENOENT. This list is what +# lets that be caught at build time instead. +# +# exec_symtab.c is generated by NuttX's tools/mksymtab from the libc, libm +# and syscall CSVs, and most of its entries sit behind #if defined(CONFIG_...) +# guards taken from a condition column in those files. The table NuttX +# compiles is therefore correct; it is reading the *source* that is not. +# +# So the file is run through the C preprocessor before the names are pulled +# out. Anything else gets it wrong in one direction or the other: +# +# * plain sed over the source ignores the guards and offers every symbol +# that could be exported rather than the ones that were. On one +# ordinary configuration that was 127 of 668 -- dlopen and the rest of +# dlfcn, the socket calls, alarm, the aio family. A module importing one +# linked cleanly, passed this check, and failed at load with a bare +# -ENOENT. +# +# * intersecting with nm on the linked firmware is closer but still wrong +# both ways. It offers symbols that are in the binary yet absent from +# the table (flockfile, task_testcancel), and withholds crc32, which is +# a macro: the table entry reads { "crc32", crc32full }, so the name +# resolves at load time even though no symbol called crc32 exists. +# +# What the loader matches is the name string in the table, so the +# preprocessed table is exactly the right question to ask. Membership in it +# is also sufficient: if a name is there, the link had to resolve its value, +# so the firmware has it. +# +# Usage: nuttx-exports.sh +# +# CC may be set to override the compiler used to preprocess. + +set -e +NUTTX="${1:?usage: nuttx-exports.sh }" +SYMTAB="$NUTTX/libs/libc/exec_symtab.c" + +if [ ! -f "$SYMTAB" ]; then + echo "nuttx-exports.sh: $SYMTAB not found." >&2 + echo " Build NuttX with CONFIG_EXECFUNCS_SYSTEM_SYMTAB=y first." >&2 + exit 1 +fi + +if [ -z "$CC" ]; then + for candidate in arm-none-eabi-gcc cc gcc; do + if command -v "$candidate" >/dev/null 2>&1; then + CC="$candidate" + break + fi + done +fi + +if [ -z "$CC" ]; then + echo "nuttx-exports.sh: no compiler found; set CC." >&2 + exit 1 +fi + +# The guards read CONFIG_* out of nuttx/config.h, which the configure step +# generates into the tree, so no -D flags are needed beyond the include path. + +if ! "$CC" -E -P -I "$NUTTX/include" -D__NuttX__ "$SYMTAB" \ + > "${TMPDIR:-/tmp}/nuttx-exports.$$" 2>"${TMPDIR:-/tmp}/nuttx-exports-err.$$" +then + echo "nuttx-exports.sh: failed to preprocess $SYMTAB with $CC:" >&2 + head -5 "${TMPDIR:-/tmp}/nuttx-exports-err.$$" >&2 + echo " A configured and built tree is required." >&2 + rm -f "${TMPDIR:-/tmp}/nuttx-exports.$$" \ + "${TMPDIR:-/tmp}/nuttx-exports-err.$$" + exit 1 +fi + +sed -n 's/^[[:space:]]*{[[:space:]]*"\([^"]*\)".*/\1/p' \ + "${TMPDIR:-/tmp}/nuttx-exports.$$" | sort -u + +rm -f "${TMPDIR:-/tmp}/nuttx-exports.$$" "${TMPDIR:-/tmp}/nuttx-exports-err.$$" diff --git a/tools/fdpic/nuttx-fdpic.mk b/tools/fdpic/nuttx-fdpic.mk new file mode 100644 index 00000000000..9780a866b8c --- /dev/null +++ b/tools/fdpic/nuttx-fdpic.mk @@ -0,0 +1,221 @@ +############################################################################ +# tools/fdpic/nuttx-fdpic.mk +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +############################################################################ +# tools/fdpic/nuttx-fdpic.mk +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +# nuttx-fdpic.mk -- build out-of-tree FDPIC modules for NuttX +# +# A module is an ELF shared object whose read-only segment the target maps +# straight out of flash and executes in place, and whose writable segment is +# copied to RAM once per running instance. It links against nothing: libc +# and everything else are imported from the firmware's exported symbol table +# at load time. +# +# Usage -- a whole module is this: +# +# MODULE = hello +# SRCS = hello.c +# include /path/to/nuttx/tools/fdpic/nuttx-fdpic.mk +# +# C++ sources go in CXXSRCS instead of SRCS. +# +# Two toolchains are involved, and the split is the whole trick: +# +# * the stock Arm bare-metal compiler does the compiling. It emits +# perfectly good FDPIC objects for both C and C++. +# * arm-uclinuxfdpiceabi *binutils* does the linking, because +# arm-none-eabi-ld is built with only the `armelf` emulation and cannot +# produce an FDPIC object at all -- it silently marks the output +# "UNIX - System V" and the loader refuses it. +# +# So the from-source dependency is binutils alone, which takes about a +# minute to build. No FDPIC GCC is needed. See build-binutils.sh beside +# this file, and Documentation/components/fdpic.rst. +# +# Required: +# NUTTX_DIR a configured, built NuttX tree (headers + export list) +# +# Optional: +# ARM_TOOLCHAIN prefix of the bare-metal compiler; default arm-none-eabi +# FDPIC_TOOLCHAIN prefix of the FDPIC binutils; default +# arm-uclinuxfdpiceabi +# CPU default cortex-m33 +# ENTRY entry symbol; default main, use 0 for a library +# OPT default -Os +# LIBS shared libraries to link against +# BINDNOW '-z now' by default; set empty to leave imported +# descriptors in the lazy binding table (DT_JMPREL) +# EXTRA_CFLAGS EXTRA_CXXFLAGS EXTRA_LDFLAGS +# +# EXTRA_LDFLAGS is passed straight to ld, not through a compiler driver, so +# it takes bare linker flags: `-soname libfoo.so`, not `-Wl,-soname,libfoo.so`. + +MODULE ?= module +SRCS ?= +CXXSRCS ?= +CPU ?= cortex-m33 +ENTRY ?= main +OPT ?= -Os + +ARM_TOOLCHAIN ?= arm-none-eabi +FDPIC_TOOLCHAIN ?= arm-uclinuxfdpiceabi + +FDPIC_DIR := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) + +ifeq ($(NUTTX_DIR),) + $(error Set NUTTX_DIR to a configured, built NuttX tree) +endif + +# make has built-in defaults for CC and CXX, so ?= never fires for them and +# the host compiler silently gets the job. Test the origin instead. + +ifeq ($(origin CC),default) + CC := $(ARM_TOOLCHAIN)-gcc +endif + +ifeq ($(origin CXX),default) + CXX := $(ARM_TOOLCHAIN)-g++ +endif + +LD := $(FDPIC_TOOLCHAIN)-ld +READELF := $(FDPIC_TOOLCHAIN)-readelf + +# Common compile flags. +# +# -mfdpic is stated rather than assumed, so a mis-set toolchain fails loudly +# instead of quietly producing a plain ELF the loader will refuse. +# +# -fPIC is not optional and not implied. -mfdpic alone does not turn on PIC +# under the bare-metal compiler, and without it the link emits TEXTREL -- +# text relocations -- which cannot work when the text is executed in place +# out of read-only flash. +# +# -fno-builtin keeps GCC from open-coding calls into libc routines the +# module is supposed to import from the firmware. +# +# __STDC_NO_ATOMICS__ steers NuttX's away from the branch +# that includes and then redefines its macros. The effect is +# that a module using C11 atomics gets NuttX's implementation -- the same one +# the firmware uses -- rather than the compiler's. + +MODCOMMON = -mcpu=$(CPU) -mthumb -mfdpic -fPIC $(OPT) \ + -fno-builtin -Wall -Wa,--noexecstack \ + -D__STDC_NO_ATOMICS__ -D__NuttX__ + +MODCFLAGS = $(MODCOMMON) -I$(NUTTX_DIR)/include $(EXTRA_CFLAGS) + +# C++ adds three flags, none of them optional. +# +# -fno-use-cxa-atexit, because the default registers each static object's +# destructor with __cxa_atexit(dtor, obj, &__dso_handle), and __dso_handle +# comes from crtbegin, which a module does not link. The link fails +# outright with "hidden symbol `__dso_handle' isn't defined". Turning it off +# also puts the destructors in .fini_array, which is what the loader walks on +# unload -- so the flag that makes the link work is also the flag that makes +# destructors run. +# +# -fno-exceptions -fno-rtti, because both need libsupc++, which a module +# linking against nothing cannot reach. + +MODCXXFLAGS = $(MODCOMMON) \ + -fno-exceptions -fno-rtti -fno-use-cxa-atexit \ + -I$(NUTTX_DIR)/include/cxx -I$(NUTTX_DIR)/include \ + $(EXTRA_CXXFLAGS) + +# Link flags, passed straight to ld. +# +# -shared is load bearing and easy to get wrong. It is what preserves +# R_ARM_FUNCDESC_VALUE relocations for imported symbols. Linking as a PIE +# with --unresolved-symbols=ignore-all also appears to work, but silently +# degrades every import to R_ARM_NONE, and the module then branches to zero +# on its first call out. +# +# The emulation has to be named because this ld supports four. +# +# -z now keeps imported function descriptors in DT_REL rather than the lazy +# binding table DT_JMPREL. The loader binds both, so this is a default rather +# than a requirement: it keeps built modules on the layout that has had the +# most hardware exposure, and leaves the lazymod fixture, which empties +# BINDNOW, a distinct case rather than what everything does. + +BINDNOW ?= -z now + +MODLDFLAGS = -m armelf_linux_fdpiceabi -shared $(BINDNOW) -e $(ENTRY) \ + $(EXTRA_LDFLAGS) + +OBJS := $(SRCS:.c=.o) $(CXXSRCS:.cpp=.o) +TARGET := $(MODULE).fdpic +EXPORTS := .nuttx-exports + +.PHONY: all clean verify exports + +all: verify + +$(EXPORTS): $(NUTTX_DIR)/libs/libc/exec_symtab.c + @$(FDPIC_DIR)/nuttx-exports.sh $(NUTTX_DIR) > $@ + +exports: $(EXPORTS) + @echo "$(shell wc -l < $(EXPORTS)) symbols exported by the firmware" + +%.o: %.c + @echo " CC $<" + @$(CC) $(MODCFLAGS) -c $< -o $@ + +%.o: %.cpp + @echo " CXX $<" + @$(CXX) $(MODCXXFLAGS) -c $< -o $@ + +$(TARGET): $(OBJS) + @echo " LD $@" + @$(LD) $(MODLDFLAGS) -o $@ $(OBJS) $(LIBS) + +# Verification is part of the default build on purpose. A module that +# imports a symbol the firmware does not export links perfectly happily and +# only fails once it is on the target, as a bare -ENOENT with no indication +# of which symbol was at fault. + +verify: $(TARGET) $(EXPORTS) + @READELF=$(READELF) $(FDPIC_DIR)/fdpic-verify.sh \ + $(TARGET) $(EXPORTS) $(LIBS) + +clean: + @rm -f $(OBJS) $(TARGET) $(MODULE).so $(EXPORTS)