diff --git a/Documentation/components/fdpic.rst b/Documentation/components/fdpic.rst new file mode 100644 index 0000000000000..ed212d03704f5 --- /dev/null +++ b/Documentation/components/fdpic.rst @@ -0,0 +1,406 @@ +.. _fdpic: + +============= +FDPIC Modules +============= + +Overview +======== + +An FDPIC module is an ELF shared object whose read-only and writable +segments are placed independently of one another. NuttX uses the +read-only segment where it already lies on the media and never copies +it; only the writable segment is copied to RAM, once per running +instance. A module's code and ``.rodata`` therefore cost no RAM at all, +and several instances of one module share them. + +FDPIC is not a separate binary format and has no loader of its own. An +object announces itself in its OS/ABI byte, +``e_ident[EI_OSABI] == ELFOSABI_ARM_FDPIC`` (65), which ``readelf -h`` +reports as *OS/ABI: ARM FDPIC*, and the ELF loader takes it from there. +Everything else -- ``exec()``, ``posix_spawn()``, ``dlopen()``, the +symbol table -- is the ordinary ELF path. + +What FDPIC adds over the position independent ELF support already in the +tree is a function pointer that carries its own data base. That is what +lets a module be called back on a thread it did not create, and what +lets a module and the libraries it uses hold distinct data bases at the +same time. + +Function descriptors +-------------------- + +Code reaches its own data through a base register -- **r9** on ARM -- +holding the address of that object's GOT. Because code and data are +placed independently, a bare code address is not enough to call a +function: the callee needs its data base too. FDPIC therefore +represents a function pointer as a two word *descriptor*: + +=========== ============================================================== +Word Contents +=========== ============================================================== +``entry`` Code address, including its Thumb bit +``got`` Data base to install in the PIC base register before + branching +=========== ============================================================== + +Building those descriptors is most of what relocation does. Because +each one names its own base, a pointer handed to the base firmware +carries everything needed to call back into the module later, from any +thread. + +A module links against nothing. libc and everything else are undefined +imports, resolved at load time against the globally registered symbols +first, then any shared libraries the module names, then the symbol table +``exec()`` supplied. + +Placement +--------- + +The loader asks the filesystem where the file lies on its media. Two +mechanisms exist and they are not interchangeable: + +* ``XIPFSIOC_PIN`` is for a filesystem that can move a file's blocks. It + returns an address together with a pin that holds the extent still, and + the pin is given back with ``XIPFSIOC_UNPIN`` when the module is + unloaded. :doc:`XIPFS ` is the one in tree. + +* ``FIOC_XIPBASE`` is for a filesystem whose layout never changes, which + has nothing to hold and answers with a bare address. ROMFS and TMPFS + are those. + +The pin is asked for first, because a filesystem that needs one cannot +safely be used without it. A module whose text cannot be reached this +way is refused rather than copied to RAM: loading it anyway would +silently cost the memory the mechanism exists to save. + +The writable segment is allocated and copied per instance, and a pool of +function descriptors is reserved behind it for the relocations that ask +the loader to manufacture one. When the task starts, +``up_initial_state()`` installs the object's data base -- ``DT_PLTGOT``, +or the GOT immediately after ``PT_DYNAMIC`` in an object with no +imports -- into the PIC base register. + +Shared libraries +---------------- + +A module may name shared libraries in ``DT_NEEDED``. Each is loaded +during relocation by calling ``dlopen()`` on the name, and the module's +undefined symbols are then bound against that library's exports. This +requires ``CONFIG_LIBC_DLFCN``; without it, a module carrying +``DT_NEEDED`` is refused, because there is no way to bring in what it +asks for. ``CONFIG_LIBC_ELF_MAXNEEDED`` caps how many one module may +name. + +Because ``dlopen()`` does the work, libraries are found the way it finds +them: an absolute path is used as given, and a bare name is searched for +along ``LD_LIBRARY_PATH``, which needs ``CONFIG_LIBC_ENVPATH`` and is +seeded from ``CONFIG_LDPATH_INITIAL``. + +A library lands in the module registry, which holds one instance per +name, so its data is shared by everything that opens it. A module +started with ``exec()`` is different: that path loads a fresh copy each +time, so two running instances of one module have separate data while +sharing one copy of the text in flash. + +Comparison with NXFLAT and PIC ELF +================================== + +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 PIC ELF FDPIC +========================= ============== ============== ============= +Format NuttX only ELF ELF +Extra build tools yes none assembler and + linker +Data base per task task object +Shared libraries no no yes +Foreign-thread callback no no yes +Instruction set ARM, Thumb-2 unrestricted Thumb-2 only +========================= ============== ============== ============= + +:ref:`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. + +**PIC ELF** needs no extra tools. With ``CONFIG_PIC`` the ELF loader +allocates the writable sections separately and, when the filesystem +answers ``FIOC_XIPBASE``, leaves the read-only ones on the media. Two +limits follow from having one base register per task: a shared object is +loaded as a single allocation, because the distance between its text and +its data is compiled into it, and the data base is installed once per +task, so every object in a task shares one. + +**FDPIC** pays for its descriptors with an ``arm-uclinuxfdpiceabi`` +assembler and linker, and gets back the two things a single register +cannot express. A task or pthread that a module starts inherits the +module's D-Space, so a register would be enough there; a work queue +worker was created at boot and carries no module base, and a descriptor +supplies one, which is how ``SIGEV_THREAD`` notifications reach module +code. + +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 +========================= ========================== ===== + +RISC-V has no FDPIC ABI -- the psABI addendum is an unmerged proposal and +no ``EI_OSABI`` value is assigned -- so a RISC-V target cannot use this. + +**Flash that is memory mapped and executable**, exposed by a filesystem +that answers ``XIPFSIOC_PIN`` or ``FIOC_XIPBASE``. + +**An FDPIC assembler and linker.** A stock ``arm-none-eabi`` GCC +compiles correct FDPIC code for both C and C++, but the assembler has to +be in FDPIC mode to accept the relocations that code produces, and only +``arm-uclinuxfdpiceabi`` binutils carry the ``armelf_linux_fdpiceabi`` +emulation the link needs. ``arm-none-eabi-ld``, rather than failing, +marks its output *UNIX - System V*, which the loader will not treat as +FDPIC. + +No distribution packages that target, so build binutils for it -- which +takes about a minute and needs nothing else:: + + configure --target=arm-uclinuxfdpiceabi --prefix=$HOME/fdpic \ + --disable-nls --disable-werror + make && make install + export PATH=$HOME/fdpic/bin:$PATH + +An FDPIC GCC is not needed. + +**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 data base in r9 only if the compiler was never +free to allocate that register elsewhere. ``CONFIG_FDPIC`` selects +``CONFIG_PIC``, under which ``arch/arm/src/common/Toolchain.defs`` adds +``--fixed-r9``; see :ref:`nxflat` for why it goes into ``ARCHCFLAGS`` +rather than ``CFLAGS`` and how to check that it arrived. + +Configuration +============= + +``CONFIG_FDPIC`` lives under ``CONFIG_ELF``. A working configuration +also needs a symbol table for modules to import from and a filesystem +that can expose its media:: + + CONFIG_ELF=y + CONFIG_FDPIC=y + CONFIG_LIBC_EXECFUNCS=y + CONFIG_EXECFUNCS_HAVE_SYMTAB=y + CONFIG_EXECFUNCS_SYSTEM_SYMTAB=y + CONFIG_FS_XIPFS=y + +Shared libraries need three more, the last two so that a library can be +named rather than spelled out as an absolute path:: + + CONFIG_LIBC_DLFCN=y + CONFIG_LIBC_ENVPATH=y + CONFIG_LDPATH_INITIAL="/mnt/xipfs" + +``CONFIG_ELF_STACKSIZE`` gives the stack a module runs with. A module +that needs a different one can export an ``nx_stacksize`` symbol, which +the loader prefers when present. + +Building a module +================= + +Three steps: compile to assembly with the stock compiler, assemble with +the FDPIC assembler, link with the FDPIC linker:: + + arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -mfdpic -fPIC -Os \ + -fno-builtin -D__NuttX__ -I$NUTTX/include -S mod.c -o mod.s + + arm-uclinuxfdpiceabi-as --fdpic -mthumb -mcpu=cortex-m3 \ + mod.s -o mod.o + + arm-uclinuxfdpiceabi-ld -m armelf_linux_fdpiceabi -shared -z now \ + -e main -o mod.fdpic mod.o + +The detour through assembly is what makes this work with any toolchain. +Whether ``arm-none-eabi-gcc -c`` can assemble FDPIC code itself depends +on the release: newer ones pass ``--fdpic`` down to the assembler, older +ones do not and fail with *"Relocation supported only in FDPIC mode"*. +Assembling separately never depends on that. Note that the assembler's +option is ``--fdpic``, not ``-mfdpic``. + +Five flags carry weight: + +* ``-mfdpic`` is stated rather than assumed, so a mis-set toolchain fails + loudly instead of producing a plain ELF the loader will not recognize. + +* ``-fPIC`` is not implied by ``-mfdpic`` on a 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 several + emulations and will not guess. + +* ``-e main`` names the entry point. There is no ``crt0``; the module is + entered directly. + +A module links with ``-shared``, so importing something the firmware does +not export links cleanly and fails only on the target. Checking the +module's undefined symbols against the generated +``libs/libc/exec_symtab.c`` is worth doing as part of the module build. + +Building a shared library +------------------------- + +A library is built the same way, with a soname and no entry point, and +the module names it on its link line:: + + arm-uclinuxfdpiceabi-ld -m armelf_linux_fdpiceabi -shared -z now \ + -e 0 -soname libfoo.so -o libfoo.so libfoo.o + + arm-uclinuxfdpiceabi-ld -m armelf_linux_fdpiceabi -shared -z now \ + -e main -o mod.fdpic mod.o libfoo.so + +At run time the library must be reachable under its soname along +``LD_LIBRARY_PATH``. + +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 +would jump into RAM data, so an entry point that accepts a callback from +a module has to resolve the descriptor first. ``CONFIG_FDPIC`` makes +these do so: + +``qsort``, ``bsearch``, ``pthread_create``, ``signal``/``sigaction``, +``task_create``/``task_create_with_stack``, ``task_spawn``, +``pthread_once``, ``scandir``, and ``mq_notify``/``timer_create`` with +``SIGEV_THREAD``. + +Whether a pointer is a descriptor is decided by reading the PIC base +register: a module's task runs with its data base there, a firmware task +with zero, so a kernel caller is unaffected. + +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 + public entry 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 that carries + no module base, so resolving the entry is not enough. Capture the base + at registration with ``fdpic_base()``, in the module's own 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. + +Limitations +=========== + +**Constructors run on the loading task, not the module's own.** +``DT_INIT_ARRAY`` runs at the end of the load and ``DT_FINI_ARRAY`` at +unload, which needs ``CONFIG_BINFMT_CONSTRUCTORS``. Both are entered +through ``fdpic_invoke()`` with the object's own data base, so a global +object reaches its own storage; but the task they run on is whichever one +called the loader, so a constructor that reads task-local state -- its own +pid, its environment -- sees that task's, not the one that will run +``main()``. + +A library named in ``DT_NEEDED`` is constructed before the module that +needs it, because the module's own relocation is what opens it, and +destroyed after, at the last ``dlclose()``. Since the library is one +instance, its constructors run once however many modules name it. + +Reference +========= + +Object layout +------------- + +A linked module already has the layout execute in place needs, with no +linker script:: + + LOAD vaddr 0x00000000 R E .text .rodata .hash .dynsym .dynstr + LOAD vaddr 0x00001244 RW .dynamic .got .data .bss + DYNAMIC DT_PLTGOT -> .got + +``.rodata`` lands in the read-only segment on its own, reached PC +relative or GOT indirect. That matters: in the writable segment it would +be copied to RAM with ``.data``, and most of the saving would evaporate +silently, with everything still working. + +The FDPIC marker is the OS/ABI byte alone. ``e_flags`` reads as an +ordinary ``0x5000000, Version5 EABI``. + +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_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. This + is what a *call* to an imported function produces. When the symbol + resolves to a function in another FDPIC object, both words are copied + from that object's own descriptor, so the callee runs with its own data + base; otherwise the entry is the resolved address and the base is this + object's. + +``R_ARM_FUNCDESC`` + A pointer to a descriptor that does not exist yet, which the loader + manufactures from the pool behind the writable segment. This is what + *taking the address* of a function produces -- a different thing from + calling one, and both can appear for the same symbol. + +Constants, from binutils ``include/elf/arm.h`` and mirrored in +``arch/arm/include/elf.h``: ``R_ARM_GOTFUNCDESC`` 161, +``R_ARM_GOTOFFFUNCDESC`` 162, ``R_ARM_FUNCDESC`` 163, +``R_ARM_FUNCDESC_VALUE`` 164. + +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``. Both are bound eagerly, so either link works, but the +two are not walked identically. In ``DT_REL`` the word being overwritten +is the addend and is added to the resolved value; in ``DT_JMPREL`` it is +the lazy binding bootstrap and the descriptor is overwritten outright. + +``.rofixup`` is skipped. It is the self-relocation list a *static* +executable's ``crt0`` walks to find its own GOT; a module has no +``crt0``, and the loader supplies the data base instead. + +Exported functions +------------------ + +A function exported by a module or library is published to ``dlsym()`` +as a descriptor rather than a code address, taken from the same pool, so +that an FDPIC caller can branch through what it gets back. diff --git a/Documentation/components/index.rst b/Documentation/components/index.rst index fe60af7b9851c..7b041920dced3 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 8269cc7788c05..6c4045f7ada64 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,21 @@ 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 and the +``fdpicxip`` demo, so the ``fdpic`` and ``reject`` sections of the XIPFS +test suite have something to run. The demo carries its modules as +committed byte arrays, so nothing beyond the ordinary ARM toolchain is +needed to build it; rebuilding those from source needs +``arm-uclinuxfdpiceabi`` binutils. See :doc:`/components/fdpic`. + +``CONFIG_ELF_STACKSIZE`` is 4096 here rather than the 2048 the rest of +the board's tasks use. A module that calls into the firmware's printf +family overflows 2048, and with no MPU that is a lockup rather than a +diagnostic. + xipfs-nxflat ------------ diff --git a/arch/arm/include/elf.h b/arch/arm/include/elf.h index e5c2780472c18..1975454677b7b 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. */ @@ -247,10 +265,72 @@ #define DT_ARM_PREEMPTMAP 0x70000002 #define DT_ARM_RESERVED2 0x70000003 +/* Loader state the FDPIC relocations need but a relocation cannot carry: + * the object's data base, and a descriptor pool cursor that has to survive + * from one relocation to the next. + */ + +/* The relocations that only an FDPIC object may use. Seeing one in an + * object whose OS/ABI byte does not say FDPIC means the marker was lost. + */ + +#define ARCH_ELF_RELOC_ISFDPIC(t) \ + ((t) == R_ARM_FUNCDESC || (t) == R_ARM_FUNCDESC_VALUE) + +#define ARCH_ELFDATA 1 + +#define ARCH_ELFDATA_INIT(d, l) \ + do \ + { \ + (d)->fdpic = (l)->fdpic; \ + (d)->gotaddr = (l)->gotaddr; \ + (d)->descpool = (l)->descpool; \ + (d)->ndesc = (l)->ndesc; \ + (d)->usedesc = (l)->usedesc; \ + } \ + while (0) + +#define ARCH_ELFDATA_FINI(d, l) \ + do \ + { \ + (l)->usedesc = (d)->usedesc; \ + } \ + while (0) + /**************************************************************************** * Public Types ****************************************************************************/ +#ifndef __ASSEMBLY__ + +/* An FDPIC function pointer: the code, plus the data base to enter it + * with. + */ + +struct arm_fdpic_desc_s +{ + uintptr_t entry; /* Address of the code */ + uintptr_t got; /* Data base to install before branching */ +}; + +struct arch_elfdata_s +{ + uint8_t fdpic; /* The object is an FDPIC one */ + uintptr_t gotaddr; /* DT_PLTGOT: this object's data base */ + uintptr_t descpool; /* Base of the descriptor pool */ + uint16_t ndesc; /* Capacity, in descriptors */ + uint16_t usedesc; /* Next free slot */ + uint8_t symisdesc; /* Symbol value is a descriptor, not code */ + uint8_t pltrel; /* Relocation comes from DT_JMPREL, so the word + * it overwrites is a lazy binding stub and not + * an addend + */ +}; + +typedef struct arch_elfdata_s arch_elfdata_t; + +#endif /* __ASSEMBLY__ */ + typedef struct __EIT_entry { unsigned long fnoffset; diff --git a/binfmt/Kconfig b/binfmt/Kconfig index 93844898da0d4..810fbf648a3bd 100644 --- a/binfmt/Kconfig +++ b/binfmt/Kconfig @@ -60,6 +60,36 @@ config ELF_STACKSIZE default DEFAULT_TASK_STACKSIZE ---help--- This is the default stack size that will be used when starting ELF binaries. + +config FDPIC + bool "FDPIC modules" + default n + select PIC + depends on ARCH_ARMV7M || ARCH_ARMV8M + depends on BUILD_FLAT + ---help--- + Load ELF modules built for the FDPIC ABI. + + An FDPIC module places its read-only and writable segments + independently, so its text can be executed directly out of flash + while only the writable segment is copied to RAM, once per running + instance. This needs a filesystem that can expose its media, such + as XIPFS or ROMFS, and an arm-uclinuxfdpiceabi linker to build the + modules; the stock arm-none-eabi compiler emits correct FDPIC + objects for both C and C++, so only the link needs it. + + What this adds over the position independent ELF support already + present is a function pointer that carries its own data base, as a + two word descriptor rather than a bare code address. That is what + lets a module be called back on a thread it did not create, such as + the work queue worker that runs a SIGEV_THREAD notification. + + Selecting this makes ten libc and sched entry points that can + accept a callback from a module resolve such a descriptor before + storing or branching to it. Each costs a register read and a + branch on a path that is not hot. + + FDPIC is specified only for ARM Thumb-2. endif endif diff --git a/binfmt/elf.c b/binfmt/elf.c index fc89630053806..e3d0b6ec4d81a 100644 --- a/binfmt/elf.c +++ b/binfmt/elf.c @@ -35,6 +35,7 @@ #include #include +#include #include #include "binfmt.h" @@ -98,6 +99,10 @@ static int elf_loadbinary(FAR struct binary_s *binp, int nexports) { struct mod_loadinfo_s loadinfo; +#ifdef CONFIG_BINFMT_CONSTRUCTORS + FAR void (**array)(void); + int i; +#endif Elf_Sym sym; int ret; @@ -252,6 +257,8 @@ static int elf_loadbinary(FAR struct binary_s *binp, binp->mod.textalloc = (FAR void *)loadinfo.textalloc; binp->mod.dataalloc = (FAR void *)loadinfo.datastart; + binp->mod.fdpic = loadinfo.fdpic; + binp->mod.gotaddr = loadinfo.gotaddr; # ifdef CONFIG_BINFMT_CONSTRUCTORS binp->mod.initarr = loadinfo.initarr; binp->mod.finiarr = loadinfo.finiarr; @@ -270,7 +277,7 @@ static int elf_loadbinary(FAR struct binary_s *binp, libelf_dumpentrypt(&loadinfo); #ifdef CONFIG_PIC - if (loadinfo.gotindex >= 0) + if (loadinfo.gotindex >= 0 || loadinfo.fdpic) { FAR struct dspace_s *dspaces = kmm_zalloc(sizeof(struct dspace_s)); @@ -280,12 +287,75 @@ static int elf_loadbinary(FAR struct binary_s *binp, goto errout_with_load; } - dspaces->region = (FAR void *)loadinfo.shdr[loadinfo.gotindex].sh_addr; + /* An FDPIC object names its data base in DT_PLTGOT, which the + * loader has already translated; everything else has it as the + * address of the .got section. The two are the same idea reached + * by different routes, and both are what up_initial_state() puts + * in the PIC base register when the task starts. + */ + + if (loadinfo.fdpic) + { + dspaces->region = (FAR void *)loadinfo.gotaddr; + } + else + { + dspaces->region = + (FAR void *)loadinfo.shdr[loadinfo.gotindex].sh_addr; + } + dspaces->crefs = 1; binp->picbase = (FAR void *)dspaces; } #endif +#ifdef CONFIG_BINFMT_CONSTRUCTORS + /* Run the constructors. This is the last thing the load does, so a + * global is initialized by the time the module's main() can see it, and + * nothing that could still fail runs after a constructor has. + * + * They run here rather than on the spawned task because there is no hook + * to enter it with, and because it is where libelf_insert() runs them for + * a module that arrives through dlopen(). An FDPIC object's reach its + * globals through its own data base, which this task does not carry. + */ + + array = (FAR void (**)(void))loadinfo.preiarr; + for (i = 0; i < loadinfo.nprei; i++) + { + if (loadinfo.fdpic) + { + fdpic_invoke((uintptr_t)array[i], 0, loadinfo.gotaddr); + } + else + { + array[i](); + } + } + + array = (FAR void (**)(void))loadinfo.initarr; + for (i = 0; i < loadinfo.ninit; i++) + { + if (loadinfo.fdpic) + { + fdpic_invoke((uintptr_t)array[i], 0, loadinfo.gotaddr); + } + else + { + array[i](); + } + } +#endif + +#ifdef HAVE_LIBC_ELF_PIN + /* Past the last thing that can fail, so the module owns the pin now: it + * is given back when the task that runs the module exits. + */ + + binp->mod.pinfile = loadinfo.pinfile; + loadinfo.pinfile = NULL; +#endif + libelf_uninitialize(&loadinfo); return OK; 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 0000000000000..c01854b600c02 --- /dev/null +++ b/boards/arm/rp23xx/pimoroni-pico-2-plus/configs/xipfs-fdpic/defconfig @@ -0,0 +1,67 @@ +# +# 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_BINFMT_CONSTRUCTORS=y +CONFIG_BOARDCTL_RESET=y +CONFIG_BOARD_LOOPSPERMSEC=10450 +CONFIG_BUILTIN=y +CONFIG_DEBUG_FULLOPT=y +CONFIG_DEBUG_SYMBOLS=y +CONFIG_DEFAULT_TASK_STACKSIZE=4096 +CONFIG_ELF=y +CONFIG_ELF_STACKSIZE=4096 +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_HAVE_CXX=y +CONFIG_INIT_ENTRYPOINT="nsh_main" +CONFIG_INIT_STACKSIZE=16384 +CONFIG_LDPATH_INITIAL="/mnt/xipfs" +CONFIG_LIBC_DLFCN=y +CONFIG_LIBC_ELF_HAVE_SYMTAB=y +CONFIG_LIBC_ENVPATH=y +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 a3d6dc8f927fa..fdecf44c075c4 100644 --- a/include/elf.h +++ b/include/elf.h @@ -149,6 +149,7 @@ #define ELFOSABI_MODESTO 11 /* Novell Modesto. */ #define ELFOSABI_OPENBSD 12 /* OpenBSD. */ #define ELFOSABI_ARM_AEABI 64 /* ARM EABI */ +#define ELFOSABI_ARM_FDPIC 65 /* ARM FDPIC */ #define ELFOSABI_ARM 97 /* ARM */ #define ELFOSABI_STANDALONE 255 /* Standalone (embedded) application */ @@ -278,6 +279,12 @@ #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_PREINIT_ARRAY 32 /* d_un=d_ptr */ +#define DT_PREINIT_ARRAYSZ 33 /* d_un=d_val */ #define DT_LOPROC 0x70000000 /* d_un=unspecified */ #define DT_HIPROC 0x7fffffff /* d_un= unspecified */ diff --git a/include/nuttx/fdpic.h b/include/nuttx/fdpic.h new file mode 100644 index 0000000000000..6babd3516e95e --- /dev/null +++ b/include/nuttx/fdpic.h @@ -0,0 +1,178 @@ +/**************************************************************************** + * include/nuttx/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_FDPIC_H +#define __INCLUDE_NUTTX_FDPIC_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include + +#include +#include + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +/* A function descriptor: what a function pointer is under FDPIC. + * + * The base firmware is not built FDPIC, so to it a function pointer is a + * code address and it simply branches there. An FDPIC module passes the + * address of one of these instead, because its code and data are placed + * independently and a bare code address would leave the callee unable to + * find its own data. + */ + +struct fdpic_desc_s +{ + uintptr_t entry; /* Address of the code */ + uintptr_t got; /* Data base to install before branching */ +}; + +/**************************************************************************** + * Inline Functions + ****************************************************************************/ + +#ifdef CONFIG_FDPIC + +/**************************************************************************** + * Name: fdpic_base + * + * Description: + * The data base of the calling context, read from the PIC base register. + * Non-zero means the caller is an FDPIC module; zero means base firmware, + * because up_initial_state() only installs a value for a task that has a + * D-Space. + * + * This is what lets a shared entry point tell the two apart without being + * told, so that a plain kernel task calling qsort() is unaffected. + * + ****************************************************************************/ + +static inline uintptr_t fdpic_base(void) +{ + uintptr_t base; + + up_getpicbase(&base); + return base; +} + +/**************************************************************************** + * Name: fdpic_callback + * + * Description: + * Resolve a function pointer that arrived from a caller which may be an + * FDPIC module. + * + * 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 own base survives the call in, and + * any task the module creates inherits its D-Space. + * + * Input Parameters: + * fn - The pointer as it was received. + * + * Returned Value: + * An address that can be branched to directly. + * + ****************************************************************************/ + +static inline FAR void *fdpic_callback(FAR void *fn) +{ + if (fn != NULL && fdpic_base() != 0) + { + return (FAR void *)((FAR struct fdpic_desc_s *)fn)->entry; + } + + return fn; +} + +/**************************************************************************** + * Name: fdpic_invoke + * + * Description: + * Call a resolved module entry point with the module's data base in the + * PIC base register, and restore the caller's afterwards. + * + * This is for the one case where the register cannot already be right: a + * callback a module registered that runs on a shared thread -- the + * signal-notification work queue -- which carries no module's base. The + * base is captured at registration time, in the module's own context, and + * installed here around the call. Everywhere else the callback runs on a + * task that inherited the module's D-Space and fdpic_callback() suffices. + * + * A context switch or interrupt during the call is safe: the register is + * REG_PIC in the saved context, so it is preserved across a switch, and + * the base firmware is built with it reserved so no 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. + * + ****************************************************************************/ + +static inline void fdpic_invoke(uintptr_t entry, uintptr_t arg, + uintptr_t got) +{ + register uintptr_t r0v __asm__ ("r0") = arg; + + /* up_setpicbase() cannot serve here. The register has to hold the + * module's base for the duration of one call and then go back, and + * nothing in C tells the compiler the register is live across that call, + * so save, install, branch and restore have to be one sequence. + * + * arg is pinned in r0, the first argument and the call's scratch, so the + * asm needs registers only for entry and got -- deliberately few, so the + * allocator has room on builds that reserve a frame pointer. The PIC + * register is saved on the stack rather than in a scratch register; r4 + * rides along only to keep the push 8-byte aligned and comes back + * untouched. + */ + + __asm__ __volatile__ + ( + "push {r4, " PIC_REG_STRING "}\n" /* Save the caller's base */ + "mov " PIC_REG_STRING ", %[got]\n" /* Install the module's base */ + "blx %[entry]\n" /* Enter the module */ + "pop {r4, " PIC_REG_STRING "}\n" /* Restore the caller's base */ + : "+r" (r0v) + : [entry] "r" (entry), [got] "r" (got) + : "r1", "r2", "r3", "r12", "lr", "cc", "memory" + ); +} + +#else + +# define fdpic_base() (0) +# define fdpic_callback(fn) (fn) +# define fdpic_invoke(entry, arg, got) \ + ((void)(got), (((CODE void (*)(uintptr_t))(uintptr_t)(entry))(arg))) + +#endif /* CONFIG_FDPIC */ + +#endif /* __INCLUDE_NUTTX_FDPIC_H */ diff --git a/include/nuttx/lib/elf.h b/include/nuttx/lib/elf.h index bcb8a039c8090..317bce0e144c9 100644 --- a/include/nuttx/lib/elf.h +++ b/include/nuttx/lib/elf.h @@ -44,6 +44,21 @@ # define CONFIG_LIBC_ELF_MAXDEPEND 0 #endif +#ifndef CONFIG_LIBC_ELF_MAXNEEDED +# define CONFIG_LIBC_ELF_MAXNEEDED 0 +#endif + +/* Holding an XIP pin past the load means holding the file itself: the pin is + * released when the module is unloaded, which happens on a task other than + * the one that loaded it, so a descriptor from that task's group cannot + * serve. That needs the file interface, which is why CONFIG_FDPIC depends + * on the flat build. + */ + +#ifdef CONFIG_FDPIC +# define HAVE_LIBC_ELF_PIN 1 +#endif + #ifndef CONFIG_LIBC_ELF_ALIGN_LOG2 # define CONFIG_LIBC_ELF_ALIGN_LOG2 2 #endif @@ -123,6 +138,7 @@ typedef CODE int (*mod_uninitializer_t)(FAR void *arg); * nexports - The number of symbols in the exported symbol table. */ +struct file; struct symtab_s; struct mod_info_s { @@ -174,6 +190,18 @@ struct module_s uint16_t nsect; /* Number of entries in sectalloc array */ #endif int dynamic; /* Module is a dynamic shared object */ + bool fdpic; /* Module is an FDPIC object: its two + * segments were placed separately and + * the text is media, not an allocation + */ + uintptr_t gotaddr; /* An FDPIC object's data base, to + * enter its destructors with + */ +#ifdef HAVE_LIBC_ELF_PIN + FAR struct file *pinfile; /* Holds the XIP pin on the text until + * the module is unloaded + */ +#endif #if defined(CONFIG_FS_PROCFS) && !defined(CONFIG_FS_PROCFS_EXCLUDE_MODULE) size_t textsize; /* Size of the kernel .text memory allocation */ size_t datasize; /* Size of the kernel .bss/.data memory allocation */ @@ -185,6 +213,16 @@ struct module_s * module goes when the last does */ +#ifdef CONFIG_LIBC_DLFCN + /* Libraries opened with dlopen() for this module's DT_NEEDED entries. + * These are references this module holds on others, where nopen above + * counts the references others hold on this one. + */ + + FAR void *libs[CONFIG_LIBC_ELF_MAXNEEDED]; + uint8_t nlibs; +#endif + #if CONFIG_LIBC_ELF_MAXDEPEND > 0 uint8_t dependents; /* Number of modules that depend on this module */ @@ -252,6 +290,47 @@ struct mod_loadinfo_s * skip the copy. */ + /* FDPIC state. + * + * An FDPIC object places its two PT_LOAD segments independently: the + * read-only one is mapped where it already sits on the media and the + * writable one is copied to RAM, once per running instance. That is + * what lets several instances share one copy of the text. + * + * fdpic - True if e_ident[EI_OSABI] marked this an FDPIC object. + * textpin - True if the read-only segment is held by a filesystem pin + * that has to be dropped at unload, rather than by an + * address the filesystem simply handed over. + */ + + bool fdpic; + bool textpin; + +#ifdef HAVE_LIBC_ELF_PIN + /* The file the pin is held through, handed to the module once it loads. */ + + FAR struct file *pinfile; +#endif + + /* Where the object's data base lives, from DT_PLTGOT. An FDPIC module + * runs with this in the PIC base register, and it is the base every + * function descriptor built for the module names. + */ + + uintptr_t gotaddr; + + /* Pool of function descriptors, carved out behind the writable segment. + * + * R_ARM_FUNCDESC asks the loader to manufacture a descriptor and hand + * back its address, so the space has to be reserved when the segment is + * sized, before any relocation is applied. Sized from the relocation + * count, which bounds how many can be asked for. + */ + + uintptr_t descpool; + uint16_t ndesc; /* Capacity */ + uint16_t usedesc; /* Next free slot */ + /* Address environment. * * addrenv - This is the handle created by addrenv_allocate() that can be @@ -743,7 +822,11 @@ FAR const void *libelf_getsymbol(FAR void *handle, FAR const char *name); * Name: libelf_uninit * * Description: - * Uninitialize module resources. + * Uninitialize module resources. This gives up everything the module + * holds, including any libraries it opened for its DT_NEEDED entries, so + * the caller must be releasing the last reference to it: libelf_remove() + * calls this only once nopen reaches zero, and the copy binfmt keeps + * belongs to a single exec'd binary and is never shared. * ****************************************************************************/ diff --git a/include/nuttx/signal.h b/include/nuttx/signal.h index 79fa22b39d501..46b3a0e703f7e 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 c734ff1d243cb..5557ade6fdf8f 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/elf/Kconfig b/libs/libc/elf/Kconfig index 2fd36399d96b4..fafff3251992c 100644 --- a/libs/libc/elf/Kconfig +++ b/libs/libc/elf/Kconfig @@ -12,6 +12,13 @@ config LIBC_ELF menu "Module library configuration" depends on LIBC_ELF +config LIBC_ELF_MAXNEEDED + int "Maximum DT_NEEDED libraries per module" + default 4 + ---help--- + How many shared libraries one module may name in DT_NEEDED. Each + is opened with dlopen() while the module loads. + config LIBC_ELF_MAXDEPEND int "Max dependencies" default 2 diff --git a/libs/libc/elf/elf.h b/libs/libc/elf/elf.h index 986a3fa5803a9..ef3edf53dcefb 100644 --- a/libs/libc/elf/elf.h +++ b/libs/libc/elf/elf.h @@ -238,6 +238,52 @@ int libelf_reallocbuffer(FAR struct mod_loadinfo_s *loadinfo, int libelf_freebuffers(FAR struct mod_loadinfo_s *loadinfo); +/**************************************************************************** + * Name: libelf_addr + * + * Description: + * Translate a link-time address in a loaded object to the address it + * actually occupies now. + * + * An object is placed as two pieces, text and data, and this is the one + * place that knows how to get from one space to the other. The split is + * the data segment's link-time base: anything below it belongs to text, + * anything at or above it to data. For ET_REL the two pieces are already + * placed independently; for ET_DYN they are, today, adjacent in a single + * allocation, in which case this returns exactly what adding a single + * load bias would have. + * + * Input Parameters: + * loadinfo - Load state information + * vaddr - The link-time address to translate + * + * Returned Value: + * The run-time address. + * + ****************************************************************************/ + +/**************************************************************************** + * Name: libelf_symname + * + * Description: + * Read a name out of a string table into the I/O buffer. + * + ****************************************************************************/ + +int libelf_symname(FAR struct mod_loadinfo_s *loadinfo, + FAR const Elf_Sym *sym, Elf_Off sh_offset); + +static inline uintptr_t libelf_addr(FAR struct mod_loadinfo_s *loadinfo, + uintptr_t vaddr) +{ + if (loadinfo->datasec != 0 && vaddr >= loadinfo->datasec) + { + return loadinfo->datastart + (vaddr - loadinfo->datasec); + } + + return loadinfo->textalloc + vaddr; +} + #ifdef CONFIG_ARCH_ADDRENV /**************************************************************************** @@ -321,4 +367,24 @@ int libelf_addrenv_restore(FAR struct mod_loadinfo_s *loadinfo); void libelf_addrenv_free(FAR struct mod_loadinfo_s *loadinfo); #endif /* CONFIG_ARCH_ADDRENV */ + +#ifdef HAVE_LIBC_ELF_PIN +/**************************************************************************** + * Name: libelf_pinrelease + * + * Description: + * Give back an XIP pin taken while loading, and the file it was held + * through. Does nothing if no pin was taken. + * + * Input Parameters: + * pinfile - The held file, cleared on return. + * + * Returned Value: + * None. + * + ****************************************************************************/ + +void libelf_pinrelease(FAR struct file **pinfile); +#endif + #endif /* __LIBS_LIBC_LIBC_ELF_LIBC_ELF_H */ diff --git a/libs/libc/elf/elf_bind.c b/libs/libc/elf/elf_bind.c index 34f3fddf79d14..70469ac5078aa 100644 --- a/libs/libc/elf/elf_bind.c +++ b/libs/libc/elf/elf_bind.c @@ -35,6 +35,9 @@ #include #include #include +#include + +#include #include #include "libc.h" @@ -48,6 +51,22 @@ #define I_PLT 1 /* ... for PLTs */ #define N_RELS 2 /* Number of relxxx[] indexes */ +/* Relocation types that only an FDPIC object may use. An architecture + * that has none leaves this alone. + */ + +#ifdef ARCH_ELF_RELOC_ISFDPIC + +/* An architecture that has FDPIC relocations also carries the fields they + * need in its arch_elfdata_t. Everyone else has neither, so the code that + * fills those fields has to go with them. + */ + +# define HAVE_ARCH_ELF_FDPIC 1 +#else +# define ARCH_ELF_RELOC_ISFDPIC(t) 0 +#endif + #ifdef ARCH_ELFDATA # define ARCH_ELFDATA_DEF arch_elfdata_t arch_data; \ memset(&arch_data, 0, sizeof(arch_elfdata_t)) @@ -57,6 +76,20 @@ # define ARCH_ELFDATA_PARM NULL #endif +/* Some relocations need state the loader holds rather than state a single + * relocation carries. An architecture that has such relocations defines + * these to move it in and out of the arch_data block; for everyone else + * they are nothing. + */ + +#if defined(ARCH_ELFDATA) && defined(ARCH_ELFDATA_INIT) +# define ARCH_ELFDATA_SETUP(l) ARCH_ELFDATA_INIT(&arch_data, l) +# define ARCH_ELFDATA_TEARDOWN(l) ARCH_ELFDATA_FINI(&arch_data, l) +#else +# define ARCH_ELFDATA_SETUP(l) +# define ARCH_ELFDATA_TEARDOWN(l) +#endif + /**************************************************************************** * Private Types ****************************************************************************/ @@ -72,7 +105,14 @@ typedef struct int idx; } Elf_SymCache; -struct +/* Where a dynamic object's relocation tables live. This is per load, not + * per file: libelf_relocatedyn() dlopen()s what the object needs, which + * re-enters it for the library, so a shared instance would be overwritten + * by the nested load and the outer one would resume with the library's + * offsets. + */ + +struct reldata_s { int stroff; /* offset to string table */ int symoff; /* offset to symbol table */ @@ -81,7 +121,7 @@ struct int reloff[2]; /* offset to the relocation section */ int relsz[2]; /* size of relocation table */ int relrela[2]; /* type of relocation type - 0: DT_REL / 1: DT_RELA */ -} reldata; +}; /**************************************************************************** * Private Functions @@ -637,7 +677,9 @@ static int libelf_relocateadd(FAR struct module_s *modp, static int libelf_relocatedyn(FAR struct module_s *modp, FAR struct mod_loadinfo_s *loadinfo, - int relidx) + int relidx, + FAR const struct symtab_s *exports, + int nexports) { FAR Elf_Shdr *shdr = &loadinfo->shdr[relidx]; FAR Elf_Shdr *symhdr; @@ -652,6 +694,15 @@ static int libelf_relocatedyn(FAR struct module_s *modp, int i; int idx_rel; int idx_sym; +#ifdef CONFIG_LIBC_DLFCN + int j; + uintptr_t libs[CONFIG_LIBC_ELF_MAXNEEDED]; +#endif + int nlibs = 0; + struct reldata_s reldata; +#ifdef HAVE_ARCH_ELF_FDPIC + bool symfromlib; +#endif /* Define potential architecture specific elf data container */ @@ -712,12 +763,90 @@ static int libelf_relocatedyn(FAR struct module_s *modp, case DT_PLTRELSZ: reldata.relsz[I_PLT] = dyn[i].d_un.d_val; break; + case DT_NEEDED: + + /* Remember it; the name lives in the string table, which is + * not located until the loop has seen DT_STRTAB. + */ + + if (nlibs >= CONFIG_LIBC_ELF_MAXNEEDED) + { + berr("ERROR: More than %d DT_NEEDED entries\n", + CONFIG_LIBC_ELF_MAXNEEDED); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ENOEXEC; + } + +#ifdef CONFIG_LIBC_DLFCN + libs[nlibs] = dyn[i].d_un.d_val; +#endif + nlibs++; + break; + + case DT_PLTGOT: + + /* Where the object's data base lives. An FDPIC module is + * entered with this in the PIC base register, and every + * function descriptor built for it names it as the base its + * callee should run with. + */ + + loadinfo->gotaddr = libelf_addr(loadinfo, + dyn[i].d_un.d_ptr); + break; + + /* The constructor and destructor tables. These are also + * reachable through the section headers, and are read from + * there below, but an object is not obliged to carry section + * headers and the dynamic tags are the authoritative copy. + */ + + case DT_INIT_ARRAY: + loadinfo->initarr = libelf_addr(loadinfo, dyn[i].d_un.d_ptr); + break; + + case DT_INIT_ARRAYSZ: + loadinfo->ninit = dyn[i].d_un.d_val / sizeof(uintptr_t); + break; + + case DT_FINI_ARRAY: + loadinfo->finiarr = libelf_addr(loadinfo, dyn[i].d_un.d_ptr); + break; + + case DT_FINI_ARRAYSZ: + loadinfo->nfini = dyn[i].d_un.d_val / sizeof(uintptr_t); + break; + + case DT_PREINIT_ARRAY: + loadinfo->preiarr = libelf_addr(loadinfo, dyn[i].d_un.d_ptr); + break; + + case DT_PREINIT_ARRAYSZ: + loadinfo->nprei = dyn[i].d_un.d_val / sizeof(uintptr_t); + break; + case DT_PLTREL: if (dyn[i].d_un.d_val == DT_REL) { reldata.relentsz[I_PLT] = sizeof(Elf_Rel); reldata.relrela[I_PLT] = 0; } + else if (loadinfo->fdpic) + { + /* The ARM FDPIC ABI is REL throughout. An object claiming + * RELA for its PLT has to be refused rather than walked as + * REL: the entries are half as long again, so every one + * after the first would be read from the wrong place. + */ + + berr("ERROR: FDPIC object claims RELA PLT relocations\n"); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ENOEXEC; + } else { reldata.relentsz[I_PLT] = sizeof(Elf_Rela); @@ -727,6 +856,94 @@ static int libelf_relocatedyn(FAR struct module_s *modp, } } + /* An object with no imports has no PLT and so no DT_PLTGOT, but it still + * has a GOT and still has to be entered with it: the linker puts it + * immediately after the dynamic section. + */ + + if (loadinfo->fdpic && loadinfo->gotaddr == 0) + { + loadinfo->gotaddr = libelf_addr(loadinfo, + shdr->sh_addr + shdr->sh_size); + binfo("No DT_PLTGOT; taking the GOT at %08lx\n", + (unsigned long)loadinfo->gotaddr); + } + + /* Open whatever the object names in DT_NEEDED. dlopen() is the loader + * for a shared library, so hand the work to it. + */ + +#ifdef CONFIG_LIBC_DLFCN + + symhdr = &loadinfo->shdr[loadinfo->dsymtabidx]; + + for (i = 0; i < nlibs; i++) + { + Elf_Sym namesym; + FAR void *handle; + + /* The name is a string table offset, which is what st_name is, so + * the existing reader can fetch it. + */ + + memset(&namesym, 0, sizeof(namesym)); + namesym.st_name = libs[i]; + + ret = libelf_symname(loadinfo, &namesym, + loadinfo->shdr[symhdr->sh_link].sh_offset); + if (ret < 0) + { + berr("ERROR: DT_NEEDED %d has no name\n", i); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return ret; + } + + handle = dlopen((FAR const char *)loadinfo->iobuffer, RTLD_NOW); + if (handle == NULL) + { + berr("ERROR: Cannot open needed library %s\n", + (FAR char *)loadinfo->iobuffer); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ELIBACC; + } + + binfo("Opened needed library %s\n", (FAR char *)loadinfo->iobuffer); + + if (modp->nlibs >= CONFIG_LIBC_ELF_MAXNEEDED) + { + dlclose(handle); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ENOMEM; + } + + modp->libs[modp->nlibs++] = handle; + } + +#else + if (nlibs > 0) + { + berr("ERROR: DT_NEEDED needs CONFIG_LIBC_DLFCN to load %d " + "librar%s\n", nlibs, nlibs == 1 ? "y" : "ies"); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ENOSYS; + } +#endif + + /* Must follow the tag loop, which is where DT_PLTGOT is read. Both + * relocation tables are walked under this one arch_data, so a cursor in + * it spans the object. + */ + + ARCH_ELFDATA_SETUP(loadinfo); + symhdr = &loadinfo->shdr[loadinfo->dsymtabidx]; sym = lib_malloc(symhdr->sh_size); if (!sym) @@ -764,6 +981,15 @@ static int libelf_relocatedyn(FAR struct module_s *modp, ret = OK; lrelent = reldata.relsz[idx_rel] / reldata.relentsz[idx_rel]; +#ifdef HAVE_ARCH_ELF_FDPIC + /* Say which table this is. A relocation out of DT_JMPREL overwrites + * a word the linker pre-loaded with a lazy binding stub, which is not + * an addend and must not be added to. + */ + + arch_data.pltrel = (idx_rel == I_PLT); +#endif + for (i = 0; i < lrelent; i++) { /* Process each relocation entry @@ -807,6 +1033,25 @@ static int libelf_relocatedyn(FAR struct module_s *modp, } } + /* An object that uses FDPIC relocations but does not say it is + * FDPIC cannot be run: nothing would place its segments apart or + * install its data base, and the relocations below would write + * plain addresses where descriptors belong. The OS/ABI byte is + * the only thing that says so, so a cleared one has to be an + * error rather than something to march past. + */ + + if (!loadinfo->fdpic && + ARCH_ELF_RELOC_ISFDPIC(ELF_R_TYPE(rel->r_info))) + { + berr("ERROR: FDPIC relocation %d in an object that is not " + "marked FDPIC\n", (int)ELF_R_TYPE(rel->r_info)); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ENOEXEC; + } + /* Now perform the architecture-specific relocation */ if ((idx_sym = ELF_R_SYM(rel->r_info)) != 0) @@ -817,8 +1062,52 @@ static int libelf_relocatedyn(FAR struct module_s *modp, { FAR void *ep; +#ifdef HAVE_ARCH_ELF_FDPIC + symfromlib = false; +#endif ep = libelf_findglobal(modp, loadinfo, symhdr, &sym[idx_sym]); + + /* libelf_findglobal() searches only the globally + * registered symbols, and has left the name in the + * I/O buffer. Try the DT_NEEDED libraries next, then + * the table exec() supplied. + */ + +#ifdef CONFIG_LIBC_DLFCN + for (j = 0; ep == NULL && j < modp->nlibs; j++) + { + ep = (FAR void *) + libelf_getsymbol(modp->libs[j], + (FAR char *)loadinfo->iobuffer); + if (ep != NULL) + { +# ifdef HAVE_ARCH_ELF_FDPIC + /* Coming from a library is what tells the + * relocation this is a descriptor. + */ + + symfromlib = true; +# endif + break; + } + } +#endif + + if (ep == NULL && exports != NULL) + { + FAR const struct symtab_s *sm; + + sm = symtab_findbyname(exports, + (FAR char *) + loadinfo->iobuffer, + nexports); + if (sm != NULL) + { + ep = (FAR void *)sm->sym_value; + } + } + if ((ep == NULL) && (ELF_ST_BIND(sym[idx_sym].st_info) != STB_WEAK)) { @@ -831,14 +1120,79 @@ static int libelf_relocatedyn(FAR struct module_s *modp, return ret; } - addr = rel->r_offset + loadinfo->textalloc; + addr = libelf_addr(loadinfo, rel->r_offset); if (reldata.relrela[idx_rel] == 1) { addr += rela->r_addend; } - *(FAR uintptr_t *)addr = (uintptr_t)ep; + if (loadinfo->fdpic) + { + /* An imported symbol is not always a plain + * address to be stored. Under FDPIC it may be a + * function descriptor, which is two words and has + * to be built rather than assigned, so hand the + * resolved value to up_relocate() and let the + * relocation type decide what to write. + */ + + Elf_Sym extsym = + { + 0 + }; + + extsym.st_value = (uintptr_t)ep; +#ifdef HAVE_ARCH_ELF_FDPIC + arch_data.symisdesc = symfromlib; +#endif + ret = up_relocate(rel, &extsym, addr, + ARCH_ELFDATA_PARM); + if (ret < 0) + { + berr("ERROR: Section %d reloc %d: " + "Relocation failed: %d\n", + relidx, i, ret); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return ret; + } + } + else + { + *(FAR uintptr_t *)addr = (uintptr_t)ep; + } + } + else if (loadinfo->fdpic) + { + /* A symbol defined inside this object. Its value is + * the symbol's own, translated; the addend stays where + * the relocation type expects it. + */ + + Elf_Sym defsym = sym[idx_sym]; + + defsym.st_value = libelf_addr(loadinfo, + sym[idx_sym].st_value); + + addr = libelf_addr(loadinfo, rel->r_offset); + + if (reldata.relrela[idx_rel] == 1) + { + addr += rela->r_addend; + } + + ret = up_relocate(rel, &defsym, addr, ARCH_ELFDATA_PARM); + if (ret < 0) + { + berr("ERROR: Section %d reloc %d: " + "Relocation failed: %d\n", relidx, i, ret); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return ret; + } } } else @@ -848,23 +1202,15 @@ static int libelf_relocatedyn(FAR struct module_s *modp, 0 }; - addr = rel->r_offset - loadinfo->datasec + loadinfo->datastart; + addr = libelf_addr(loadinfo, rel->r_offset); if (reldata.relrela[idx_rel] == 1) { addr += rela->r_addend; } - if ((*(FAR uint32_t *)addr) < loadinfo->datasec) - { - dynsym.st_value = *(FAR uint32_t *)addr + - loadinfo->textalloc; - } - else - { - dynsym.st_value = *(FAR uint32_t *)addr - - loadinfo->datasec + loadinfo->datastart; - } + dynsym.st_value = libelf_addr(loadinfo, + *(FAR uint32_t *)addr); ret = up_relocate(rel, &dynsym, addr, ARCH_ELFDATA_PARM); } @@ -881,6 +1227,10 @@ static int libelf_relocatedyn(FAR struct module_s *modp, } } + /* Hand back what the relocations consumed. */ + + ARCH_ELFDATA_TEARDOWN(loadinfo); + lib_free(sym); lib_free(rels); lib_free(dyn); @@ -962,29 +1312,27 @@ int libelf_bind(FAR struct module_s *modp, switch (loadinfo->shdr[i].sh_type) { case SHT_DYNAMIC: - ret = libelf_relocatedyn(modp, loadinfo, i); + ret = libelf_relocatedyn(modp, loadinfo, i, + exports, nexports); break; case SHT_DYNSYM: loadinfo->dsymtabidx = i; break; case SHT_INIT_ARRAY: - loadinfo->initarr = loadinfo->shdr[i].sh_addr - - loadinfo->datasec + - loadinfo->datastart; + loadinfo->initarr = libelf_addr(loadinfo, + loadinfo->shdr[i].sh_addr); loadinfo->ninit = loadinfo->shdr[i].sh_size / sizeof(uintptr_t); break; case SHT_FINI_ARRAY: - loadinfo->finiarr = loadinfo->shdr[i].sh_addr - - loadinfo->datasec + - loadinfo->datastart; + loadinfo->finiarr = libelf_addr(loadinfo, + loadinfo->shdr[i].sh_addr); loadinfo->nfini = loadinfo->shdr[i].sh_size / sizeof(uintptr_t); break; case SHT_PREINIT_ARRAY: - loadinfo->preiarr = loadinfo->shdr[i].sh_addr - - loadinfo->datasec + - loadinfo->datastart; + loadinfo->preiarr = libelf_addr(loadinfo, + loadinfo->shdr[i].sh_addr); loadinfo->nprei = loadinfo->shdr[i].sh_size / sizeof(uintptr_t); break; diff --git a/libs/libc/elf/elf_insert.c b/libs/libc/elf/elf_insert.c index 5b646115248bf..208e8f16ce4c2 100644 --- a/libs/libc/elf/elf_insert.c +++ b/libs/libc/elf/elf_insert.c @@ -29,6 +29,7 @@ #include #include +#include #include #include "elf.h" @@ -247,9 +248,22 @@ static int libelf_loadsymtab(FAR struct module_s *modp, if (sym[i].st_shndx != SHN_UNDEF && sym[i].st_shndx < loadinfo->ehdr.e_shnum) { - FAR Elf_Shdr *s = &loadinfo->shdr[sym[i].st_shndx]; + if (loadinfo->ehdr.e_type == ET_DYN) + { + /* A shared object's symbol value is already the full + * link-time address, and so is its section's, so adding + * the two would count the section twice. What it needs is + * translating onto wherever the object was placed. + */ + + sym[i].st_value = libelf_addr(loadinfo, sym[i].st_value); + } + else + { + FAR Elf_Shdr *s = &loadinfo->shdr[sym[i].st_shndx]; - sym[i].st_value = sym[i].st_value + s->sh_addr; + sym[i].st_value = sym[i].st_value + s->sh_addr; + } } } @@ -391,6 +405,12 @@ FAR void *libelf_insert(FAR const char *filename, FAR const char *modname) modp->textalloc = (FAR void *)loadinfo.textalloc; modp->dataalloc = (FAR void *)loadinfo.datastart; + modp->fdpic = loadinfo.fdpic; + modp->gotaddr = loadinfo.gotaddr; +#ifdef HAVE_LIBC_ELF_PIN + modp->pinfile = loadinfo.pinfile; + loadinfo.pinfile = NULL; +#endif #ifdef CONFIG_ARCH_USE_SEPARATED_SECTION modp->sectalloc = (FAR void **)loadinfo.sectalloc; modp->nsect = loadinfo.ehdr.e_shnum; @@ -408,12 +428,25 @@ FAR void *libelf_insert(FAR const char *filename, FAR const char *modname) case ET_REL : case ET_DYN : - /* Process any preinit_array entries */ + /* Process any preinit_array entries. + * + * An FDPIC object's constructors touch its globals, so they have + * to run with its own data base rather than with whatever the + * loading thread happens to carry -- which for a DT_NEEDED + * library is the importing module's. + */ array = (FAR void (**)(void))loadinfo.preiarr; for (i = 0; i < loadinfo.nprei; i++) { - array[i](); + if (loadinfo.fdpic) + { + fdpic_invoke((uintptr_t)array[i], 0, loadinfo.gotaddr); + } + else + { + array[i](); + } } /* Process any init_array entries */ @@ -421,7 +454,14 @@ FAR void *libelf_insert(FAR const char *filename, FAR const char *modname) array = (FAR void (**)(void))loadinfo.initarr; for (i = 0; i < loadinfo.ninit; i++) { - array[i](); + if (loadinfo.fdpic) + { + fdpic_invoke((uintptr_t)array[i], 0, loadinfo.gotaddr); + } + else + { + array[i](); + } } modp->initarr = loadinfo.initarr; diff --git a/libs/libc/elf/elf_load.c b/libs/libc/elf/elf_load.c index c58f8060ade67..18a6e25793ded 100644 --- a/libs/libc/elf/elf_load.c +++ b/libs/libc/elf/elf_load.c @@ -40,6 +40,7 @@ #include #include +#include #include #include "libc.h" @@ -239,6 +240,60 @@ static void libelf_elfsize(FAR struct mod_loadinfo_s *loadinfo, bool alloc) } } + /* Reserve the descriptor pool now: a relocation may ask the loader to + * manufacture one, and by then the segment has been placed. Bounded by + * the relocation and symbol counts, which is loose but cheap. + */ + + if (loadinfo->fdpic) + { + size_t nrels = 0; + + for (i = 0; i < loadinfo->ehdr.e_shnum; i++) + { + FAR Elf_Shdr *shdr = &loadinfo->shdr[i]; + + if (shdr->sh_type == SHT_REL && shdr->sh_entsize != 0) + { + nrels += shdr->sh_size / shdr->sh_entsize; + } + } + + /* Exported functions get one each, for dlsym(). */ + + for (i = 0; i < loadinfo->ehdr.e_shnum; i++) + { + FAR Elf_Shdr *shdr = &loadinfo->shdr[i]; + + if (shdr->sh_type == SHT_DYNSYM && shdr->sh_entsize != 0) + { + nrels += shdr->sh_size / shdr->sh_entsize; + } + } + + loadinfo->ndesc = nrels; + loadinfo->descpool = datasize; + datasize += nrels * 2 * sizeof(uintptr_t); + + binfo("fdpic: reserving %zu descriptors behind the data\n", nrels); + } + + /* A shared object is sized from its program headers, which carry no + * per-section alignment to take: p_align is the linker's page + * granularity, and honouring it would cost a page per module for nothing. + * Its sections need no more than a natural word, so ask for that. + */ + + if (loadinfo->textalign == 0) + { + loadinfo->textalign = sizeof(uintptr_t); + } + + if (loadinfo->dataalign == 0) + { + loadinfo->dataalign = sizeof(uintptr_t); + } + /* Save the allocation size */ loadinfo->textsize = textsize; @@ -350,6 +405,13 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) { if (phdr->p_flags & PF_X) { + if (loadinfo->fdpic) + { + /* Mapped, not copied. */ + + continue; + } + ret = libelf_read(loadinfo, buffer_data_address(text), phdr->p_filesz, phdr->p_offset); @@ -525,6 +587,123 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) return OK; } +/**************************************************************************** + * Name: libelf_xipacquire + * + * Description: + * Ask the filesystem for the address of this file on its media, so that + * the read-only part of the object can be used where it lies instead of + * being copied. + * + * Two mechanisms exist and they are not interchangeable. A compacting + * filesystem can move a file's blocks, so it hands out an address only + * together with a pin that holds them still, and expects the pin back; + * xipfs is the one in tree. A filesystem whose layout never changes has + * nothing to hold and answers FIOC_XIPBASE with a bare address; romfs + * and tmpfs are those. Ask for the pin first, because a filesystem that + * needs one cannot safely be used without it. + * + * mmap() is deliberately not used here even though both filesystems + * implement it. The mapping would be recorded against whichever task + * called the loader, while the release happens when the module's own + * task exits -- a different group -- so the pin would outlive the module + * and the extent would never become movable again. + * + * Returned Value: + * Zero if an address was obtained, a negated errno otherwise. Callers + * that can live without one may ignore the failure. + * + ****************************************************************************/ + +#ifdef HAVE_LIBC_ELF_PIN +static int libelf_pinhold(FAR struct mod_loadinfo_s *loadinfo) +{ + FAR struct file *filep; + int ret; + + /* The descriptor the pin was taken through belongs to whichever task + * called the loader, and the pin has to be given back when the module is + * unloaded -- which for an executed module happens on the spawned task, + * in another group entirely. Take a reference to the file instead, which + * belongs to no group. + */ + + loadinfo->pinfile = lib_zalloc(sizeof(struct file)); + if (loadinfo->pinfile == NULL) + { + return -ENOMEM; + } + + ret = file_get(loadinfo->filfd, &filep); + if (ret >= 0) + { + ret = file_dup2(filep, loadinfo->pinfile); + file_put(filep); + } + + if (ret < 0) + { + lib_free(loadinfo->pinfile); + loadinfo->pinfile = NULL; + } + + return ret; +} + +/**************************************************************************** + * Name: libelf_pinrelease + * + * Description: + * Give back an XIP pin and the file it was held through. A compacting + * filesystem cannot reclaim the extent until every instance executing + * from it has let go, so this is not merely tidiness. + * + ****************************************************************************/ + +void libelf_pinrelease(FAR struct file **pinfile) +{ + if (*pinfile != NULL) + { + file_ioctl(*pinfile, XIPFSIOC_UNPIN, 0); + file_close(*pinfile); + lib_free(*pinfile); + *pinfile = NULL; + } +} +#endif + +static int libelf_xipacquire(FAR struct mod_loadinfo_s *loadinfo) +{ + uintptr_t base = 0; + + if (ioctl(loadinfo->filfd, XIPFSIOC_PIN, (unsigned long)&base) >= 0) + { +#ifdef HAVE_LIBC_ELF_PIN + int ret = libelf_pinhold(loadinfo); + if (ret < 0) + { + berr("ERROR: Failed to hold the pinned file: %d\n", ret); + ioctl(loadinfo->filfd, XIPFSIOC_UNPIN, 0); + return ret; + } +#endif + + loadinfo->xipbase = base; + loadinfo->textpin = true; + binfo("pinned xipbase %zx\n", (size_t)loadinfo->xipbase); + return OK; + } + + if (ioctl(loadinfo->filfd, FIOC_XIPBASE, (unsigned long)&base) >= 0) + { + loadinfo->xipbase = base; + binfo("can use xipbase %zx\n", (size_t)loadinfo->xipbase); + return OK; + } + + return -ENOTTY; +} + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -545,6 +724,7 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) int libelf_load(FAR struct mod_loadinfo_s *loadinfo) { int ret; + int i; binfo("loadinfo: %p\n", loadinfo); DEBUGASSERT(loadinfo && loadinfo->filfd >= 0); @@ -559,14 +739,10 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo) } loadinfo->gotindex = libelf_findsection(loadinfo, ".got"); - if (loadinfo->gotindex >= 0) + if (loadinfo->gotindex >= 0 || loadinfo->fdpic) { binfo("GOT section found! index %d\n", loadinfo->gotindex); - if (ioctl(loadinfo->filfd, FIOC_XIPBASE, - (unsigned long)&loadinfo->xipbase) >= 0) - { - binfo("can use xipbase %zu\n", loadinfo->xipbase); - } + libelf_xipacquire(loadinfo); } /* Determine total size to allocate */ @@ -633,21 +809,77 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo) } else if (loadinfo->ehdr.e_type == ET_DYN) { - loadinfo->textalloc = (uintptr_t)lib_memalign(loadinfo->textalign, - loadinfo->textsize + - loadinfo->datasize + - loadinfo->segpad); - - if (!loadinfo->textalloc) + if (loadinfo->fdpic) { - berr("ERROR: Failed to allocate memory for the module\n"); - ret = -ENOMEM; - goto errout_with_buffers; + /* Text is mapped where it lies on the media and never copied; + * only the writable segment is allocated, once per instance. + */ + + if (loadinfo->xipbase == 0) + { + berr("ERROR: FDPIC module cannot be executed in place\n"); + ret = -ENOEXEC; + goto errout_with_buffers; + } + + /* The media address is the base of the file, so add the + * segment's own file offset. + */ + + for (i = 0; i < loadinfo->ehdr.e_phnum; i++) + { + FAR Elf_Phdr *phdr = &loadinfo->phdr[i]; + + if (phdr->p_type == PT_LOAD && (phdr->p_flags & PF_X) != 0) + { + loadinfo->textalloc = loadinfo->xipbase + phdr->p_offset; + break; + } + } + + if (loadinfo->datasize > 0) + { + loadinfo->datastart = + (uintptr_t)lib_memalign(loadinfo->dataalign, + loadinfo->datasize); + if (!loadinfo->datastart) + { + berr("ERROR: Failed to allocate the module's data\n"); + ret = -ENOMEM; + goto errout_with_buffers; + } + } + + /* The pool was sized as an offset past the end of the real + * data; now that the segment has an address, make it one. + */ + + loadinfo->descpool += loadinfo->datastart; } + else + { + /* Everything else keeps the relative position of text and data, + * because its data references are at a fixed offset from the + * code that makes them. One allocation, data behind text. + */ + + loadinfo->textalloc = (uintptr_t) + lib_memalign(loadinfo->textalign, + loadinfo->textsize + + loadinfo->datasize + + loadinfo->segpad); + + if (!loadinfo->textalloc) + { + berr("ERROR: Failed to allocate memory for the module\n"); + ret = -ENOMEM; + goto errout_with_buffers; + } - loadinfo->datastart = loadinfo->textalloc + - loadinfo->textsize + - loadinfo->segpad; + loadinfo->datastart = loadinfo->textalloc + + loadinfo->textsize + + loadinfo->segpad; + } } #endif /* CONFIG_LIBC_ELF_LOADTO_LMA */ @@ -715,14 +947,10 @@ int libelf_load_with_addrenv(FAR struct mod_loadinfo_s *loadinfo) } loadinfo->gotindex = libelf_findsection(loadinfo, ".got"); - if (loadinfo->gotindex >= 0) + if (loadinfo->gotindex >= 0 || loadinfo->fdpic) { binfo("GOT section found! index %d\n", loadinfo->gotindex); - if (ioctl(loadinfo->filfd, FIOC_XIPBASE, - (unsigned long)&loadinfo->xipbase) >= 0) - { - binfo("can use xipbase %zu\n", loadinfo->xipbase); - } + libelf_xipacquire(loadinfo); } /* Determine total size to allocate */ diff --git a/libs/libc/elf/elf_loadhdrs.c b/libs/libc/elf/elf_loadhdrs.c index 6e3e3afb0410d..09ddf21d4b334 100644 --- a/libs/libc/elf/elf_loadhdrs.c +++ b/libs/libc/elf/elf_loadhdrs.c @@ -66,6 +66,25 @@ int libelf_loadhdrs(FAR struct mod_loadinfo_s *loadinfo) /* Verify that there are sections */ + /* An FDPIC object announces itself in the OS/ABI byte. Note it once, + * here, so that placement and relocation do not each have to re-derive + * it from the header. + */ + + loadinfo->fdpic = (loadinfo->ehdr.e_ident[EI_OSABI] == ELFOSABI_ARM_FDPIC); + + /* A module is a shared object. An object that claims the FDPIC ABI and + * is anything else would be placed and entered through the paths meant + * for a relocatable object, which is not what its relocations expect. + */ + + if (loadinfo->fdpic && loadinfo->ehdr.e_type != ET_DYN) + { + berr("ERROR: FDPIC object is not a shared object: e_type=%u\n", + loadinfo->ehdr.e_type); + return -ENOEXEC; + } + if (loadinfo->ehdr.e_shnum < 1) { berr("ERROR: No sections(?)\n"); diff --git a/libs/libc/elf/elf_remove.c b/libs/libc/elf/elf_remove.c index 67722937b3302..fb11c47a5ef0a 100644 --- a/libs/libc/elf/elf_remove.c +++ b/libs/libc/elf/elf_remove.c @@ -29,8 +29,13 @@ #include #include +#include + +#include #include +#include "elf/elf.h" + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -39,7 +44,11 @@ * Name: libelf_uninit * * Description: - * Uninitialize module resources. + * Uninitialize module resources. This gives up everything the module + * holds, including any libraries it opened for its DT_NEEDED entries, so + * the caller must be releasing the last reference to it: libelf_remove() + * calls this only once nopen reaches zero, and the copy binfmt keeps + * belongs to a single exec'd binary and is never shared. * ****************************************************************************/ @@ -59,12 +68,33 @@ int libelf_uninit(FAR struct module_s *modp) } #endif +#ifdef CONFIG_LIBC_DLFCN + /* Let go of anything opened for DT_NEEDED. */ + + while (modp->nlibs > 0) + { + dlclose(modp->libs[--modp->nlibs]); + } +#endif + /* Is there an uninitializer? */ array = (FAR void (**)(void))modp->finiarr; for (i = 0; i < modp->nfini; i++) { - array[i](); + /* Like the constructors, an FDPIC object's destructors reach its + * globals through its own data base, which the unloading thread does + * not carry. + */ + + if (modp->fdpic) + { + fdpic_invoke((uintptr_t)array[i], 0, modp->gotaddr); + } + else + { + array[i](); + } } if (modp->modinfo.uninitializer != NULL) @@ -148,6 +178,21 @@ int libelf_uninit(FAR struct module_s *modp) # endif #endif } + else if (modp->fdpic) + { +#ifdef HAVE_LIBC_ELF_PIN + /* Give the pin back before the text goes out of use. */ + + libelf_pinrelease(&modp->pinfile); +#endif + + /* An FDPIC object placed its two segments separately, and its + * text was never allocated at all -- it is media the filesystem + * lent us. Free the data on its own and leave the text alone. + */ + + lib_free((FAR void *)modp->dataalloc); + } else { lib_free((FAR void *)modp->textalloc); diff --git a/libs/libc/elf/elf_symbols.c b/libs/libc/elf/elf_symbols.c index 39ad66f085853..078f8ef8cedff 100644 --- a/libs/libc/elf/elf_symbols.c +++ b/libs/libc/elf/elf_symbols.c @@ -34,6 +34,7 @@ #include #include +#include #include #include "libc.h" @@ -92,8 +93,8 @@ extern int nglobals; * ****************************************************************************/ -static int libelf_symname(FAR struct mod_loadinfo_s *loadinfo, - FAR const Elf_Sym *sym, Elf_Off sh_offset) +int libelf_symname(FAR struct mod_loadinfo_s *loadinfo, + FAR const Elf_Sym *sym, Elf_Off sh_offset) { FAR uint8_t *buffer; off_t offset; @@ -107,7 +108,14 @@ static int libelf_symname(FAR struct mod_loadinfo_s *loadinfo, if (sym->st_name == 0) { - berr("ERROR: Symbol has no name\n"); + /* Not a failure. A section symbol has no name, and + * libelf_findsymbol() walks the whole table looking for optional + * symbols such as nx_stacksize, so it meets these routinely and + * checks for -ESRCH itself. Reporting it as an error buries the + * real diagnostics on every module load. + */ + + binfo("Symbol has no name\n"); return -ESRCH; } @@ -538,6 +546,27 @@ int libelf_insertsymtab(FAR struct module_s *modp, strdup((FAR char *)loadinfo->iobuffer); symbol[j].sym_value = (FAR const void *)(uintptr_t)sym[i].st_value; + + /* Publish a function as a descriptor, not a code + * address, so dlsym() returns something an FDPIC caller + * can branch through. Only here does st_info still say + * which symbols are functions. + */ + + if (loadinfo->fdpic && + ELF_ST_TYPE(sym[i].st_info) == STT_FUNC && + loadinfo->usedesc < loadinfo->ndesc) + { + FAR struct fdpic_desc_s *desc = + (FAR struct fdpic_desc_s *)loadinfo->descpool + + loadinfo->usedesc++; + + desc->entry = sym[i].st_value; + desc->got = loadinfo->gotaddr; + + symbol[j].sym_value = (FAR const void *)desc; + } + j++; } } diff --git a/libs/libc/elf/elf_unload.c b/libs/libc/elf/elf_unload.c index c1c7609bf2f6c..6bc0281f36d3d 100644 --- a/libs/libc/elf/elf_unload.c +++ b/libs/libc/elf/elf_unload.c @@ -30,6 +30,8 @@ #include #include +#include +#include #include #include "libc.h" @@ -68,9 +70,42 @@ int libelf_unload(FAR struct mod_loadinfo_s *loadinfo) #endif /* Release memory holding the relocated ELF image */ - /* ET_DYN has a single allocation so we only free textalloc */ + /* An FDPIC object placed its two segments separately, and its text was + * never allocated at all -- it is media the filesystem lent us. Free + * the data on its own and leave the text alone. + */ - if (loadinfo->ehdr.e_type != ET_DYN) + if (loadinfo->fdpic) + { + /* Give the pin back if one was taken. A compacting filesystem + * cannot reclaim the extent until every instance executing from it + * has let go, so this is not merely tidiness. + */ + + if (loadinfo->textpin) + { +#ifdef HAVE_LIBC_ELF_PIN + libelf_pinrelease(&loadinfo->pinfile); +#else + ioctl(loadinfo->filfd, XIPFSIOC_UNPIN, 0); +#endif + loadinfo->textpin = false; + } + + if (loadinfo->datastart != 0) + { + lib_free((FAR void *)loadinfo->datastart); + loadinfo->datastart = 0; + } + + loadinfo->textalloc = 0; + loadinfo->textsize = 0; + loadinfo->datasize = 0; + } + + /* Any other ET_DYN has a single allocation so we only free textalloc */ + + else if (loadinfo->ehdr.e_type != ET_DYN) { #ifdef CONFIG_ARCH_USE_SEPARATED_SECTION int i; diff --git a/libs/libc/machine/arm/armv7-m/arch_elf.c b/libs/libc/machine/arm/armv7-m/arch_elf.c index c91e92460d7ac..d93968c859380 100644 --- a/libs/libc/machine/arm/armv7-m/arch_elf.c +++ b/libs/libc/machine/arm/armv7-m/arch_elf.c @@ -175,6 +175,107 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, } break; + case R_ARM_FUNCDESC_VALUE: + { + /* The target is the descriptor itself. REL keeps the addend in + * the word about to become the entry point, and it carries the + * Thumb bit, so it must be added rather than dropped. + */ + + FAR struct arm_fdpic_desc_s *desc = + (FAR struct arm_fdpic_desc_s *)addr; + FAR arch_elfdata_t *data = (FAR arch_elfdata_t *)arch_data; + + if (data == NULL) + { + berr("ERROR: FUNCDESC_VALUE without loader state\n"); + return -EINVAL; + } + + /* A descriptor only means anything in an FDPIC object. An object + * that carries these relocations without saying it is FDPIC cannot + * be run: nothing would install its data base. + */ + + if (!data->fdpic) + { + berr("ERROR: FUNCDESC_VALUE in a non-FDPIC object\n"); + return -ENOEXEC; + } + + binfo("Performing FUNCDESC_VALUE link " + "at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n", + addr, sym, sym->st_value); + + if (data->symisdesc) + { + /* Resolved to a function in another object, which published a + * descriptor of its own. Take both words: the callee has to + * run with its own data base, not ours. + */ + + *desc = *(FAR struct arm_fdpic_desc_s *)sym->st_value; + } + else if (data->pltrel) + { + /* A lazy descriptor holds the address of its own PLT resolution + * stub and a data base of -1, for a resolver to overwrite on + * the first call. Binding eagerly means overwriting it here; + * adding to it would produce an arbitrary address. + */ + + desc->entry = sym->st_value; + desc->got = data->gotaddr; + } + else + { + desc->entry = sym->st_value + desc->entry; + desc->got = data->gotaddr; + } + } + break; + + case R_ARM_FUNCDESC: + { + /* A pointer to a descriptor the loader must supply. Carve one + * from the pool and store its address. + */ + + FAR struct arm_fdpic_desc_s *desc; + FAR arch_elfdata_t *data = (FAR arch_elfdata_t *)arch_data; + + if (data == NULL) + { + berr("ERROR: FUNCDESC without loader state\n"); + return -EINVAL; + } + + if (!data->fdpic) + { + berr("ERROR: FUNCDESC in a non-FDPIC object\n"); + return -ENOEXEC; + } + + if (data->usedesc >= data->ndesc) + { + berr("ERROR: Out of function descriptors\n"); + return -ENOMEM; + } + + desc = (FAR struct arm_fdpic_desc_s *)data->descpool + + data->usedesc++; + + binfo("Performing FUNCDESC link " + "at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n", + addr, sym, sym->st_value); + + desc->entry = sym->st_value + *(uint32_t *)addr; + desc->got = data->gotaddr; + + *(uint32_t *)addr = (uint32_t)(uintptr_t)desc; + } + break; + case R_ARM_ABS32: case R_ARM_TARGET1: /* New ABI: TARGET1 always treated as ABS32 */ { diff --git a/libs/libc/machine/arm/armv8-m/arch_elf.c b/libs/libc/machine/arm/armv8-m/arch_elf.c index 9a3c68811bd65..50037bb3570b9 100644 --- a/libs/libc/machine/arm/armv8-m/arch_elf.c +++ b/libs/libc/machine/arm/armv8-m/arch_elf.c @@ -175,6 +175,107 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, } break; + case R_ARM_FUNCDESC_VALUE: + { + /* The target is the descriptor itself. REL keeps the addend in + * the word about to become the entry point, and it carries the + * Thumb bit, so it must be added rather than dropped. + */ + + FAR struct arm_fdpic_desc_s *desc = + (FAR struct arm_fdpic_desc_s *)addr; + FAR arch_elfdata_t *data = (FAR arch_elfdata_t *)arch_data; + + if (data == NULL) + { + berr("ERROR: FUNCDESC_VALUE without loader state\n"); + return -EINVAL; + } + + /* A descriptor only means anything in an FDPIC object. An object + * that carries these relocations without saying it is FDPIC cannot + * be run: nothing would install its data base. + */ + + if (!data->fdpic) + { + berr("ERROR: FUNCDESC_VALUE in a non-FDPIC object\n"); + return -ENOEXEC; + } + + binfo("Performing FUNCDESC_VALUE link " + "at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n", + addr, sym, sym->st_value); + + if (data->symisdesc) + { + /* Resolved to a function in another object, which published a + * descriptor of its own. Take both words: the callee has to + * run with its own data base, not ours. + */ + + *desc = *(FAR struct arm_fdpic_desc_s *)sym->st_value; + } + else if (data->pltrel) + { + /* A lazy descriptor holds the address of its own PLT resolution + * stub and a data base of -1, for a resolver to overwrite on + * the first call. Binding eagerly means overwriting it here; + * adding to it would produce an arbitrary address. + */ + + desc->entry = sym->st_value; + desc->got = data->gotaddr; + } + else + { + desc->entry = sym->st_value + desc->entry; + desc->got = data->gotaddr; + } + } + break; + + case R_ARM_FUNCDESC: + { + /* A pointer to a descriptor the loader must supply. Carve one + * from the pool and store its address. + */ + + FAR struct arm_fdpic_desc_s *desc; + FAR arch_elfdata_t *data = (FAR arch_elfdata_t *)arch_data; + + if (data == NULL) + { + berr("ERROR: FUNCDESC without loader state\n"); + return -EINVAL; + } + + if (!data->fdpic) + { + berr("ERROR: FUNCDESC in a non-FDPIC object\n"); + return -ENOEXEC; + } + + if (data->usedesc >= data->ndesc) + { + berr("ERROR: Out of function descriptors\n"); + return -ENOMEM; + } + + desc = (FAR struct arm_fdpic_desc_s *)data->descpool + + data->usedesc++; + + binfo("Performing FUNCDESC link " + "at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n", + addr, sym, sym->st_value); + + desc->entry = sym->st_value + *(uint32_t *)addr; + desc->got = data->gotaddr; + + *(uint32_t *)addr = (uint32_t)(uintptr_t)desc; + } + break; + case R_ARM_ABS32: case R_ARM_TARGET1: /* New ABI: TARGET1 always treated as ABS32 */ { diff --git a/libs/libc/pthread/pthread_create.c b/libs/libc/pthread/pthread_create.c index 6c87140361cd4..34676fada518e 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 ccd854b878843..267f4e6891a7e 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 8ed40cf2ef2d6..bf19d765ea088 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 a4e3047bf9030..c449480ad9152 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 5646452388f1b..d57b38e007a51 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 @@ void qsort(FAR void *base, size_t nel, size_t width, 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 @@ void qsort(FAR void *base, size_t nel, size_t width, 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 ea1f35fc27933..3dd9372427b10 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 ab37db83d262b..a6b32feffbdc0 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 86909faf62bc5..30010472c23d2 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 4530a742491a0..6f7e99d657bdb 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 a29db7f893c0a..d475eddeebf17 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 1811f3e68af00..36562fad88d39 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 0000000000000..3e1f84413c821 --- /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 0000000000000..28e3277441b56 --- /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 0000000000000..5ff55d5f65d29 --- /dev/null +++ b/tools/fdpic/fdpic-embed.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +############################################################################ +# tools/fdpic/fdpic-embed +# +# 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 -- 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 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 [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): + end = i + 12 + row = ", ".join("0x%02x" % b for b in blob[i:end]) + out.write(" %s%s\n" % (row, "," if end < 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 0000000000000..164b3372c8876 --- /dev/null +++ b/tools/fdpic/fdpic-verify.sh @@ -0,0 +1,99 @@ +#!/bin/sh +############################################################################ +# tools/fdpic/fdpic-verify +# +# 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 [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 [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 0000000000000..f68931d26700e --- /dev/null +++ b/tools/fdpic/nuttx-exports.sh @@ -0,0 +1,102 @@ +#!/bin/sh +############################################################################ +# tools/fdpic/nuttx-exports +# +# 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 +# +# CC may be set to override the compiler used to preprocess. + +set -e +NUTTX="${1:?usage: nuttx-exports }" +SYMTAB="$NUTTX/libs/libc/exec_symtab.c" + +if [ ! -f "$SYMTAB" ]; then + echo "nuttx-exports: $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: 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: 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 0000000000000..9780a866b8c78 --- /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)