This commit is contained in:
Marco Casaroli 2026-08-01 10:29:50 +00:00 committed by GitHub
commit 65c9105583
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 3585 additions and 8 deletions

View file

@ -47,6 +47,7 @@ ignore-words-list =
mot,
mis,
nexted,
nneeded,
numer,
nwe,
oen,

View file

@ -0,0 +1,81 @@
================================================
``fdpicxip`` FDPIC Executed In Place from XIPFS
================================================
Writes FDPIC modules into a :doc:`XIPFS </components/filesystem/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 </applications/testing/xipfs/index>`.
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``.

View file

@ -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 </components/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
=============

View file

@ -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 <filesystem/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`.

View file

@ -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

View file

@ -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
------------

View file

@ -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 */

View file

@ -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 */

View file

@ -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. */

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -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);
}

1546
binfmt/fdpic.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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 */

View file

@ -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 <nuttx/config.h>
#include <sys/types.h>
#include <elf.h>
#include <stdint.h>
#include <nuttx/fs/fs.h>
#include <nuttx/sched.h>
/****************************************************************************
* 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 */

View file

@ -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

View file

@ -31,6 +31,10 @@
#include <errno.h>
#include <stdlib.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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);

View file

@ -30,6 +30,10 @@
#include <nuttx/pthread.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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);
}

View file

@ -33,6 +33,10 @@
#include <nuttx/mutex.h>
#include <nuttx/debug.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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);

View file

@ -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;

View file

@ -37,6 +37,10 @@
****************************************************************************/
#include <stdlib.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#endif
#include <assert.h>
/****************************************************************************
@ -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;

View file

@ -45,6 +45,10 @@
#include <sys/param.h>
#include <stdlib.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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);
}

View file

@ -34,6 +34,10 @@
#include <nuttx/irq.h>
#include <nuttx/sched.h>
#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD)
# include <nuttx/binfmt/fdpic.h>
#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
}
}

View file

@ -38,6 +38,10 @@
#include <nuttx/signal.h>
#include <nuttx/spinlock.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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.

View file

@ -34,6 +34,10 @@
#include <nuttx/signal.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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,

View file

@ -37,6 +37,10 @@
#include <nuttx/kthread.h>
#include <nuttx/fs/fs.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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);

View file

@ -38,6 +38,10 @@
#include <nuttx/kthread.h>
#include <nuttx/spawn.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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);

View file

@ -37,6 +37,10 @@
#include <nuttx/kmalloc.h>
#include <nuttx/spinlock.h>
#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD)
# include <nuttx/binfmt/fdpic.h>
#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
{

61
tools/fdpic/README.md Normal file
View file

@ -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.

80
tools/fdpic/build-binutils.sh Executable file
View file

@ -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 <workdir> [install-prefix]
#
# Then add <install-prefix>/bin to PATH.
set -e
WORK="${1:?usage: build-binutils.sh <workdir> [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"

114
tools/fdpic/fdpic-embed.py Executable file
View file

@ -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 <base> and its
# length is <base>_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 <module> <symbol> [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))

99
tools/fdpic/fdpic-verify.sh Executable file
View file

@ -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 <module.fdpic> [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 <module> [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

102
tools/fdpic/nuttx-exports.sh Executable file
View file

@ -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 <nuttx-dir>
#
# CC may be set to override the compiler used to preprocess.
set -e
NUTTX="${1:?usage: nuttx-exports.sh <nuttx-dir>}"
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.$$"

221
tools/fdpic/nuttx-fdpic.mk Normal file
View file

@ -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 <nuttx/atomic.h> away from the branch
# that includes <stdatomic.h> 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)