Skip to content

libs: Keep the coverage of a loadable module with CONFIG_COVERAGE_MINI. - #20813

Merged
acassis merged 3 commits into
apache:masterfrom
casaroli:cmake-module-no-coverage
Oct 22, 2026
Merged

acassis merged 3 commits into
apache:masterfrom
casaroli:cmake-module-no-coverage

Conversation

@casaroli

@casaroli casaroli commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

CONFIG_COVERAGE_ALL adds -fprofile-arcs -ftest-coverage (clang: -fprofile-instr-generate -fcoverage-mapping) to the compile options. A loadable module of a flat or protected build takes them too: in CMake through the targets of nuttx_add_application(), and in both build systems through crt0. The module then refers to __gcov_init(), __gcov_merge_add() and __gcov_exit(). The image has them, but the system symbol table does not export them, so no module loads:

libelf_symvalue: ERROR: SHN_UNDEF: Exported symbol "__gcov_init" not found
nsh: hello: command not found

Exporting the functions is not enough. __gcov_init() adds the gcov_info of the module to the list of the runtime, and the list keeps it after the module is unloaded, so the next dump or reset reads freed memory. What a module can get depends on the runtime:

  • CONFIG_COVERAGE_MINI with GCC (libs/libbuiltin/libgcc/gcov.c): modules keep their coverage. __gcov_unload() removes from the list each gcov_info in the memory of a module, and libelf_uninit() calls it before it frees the module (dlclose(), rmmod(), the exit of an exec() program). A mutex protects the list. libc.csv exports __gcov_init, __gcov_merge_add, __gcov_exit, __gcov_fork, __gcov_dump and __gcov_reset.
  • CONFIG_COVERAGE_TOOLCHAIN (libgcov), CONFIG_COVERAGE_COMPILER_RT and clang with CONFIG_COVERAGE_MINI: not changed here, as agreed in the review. libgcov keeps its functions hidden, has no call to remove one gcov_info, and the __gcov_exit() of a module would dump and unlink the data of the whole image; the clang runtimes find their data through the linker sections of the image. A module built with the coverage options still does not load with these runtimes; a later PR can handle them.

The unload does not dump the data of the module: the dump writes the data of all object files and can run in the context of an exiting task. To keep the data of a module, dump it while the module is loaded (gcov -d <path>, or __gcov_dump() in the module). The documentation commit describes both behaviours in Documentation/os/libs/libbuiltin.rst.

Impact

  • CONFIG_COVERAGE_MINI with GCC and loadable modules: the modules load and have coverage data. struct module_s always keeps textsize and datasize (two size_t, before only with the module procfs).
  • The other coverage runtimes: no change.
  • No coverage, or a kernel build: no change. Without CONFIG_COVERAGE_MINI, __gcov_unload() is an empty macro.

Testing

qemu-armv8a:nsh (flat, Arm GNU 14.2, QEMU HVF) with CONFIG_COVERAGE_ALL, CONFIG_COVERAGE_MINI, CONFIG_ELF, CONFIG_LIBC_DLFCN, CONFIG_EXECFUNCS_SYSTEM_SYMTAB, CONFIG_MM_FILL_ALLOCATIONS (freed memory is overwritten) and CONFIG_NAME_MAX=255 (for the long .gcda names). hello and two test programs as instrumented modules from a 9p share; get_environ_ptr exported for the test as in #20807:

Step Result
hello, gcovt (crt0.o has 9 gcov symbols, the modules refer to __gcov_init) load and run
gcovt 9 0 dump (the module calls __gcov_dump()) 863 .gcda files, with the one of gcovt
gcovt 11 3 &, then gcov -d while it sleeps .gcda of gcovt; gcov on the host: the loop line ran 12 times, odd++ 5 times
25 x gcovt <n> + 25 x hello, then gcov -r -d 861 files, none of a module (their data is gone with them)
gcovdl 40: 40 x dlopen() / dlclose() of an instrumented libgcovso.so, __gcov_reset() after each dlclose(), __gcov_dump() gcovdl: PASS
ostest Exiting with status 0
The same test without the call in libelf_uninit() the first dump after an unload never returns (it walks the freed gcov_info of hello)

The CMake build of the same configuration: crt0 has the 9 gcov symbols, gcovt loads 21 times and dumps its data.

qemu-armv8a:knsh (kernel build, HVF): ostest exits with status 0, as does ostest on the default qemu-armv8a:nsh. The programs of the export (init, sh, hello, ostest, getprime, dd) are byte-identical to master, except the commit and build-date string in sh and init.

This version (rebased on master d748a36), qemu-armv8a:nsh with CONFIG_COVERAGE_ALL, apps master, hello as a loadable module from a 9p share: CONFIG_COVERAGE_MINI with Make: hello and crt0 keep coverage, Hello, World!!; with CMake: the same symbols. CONFIG_COVERAGE_TOOLCHAIN: hello does not load, as on master.

tools/checkpatch.sh -c -u -m -g passes.

Comment thread tools/Config.mk Resolved
@github-actions github-actions Bot added Area: Build system Arch: arm Issues related to ARM (32-bit) architecture Arch: arm64 Issues related to ARM64 (64-bit) architecture Arch: risc-v Issues related to the RISC-V (32-bit or 64-bit) architecture Arch: x86_64 Issues related to the x86_64 architecture Size: S The size of the change in this PR is small labels Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

s698pm-dkit

  • Code: .text +16 B (+0.0%, 379,216 B)

No memory changes detected for:


Catch memory regressions in CI with MemBrowse. Free for open source.

@casaroli casaroli changed the title cmake, arch: Build a loadable module without the coverage options. cmake, arch: Keep coverage in a module only with CONFIG COVERAGE MINI. Oct 19, 2026
@casaroli
casaroli force-pushed the cmake-module-no-coverage branch from ecaee95 to d1212c8 Compare October 19, 2026 22:32
@casaroli
casaroli requested review from Donny9 and yamt as code owners October 19, 2026 22:32
@github-actions github-actions Bot added Size: M The size of the change in this PR is medium Area: BINFMT and removed Area: Build system Size: S The size of the change in this PR is small labels Oct 19, 2026
@casaroli
casaroli force-pushed the cmake-module-no-coverage branch from d1212c8 to c3f244b Compare October 20, 2026 22:27
@github-actions github-actions Bot added Board: simulator and removed Arch: risc-v Issues related to the RISC-V (32-bit or 64-bit) architecture Arch: x86_64 Issues related to the x86_64 architecture labels Oct 21, 2026
Comment thread libs/libc/libc.csv Resolved
Comment thread tools/Config.mk Resolved
With CONFIG_COVERAGE_MINI, __gcov_init() adds the gcov_info of each
object file to a list, and __gcov_dump() and __gcov_reset() walk this
list.  A loadable module that is built with the coverage options adds
its own gcov_info, which is in the memory of the module.  After the
module is unloaded, the list still points to this memory, and the next
dump or reset reads freed memory.

Add __gcov_unload(), which removes from the list each gcov_info in an
address range.  libelf_uninit() calls it for the memory of a module
before it frees the memory.  This is the path of dlclose(), rmmod() and
the exit of a program that exec() loaded.  struct module_s now always
keeps the sizes of the text and the data, and binfmt saves them too.  A
mutex protects the list, because a module can load or unload while a
dump runs.

The unload does not dump the data of the module.  The dump writes the
data of all object files, and it can run in the context of a task that
exits.  To keep the data of a module, dump it while the module is
loaded: with "gcov -d", or with a call to __gcov_dump() in the module.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Code that is built with the GCC coverage options calls __gcov_init()
from a constructor and __gcov_exit() from a destructor, refers to
__gcov_merge_add(), and calls __gcov_fork() in place of fork().  With
CONFIG_COVERAGE_MINI the image has these functions, but the system
symbol table does not export them, so an instrumented module does not
load: "Exported symbol "__gcov_init" not found".

Declare them in gcov.h and add them to libc.csv, together with
__gcov_dump() and __gcov_reset(), so that a module can also dump or
reset the coverage data.  The other coverage runtimes do not change:
libgcov of CONFIG_COVERAGE_TOOLCHAIN keeps its functions hidden, and
the clang runtimes need other functions.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
A loadable module of a flat or protected build can use the coverage of
CONFIG_COVERAGE_ALL with CONFIG_COVERAGE_MINI and GCC, and its data
goes away when the module is unloaded.  Say so, say how to keep the
data, and say that the other runtimes do not export their functions.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@casaroli casaroli changed the title cmake, arch: Keep coverage in a module only with CONFIG COVERAGE MINI. libs: Keep the coverage of a loadable module with CONFIG_COVERAGE_MINI. Oct 21, 2026
@casaroli
casaroli force-pushed the cmake-module-no-coverage branch from c3f244b to c1d976f Compare October 21, 2026 11:54
@github-actions github-actions Bot removed Arch: arm Issues related to ARM (32-bit) architecture Arch: arm64 Issues related to ARM64 (64-bit) architecture Board: simulator labels Oct 21, 2026
@acassis
acassis merged commit 8b6495a into apache:master Oct 22, 2026
54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: BINFMT Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants