From 42fa139939871bbcdafadc9e230833c93fbbe656 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Fri, 28 Aug 2026 18:22:08 +0200 Subject: [PATCH 1/3] arch/arm, libs/libc/elf: Build FDPIC modules in the normal ELF build. With CONFIG_FDPIC selected, a module built by apps/Application.mk is now an FDPIC shared object. Nothing about how a module is written or built changes: the same MODULE = m in the same Makefile, the same crt0 and the same linker script. Two things differ from the position independent build beside it. The compiler is told -mfdpic -fPIC, and the link is done by an arm-uclinuxfdpiceabi linker. The stock arm-none-eabi compiler emits correct FDPIC objects for both C and C++, so only the link needs it: the stock linker carries the armelf emulation alone and would turn every import into an R_ARM_JUMP_SLOT, one word, where the ABI wants an R_ARM_FUNCDESC_VALUE, which is two, a code address and the data base that goes with it. Such a module links cleanly and then calls out of itself with the caller's data base still in r9. That linker is in the CI image. gnu-elf.ld.in gains the two segments an FDPIC module needs, under CONFIG_FDPIC, because the loader places its read-only and writable segments independently, and names .dynamic, because a shared object is bound through it. The sections themselves are untouched and so are the symbols crt0.c walks, so one script serves both and both build systems get it. .bss moves to the end of the script, for every configuration and not only FDPIC. It held no file content but sat ahead of .got and .dynamic, which do, so the writable segment's p_filesz had to span it and the module file carried the whole of .bss. A module with 16 KiB of .bss went from 26724 to 10340 bytes, and its writable segment from p_filesz 0x40ac to 0xac against an unchanged p_memsz. The loader reads p_filesz off the media, so it read those bytes too. Built for mps3-an547:picostest with apps/examples/elf, CONFIG_FDPIC both ways. With it on, every module in apps/bin is ARM FDPIC with two PT_LOAD segments and enters at _start; hello++3, which has a static C++ object, carries DT_INIT_ARRAY and DT_FINI_ARRAY. With it off the generated script has no PHDRS and the modules are what they were. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- arch/arm/src/common/Toolchain.defs | 18 +++++++++ libs/libc/elf/gnu-elf.ld.in | 64 +++++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/arch/arm/src/common/Toolchain.defs b/arch/arm/src/common/Toolchain.defs index 155ef3f3d7ce3..949b7161573e9 100644 --- a/arch/arm/src/common/Toolchain.defs +++ b/arch/arm/src/common/Toolchain.defs @@ -642,11 +642,29 @@ ifeq ($(CONFIG_PIC),y) # after including this file, which would discard the flag. ARCHCFLAGS += --fixed-r9 + +ifeq ($(CONFIG_FDPIC),y) + # An FDPIC module is a shared object whose two segments the loader places + # independently. The stock compiler emits correct FDPIC objects for both C + # and C++, so only the link needs the arm-uclinuxfdpiceabi linker: the + # stock one carries the armelf emulation alone and would turn every import + # into a jump slot where the ABI wants a function descriptor. + + FDPIC_CROSSDEV ?= arm-uclinuxfdpiceabi- + MODULELD = $(FDPIC_CROSSDEV)ld + + CELFFLAGS += -mfdpic -fPIC -Wa,--noexecstack + CXXELFFLAGS += -mfdpic -fPIC -Wa,--noexecstack + + LDELFFLAGS += -m armelf_linux_fdpiceabi -shared -z now +else CELFFLAGS += $(PICFLAGS) -mpic-register=r9 CXXELFFLAGS += $(PICFLAGS) -mpic-register=r9 # Generate an executable elf, need to ignore undefined symbols LDELFFLAGS += --unresolved-symbols=ignore-in-object-files --emit-relocs +endif + else ifneq ($(CONFIG_BINFMT_ELF_EXECUTABLE),y) LDELFFLAGS += -r diff --git a/libs/libc/elf/gnu-elf.ld.in b/libs/libc/elf/gnu-elf.ld.in index 340a91df5a207..be0c87760501f 100644 --- a/libs/libc/elf/gnu-elf.ld.in +++ b/libs/libc/elf/gnu-elf.ld.in @@ -39,6 +39,34 @@ # define SECTIONS_ALIGN 4 #endif +/* An FDPIC module is a shared object whose read-only and writable segments + * the loader places independently: the read-only one runs where the + * filesystem already holds it and only the writable one is copied to RAM, + * once per running instance. So the two go into segments of their own, and + * .dynamic is named, because a shared object is bound through it. + * + * Everything else is common, the symbols crt0.c walks included, so a module + * is built and entered the same way whichever this is. + */ + +#ifdef CONFIG_FDPIC +# define PHDR_TEXT :text +# define PHDR_DATA :data +# define DATA_ALIGN . = ALIGN(0x1000); + +PHDRS +{ + text PT_LOAD FLAGS(5); /* Read and execute */ + data PT_LOAD FLAGS(6); /* Read and write */ + dynamic PT_DYNAMIC FLAGS(6); +} + +#else +# define PHDR_TEXT +# define PHDR_DATA +# define DATA_ALIGN +#endif + SECTIONS { .text TEXT : @@ -53,7 +81,7 @@ SECTIONS *(.jcr) . = ALIGN(SECTIONS_ALIGN); _etext = . ; - } + } PHDR_TEXT .rodata : { @@ -64,7 +92,9 @@ SECTIONS *(.gnu.linkonce.r*) . = ALIGN(SECTIONS_ALIGN); _erodata = . ; - } + } PHDR_TEXT + + DATA_ALIGN .data DATA : { @@ -75,7 +105,7 @@ SECTIONS *(.gnu.linkonce.d*) . = ALIGN(SECTIONS_ALIGN); _edata = . ; - } + } PHDR_DATA .init_array : { @@ -86,7 +116,7 @@ SECTIONS . = ALIGN(SECTIONS_ALIGN); _einit = .; _ectors = .; - } + } PHDR_DATA .fini_array : { @@ -98,7 +128,24 @@ SECTIONS . = ALIGN(SECTIONS_ALIGN); _efini = .; _edtors = .; - } + } PHDR_DATA + +#ifdef CONFIG_FDPIC + .dynamic : + { + *(.dynamic) + } :data :dynamic +#endif + + .got : + { + *(.got*) + } PHDR_DATA + + /* .bss holds no file content, so it comes last. Everything ahead of it + * has content, thus the writable segment's p_filesz stops where .bss + * starts and the file does not carry it. + */ .bss : { @@ -111,12 +158,7 @@ SECTIONS *(COMMON) . = ALIGN(SECTIONS_ALIGN); _ebss = . ; - } - - .got : - { - *(.got*) - } + } PHDR_DATA /* Stabs debugging sections. */ From 34ce5ad9e5aebd44f450c7aa30a3f528dd0af2fb Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Fri, 28 Aug 2026 18:35:28 +0200 Subject: [PATCH 2/3] cmake: Build FDPIC modules the way the make build does. The same two differences as in common/Toolchain.defs: the compiler is told -mfdpic -fPIC, and the module link is done by an arm-uclinuxfdpiceabi linker. That linker is not the one that links the firmware, so the module link needs a variable of its own. CMAKE_ELF_LD is the ordinary linker unless the architecture sets it, which arm does under CONFIG_FDPIC. The linker script needs nothing here: it is generated from libs/libc/elf/gnu-elf.ld.in, which both build systems preprocess, and the FDPIC segments are already in it. -r is now conditional on CONFIG_PIC being off, which is what common/Toolchain.defs has always done and the cmake build did not: a position independent module is linked as an executable, and an FDPIC one as a shared object, so neither wants it. -fno-use-cxa-atexit mirrors CXXELFFLAGS for the same reason it was added there. Configured and built mps3-an547:picostest with CONFIG_FDPIC through cmake and ninja: the modules in bin/ are ARM FDPIC with two PT_LOAD segments. Signed-off-by: Marco Casaroli --- arch/arm/src/cmake/elf.cmake | 45 +++++++++++++++++++++++++------ cmake/nuttx_add_application.cmake | 12 +++++++-- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/arch/arm/src/cmake/elf.cmake b/arch/arm/src/cmake/elf.cmake index 7108aa4c17485..91b1037d63129 100644 --- a/arch/arm/src/cmake/elf.cmake +++ b/arch/arm/src/cmake/elf.cmake @@ -27,17 +27,46 @@ nuttx_mod_compile_options(-fvisibility=hidden -mlong-calls) nuttx_elf_compile_options_ifdef(CONFIG_UNWINDER_ARM -fno-unwind-tables -fno-asynchronous-unwind-tables) -# An ELF module needs r9 as its PIC base, so it must not also have the register -# fixed: GCC rejects that pair with "unable to use 'r9' for PIC register". This -# mirrors CELFFLAGS in common/Toolchain.defs, which filters --fixed-r9 back out -# of the inherited CFLAGS for the same reason. +if(CONFIG_FDPIC) -nuttx_elf_compile_options_ifdef(CONFIG_PIC -mpic-register=r9) + # An FDPIC module is a shared object whose two segments the loader places + # independently. The stock compiler emits correct FDPIC objects for both C + # and C++, so only the link needs the arm-uclinuxfdpiceabi linker: the stock + # one carries the armelf emulation alone and would turn every import into a + # jump slot where the ABI wants a function descriptor. -nuttx_elf_link_options_ifdef( - CONFIG_PIC --unresolved-symbols=ignore-in-object-files --emit-relocs) + if(NOT FDPIC_CROSSDEV) + set(FDPIC_CROSSDEV arm-uclinuxfdpiceabi-) + endif() + + set(CMAKE_ELF_LD + "${FDPIC_CROSSDEV}ld" + CACHE INTERNAL "Linker for FDPIC modules") + + nuttx_elf_compile_options(-mfdpic -fPIC -Wa,--noexecstack) + + nuttx_elf_link_options(-m armelf_linux_fdpiceabi -shared -z now) + +elseif(CONFIG_PIC) + + # An ELF module needs r9 as its PIC base, so it must not also have the + # register fixed: GCC rejects that pair with "unable to use 'r9' for PIC + # register". This mirrors CELFFLAGS in common/Toolchain.defs, which filters + # --fixed-r9 back out of the inherited CFLAGS for the same reason. -nuttx_elf_link_options_ifdef(CONFIG_BINFMT_ELF_RELOCATABLE -r) + nuttx_elf_compile_options(-mpic-register=r9) + + nuttx_elf_link_options(--unresolved-symbols=ignore-in-object-files + --emit-relocs) + +endif() + +# Not with CONFIG_PIC: there the module is linked as an executable, which is +# what common/Toolchain.defs does too. + +if(CONFIG_BINFMT_ELF_RELOCATABLE AND NOT CONFIG_PIC) + nuttx_elf_link_options(-r) +endif() nuttx_mod_link_options(-r) diff --git a/cmake/nuttx_add_application.cmake b/cmake/nuttx_add_application.cmake index 4864eaedbd683..bf4b626ce93cc 100644 --- a/cmake/nuttx_add_application.cmake +++ b/cmake/nuttx_add_application.cmake @@ -149,7 +149,15 @@ function(nuttx_add_application) if(TARGET STARTUP_OBJS) add_dependencies(${TARGET} STARTUP_OBJS) endif() - if(NOT "${CMAKE_LD}" MATCHES "gcc$") + # A module may need a different linker from the one that links the + # firmware: an FDPIC module does, because the stock linker cannot + # produce one. CMAKE_ELF_LD is that linker, and it is the ordinary one + # unless the architecture says otherwise. + + if(NOT CMAKE_ELF_LD) + set(CMAKE_ELF_LD ${CMAKE_LD}) + endif() + if(NOT "${CMAKE_ELF_LD}" MATCHES "gcc$") set(USE_LINKER True) endif() if(STACKSIZE) @@ -179,7 +187,7 @@ function(nuttx_add_application) POST_BUILD COMMAND # add default link option - ${CMAKE_LD} -T ${NUTTX_BINARY_DIR}/gnu-elf.ld + ${CMAKE_ELF_LD} -T ${NUTTX_BINARY_DIR}/gnu-elf.ld # add global MOD link option if dynlib link $<$:$> # add global ELF link option if m&kernel link From dfbab0c528f4f94a4bb667509cdb2996d83c5201 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Fri, 28 Aug 2026 18:42:26 +0200 Subject: [PATCH 3/3] arch/arm: Say which linker is missing when FDPIC has none. Without this the build says "arm-uclinuxfdpiceabi-ld: Command not found", which does not say what that is, where to get it, or that the prefix can be changed. The make build reports at the link rather than while parsing, so that a tree configured for FDPIC on a host without the linker can still be cleaned and reconfigured: an error at parse time takes make distclean with it. The cmake build reports while configuring, where nothing is built yet. Both name FDPIC_CROSSDEV, so a linker under another prefix can be used. Checked on mps3-an547:picostest with CONFIG_FDPIC and the linker off PATH: make distclean succeeds, and a module link stops with the message. With the linker present the modules build as before. Signed-off-by: Marco Casaroli --- arch/arm/src/cmake/elf.cmake | 15 ++++++++++++++- arch/arm/src/common/Toolchain.defs | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/arch/arm/src/cmake/elf.cmake b/arch/arm/src/cmake/elf.cmake index 91b1037d63129..ab09828847c8b 100644 --- a/arch/arm/src/cmake/elf.cmake +++ b/arch/arm/src/cmake/elf.cmake @@ -39,8 +39,21 @@ if(CONFIG_FDPIC) set(FDPIC_CROSSDEV arm-uclinuxfdpiceabi-) endif() + # Say which linker is missing rather than failing later with a command that + # cannot be run. + + find_program(FDPIC_LD "${FDPIC_CROSSDEV}ld") + + if(NOT FDPIC_LD) + message( + FATAL_ERROR + "CONFIG_FDPIC needs ${FDPIC_CROSSDEV}ld, which is not on PATH. " + "It is in the NuttX CI image, and tools/ci/docker/linux/Dockerfile " + "shows how it is built. Set FDPIC_CROSSDEV to use a different prefix") + endif() + set(CMAKE_ELF_LD - "${FDPIC_CROSSDEV}ld" + "${FDPIC_LD}" CACHE INTERNAL "Linker for FDPIC modules") nuttx_elf_compile_options(-mfdpic -fPIC -Wa,--noexecstack) diff --git a/arch/arm/src/common/Toolchain.defs b/arch/arm/src/common/Toolchain.defs index 949b7161573e9..a1d6580326384 100644 --- a/arch/arm/src/common/Toolchain.defs +++ b/arch/arm/src/common/Toolchain.defs @@ -653,6 +653,22 @@ ifeq ($(CONFIG_FDPIC),y) FDPIC_CROSSDEV ?= arm-uclinuxfdpiceabi- MODULELD = $(FDPIC_CROSSDEV)ld + # Say which linker is missing rather than letting make report a command it + # cannot run. The report is deferred to the link itself rather than made + # here, so that a tree configured for FDPIC on a host without the linker can + # still be cleaned and reconfigured. + + FDPIC_LD_FOUND := $(shell command -v $(MODULELD) 2> /dev/null) + + ifeq ($(FDPIC_LD_FOUND),) + FDPIC_NO_LD_MSG = CONFIG_FDPIC needs $(FDPIC_CROSSDEV)ld, which is not \ + on PATH. It is in the NuttX CI image, and \ + tools/ci/docker/linux/Dockerfile shows how it is built. Set \ + FDPIC_CROSSDEV to use a different prefix. + + MODULELD = $(SHELL) -c 'echo "ERROR: $(FDPIC_NO_LD_MSG)" 1>&2; exit 1' -- + endif + CELFFLAGS += -mfdpic -fPIC -Wa,--noexecstack CXXELFFLAGS += -mfdpic -fPIC -Wa,--noexecstack