Skip to content

libs/libc/tls: Keep a copy of each __thread variable per thread. - #20723

Open
casaroli wants to merge 4 commits into
apache:masterfrom
casaroli:arm64-tls-tpidr
Open

casaroli wants to merge 4 commits into
apache:masterfrom
casaroli:arm64-tls-tpidr

Conversation

@casaroli

@casaroli casaroli commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Four commits for __thread variables with a toolchain that has no native TLS. The arm64 TPIDR_EL0 work is in #20821.

  1. boards: Five boards linked libgcc inside the group of the user libraries of a protected build (at32f437-mini, qemu-armv7r, stm32f4discovery, lm3s6965-ek, fvp-armv8r). A function that both libc and libgcc define then came from libgcc. They now link libgcc after the group, as the other boards do.
  2. libs/libc/tls: A toolchain without native TLS (for example the Arm GNU toolchain for aarch64-none-elf, or for arm-none-eabi 13.2) compiles __thread into calls to __emutls_get_address(), and that toolchain's libgcc keeps one copy per program. libc now provides __wrap___emutls_get_address() and __wrap___emutls_register_common() with one copy per thread, and the image links with --wrap=__emutls_get_address --wrap=__emutls_register_common (Make and CMake), so the calls go to libc and never to libgcc. tools/Config.mk gives the same options as EMUTLS_LDFLAGS. The Toolchain.defs of each architecture that can have CONFIG_TLS_EMUTLS (and the sim Make.defs) adds it to LDELFFLAGS and LDMODULEFLAGS, and the export keeps it, so the modules and programs of the apps build get it with no change in apps (cmake: Link the emutls calls of an ELF program to libc. #20812 does the same for CMake). The boards that set these variables again in their Make.defs get it after boards: Remove the ELF and module flags that replace the arch values. #20844. The real names stay as weak aliases, for a link without --wrap: libc comes before libgcc there, and in a --whole-archive link the strong symbols of libgcc win without a conflict. With LTO the link also gets -u for the two __wrap_ names, because the compiler makes the calls only after the linker has chosen the archive members. CONFIG_TLS_EMUTLS (no prompt) builds this for the GCC and clang toolchains, but not for armclang, GHS or 8-bit AVR (no atomic pointer loads); without it nothing changes. The sim has it with GCC and the GNU linker (Linux, MinGW, whose GCC uses emulated TLS): it links NuttX into nuttx.rel with --wrap, and nuttx-names.in renames the weak real names, so the host program keeps the copy of its own libgcc. The macOS and MSVC linkers have no --wrap, so the sim there does not have it. The control object and __wrap___emutls_register_common() use the integer types of libgcc (mode(word), mode(pointer)), as GCC declares its built-in. The TLS info of each thread points to its copies; exit(), quick_exit() and pthread_exit() free them after the exit functions and the TLS destructors. It does not use pthread keys and does not need CONFIG_TLS_NELEM. libc.csv lists both __wrap_ functions, so code loaded at run time finds them in the system symbol table.
  3. libs/libc: When a module or shared library is unloaded, the loader gives libc the data range of the module. libc frees the copies of its variables in every thread and reuses their indexes. Before, each load took a new index and the old copies stayed until the threads exited.
  4. Documentation: thread_local_storage.rst said that libgcc's emutls implements __thread.

Impact

__thread variables are per thread in flat, protected and kernel builds, with any value of CONFIG_TLS_NELEM, on every architecture whose GCC has no native TLS. An image without __thread variables links only two pointers and their checks (lm3s6965-ek:qemu-flat: +64 bytes text). exit(), pthread_exit() and the loader call the code through these pointers, which libc sets on the first use, so a program without __thread variables does not link it. armclang, GHS, 8-bit AVR and the sim on macOS or with MSVC do not change. On the Linux sim, GCC uses native TLS, so only code that calls __emutls_get_address() (as code built for emulated TLS does) uses it.

Testing

QEMU 11 with HVF on an Apple M4 (arm64), TCG for mps2-an500. Test programs: several threads that use __thread variables (copy per thread, initial value, alignment, 12 variables, a makecontext() stack, C++ thread_local, an atexit() handler that reads __thread), and a load test that loads and unloads a module with a __thread variable 300 times from two threads (dlopen() and exec()).

