authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-07-07 13:03:15+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-07-07 13:05:31+01:00
log6c28d6cce8ada3317cfdec39640d001a3ae35c98
tree4b60f19d596c65443b959fc0b7e7abca64ba3b75
parentbd0a3f0a0c8058cbad1d8e110fe900ec877c1909
signaturelock-open Commit is signed but in an unrecognized format.

std.os.linux: allow unrelaxed TLS accesses in static executables

In a static binary, it is possible for every thread-local variable access to be relaxed to the LE model, where no function call is involved. Previously, `std.os.linux.tls` relied on this for correctness when not linking libc, because it did not export the `__tls_get_addr` symbol. However, the self-hosted `Elf2` linker currently does not implement relaxations, resulting in a link error when creating static PIEs (and probably in other cases involving external link inputs). To solve this, just export a trivial implementation of `__tls_get_addr` when creating a static executable without libc. Based on previous work in https://github.com/ziglang/zig/issues/20625.

1 files changed, 138 insertions(+), 8 deletions(-)

lib/std/os/linux/tls.zig+138-8
...@@ -14,7 +14,8 @@ const mem = std.mem;...@@ -14,7 +14,8 @@ const mem = std.mem;
14const elf = std.elf;14const elf = std.elf;
15const math = std.math;15const math = std.math;
16const assert = std.debug.assert;16const assert = std.debug.assert;
17const native_arch = @import("builtin").cpu.arch;17const builtin = @import("builtin");
18const native_arch = builtin.cpu.arch;
18const linux = std.os.linux;19const linux = std.os.linux;
19const page_size_min = std.heap.page_size_min;20const page_size_min = std.heap.page_size_min;
2021
...@@ -41,13 +42,13 @@ const Variant = enum {...@@ -41,13 +42,13 @@ const Variant = enum {
41 I_original,42 I_original,
42 /// The modified Variant I:43 /// The modified Variant I:
43 ///44 ///
44 /// ---------------------------------------------------45 /// --------------------------------------------
45 /// | DTV | Zig TCB | ABI TCB | [Offset] | TLS Blocks |46 /// | DTV | Zig TCB | ABI TCB | TLS Blocks |
46 /// -------------------------------------^-------------47 /// ------------------------------^-------------
47 /// `-- The TP register points here.48 /// `-- The TP register points here (*inside* the TLS blocks).
48 ///49 ///
49 /// The offset (which can be zero) is applied to the TP only; there is never a physical gap50 /// The offset from the start of the TLS blocks to the TP register is `current_tp_offset`. It
50 /// between the ABI TCB and the TLS blocks. This implies that we only need to align the TP.51 /// may be zero, in which case the TP register points to the start of the TLS blocks.
51 ///52 ///
52 /// The first (and only) word in the ABI TCB points to the DTV.53 /// The first (and only) word in the ABI TCB points to the DTV.
53 I_modified,54 I_modified,
...@@ -106,7 +107,7 @@ const current_variant: Variant = switch (native_arch) {...@@ -106,7 +107,7 @@ const current_variant: Variant = switch (native_arch) {
106 else => @compileError("undefined TLS variant for this architecture"),107 else => @compileError("undefined TLS variant for this architecture"),
107};108};
108109
109/// The Offset value for the modified Variant I.110/// The offset value for the modified Variant I.
110const current_tp_offset = switch (native_arch) {111const current_tp_offset = switch (native_arch) {
111 .m68k,112 .m68k,
112 .mips,113 .mips,
...@@ -379,6 +380,106 @@ pub fn setThreadPointer(addr: usize) void {...@@ -379,6 +380,106 @@ pub fn setThreadPointer(addr: usize) void {
379 }380 }
380}381}
381382
383pub fn getThreadPointer() usize {
384 @setRuntimeSafety(false);
385 @disableInstrumentation();
386
387 return switch (native_arch) {
388 .aarch64, .aarch64_be => asm (
389 \\ mrs %[ret], tpidr_el0
390 : [ret] "=r" (-> usize),
391 ),
392 .alpha => asm (
393 \\ rduniq
394 : [ret] "={$0}" (-> usize),
395 ),
396 .arc, .arceb => asm (
397 \\ mov %[ret], r25
398 : [ret] "=r" (-> usize),
399 ),
400 .arm, .armeb, .thumb, .thumbeb => asm (
401 \\ mrc p15, 0, %[ret], c13, c0, 3
402 : [ret] "=r" (-> usize),
403 ),
404 .csky => asm (
405 \\ mov %[ret], r31
406 : [ret] "=r" (-> usize),
407 ),
408 .hexagon => asm (
409 \\ %[ret] = ugp
410 : [ret] "=r" (-> usize),
411 ),
412 .hppa => asm (
413 \\ mfctl %%cr27, %[ret]
414 : [ret] "=r" (-> usize),
415 ),
416 .loongarch32, .loongarch64 => asm (
417 \\ move %[ret], $tp
418 : [ret] "=r" (-> usize),
419 ),
420 .m68k => linux.syscall1(.get_thread_area),
421 .mips, .mipsel, .mips64, .mips64el => asm (
422 \\ rdhwr %[ret], $29
423 : [ret] "=r" (-> usize),
424 ),
425 .microblaze, .microblazeel => asm (
426 \\ ori %[ret], r21, 0
427 : [ret] "=r" (-> usize),
428 ),
429 .or1k => asm (
430 \\ l.ori %[ret], r10, 0
431 : [ret] "=r" (-> usize),
432 ),
433 .riscv32, .riscv64 => asm (
434 \\ mv %[ret], tp
435 : [ret] "=r" (-> usize),
436 ),
437 .powerpc, .powerpcle => asm (
438 \\ mr %[ret], 2
439 : [ret] "=r" (-> usize),
440 ),
441 .powerpc64, .powerpc64le => asm (
442 \\ mr %[ret], 13
443 : [ret] "=r" (-> usize),
444 ),
445 .s390x => asm (
446 \\ ear %[ret], %%a0
447 \\ sllg %[ret], %[ret], 32
448 \\ ear %[ret], %%a1
449 : [ret] "=r" (-> usize),
450 ),
451 .sh, .sheb => asm (
452 \\ stc %[ret], gbr
453 : [ret] "=r" (-> usize),
454 ),
455 .sparc, .sparc64 => asm (
456 \\ mov %%g7, %[ret]
457 : [ret] "=r" (-> usize),
458 ),
459 .x86 => asm (
460 \\ movl %%gs:0, %[ret]
461 : [ret] "=r" (-> usize),
462 ),
463 .x86_64 => switch (@sizeOf(usize)) {
464 8 => asm (
465 \\ movq %%fs:0, %[ret]
466 : [ret] "=r" (-> usize),
467 ),
468 // On x32, usize is 32 bits.
469 4 => asm (
470 \\ movl %%fs:0, %[ret]
471 : [ret] "=r" (-> usize),
472 ),
473 else => comptime unreachable,
474 },
475 .xtensa, .xtensaeb => asm (
476 \\ rur %[ret], threadptr
477 : [ret] "=r" (-> usize),
478 ),
479 else => @compileError("Unsupported architecture"),
480 };
481}
482
382fn computeAreaDesc(phdrs: []elf.Phdr) void {483fn computeAreaDesc(phdrs: []elf.Phdr) void {
383 @setRuntimeSafety(false);484 @setRuntimeSafety(false);
384 @disableInstrumentation();485 @disableInstrumentation();
...@@ -616,3 +717,32 @@ inline fn mmap_tls(length: usize) usize {...@@ -616,3 +717,32 @@ inline fn mmap_tls(length: usize) usize {
616 });717 });
617 }718 }
618}719}
720
721comptime {
722 assert(!builtin.link_libc); // otherwise libc should control TLS
723
724 if (builtin.output_mode == .Exe and builtin.link_mode == .static) {
725 // This is a static executable without libc, so it is our job to provide the TLS accessor
726 // function for the GD and LD models. This function is unlikely to actually be used, since
727 // the linker should be able to relax every TLS access to the LE model and therefore
728 // eliminate all calls to this function, but that isn't guaranteed.
729 _ = struct {
730 const TlsIndex = switch (native_arch) {
731 .x86_64 => extern struct { module: u64, offset: u64 }, // Even for x32...
732 else => extern struct { module: usize, offset: usize }, // ...but not MIPS N32!
733 };
734 export fn __tls_get_addr(ti: *const TlsIndex) *anyopaque {
735 assert(ti.module == 1); // The executable's module ID is always 1
736 const tp = getThreadPointer();
737 const block: [*]u8 = switch (current_variant) {
738 .I_original => @ptrFromInt(tp -% area_desc.abi_tcb.offset +% area_desc.block.offset),
739 .I_modified => @ptrFromInt(tp -% current_tp_offset),
740 // The `.I_original` approach would also work for `.II`, but there is an
741 // alternative strategy which is one less operation:
742 .II => @ptrFromInt(tp -% area_desc.block.size),
743 };
744 return block[@intCast(ti.offset)..];
745 }
746 };
747 }
748}