Conversation
3bdf075 to
5011bd0
Compare
|
6123697 to
f0e23e1
Compare
f0e23e1 to
b3eb1a6
Compare
b67a0d3 to
c99788d
Compare
c99788d to
c931d48
Compare
9aac60f to
276b6a8
Compare
276b6a8 to
d07b099
Compare
d07b099 to
5fa4270
Compare
The fixtures were built by makefiles and scripts in nuttx/tools/fdpic. Review of apache/nuttx#19940 asked that NuttX not carry a module build of its own, so the build moves here, beside the sources it builds. They keep a build of their own because they are loader edge cases: a library with a SONAME, a module with more DT_NEEDED entries than the loader follows, one whose imports stay in the lazy binding table, and one naming a symbol the firmware does not export. Application.mk cannot say any of that. The build uses the configured tree's module flags (CMODULEFLAGS, CXXMODULEFLAGS, LDMODULEFLAGS), linker script and crt0 source. Three things are pinned instead, so that the committed headers do not depend on how the tree is configured: the CPU (cortex-m3, so one set of headers runs on both v7-M and v8-M), no debug information, and -Os. A library is compiled with default visibility, because the module flags hide every symbol and a library must export its functions. fdpic-embed.py becomes xxd and a template. fdpic-verify.sh and nuttx-exports.sh go with no replacement: they checked at build time what the xipfs suite already asserts at run time. The headers are regenerated, and the .fdpic suffix is gone: a module is named like any other module. A fixture now starts in the tree's crt0, which needs apache/nuttx#20388 to run its constructors, and the library fixtures need apache/nuttx#20368. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
| ****************************************************************************/ | ||
|
|
||
| /* With an address environment the program lives in its own address space, | ||
| * which a library loaded by libelf_insert() cannot reach. |
There was a problem hiding this comment.
but still can reach as per task list, so let's remove LIBELF_NEEDED and use CONFIG_LIBC_ELF_MAXDEPEND directly
There was a problem hiding this comment.
Without this guard qemu-armv8a:knsh does not link. We could fix ld-kernel.script for qemu-armv8a so that it links, but DT_NEEDED would still not work: libelf_insert() loads the library into kernel memory, which the task's address environment does not map.
Ideally the library is loaded only once and mapped into each task that needs it. That needs reference counting across tasks and a much larger design change, so I would like to do it in follow-up PRs. The other option is to load the library into the task's own address space, but then a library used by several tasks is copied into every one of them.
5fa4270 to
43db246
Compare
The fixtures were built by makefiles and scripts in nuttx/tools/fdpic. Review of apache/nuttx#19940 asked that NuttX not carry a module build of its own, so the build moves here, beside the sources it builds. They keep a build of their own because they are loader edge cases: a library with a SONAME, a module with more DT_NEEDED entries than the loader follows, one whose imports stay in the lazy binding table, and one naming a symbol the firmware does not export. Application.mk cannot say any of that. The build uses the configured tree's module flags (CMODULEFLAGS, CXXMODULEFLAGS, LDMODULEFLAGS), linker script and crt0 source. Three things are pinned instead, so that the committed headers do not depend on how the tree is configured: the CPU (cortex-m3, so one set of headers runs on both v7-M and v8-M), no debug information, and -Os. A library is compiled with default visibility, because the module flags hide every symbol and a library must export its functions. fdpic-embed.py becomes xxd and a template. fdpic-verify.sh and nuttx-exports.sh go with no replacement: they checked at build time what the xipfs suite already asserts at run time. The headers are regenerated, and the .fdpic suffix is gone: a module is named like any other module. A fixture now starts in the tree's crt0, which needs apache/nuttx#20388 to run its constructors, and the library fixtures need apache/nuttx#20368. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
A module that names a shared library in DT_NEEDED now gets it loaded and its imports bound against it, rather than being refused. libelf_insert() does the loading, which is what dlopen() calls anyway: the library lands in the module registry like anything else, its exports come back through libelf_getsymbol() -- the same call dlsym() uses -- and a library named by two modules is loaded once. A bare name is looked for along LD_LIBRARY_PATH, where dlopen() looks for it. Undefined symbols resolve against the globally registered symbols first, then the modules this one depends on, then the table exec() supplied. Nothing here calls into dlfcn, because this loader is also the kernel's module loader, which has none. Each library becomes one of the module's dependencies[], and the dependency holds it in place of the reference libelf_insert() took. So a library loaded only for DT_NEEDED is kept by the modules that depend on it, and libelf_undepend() unloads it with the last of them; one that dlopen() or insmod also opened stays until that reference goes too. CONFIG_LIBC_ELF_MAXDEPEND bounds how many libraries a module may name, which is what it already meant. Six things had to be fixed to make it work, none of which a build shows. reldata was a file-scope global. Loading a library from inside libelf_relocatedyn() makes that function reentrant, so the nested load overwrote the outer one's relocation offsets and the module resumed binding with the library's DT_REL. It is now per call. A cross-object call needs the callee's data base, not the caller's. A symbol resolved from an FDPIC library comes back as a descriptor, and R_ARM_FUNCDESC_VALUE was treating it as a code address and pairing it with the importing module's GOT. It now copies both words, so the library runs with its own. An object with no imports has no PLT and so no DT_PLTGOT, but it still has a GOT and still has to be entered with it. Without the fallback its descriptors carried a data base of zero and the library read its globals through a null pointer. R_ARM_FUNCDESC, a pointer to a descriptor, wrapped a library's descriptor in a second one. It now stores the library's descriptor as it is. The flag that says a resolved value is a descriptor was set only for an import and never cleared, so the next relocation against a symbol of the module itself took that symbol for a descriptor too. It is cleared there. libelf_symname() was static, and reading a DT_NEEDED name needs it. A module with DT_NEEDED is refused where CONFIG_LIBC_ELF_MAXDEPEND is zero, since that is where the dependency logic is compiled out. A DT_NEEDED library is one shared instance, its data included, because the loader returns the object already in the registry. A module started with exec() is different: that path loads the module afresh each time, so two running instances have separate data while sharing one copy of the text. Built for mps3-an547:picostest with CONFIG_FDPIC both ways. Run on mps2-an500:xipfs under QEMU: fdpicxip solib loads libcounter.so by name out of DT_NEEDED, two instances share one pinned copy of its text, and the library is unloaded, and its pin given back, when the second one exits. A library also opened with dlopen() stays loaded after its DT_NEEDED user exits, and dlclose() unloads it. With CONFIG_ARCH_ADDRENV the program runs in its own address space, which a library libelf_insert() loads cannot reach, so DT_NEEDED is refused there as before. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
43db246 to
22feb12
Compare
Summary
[8/10]#20131 is merged, so an FDPIC module loads and runs. A module that names a shared library inDT_NEEDEDstill has nothing to load it with, and this loads it and binds the module's imports against it.libelf_insert()does the loading, which is whatdlopen()calls anyway: the library lands in the module registry like anything else, its exports come back throughlibelf_getsymbol(), which is the calldlsym()uses, and a library named by two modules is loaded once. A bare name is looked for alongLD_LIBRARY_PATH, wheredlopen()looks for it. Undefined symbols resolve against the globally registered symbols first, then the modules this one depends on, then the tableexec()supplied. Nothing here calls into dlfcn, because this loader is also the kernel's module loader, which has none.Six things had to be fixed to make it work, none of which a build shows.
reldatawas a file-scope global. Opening a library from insidelibelf_relocatedyn()makes that function reentrant, so the nested load overwrote the outer one's relocation offsets and the module resumed binding with the library'sDT_REL. It is per call now.A cross-object call needs the callee's data base, not the caller's. A symbol resolved from an FDPIC library comes back as a descriptor, and
R_ARM_FUNCDESC_VALUEwas treating it as a code address and pairing it with the importing module's GOT. It copies both words now, so the library runs with its own.An object with no imports has no PLT and so no
DT_PLTGOT, but it still has a GOT and still has to be entered with it. Without the fallback its descriptors carried a data base of zero and the library read its globals through a null pointer.R_ARM_FUNCDESC, a pointer to a descriptor, wrapped a library's descriptor in a second one. It now stores the library's descriptor as it is.The flag that says a resolved value is a descriptor was set only for an import and never cleared, so the next relocation against a symbol of the module itself took that symbol for a descriptor too. It is cleared there.
libelf_symname()was static, and reading aDT_NEEDEDname needs it.Impact
Nothing happens without
CONFIG_LIBC_DLFCN: a module withDT_NEEDEDis refused there, as before, since there is no way to load what it asks for.With
CONFIG_ARCH_ADDRENVa module withDT_NEEDEDis refused too, as before: the program runs in its own address space, which a librarylibelf_insert()loads cannot reach.dlopen()is still a stub in kernel builds, so neither path exists there yet.A
DT_NEEDEDlibrary is one shared instance, its data included, because the loader returns the object already in the registry. A module started withexec()is different: that path loads the module afresh each time, so two running instances have separate data while sharing one copy of the text.Each library becomes one of the module's
dependencies[], and the dependency holds it in place of the referencelibelf_insert()took. A library loaded only for DT_NEEDED is kept by the modules that depend on it, andlibelf_undepend()unloads it with the last of them; one thatdlopen()orinsmodalso opened stays until that reference goes too.CONFIG_LIBC_ELF_MAXDEPENDbounds how many libraries one module may name, which is what it already meant.Testing
mps3-an547:picostestbuilds three ways on master: withCONFIG_FDPICoff, with it on, and with it on plusCONFIG_LIBC_DLFCN, which is the path this patch adds. WithCONFIG_FDPICon, the applications the configuration carries are FDPIC objects (OS/ABI: ARM FDPIC).mps2-an500:xipfswithCONFIG_FDPICunder QEMU,xipfs_test fdpic, with the module fixtures rebuilt by the tree's module flags (apache/nuttx-apps#3762) and #20388: 34 passed, 0 failed. Without the last two fixes the C++ library tests crash.qemu-armv8a:knsh(CONFIG_ARCH_ADDRENV) links with-Werror; in a flat buildelf_bind.ois unchanged by the ADDRENV guard.tools/checkpatch.sh -c -u -m -gpasses.Review
Everything below this in the series is merged, so it is one commit on master.