Repository navigation
Conversation
casaroli
requested review from
GUIDINGLI,
jerpelea,
pussuw and
xiaoxiang781216
as code owners
October 17, 2026 17:17
|
acassis
previously approved these changes
Oct 17, 2026
acassis
previously approved these changes
Oct 18, 2026
casaroli
force-pushed
the
arm64-tls-tpidr
branch
from
October 19, 2026 00:55
6e64782 to
e79c99e
Compare
This was referenced Oct 19, 2026
casaroli
force-pushed
the
arm64-tls-tpidr
branch
from
October 19, 2026 11:40
e79c99e to
4600c89
Compare
casaroli
requested review from
Donny9,
anchao,
tmedicci,
yamt and
yf13
as code owners
October 19, 2026 11:40
This was referenced Oct 19, 2026
casaroli
force-pushed
the
arm64-tls-tpidr
branch
from
October 20, 2026 05:36
4e6c379 to
e0090ca
Compare
casaroli
force-pushed
the
arm64-tls-tpidr
branch
from
October 20, 2026 22:19
e0090ca to
d144398
Compare
casaroli
force-pushed
the
arm64-tls-tpidr
branch
from
October 21, 2026 07:38
d144398 to
a7a9768
Compare
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>
casaroli
force-pushed
the
arm64-tls-tpidr
branch
from
October 22, 2026 11:55
a7a9768 to
8df6bd7
Compare
acassis
approved these changes
Oct 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four commits for
__threadvariables with a toolchain that has no native TLS. The arm64TPIDR_EL0work is in #20821.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.libs/libc/tls: A toolchain without native TLS (for example the Arm GNU toolchain foraarch64-none-elf, or forarm-none-eabi13.2) compiles__threadinto 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.mkgives the same options asEMUTLS_LDFLAGS. TheToolchain.defsof each architecture that can haveCONFIG_TLS_EMUTLS(and the simMake.defs) adds it toLDELFFLAGSandLDMODULEFLAGS, 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 theirMake.defsget 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-archivelink the strong symbols of libgcc win without a conflict. With LTO the link also gets-ufor 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 intonuttx.relwith--wrap, andnuttx-names.inrenames 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()andpthread_exit()free them after the exit functions and the TLS destructors. It does not use pthread keys and does not needCONFIG_TLS_NELEM.libc.csvlists both__wrap_functions, so code loaded at run time finds them in the system symbol table.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.Documentation:thread_local_storage.rstsaid that libgcc's emutls implements__thread.Impact
__threadvariables are per thread in flat, protected and kernel builds, with any value ofCONFIG_TLS_NELEM, on every architecture whose GCC has no native TLS. An image without__threadvariables 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__threadvariables 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__threadvariables (copy per thread, initial value, alignment, 12 variables, amakecontext()stack, C++thread_local, anatexit()handler that reads__thread), and a load test that loads and unloads a module with a__threadvariable 300 times from two threads (dlopen()andexec()).qemu-armv8a:nsh(flat), tests built in,CONFIG_TLS_NELEM0, 4 and 16qemu-armv8a:nsh, CMake; and CMake with LTOqemu-armv8a:knsh(kernel build), programs from the apps build and from the exportqemu-armv8a:nsh, apps master,tlsgroupas a loadable module (CONFIG_EXAMPLES_HELLO=m,CONFIG_EXECFUNCS_SYSTEM_SYMTAB)__emutls_get_address;tlsgroup: FAILU __wrap___emutls_get_address;tlsgroup: PASSqemu-armv8a:knshexport,scripts/Make.defsLDELFFLAGS = -r -e _start,LDMODULEFLAGS = -r--wrap=__emutls_get_address --wrap=__emutls_register_commonqemu-armv8a:nshload test,CONFIG_TLS_NELEM0 and 4, 1 and 4 CPUsmps2-an500:nsh, Arm GNU Toolchain 13.2, LTO (links through gcc)mps2-an500:knsh(protected), 13.2pimoroni-pico-2-plus:nsh, Arm GNU 13.2 (emulated TLS),tlsgrouprun twicemainuse one copy (all at the same address);tlsgroup: FAILtlsgroup: PASSboth runs;ostest_main: Exiting with status 0ostestonqemu-armv8a:knshandqemu-armv8a:nshostest_main: Exiting with status 0CONFIG_DEBUG_OPT_UNUSED_SECTIONS+CONFIG_DEBUG_LINK_WHOLE_ARCHIVE,qemu-armv7a:nshandmps2-an500:nsh, Arm GNU 13.2 and 15.3;qemu-armv7a:nshwith CMake;rv-virt:nshmultiple definitionsim:nsh, Linux x86_64, GCC 12 (Make and CMake)CONFIG_TLS_EMUTLSon,nuttx.rellinked with--wrap, the host link without it;ostest_main: Exiting with status 0sim: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: PASSsim:nsh, macOS (clang)CONFIG_TLS_EMUTLSoff, no--wrap;ostest_main: Exiting with status 0sim:windowsconfiguration (CMake):nuttx/tls.hcompiled without GNU CBuilds: each commit on
qemu-armv8a:nsh,qemu-armv8a:knshandlm3s6965-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 -gpasses.