Configuration master This PR
qemu-armv8a:nsh (flat), tests built in, CONFIG_TLS_NELEM 0, 4 and 16 all threads share one copy pass
qemu-armv8a:nsh, CMake; and CMake with LTO all threads share one copy pass
qemu-armv8a:knsh (kernel build), programs from the apps build and from the export all threads share one copy pass
qemu-armv8a:nsh, apps master, tlsgroup as a loadable module (CONFIG_EXAMPLES_HELLO=m, CONFIG_EXECFUNCS_SYSTEM_SYMTAB) the module has libgcc's __emutls_get_address; tlsgroup: FAIL the module has U __wrap___emutls_get_address; tlsgroup: PASS
qemu-armv8a:knsh export, scripts/Make.defs LDELFFLAGS = -r -e _start, LDMODULEFLAGS = -r both end with --wrap=__emutls_get_address --wrap=__emutls_register_common
qemu-armv8a:nsh load test, CONFIG_TLS_NELEM 0 and 4, 1 and 4 CPUs index grows to 2400, heap grows index 1 or 2, heap flat
mps2-an500:nsh, Arm GNU Toolchain 13.2, LTO (links through gcc) pass
mps2-an500:knsh (protected), 13.2 pass
Pimoroni Pico Plus 2 (RP2350, Cortex-M33), pimoroni-pico-2-plus:nsh, Arm GNU 13.2 (emulated TLS), tlsgroup run twice the two threads and main use one copy (all at the same address); tlsgroup: FAIL each thread has its own copy; tlsgroup: PASS both runs; ostest_main: Exiting with status 0
ostest on qemu-armv8a:knsh and qemu-armv8a:nsh ostest_main: Exiting with status 0
CONFIG_DEBUG_OPT_UNUSED_SECTIONS + CONFIG_DEBUG_LINK_WHOLE_ARCHIVE, qemu-armv7a:nsh and mps2-an500:nsh, Arm GNU 13.2 and 15.3; qemu-armv7a:nsh with CMake; rv-virt:nsh links links, no multiple definition
sim:nsh, Linux x86_64, GCC 12 (Make and CMake) CONFIG_TLS_EMUTLS on, nuttx.rel linked with --wrap, the host link without it; ostest_main: Exiting with status 0
sim:nsh, Linux x86_64, a program with the code that a compiler with emulated TLS makes for __thread (2 threads) emutest: main has 219 ..., bad 41 / FAIL (host libgcc __emutls_get_address@GCC_4.3.0) emutest: PASS
sim:nsh, macOS (clang) CONFIG_TLS_EMUTLS off, no --wrap; ostest_main: Exiting with status 0
sim:windows configuration (CMake): nuttx/tls.h compiled without GNU C no emutls declarations, compiles

Builds: each commit on qemu-armv8a:nsh, qemu-armv8a:knsh and lm3s6965-ek:qemu-flat; the protected configurations of the five boards: qemu-armv7r:pnsh, stm32f4discovery:kostest, lm3s6965-ek:qemu-kostest, lm3s6965-ek:qemu-protected, fvp-armv8r:pnsh.

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

@github-actions github-actions Bot added Arch: arm64 Issues related to ARM64 (64-bit) architecture Area: OS Components OS Components issues Size: M The size of the change in this PR is medium labels Oct 17, 2026
@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

esp32-devkitc

  • ROM: .flash.text +28 B (+0.0%, 127,200 B / 4,194,272 B, total: 3% used)
  • irom0_0_seg: .flash.text +28 B (+0.0%, 91,216 B / 3,342,304 B, total: 3% used)

mirtoo

  • kseg0_progmem: .text +20 B (+0.0%, 67,404 B / 131,072 B, total: 51% used)
  • kseg1_datamem: .sbss +4 B (+0.2%, 2,604 B / 32,256 B, total: 8% used)

qemu-armv8a

  • Code: .text.exit +16 B, .text.pthread_exit +16 B (+0.0%, 351,920 B)
  • Data: +8 B (+0.0%, 77,711 B)

qemu-intel64

  • Code: .text +48 B (+0.0%, 8,685,725 B)
  • Data: .bss +64 B (+0.1%, 125,113 B)

s698pm-dkit

  • Code: .text +192 B (+0.1%, 379,408 B)
  • Data: .bss +16 B (+0.1%, 11,883 B)

stm32-nucleo-f103rb

  • flash: .text +16 B (+0.0%, 34,612 B / 131,072 B, total: 26% used)
  • sram: .bss +4 B (+0.2%, 2,476 B / 20,480 B, total: 12% used)

No memory changes detected for:


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

acassis
acassis previously approved these changes Oct 17, 2026
@github-actions github-actions Bot added Area: Documentation Improvements or additions to documentation and removed Area: OS Components OS Components issues labels Oct 17, 2026
acassis
acassis previously approved these changes Oct 18, 2026
Comment thread arch/arm64/include/irq.h
Comment thread libs/libc/tls/Make.defs Outdated
@github-actions github-actions Bot removed Arch: tricore Issues related to the TriCore architecture from Infineon Arch: x86_64 Issues related to the x86_64 architecture Arch: xtensa Issues related to the Xtensa architecture labels Oct 19, 2026
@casaroli casaroli closed this Oct 20, 2026
@casaroli casaroli reopened this Oct 20, 2026
@github-actions github-actions Bot added the Arch: simulator Issues related to the SIMulator label Oct 20, 2026
@github-actions github-actions Bot added Arch: arm Issues related to ARM (32-bit) architecture Arch: arm64 Issues related to ARM64 (64-bit) architecture Arch: avr Issues related to all AVR(8-bit or 32-bit) architectures Arch: mips Issues related to the MIPS architecture Arch: risc-v Issues related to the RISC-V (32-bit or 64-bit) architecture Arch: sparc Issues related to the SPARC architecture Arch: x86_64 Issues related to the x86_64 architecture Arch: xtensa Issues related to the Xtensa architecture Board: simulator labels Oct 21, 2026
Five boards linked libgcc inside the group of the user libraries of a
protected build: at32f437-mini, qemu-armv7r, stm32f4discovery,
lm3s6965-ek and fvp-armv8r.  Inside the group, the linker takes a
function from libgcc as soon as the applications refer to it, before it
scans libc again.  So a function that both libc and libgcc define, such
as __emutls_get_address(), came from libgcc.

