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