libs/libc/elf: Place an FDPIC object's segments independently.

An ET_DYN object is loaded into one allocation with its data behind its
text, because its data references sit at a fixed distance from the code
that makes them.  An FDPIC object does not work that way: it reaches its
data through a base register, so the two segments can be placed wherever
suits, and the point of the format is that the read-only one is left on
the media and executed there while only the writable one is copied.  One
copy of the text then serves every instance.

So libelf_load() grows a second case.  The object announces itself in the
OS/ABI byte, which is noted once in libelf_loadhdrs() rather than
re-derived; e_flags cannot be used for this, as an FDPIC object's are an
unremarkable EABI version and testing them would reject every valid
module.  Text is taken from the media address plus the segment's own file
offset -- the same arithmetic the ET_REL path already does with
sh_offset -- and libelf_loadfile() does not read it.  If the filesystem
cannot show its media, the loader copies the text to RAM instead.  The
module then loses the shared text and the flash saving, but it runs.

Obtaining that address needs two mechanisms, and they are not
interchangeable.  A compacting filesystem can move a file's blocks, so it
hands out an address only with a pin that holds them still and expects
the pin back; xipfs is the one in tree.  A filesystem whose layout never
changes has nothing to hold and answers FIOC_XIPBASE with a bare address;
romfs and tmpfs are those.  libelf_xipacquire() asks for the pin first,
because a filesystem that needs one is not safe without it, and
libelf_unload() gives it back.  The loader asks for a pin only if it can
hold one, or the pin would stay for ever.

The pin is thus not specific to FDPIC.  Any module that executes in place
from a compacting filesystem takes one, and gives it back at unload.

mmap() is not used, though both filesystems implement it.  The mapping
would be recorded against whichever task called the loader, while the
release happens when the module's own task exits, which is a different
group -- so the pin would outlive the module and the extent would never
become movable again.

Unloading has to change with placement: the existing path frees only
textalloc because ET_DYN had a single allocation, which would leak an
FDPIC object's data and free media the filesystem only lent us.

Nothing here runs for a non-FDPIC object; every branch is behind the flag
and the single-allocation path is untouched.  Built and booted
mps3-an547:picostest, which is CONFIG_ELF with CONFIG_PIC, with no change
in behaviour.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
This commit is contained in:
Marco Casaroli 2026-08-03 00:56:27 +02:00 committed by Alan C. Assis
parent 9f1107862b
commit 1aa32bbc07
10 changed files with 342 additions and 25 deletions

View file

@ -41,6 +41,11 @@
#define EM_ARCH EM_ARM
/* An object built for the FDPIC ABI says so in the OS/ABI byte. */
#define ELF_IS_FDPIC(ehdr) \
((ehdr)->e_ident[EI_OSABI] == ELFOSABI_ARM_FDPIC)
/* Table 4-2, ARM-specific e_flags */
#define EF_ARM_EABI_MASK 0xff000000
@ -206,6 +211,18 @@
#define R_ARM_THM_TLS_DESCSEQ16 129 /* Thumb16 */
#define R_ARM_THM_TLS_DESCSEQ32 130 /* Thumb32 */
/* FDPIC relocations. Values from the ARM FDPIC ABI as implemented by
* binutils (include/elf/arm.h).
*/
#define R_ARM_GOTFUNCDESC 161 /* Data GOT entry holding a descriptor */
#define R_ARM_GOTOFFFUNCDESC 162 /* Data GOT-relative descriptor */
#define R_ARM_FUNCDESC 163 /* Data Address of a descriptor */
#define R_ARM_FUNCDESC_VALUE 164 /* Data The descriptor itself: {code, GOT} */
#define R_ARM_TLS_GD32_FDPIC 165 /* Data */
#define R_ARM_TLS_LDM32_FDPIC 166 /* Data */
#define R_ARM_TLS_IE32_FDPIC 167 /* Data */
/* Processor specific values for the Phdr p_type field. */
#define PT_ARM_EXIDX (PT_LOPROC + 1) /* ARM unwind segment. */

View file

@ -94,6 +94,17 @@ config PSEUDOFS_ATTRIBUTES
Enable support for attributes(e.g. mode, uid, gid and time)
in the pseudo file system.
config FS_PIN
bool
default n
---help---
Selected by a file system that can pin a file's blocks in place and
hand out their media address, so that a module's read-only segment
can be executed where it already lies instead of being copied.
The pin is held for as long as the module is loaded and given back
when it is unloaded.
config FS_PERMISSION
bool "Enable UNIX Filesystem Permission Support"
default n

View file

@ -7,6 +7,7 @@ config FS_XIPFS
bool "XIPFS contiguous execute-in-place file system"
default n
depends on !DISABLE_MOUNTPOINT && MTD
select FS_PIN
---help---
Enable support for XIPFS, a file system that stores each file as a
single physically contiguous, erase-block aligned extent on memory

View file

@ -149,6 +149,7 @@
#define ELFOSABI_MODESTO 11 /* Novell Modesto. */
#define ELFOSABI_OPENBSD 12 /* OpenBSD. */
#define ELFOSABI_ARM_AEABI 64 /* ARM EABI */
#define ELFOSABI_ARM_FDPIC 65 /* ARM FDPIC */
#define ELFOSABI_ARM 97 /* ARM */
#define ELFOSABI_STANDALONE 255 /* Standalone (embedded) application */

View file

@ -37,6 +37,12 @@
#define ELF_PRARGSZ (80) /* Number of chars for args */
/* An architecture that has no FDPIC ABI recognises no FDPIC object. */
#ifndef ELF_IS_FDPIC
# define ELF_IS_FDPIC(ehdr) false
#endif
/****************************************************************************
* Public Types
****************************************************************************/

View file

@ -44,6 +44,19 @@
# define CONFIG_LIBC_ELF_MAXDEPEND 0
#endif
/* A compacting filesystem gives its media address with a pin that holds the
* blocks in place. The loader holds the pin through a file reference,
* because the unload runs on another task.
*
* That reference is taken with file_get() and file_dup2(), which are kernel
* side, so this is for the copy of libc that runs there.
*/
#if defined(CONFIG_FS_PIN) && \
(defined(CONFIG_BUILD_FLAT) || defined(__KERNEL__))
# define HAVE_LIBC_ELF_PIN 1
#endif
#ifndef CONFIG_LIBC_ELF_ALIGN_LOG2
# define CONFIG_LIBC_ELF_ALIGN_LOG2 2
#endif
@ -123,6 +136,7 @@ typedef CODE int (*mod_uninitializer_t)(FAR void *arg);
* nexports - The number of symbols in the exported symbol table.
*/
struct file;
struct symtab_s;
struct mod_info_s
{
@ -252,6 +266,16 @@ struct mod_loadinfo_s
* skip the copy.
*/
/* True if e_ident[EI_OSABI] marked this an FDPIC object. */
bool fdpic;
#ifdef HAVE_LIBC_ELF_PIN
/* The file the pin is held through, handed to the module once it loads. */
FAR struct file *pinfile;
#endif
/* Address environment.
*
* addrenv - This is the handle created by addrenv_allocate() that can be

View file

@ -349,4 +349,26 @@ int libelf_addrenv_restore(FAR struct mod_loadinfo_s *loadinfo);
void libelf_addrenv_free(FAR struct mod_loadinfo_s *loadinfo);
#endif /* CONFIG_ARCH_ADDRENV */
#ifdef HAVE_LIBC_ELF_PIN
/****************************************************************************
* Name: libelf_pinrelease
*
* Description:
* Give back an XIP pin that the loader took, and the file that holds it.
* Does nothing if the loader took no pin.
*
* Input Parameters:
* pinfile - The held file. Cleared on return.
*
* Returned Value:
* None.
*
****************************************************************************/
void libelf_pinrelease(FAR struct file **pinfile);
#endif
#endif /* __LIBS_LIBC_LIBC_ELF_LIBC_ELF_H */

View file

@ -29,6 +29,7 @@
#include <sys/param.h>
#include <sys/types.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@ -40,6 +41,7 @@
#include <nuttx/arch.h>
#include <nuttx/lib/elf.h>
#include <nuttx/fs/fs.h>
#include <nuttx/fs/ioctl.h>
#include "libc.h"
@ -364,6 +366,13 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo)
{
if (phdr->p_flags & PF_X)
{
if (loadinfo->fdpic && loadinfo->xipbase != 0)
{
/* Mapped, not copied. */
continue;
}
ret = libelf_read(loadinfo, buffer_data_address(text),
phdr->p_filesz,
phdr->p_offset);
@ -539,10 +548,113 @@ skipload:
return OK;
}
/****************************************************************************
* Name: libelf_xipacquire
*
* Description:
* Ask the filesystem for the address of this file on its media, so the
* read-only part of the object can run where it lies. Ask for a pin
* first: a compacting filesystem is not safe without one. Do not ask at
* all if this build cannot hold a pin.
*
* Returned Value:
* Zero if an address was obtained, a negated errno otherwise. Callers
* that can live without one may ignore the failure.
*
****************************************************************************/
#ifdef HAVE_LIBC_ELF_PIN
static int libelf_pinhold(FAR struct mod_loadinfo_s *loadinfo)
{
FAR struct file *filep;
int ret;
/* The descriptor belongs to the task that called the loader, and the
* unload runs on another task. Hold the file instead.
*/
loadinfo->pinfile = lib_zalloc(sizeof(struct file));
if (loadinfo->pinfile == NULL)
{
return -ENOMEM;
}
ret = file_get(loadinfo->filfd, &filep);
if (ret >= 0)
{
ret = file_dup2(filep, loadinfo->pinfile);
file_put(filep);
}
if (ret < 0)
{
lib_free(loadinfo->pinfile);
loadinfo->pinfile = NULL;
}
return ret;
}
#endif
static int libelf_xipacquire(FAR struct mod_loadinfo_s *loadinfo)
{
uintptr_t base = 0;
#ifdef HAVE_LIBC_ELF_PIN
if (ioctl(loadinfo->filfd, XIPFSIOC_PIN, (unsigned long)&base) >= 0)
{
int ret = libelf_pinhold(loadinfo);
if (ret < 0)
{
berr("ERROR: Failed to hold the pinned file: %d\n", ret);
ioctl(loadinfo->filfd, XIPFSIOC_UNPIN, 0);
return ret;
}
loadinfo->xipbase = base;
binfo("pinned xipbase %" PRIxPTR "\n", loadinfo->xipbase);
return OK;
}
#endif
if (ioctl(loadinfo->filfd, FIOC_XIPBASE, (unsigned long)&base) >= 0)
{
loadinfo->xipbase = base;
binfo("can use xipbase %" PRIxPTR "\n", loadinfo->xipbase);
return OK;
}
return -ENOTTY;
}
/****************************************************************************
* Public Functions
****************************************************************************/
#ifdef HAVE_LIBC_ELF_PIN
/****************************************************************************
* Name: libelf_pinrelease
*
* Description:
* Give back an XIP pin and the file it was held through, so the
* filesystem can reclaim the extent.
*
****************************************************************************/
void libelf_pinrelease(FAR struct file **pinfile)
{
if (*pinfile != NULL)
{
file_ioctl(*pinfile, XIPFSIOC_UNPIN, 0);
file_close(*pinfile);
lib_free(*pinfile);
*pinfile = NULL;
}
}
#endif
/****************************************************************************
* Name: libelf_load
*
@ -559,6 +671,7 @@ skipload:
int libelf_load(FAR struct mod_loadinfo_s *loadinfo)
{
int ret;
int i;
binfo("loadinfo: %p\n", loadinfo);
DEBUGASSERT(loadinfo && loadinfo->filfd >= 0);
@ -576,11 +689,7 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo)
if (loadinfo->gotindex >= 0)
{
binfo("GOT section found! index %d\n", loadinfo->gotindex);
if (ioctl(loadinfo->filfd, FIOC_XIPBASE,
(unsigned long)&loadinfo->xipbase) >= 0)
{
binfo("can use xipbase %zu\n", loadinfo->xipbase);
}
libelf_xipacquire(loadinfo);
}
/* Determine total size to allocate */
@ -647,21 +756,96 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo)
}
else if (loadinfo->ehdr.e_type == ET_DYN)
{
loadinfo->textalloc = (uintptr_t)lib_memalign(loadinfo->textalign,
loadinfo->textsize +
loadinfo->datasize +
loadinfo->segpad);
if (!loadinfo->textalloc)
if (loadinfo->fdpic)
{
berr("ERROR: Failed to allocate memory for the module\n");
ret = -ENOMEM;
goto errout_with_buffers;
}
/* The two segments are placed independently, thus only the
* writable segment is allocated, once per instance.
*/
loadinfo->datastart = loadinfo->textalloc +
loadinfo->textsize +
loadinfo->segpad;
if (loadinfo->xipbase != 0)
{
/* The text stays on the media. The media address is the base
* of the file, thus add the file offset of the segment.
*/
for (i = 0; i < loadinfo->ehdr.e_phnum; i++)
{
FAR Elf_Phdr *phdr = &loadinfo->phdr[i];
if (phdr->p_type == PT_LOAD &&
(phdr->p_flags & PF_X) != 0)
{
loadinfo->textalloc = loadinfo->xipbase +
phdr->p_offset;
break;
}
}
}
else if (loadinfo->textsize > 0)
{
/* The filesystem cannot show its media, thus copy the text
* to RAM. The instances no longer share it.
*/
# if defined(CONFIG_ARCH_USE_TEXT_HEAP) && \
defined(CONFIG_ARCH_USE_SEPARATED_SECTION)
loadinfo->textalloc = (uintptr_t)
up_textheap_memalign(".text",
loadinfo->textalign,
loadinfo->textsize);
# elif defined(CONFIG_ARCH_USE_TEXT_HEAP)
loadinfo->textalloc = (uintptr_t)
up_textheap_memalign(loadinfo->textalign,
loadinfo->textsize);
# else
loadinfo->textalloc = (uintptr_t)
lib_memalign(loadinfo->textalign,
loadinfo->textsize);
# endif
if (loadinfo->textalloc == 0)
{
berr("ERROR: Failed to allocate the module's text\n");
ret = -ENOMEM;
goto errout_with_buffers;
}
}
if (loadinfo->datasize > 0)
{
loadinfo->datastart =
(uintptr_t)lib_memalign(loadinfo->dataalign,
loadinfo->datasize);
if (!loadinfo->datastart)
{
berr("ERROR: Failed to allocate the module's data\n");
ret = -ENOMEM;
goto errout_with_buffers;
}
}
}
else
{
/* Everything else keeps text and data adjacent: one allocation,
* data behind text.
*/
loadinfo->textalloc = (uintptr_t)
lib_memalign(loadinfo->textalign,
loadinfo->textsize +
loadinfo->datasize +
loadinfo->segpad);
if (!loadinfo->textalloc)
{
berr("ERROR: Failed to allocate memory for the module\n");
ret = -ENOMEM;
goto errout_with_buffers;
}
loadinfo->datastart = loadinfo->textalloc +
loadinfo->textsize +
loadinfo->segpad;
}
}
#endif /* CONFIG_LIBC_ELF_LOADTO_LMA */
@ -732,11 +916,7 @@ int libelf_load_with_addrenv(FAR struct mod_loadinfo_s *loadinfo)
if (loadinfo->gotindex >= 0)
{
binfo("GOT section found! index %d\n", loadinfo->gotindex);
if (ioctl(loadinfo->filfd, FIOC_XIPBASE,
(unsigned long)&loadinfo->xipbase) >= 0)
{
binfo("can use xipbase %zu\n", loadinfo->xipbase);
}
libelf_xipacquire(loadinfo);
}
/* Determine total size to allocate */

View file

@ -29,6 +29,7 @@
#include <assert.h>
#include <errno.h>
#include <nuttx/debug.h>
#include <nuttx/elf.h>
#include <nuttx/lib/elf.h>
@ -66,6 +67,23 @@ int libelf_loadhdrs(FAR struct mod_loadinfo_s *loadinfo)
/* Verify that there are sections */
/* The architecture knows how an FDPIC object announces itself. Ask it
* once.
*/
loadinfo->fdpic = ELF_IS_FDPIC(&loadinfo->ehdr);
/* A module is a shared object. An FDPIC object that is anything else
* would be placed through the wrong path.
*/
if (loadinfo->fdpic && loadinfo->ehdr.e_type != ET_DYN)
{
berr("ERROR: FDPIC object is not a shared object: e_type=%u\n",
loadinfo->ehdr.e_type);
return -ENOEXEC;
}
if (loadinfo->ehdr.e_shnum < 1)
{
berr("ERROR: No sections(?)\n");

View file

@ -59,6 +59,17 @@ int libelf_unload(FAR struct mod_loadinfo_s *loadinfo)
libelf_freebuffers(loadinfo);
#ifdef HAVE_LIBC_ELF_PIN
/* Give the pin back if the loader took one, so the filesystem can
* reclaim the extent.
*/
if (loadinfo->pinfile != NULL)
{
libelf_pinrelease(&loadinfo->pinfile);
}
#endif
#ifdef CONFIG_ARCH_ADDRENV
if (loadinfo->addrenv != NULL)
{
@ -68,9 +79,35 @@ int libelf_unload(FAR struct mod_loadinfo_s *loadinfo)
#endif
/* Release memory holding the relocated ELF image */
/* ET_DYN has a single allocation so we only free textalloc */
/* An FDPIC object placed its two segments separately. Free each one. If
* the text stayed on the media, it was never allocated, thus leave it.
*/
if (loadinfo->ehdr.e_type != ET_DYN)
if (loadinfo->fdpic)
{
if (loadinfo->textalloc != 0 && loadinfo->xipbase == 0)
{
#ifdef CONFIG_ARCH_USE_TEXT_HEAP
up_textheap_free((FAR void *)loadinfo->textalloc);
#else
lib_free((FAR void *)loadinfo->textalloc);
#endif
}
if (loadinfo->datastart != 0)
{
lib_free((FAR void *)loadinfo->datastart);
loadinfo->datastart = 0;
}
loadinfo->textalloc = 0;
loadinfo->textsize = 0;
loadinfo->datasize = 0;
}
/* Any other ET_DYN has a single allocation so we only free textalloc */
else if (loadinfo->ehdr.e_type != ET_DYN)
{
#ifdef CONFIG_ARCH_USE_SEPARATED_SECTION
int i;