Link libgcc after the group, as the other boards do.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
A toolchain without native TLS, such as the Arm GNU toolchain for
aarch64-none-elf, compiles __thread variables into calls to
__emutls_get_address().  That toolchain's libgcc is built with the
single thread model, so its __emutls_get_address() keeps one copy of
each variable for the whole program: all threads read and write the
same copy.

Add __wrap___emutls_get_address() and __wrap___emutls_register_common()
to libc, and link the image with "--wrap=__emutls_get_address
--wrap=__emutls_register_common" (tools/Config.mk and
libs/libc/tls/CMakeLists.txt; CMake also wraps the user image of a
protected build).  The calls then go to libc whatever the order of the
libraries, and the linker never takes libgcc's copy.  With LTO the
compiler makes these calls only after the linker has chosen the archive
members, so an LTO link also gets "-u" for the two __wrap_ names.
EMUTLS_LDFLAGS gives the same options in the form for $(LD).  The
Toolchain.defs of each architecture that can have CONFIG_TLS_EMUTLS, and
the sim Make.defs, add it to LDELFFLAGS and LDMODULEFLAGS, and the
export keeps it, so the links of modules and programs get it too.  The system symbol table (libc.csv)
exports the __wrap_ names, which a module linked with --wrap refers
to.  The real names are weak aliases for a link without --wrap, such as
the user image of a protected build or a program built outside NuttX:
libc comes before libgcc there, and in a --whole-archive link libgcc's
strong definitions win without a conflict.

CONFIG_TLS_EMUTLS (no prompt) builds this for the GCC and clang
toolchains, but not for armclang, GHS or 8-bit AVR (no atomic pointer
loads).  The sim has it with GCC and the GNU linker (Linux, and MinGW,
whose GCC uses emulated TLS): the sim links NuttX into nuttx.rel with
--wrap, and nuttx-names.in renames the weak real names, so the host
program keeps its own libgcc copy.  The macOS and MSVC linkers have no
--wrap, so the sim there does not have it.  The control
object and the parameters use the types of libgcc's emutls.c, as GCC
declares its built-in __emutls_register_common(): a machine word for
the size and the alignment and an integer as wide as a pointer for the
index.

Each variable gets a global index on its first use.  Each thread keeps
its copies in an array that its TLS info points to; a copy is made on
the first use from the variable's template, or zeroed.  exit(),
quick_exit() and pthread_exit() free the copies after the exit
functions and the TLS destructors, which may still use them.  They call
the code through a pointer that it sets on its first use, so a program
without __thread variables does not link it.  Without CONFIG_TLS_EMUTLS
there is no pointer and no call.  A thread that gets a new
copy of the TLS info of its parent (vfork()) starts with no copies.
Nothing depends on pthread keys or CONFIG_TLS_NELEM.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
A module or shared library with __thread variables took a new emutls
index each time it was loaded, and the copies of its variables stayed
in each thread until the thread exited.  A module that is loaded and
unloaded again and again made the arrays of all threads grow.

libc now keeps the control object of each index and a list of the
arrays of all threads.  When a module goes, libelf_uninit() calls
lib_emutls_release() with the data range of the module, where all its
control objects are.  It calls it through a pointer that libc sets when
it gives the first index, so the loader does not link the code.  libc
frees the copies of those variables in every thread and gives their
indexes back; a new variable takes the first free index.  One lock
covers the index table, the list, and the slow path of
__emutls_get_address(); a copy that exists is still read with no lock.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
libc now provides its own __emutls_get_address(), so a __thread
variable has one copy per thread also with a single-thread libgcc.  The
TLS guide said that libgcc's emutls implements the keyword.  It now
also says when the copies are freed, that the links use --wrap, and
that code loaded at run time finds the libc version in the system
symbol table.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Arch: arm64 Issues related to ARM64 (64-bit) architecture Arch: avr Issues related to all AVR(8-bit or 32-bit) architectures Arch: mips Issues related to the MIPS architecture Arch: risc-v Issues related to the RISC-V (32-bit or 64-bit) architecture Arch: simulator Issues related to the SIMulator Arch: sparc Issues related to the SPARC architecture Arch: x86_64 Issues related to the x86_64 architecture Arch: xtensa Issues related to the Xtensa architecture Area: Build system Board: arm Board: arm64 Board: simulator Size: L The size of the change in this PR is large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants