From 5fb8343c7a0ebe6228f73cd1fe86098abba11958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Wed, 19 Aug 2026 12:58:09 +0200 Subject: [PATCH 1/7] build: pass -lc when generating compiler_rt.c --- CMakeLists.txt | 2 +- bootstrap.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e74b6f17b313a6c4bef52610f2d49b33ecda978..837a5bcd1b8de6225797bdb92031b306fdd7bf92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -690,7 +690,7 @@ add_custom_command( set(BUILD_COMPILER_RT_ARGS "${PROJECT_SOURCE_DIR}/lib" - build-obj -ofmt=c -OReleaseSmall + build-obj -ofmt=c -lc -OReleaseSmall --name compiler_rt -femit-bin="${ZIG_COMPILER_RT_C_SOURCE}" -target "${ZIG_HOST_TARGET_TRIPLE}" diff --git a/bootstrap.c b/bootstrap.c index f0cdc8e0a2e2cb418ea0029cbb0a4aaae6c258be..4e41d2ebdc8a0839182901b33e6214ad3f6ae3f4 100644 --- a/bootstrap.c +++ b/bootstrap.c @@ -212,7 +212,7 @@ int main(int argc, char **argv) { { const char *child_argv[] = { "./zig1", "lib", "build-obj", - "-ofmt=c", "-OReleaseSmall", + "-ofmt=c", "-lc", "-OReleaseSmall", "--name", "compiler_rt", "-femit-bin=compiler_rt.c", "-target", host_triple, "-Mroot=lib/compiler_rt.zig", -- 2.54.0 From 71590ad8c98b5e0186ad59a3f73e493559b05644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 18 Aug 2026 20:53:46 +0200 Subject: [PATCH 2/7] std.elf: fix Elf64_auxv_t layout on NetBSD and OpenBSD --- lib/std/elf.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/elf.zig b/lib/std/elf.zig index 966dbb6e49eae3c04ec6a4a9eb0213fb6e462098..28dc645307c31a5dacaaa0fca5664d0551659aa8 100644 --- a/lib/std/elf.zig +++ b/lib/std/elf.zig @@ -1602,7 +1602,7 @@ pub const Elf32_auxv_t = extern struct { }, }; pub const Elf64_auxv_t = extern struct { - a_type: u64, + a_type: if (native_os == .netbsd or native_os == .openbsd) u32 else u64, a_un: extern union { a_val: u64, }, -- 2.54.0 From 9bcb0f6f21e06fe2783e8f057c66cc4745f05dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Mon, 5 Oct 2026 03:23:56 +0200 Subject: [PATCH 3/7] cbe: apply zig_mangled() on extern variable forward decls --- src/codegen/c.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/codegen/c.zig b/src/codegen/c.zig index 4e58ede308fcaefe9b58ee073b3f1b21eaba8e76..c871f2d765bea5eac10790eebb9c4cff53276832 100644 --- a/src/codegen/c.zig +++ b/src/codegen/c.zig @@ -2552,6 +2552,13 @@ pub fn genDeclFwd(dg: *DeclGen, w: *Writer) Error!void { .{ .@"const" = nav.resolved.?.@"const" }, nav.resolved.?.@"align", ); + const extern_name = nav.name.toSlice(ip); + const is_mangled = isMangledIdent(extern_name, true); + if (is_mangled) { + try w.print(" zig_mangled({f}, {f})", .{ + fmtIdentSolo(extern_name), fmtStringLiteral(extern_name, null), + }); + } try w.writeAll(";\n"); return; }, -- 2.54.0 From 65b3cb2db988f7cf67fe5c2f39c96824e9497e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 18 Aug 2026 23:42:05 +0200 Subject: [PATCH 4/7] test: fix mix_o_files standalone test to not mix libc and non-libc objects Doing so might work by accident sometimes but is not actually valid in the general case because whether libc is linked is an ABI property. --- test/standalone/mix_o_files/build.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/test/standalone/mix_o_files/build.zig b/test/standalone/mix_o_files/build.zig index dcc5a09a0203d39440e92fdf0f3d7585c760e02f..3143834ffb3880ace94e618015902105e8104d94 100644 --- a/test/standalone/mix_o_files/build.zig +++ b/test/standalone/mix_o_files/build.zig @@ -13,6 +13,7 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("base64.zig"), .optimize = optimize, .target = target, + .link_libc = true, }), }); -- 2.54.0 From 292edd2440baa02864f1d2fe2dc08140d0c3c7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 18 Aug 2026 18:00:36 +0200 Subject: [PATCH 5/7] move std.os.linux.elf_aux_maybe to compiler_rt as __zig_elf_auxv This gets rid of the super confusing and (as far as I can tell) incorrect getauxval logic in std.os.linux, and clarifies exactly who owns this state. The ABI that std now expects is: extern var __zig_elf_auxv: [*]std.elf.Auxv; Users can technically override this weak symbol with a strong symbol if they wish, although I see no particularly useful reason to do so. It is now a compile error to use std.os.linux.getauxval if libc is linked, whereas previously it would just give useless results in some cases. I consider this to be strictly an improvement. Just use std.c.getauxval when linking libc, which std already does correctly where it needs getauxval. This code is expected to be further simplified once our baseline Linux version allows implementing https://codeberg.org/ziglang/zig/issues/31290. closes https://codeberg.org/ziglang/zig/issues/36203 --- lib/compiler_rt.zig | 2 ++ lib/compiler_rt/elf.zig | 3 +++ lib/compiler_rt/elf/auxv.zig | 21 ++++++++++++++++++ lib/std/os/linux.zig | 43 +++++++++--------------------------- lib/std/os/linux/vdso.zig | 4 +++- lib/std/start.zig | 5 ++++- 6 files changed, 44 insertions(+), 34 deletions(-) create mode 100644 lib/compiler_rt/elf.zig create mode 100644 lib/compiler_rt/elf/auxv.zig diff --git a/lib/compiler_rt.zig b/lib/compiler_rt.zig index 5660847ce84fbbedc64d4e980202e3865f8df592..e4d4824a38654c26da6a763f75e8df8cfd408681 100644 --- a/lib/compiler_rt.zig +++ b/lib/compiler_rt.zig @@ -164,6 +164,8 @@ comptime { symbol(&strlen, "strlen"); } + _ = @import("compiler_rt/elf.zig"); + // Temporarily used for uefi until https://github.com/ziglang/zig/issues/21630 is addressed. if (!builtin.link_libc and (builtin.os.tag == .windows or builtin.os.tag == .uefi) and (builtin.abi == .none or builtin.abi == .msvc)) { symbol(&_fltused, "_fltused"); diff --git a/lib/compiler_rt/elf.zig b/lib/compiler_rt/elf.zig new file mode 100644 index 0000000000000000000000000000000000000000..e68d5ad7c459cccfc3fefe34739507349a5ed350 --- /dev/null +++ b/lib/compiler_rt/elf.zig @@ -0,0 +1,3 @@ +comptime { + _ = @import("elf/auxv.zig"); +} diff --git a/lib/compiler_rt/elf/auxv.zig b/lib/compiler_rt/elf/auxv.zig new file mode 100644 index 0000000000000000000000000000000000000000..de5a45e501b5850f8f25aa18ffec75ccf23927ac --- /dev/null +++ b/lib/compiler_rt/elf/auxv.zig @@ -0,0 +1,21 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +const compiler_rt = @import("../../compiler_rt.zig"); +const symbol = compiler_rt.symbol; + +comptime { + // FreeBSD, NetBSD, and OpenBSD are expected to also use this in the future. + if (!builtin.link_libc and builtin.target.os.tag == .linux) { + symbol(@ptrCast(&__zig_elf_auxv), "__zig_elf_auxv"); + } +} + +/// Populated by `std.start` when libc is not linked on targets that provide an ELF auxiliary vector +/// to the process on startup. Provided by compiler-rt so that there is no confusion about where +/// this state lives regardless of how the final artifact (executable or shared library) is +/// compiled. +/// +/// It is expected that this will gradually be replaced with better mechanisms as operating systems +/// improve; see https://codeberg.org/ziglang/zig/issues/31290. +var __zig_elf_auxv: [*]std.elf.Auxv = undefined; diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 38e4abd197ba6c17245e7fe7038d7db0528ca395..e75a0d822467481b245997d4818989a9c7654b50 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -960,43 +960,22 @@ pub const RENAME = packed struct(u32) { _: u29 = 0, }; -/// Set by startup code, used by `getauxval`. -pub var elf_aux_maybe: ?[*]std.elf.Auxv = null; +/// Available only when libc is *not* linked; use `std.c.getauxval` if libc is linked. +pub const getauxval = if (builtin.link_libc) {} else struct { + /// Provided by compiler-rt and set by `std.start`. + extern var __zig_elf_auxv: [*]std.elf.Auxv; -/// Whether an external or internal getauxval implementation is used. -const extern_getauxval = switch (builtin.zig_backend) { - // Calling extern functions is not yet supported with these backends - .stage2_arm, - .stage2_loongarch, - .stage2_powerpc, - .stage2_riscv64, - .stage2_sparc64, - => false, - else => !builtin.link_libc, -}; + pub fn getauxval(index: usize) usize { + @disableInstrumentation(); -pub const getauxval = if (extern_getauxval) struct { - comptime { - const root = @import("root"); - // Export this only when building an executable, otherwise it is overriding - // the libc implementation - if (builtin.output_mode == .Exe or @hasDecl(root, "main")) { - @export(&getauxvalImpl, .{ .name = "getauxval", .linkage = .weak }); + var i: usize = 0; + while (__zig_elf_auxv[i].a_type != std.elf.AT.NULL) : (i += 1) { + if (__zig_elf_auxv[i].a_type == index) return __zig_elf_auxv[i].a_un.a_val; } - } - extern fn getauxval(index: usize) usize; -}.getauxval else getauxvalImpl; -fn getauxvalImpl(index: usize) callconv(.c) usize { - @disableInstrumentation(); - const auxv = elf_aux_maybe orelse return 0; - var i: usize = 0; - while (auxv[i].a_type != std.elf.AT.NULL) : (i += 1) { - if (auxv[i].a_type == index) - return auxv[i].a_un.a_val; + return 0; } - return 0; -} +}.getauxval; // Some architectures (and some syscalls) require 64bit parameters to be passed // in a even-aligned register pair. diff --git a/lib/std/os/linux/vdso.zig b/lib/std/os/linux/vdso.zig index bdf46337b59ecc863a543d3722ddf47bd967374a..5add23f5c42bd6f93cfdc39825b42cc969d8e321 100644 --- a/lib/std/os/linux/vdso.zig +++ b/lib/std/os/linux/vdso.zig @@ -1,3 +1,4 @@ +const builtin = @import("builtin"); const std = @import("../../std.zig"); const elf = std.elf; const linux = std.os.linux; @@ -5,7 +6,8 @@ const mem = std.mem; const maxInt = std.math.maxInt; pub fn lookup(vername: []const u8, name: []const u8) usize { - const vdso_addr = linux.getauxval(std.elf.AT.SYSINFO_EHDR); + const getauxval = if (builtin.link_libc) std.c.getauxval else linux.getauxval; + const vdso_addr = getauxval(std.elf.AT.SYSINFO_EHDR); if (vdso_addr == 0) return 0; const eh = @as(*elf.Ehdr, @ptrFromInt(vdso_addr)); diff --git a/lib/std/start.zig b/lib/std/start.zig index 96190d835614718793d94832f9a0b72e415d964d..f8c85adeded05726074c5ed0516b336892d34154 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -596,6 +596,9 @@ fn wWinMain(hInstance: *anyopaque, hPrevInstance: ?*anyopaque, pCmdLine: [*:0]u1 return root.wWinMain(@ptrCast(hInstance), @ptrCast(hPrevInstance), pCmdLine, @intCast(nCmdShow)); } +/// Provided by compiler-rt. +extern var __zig_elf_auxv: [*]std.elf.Auxv; + fn posixCallMainAndExit(argc_argv_ptr: [*]usize) callconv(.c) noreturn { // We're not ready to panic until thread local storage is initialized. @setRuntimeSafety(false); @@ -638,7 +641,7 @@ fn posixCallMainAndExit(argc_argv_ptr: [*]usize) callconv(.c) noreturn { if (native_os == .linux) { // This must be done after PIE relocations have been applied or we may crash // while trying to access the global variable (happens on MIPS at least). - std.os.linux.elf_aux_maybe = auxv; + __zig_elf_auxv = auxv; if (!builtin.single_threaded) { // ARMv6 targets (and earlier) have no support for TLS in hardware. -- 2.54.0 From 736d37ad161b255cf29fb3050026ad4852639b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Tue, 18 Aug 2026 22:43:52 +0200 Subject: [PATCH 6/7] move std.os.linux.tls to compiler_rt as __zig_elf_static_tls_* As with __zig_elf_auxv, we need a canonical location for __tls_get_addr (or __tls_get_offset on s390x), and compiler-rt is the obvious place. But that also means that moving all of std.os.linux.tls there just makes sense. I decided not to namespace this under linux because it's expected that most of this code will apply to the BSDs too. The ABI that std now expects is: extern fn __zig_elf_static_tls_init(phdrs: [*]elf.ElfN.Phdr, phnum: usize) void; extern fn __zig_elf_static_tls_fill(area: [*]u8) usize; extern var __zig_elf_static_tls: extern struct { size: usize, alignment: usize, gdt_entry_number: usize, }; Users can override all three weak symbols with strong symbols if they want to provide their own ELF TLS implementation, which may be useful to do when doing manual dynamic loading from a static executable. Note that __tls_get_addr (or __tls_get_offset on s390x) must also be provided, per the ELF TLS specification. --- lib/compiler_rt/elf.zig | 1 + lib/{std/os/linux => compiler_rt/elf}/tls.zig | 173 +++++++++--------- lib/std/Thread.zig | 18 +- lib/std/os/linux.zig | 1 - lib/std/start.zig | 13 +- 5 files changed, 114 insertions(+), 92 deletions(-) rename lib/{std/os/linux => compiler_rt/elf}/tls.zig (85%) diff --git a/lib/compiler_rt/elf.zig b/lib/compiler_rt/elf.zig index e68d5ad7c459cccfc3fefe34739507349a5ed350..11f934d30edde31a6c5f4ec01092636fa602620c 100644 --- a/lib/compiler_rt/elf.zig +++ b/lib/compiler_rt/elf.zig @@ -1,3 +1,4 @@ comptime { _ = @import("elf/auxv.zig"); + _ = @import("elf/tls.zig"); } diff --git a/lib/std/os/linux/tls.zig b/lib/compiler_rt/elf/tls.zig similarity index 85% rename from lib/std/os/linux/tls.zig rename to lib/compiler_rt/elf/tls.zig index aed76fc0cb2ee83318e5c0e2d72b0016cf44eb91..b4231387593edb88fdaff50d5d526b5eec5e3eda 100644 --- a/lib/std/os/linux/tls.zig +++ b/lib/compiler_rt/elf/tls.zig @@ -9,16 +9,36 @@ //! //! [1] https://www.akkadia.org/drepper/tls.pdf +const builtin = @import("builtin"); const std = @import("std"); -const mem = std.mem; + +const compiler_rt = @import("../../compiler_rt.zig"); +const symbol = compiler_rt.symbol; + const elf = std.elf; -const math = std.math; const assert = std.debug.assert; -const builtin = @import("builtin"); const native_arch = builtin.cpu.arch; const linux = std.os.linux; const page_size_min = std.heap.page_size_min; +comptime { + // FreeBSD, NetBSD, and OpenBSD are expected to also use this in the future. + if (!builtin.link_libc and builtin.target.os.tag == .linux) { + symbol(&__zig_elf_static_tls, "__zig_elf_static_tls"); + symbol(&__zig_elf_static_tls_init, "__zig_elf_static_tls_init"); + symbol(&__zig_elf_static_tls_fill, "__zig_elf_static_tls_fill"); + + // To make static executables without libc work, it is our job to provide the TLS accessor + // function for the GD and LD models. This function is unlikely to actually be used, since + // the linker should be able to relax every TLS access to the LE model and therefore + // eliminate all calls to this function, but that isn't guaranteed. + if (native_arch == .s390x) + symbol(&__tls_get_offset, "__tls_get_offset") + else + symbol(&__tls_get_addr, "__tls_get_addr"); + } +} + /// Represents an ELF TLS variant. /// /// In all variants, the TP and the TLS blocks must be aligned to the `p_align` value in the @@ -186,11 +206,17 @@ const Dtv = extern struct { }; /// Describes a process's TLS area. The area encompasses the DTV, both TCBs, and the TLS block, with -/// the exact layout of these being dependent primarily on `current_variant`. -const AreaDesc = struct { +/// the exact layout of these being dependent primarily on `current_variant`. See also `area_info`. +/// +/// The layout of this variable is ABI between compiler-rt and std. +var __zig_elf_static_tls: extern struct { size: usize, alignment: usize, + /// Only used on the 32-bit x86 architecture (not x86_64, nor x32). + gdt_entry_number: usize, +} = undefined; +var area_info: struct { dtv: struct { /// Offset into the TLS area. offset: usize, @@ -210,21 +236,16 @@ const AreaDesc = struct { /// This is the effective size of the TLS block, which may be greater than `init.len`. size: usize, }, +} = undefined; - /// Only used on the 32-bit x86 architecture (not x86_64, nor x32). - gdt_entry_number: usize, -}; - -pub var area_desc: AreaDesc = undefined; - -pub fn setThreadPointer(addr: usize) void { +fn setThreadPointer(addr: usize) void { @setRuntimeSafety(false); @disableInstrumentation(); switch (native_arch) { .x86 => { var user_desc: linux.user_desc = .{ - .entry_number = area_desc.gdt_entry_number, + .entry_number = __zig_elf_static_tls.gdt_entry_number, .base_addr = addr, .limit = 0xfffff, .flags = .{ @@ -241,7 +262,7 @@ pub fn setThreadPointer(addr: usize) void { const gdt_entry_number = user_desc.entry_number; // We have to keep track of our slot as it's also needed for clone() - area_desc.gdt_entry_number = gdt_entry_number; + __zig_elf_static_tls.gdt_entry_number = gdt_entry_number; // Update the %gs selector asm volatile ("movl %[gs_val], %%gs" : @@ -380,7 +401,7 @@ pub fn setThreadPointer(addr: usize) void { } } -pub fn getThreadPointer() usize { +fn getThreadPointer() usize { @setRuntimeSafety(false); @disableInstrumentation(); @@ -574,10 +595,13 @@ fn computeAreaDesc(phdrs: []elf.ElfN.Phdr) void { }, }; - area_desc = .{ + __zig_elf_static_tls = .{ .size = area_size, .alignment = align_factor, + .gdt_entry_number = @as(usize, @bitCast(@as(isize, -1))), + }; + area_info = .{ .dtv = .{ .offset = dtv_offset, }, @@ -591,8 +615,6 @@ fn computeAreaDesc(phdrs: []elf.ElfN.Phdr) void { .offset = block_offset, .size = block_size, }, - - .gdt_entry_number = @as(usize, @bitCast(@as(isize, -1))), }; } @@ -613,33 +635,35 @@ inline fn alignPtrCast(comptime T: type, ptr: [*]u8) *T { /// Initializes all the fields of the static TLS area and returns the computed architecture-specific /// value of the TP register. -pub fn prepareArea(area: []u8) usize { +fn __zig_elf_static_tls_fill(area: [*]u8) callconv(.c) usize { @setRuntimeSafety(false); @disableInstrumentation(); + const area_slice = area[0..__zig_elf_static_tls.size]; + // Clear the area we're going to use, just to be safe. - @memset(area, 0); + @memset(area_slice, 0); // Prepare the ABI TCB. - const abi_tcb = alignPtrCast(AbiTcb, area.ptr + area_desc.abi_tcb.offset); + const abi_tcb = alignPtrCast(AbiTcb, area + area_info.abi_tcb.offset); switch (current_variant) { - .I_original, .I_modified => abi_tcb.dtv = @intFromPtr(area.ptr + area_desc.dtv.offset), + .I_original, .I_modified => abi_tcb.dtv = @intFromPtr(area + area_info.dtv.offset), .II => abi_tcb.self = abi_tcb, } // Prepare the DTV. - const dtv = alignPtrCast(Dtv, area.ptr + area_desc.dtv.offset); + const dtv = alignPtrCast(Dtv, area + area_info.dtv.offset); dtv.len = 1; - dtv.tls_block = area.ptr + current_dtv_offset + area_desc.block.offset; + dtv.tls_block = area + current_dtv_offset + area_info.block.offset; // Copy the initial data. - @memcpy(area[area_desc.block.offset..][0..area_desc.block.init.len], area_desc.block.init); + @memcpy(area_slice[area_info.block.offset..][0..area_info.block.init.len], area_info.block.init); // Return the corrected value (if needed) for the TP register. Overflow here is not a problem; // the pointer arithmetic involving the TP is done with wrapping semantics. - return @intFromPtr(area.ptr) +% switch (current_variant) { - .I_original, .II => area_desc.abi_tcb.offset, - .I_modified => area_desc.block.offset +% current_tp_offset, + return @intFromPtr(area) +% switch (current_variant) { + .I_original, .II => area_info.abi_tcb.offset, + .I_modified => area_info.block.offset +% current_tp_offset, }; } @@ -651,35 +675,35 @@ var main_thread_area_buffer: [0x1000]u8 align(page_size_min) = undefined; /// Computes the layout of the static TLS area, allocates the area, initializes all of its fields, /// and assigns the architecture-specific value to the TP register. -pub fn initStatic(phdrs: []elf.ElfN.Phdr) void { +fn __zig_elf_static_tls_init(phdrs: [*]elf.ElfN.Phdr, phnum: usize) callconv(.c) void { @setRuntimeSafety(false); @disableInstrumentation(); - computeAreaDesc(phdrs); + computeAreaDesc(phdrs[0..phnum]); - const area = blk: { + const area: [*]u8 = blk: { // Fast path for the common case where the TLS data is really small, avoid an allocation and // use our local buffer. - if (area_desc.alignment <= page_size_min and area_desc.size <= main_thread_area_buffer.len) { - break :blk main_thread_area_buffer[0..area_desc.size]; + if (__zig_elf_static_tls.alignment <= page_size_min and __zig_elf_static_tls.size <= main_thread_area_buffer.len) { + break :blk &main_thread_area_buffer; } - const begin_addr = mmap_tls(area_desc.size + area_desc.alignment - 1); + const begin_addr = mmap(__zig_elf_static_tls.size + __zig_elf_static_tls.alignment - 1); if (@call(.always_inline, linux.errno, .{begin_addr}) != .SUCCESS) @trap(); const area_ptr: [*]align(page_size_min) u8 = @ptrFromInt(begin_addr); // Make sure the slice is correctly aligned. - const begin_aligned_addr = alignForward(begin_addr, area_desc.alignment); + const begin_aligned_addr = alignForward(begin_addr, __zig_elf_static_tls.alignment); const start = begin_aligned_addr - begin_addr; - break :blk area_ptr[start..][0..area_desc.size]; + break :blk area_ptr + start; }; - const tp_value = prepareArea(area); + const tp_value = __zig_elf_static_tls_fill(area); setThreadPointer(tp_value); } -inline fn mmap_tls(length: usize) usize { +inline fn mmap(length: usize) usize { const prot: linux.PROT = .{ .READ = true, .WRITE = true }; const flags: linux.MAP = .{ .TYPE = .PRIVATE, .ANONYMOUS = true }; @@ -718,52 +742,37 @@ inline fn mmap_tls(length: usize) usize { } } -comptime { - assert(!builtin.link_libc); // otherwise libc should control TLS +const TlsIndex = switch (native_arch) { + .x86_64 => extern struct { module: u64, offset: u64 }, // Even for x32... + else => extern struct { module: usize, offset: usize }, // ...but not MIPS N32! +}; - if (builtin.output_mode == .Exe and builtin.link_mode == .static) { - // This is a static executable without libc, so it is our job to provide the TLS accessor - // function for the GD and LD models. This function is unlikely to actually be used, since - // the linker should be able to relax every TLS access to the LE model and therefore - // eliminate all calls to this function, but that isn't guaranteed. - const Fns = struct { - const TlsIndex = switch (native_arch) { - .x86_64 => extern struct { module: u64, offset: u64 }, // Even for x32... - else => extern struct { module: usize, offset: usize }, // ...but not MIPS N32! - }; - fn __tls_get_addr(ti: *const TlsIndex) callconv(.c) *anyopaque { - comptime assert(native_arch != .s390x); +fn __tls_get_addr(ti: *const TlsIndex) callconv(.c) *anyopaque { + comptime assert(native_arch != .s390x); - assert(ti.module == 1); // The executable's module ID is always 1 - const tp = getThreadPointer(); - const block: [*]u8 = switch (current_variant) { - .I_original => @ptrFromInt(tp -% area_desc.abi_tcb.offset +% area_desc.block.offset), - .I_modified => @ptrFromInt(tp -% current_tp_offset), - // The `.I_original` approach would also work for `.II`, but there is an - // alternative strategy which is one less operation: - .II => @ptrFromInt(tp -% area_desc.block.size), - }; - return block[@intCast(ti.offset)..]; - } - fn __tls_get_offset() callconv(.naked) noreturn { - comptime assert(native_arch == .s390x); + assert(ti.module == 1); // The executable's module ID is always 1 + const tp = getThreadPointer(); + const block: [*]u8 = switch (current_variant) { + .I_original => @ptrFromInt(tp -% area_info.abi_tcb.offset +% area_info.block.offset), + .I_modified => @ptrFromInt(tp -% current_tp_offset), + // The `.I_original` approach would also work for `.II`, but there is an + // alternative strategy which is one less operation: + .II => @ptrFromInt(tp -% area_info.block.size), + }; + return block[@intCast(ti.offset)..]; +} - // We receive the module's GOT pointer in r12 and the GOT offset in r2. - asm volatile ( - \\ la %%r1, 0(%%r12, %%r2) - \\ lg %%r2, 8(%%r1) - \\ lgrl %%r0, %[block_size] - \\ sgr %%r2, %%r0 - \\ br %%r14 - : - : [block_size] "s" (&area_desc.block.size), - ); - } - }; +fn __tls_get_offset() callconv(.naked) noreturn { + comptime assert(native_arch == .s390x); - if (native_arch == .s390x) - @export(&Fns.__tls_get_offset, .{ .name = "__tls_get_offset" }) - else - @export(&Fns.__tls_get_addr, .{ .name = "__tls_get_addr" }); - } + // We receive the module's GOT pointer in r12 and the GOT offset in r2. + asm volatile ( + \\ la %%r1, 0(%%r12, %%r2) + \\ lg %%r2, 8(%%r1) + \\ lgrl %%r0, %[block_size] + \\ sgr %%r2, %%r0 + \\ br %%r14 + : + : [block_size] "s" (&area_info.block.size), + ); } diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 66a3662eedc92bae67d06cb9fac9625e8bde87ff..f093e3899267f2f5afb93432fb32f3c8fbd4ce20 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -1434,6 +1434,16 @@ const LinuxThreadImpl = struct { } }; + /// Provided by compiler-rt and populated by `std.start`. + extern var __zig_elf_static_tls: extern struct { + size: usize, + alignment: usize, + gdt_entry_number: usize, + }; + + /// Provided by compiler-rt. + extern fn __zig_elf_static_tls_fill(area: [*]u8) usize; + fn spawn(config: SpawnConfig, comptime f: anytype, args: anytype) !Impl { const page_size = std.heap.pageSize(); const Args = @TypeOf(args); @@ -1465,9 +1475,9 @@ const LinuxThreadImpl = struct { bytes = std.mem.alignForward(usize, bytes, page_size); stack_offset = bytes; - bytes = std.mem.alignForward(usize, bytes, linux.tls.area_desc.alignment); + bytes = std.mem.alignForward(usize, bytes, __zig_elf_static_tls.alignment); tls_offset = bytes; - bytes += linux.tls.area_desc.size; + bytes += __zig_elf_static_tls.size; bytes = std.mem.alignForward(usize, bytes, @alignOf(Instance)); instance_offset = bytes; @@ -1509,12 +1519,12 @@ const LinuxThreadImpl = struct { } // Prepare the TLS segment and prepare a user_desc struct when needed on x86 - var tls_ptr = linux.tls.prepareArea(mapped[tls_offset..][0..linux.tls.area_desc.size]); + var tls_ptr = __zig_elf_static_tls_fill(mapped.ptr + tls_offset); var user_desc: if (target.cpu.arch == .x86) linux.user_desc else void = undefined; if (target.cpu.arch == .x86) { defer tls_ptr = @intFromPtr(&user_desc); user_desc = .{ - .entry_number = linux.tls.area_desc.gdt_entry_number, + .entry_number = __zig_elf_static_tls.gdt_entry_number, .base_addr = tls_ptr, .limit = 0xfffff, .flags = .{ diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index e75a0d822467481b245997d4818989a9c7654b50..7c18ef62011d3cd07a6612d78b652446ce602344 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -123,7 +123,6 @@ pub const gid_t = u32; pub const clock_t = isize; pub const time_t = arch_bits.time_t; -pub const tls = @import("linux/tls.zig"); pub const BPF = @import("linux/bpf.zig"); pub const IOCTL = @import("linux/ioctl.zig"); pub const SECCOMP = @import("linux/seccomp.zig"); diff --git a/lib/std/start.zig b/lib/std/start.zig index f8c85adeded05726074c5ed0516b336892d34154..d77c093d23792f1422d4cfdc2e8d97a2af181b7d 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -599,6 +599,9 @@ fn wWinMain(hInstance: *anyopaque, hPrevInstance: ?*anyopaque, pCmdLine: [*:0]u1 /// Provided by compiler-rt. extern var __zig_elf_auxv: [*]std.elf.Auxv; +/// Provided by compiler-rt. +extern fn __zig_elf_static_tls_init(phdrs: [*]elf.ElfN.Phdr, phnum: usize) void; + fn posixCallMainAndExit(argc_argv_ptr: [*]usize) callconv(.c) noreturn { // We're not ready to panic until thread local storage is initialized. @setRuntimeSafety(false); @@ -616,7 +619,7 @@ fn posixCallMainAndExit(argc_argv_ptr: [*]usize) callconv(.c) noreturn { const auxv: [*]elf.Auxv = @ptrCast(@alignCast(envp.ptr + envp_count + 1)); var at_hwcap: usize = 0; - const phdrs = init: { + const phdrs: [*]elf.ElfN.Phdr, const phnum: usize = init: { var i: usize = 0; var at_phdr: usize = 0; var at_phnum: usize = 0; @@ -628,14 +631,14 @@ fn posixCallMainAndExit(argc_argv_ptr: [*]usize) callconv(.c) noreturn { else => continue, } } - break :init @as([*]elf.ElfN.Phdr, @ptrFromInt(at_phdr))[0..at_phnum]; + break :init .{ @ptrFromInt(at_phdr), at_phnum }; }; // Apply the initial relocations as early as possible in the startup process. We cannot // make calls yet on some architectures (e.g. MIPS) *because* they haven't been applied yet, // so this must be fully inlined. if (builtin.link_mode == .static and builtin.position_independent_executable) { - @call(.always_inline, std.pie.relocate, .{phdrs}); + @call(.always_inline, std.pie.relocate, .{phdrs[0..phnum]}); } if (native_os == .linux) { @@ -657,14 +660,14 @@ fn posixCallMainAndExit(argc_argv_ptr: [*]usize) callconv(.c) noreturn { } // Initialize the TLS area. - std.os.linux.tls.initStatic(phdrs); + __zig_elf_static_tls_init(phdrs, phnum); } // The way Linux executables represent stack size is via the PT.GNU_STACK // program header. However the kernel does not recognize it; it always gives 8 MiB. // Here we look for the stack size in our program headers and use setrlimit // to ask for more stack space. - expandStackSize(phdrs); + expandStackSize(phdrs[0..phnum]); } const opt_init_array_start = @extern([*]const *const fn () callconv(.c) void, .{ -- 2.54.0 From f0d0412942e52a38bcd4914a50f686f5a59ed37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Wed, 19 Aug 2026 11:28:03 +0200 Subject: [PATCH 7/7] compiler-rt: move some checks up to elf.zig --- lib/compiler_rt/elf.zig | 23 +++++++++++++++++++++-- lib/compiler_rt/elf/auxv.zig | 5 +---- lib/compiler_rt/elf/tls.zig | 32 ++++++++++++++------------------ 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/lib/compiler_rt/elf.zig b/lib/compiler_rt/elf.zig index 11f934d30edde31a6c5f4ec01092636fa602620c..713fe4845c9f77d1dcf7903a5b23b3a1484e7ed0 100644 --- a/lib/compiler_rt/elf.zig +++ b/lib/compiler_rt/elf.zig @@ -1,4 +1,23 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +const compiler_rt = @import("../compiler_rt.zig"); +const symbol = compiler_rt.symbol; + +/// More operating systems are expected to be added here: +/// +/// * FreeBSD: https://codeberg.org/ziglang/zig/issues/30981 +/// * NetBSD: https://codeberg.org/ziglang/zig/issues/30980 +/// * OpenBSD: https://codeberg.org/ziglang/zig/issues/30982 +const use_zig_start = switch (builtin.target.os.tag) { + .linux, + => !builtin.link_libc, + else => false, +}; + comptime { - _ = @import("elf/auxv.zig"); - _ = @import("elf/tls.zig"); + if (use_zig_start) { + _ = @import("elf/auxv.zig"); + _ = @import("elf/tls.zig"); + } } diff --git a/lib/compiler_rt/elf/auxv.zig b/lib/compiler_rt/elf/auxv.zig index de5a45e501b5850f8f25aa18ffec75ccf23927ac..e8452202a8b085144a08a925153271b94b39ad7f 100644 --- a/lib/compiler_rt/elf/auxv.zig +++ b/lib/compiler_rt/elf/auxv.zig @@ -5,10 +5,7 @@ const compiler_rt = @import("../../compiler_rt.zig"); const symbol = compiler_rt.symbol; comptime { - // FreeBSD, NetBSD, and OpenBSD are expected to also use this in the future. - if (!builtin.link_libc and builtin.target.os.tag == .linux) { - symbol(@ptrCast(&__zig_elf_auxv), "__zig_elf_auxv"); - } + symbol(@ptrCast(&__zig_elf_auxv), "__zig_elf_auxv"); } /// Populated by `std.start` when libc is not linked on targets that provide an ELF auxiliary vector diff --git a/lib/compiler_rt/elf/tls.zig b/lib/compiler_rt/elf/tls.zig index b4231387593edb88fdaff50d5d526b5eec5e3eda..75dda8d300ec7e079979528af638fdc3e8e90347 100644 --- a/lib/compiler_rt/elf/tls.zig +++ b/lib/compiler_rt/elf/tls.zig @@ -11,32 +11,28 @@ const builtin = @import("builtin"); const std = @import("std"); - -const compiler_rt = @import("../../compiler_rt.zig"); -const symbol = compiler_rt.symbol; - const elf = std.elf; const assert = std.debug.assert; const native_arch = builtin.cpu.arch; const linux = std.os.linux; const page_size_min = std.heap.page_size_min; +const compiler_rt = @import("../../compiler_rt.zig"); +const symbol = compiler_rt.symbol; + comptime { - // FreeBSD, NetBSD, and OpenBSD are expected to also use this in the future. - if (!builtin.link_libc and builtin.target.os.tag == .linux) { - symbol(&__zig_elf_static_tls, "__zig_elf_static_tls"); - symbol(&__zig_elf_static_tls_init, "__zig_elf_static_tls_init"); - symbol(&__zig_elf_static_tls_fill, "__zig_elf_static_tls_fill"); + symbol(&__zig_elf_static_tls, "__zig_elf_static_tls"); + symbol(&__zig_elf_static_tls_init, "__zig_elf_static_tls_init"); + symbol(&__zig_elf_static_tls_fill, "__zig_elf_static_tls_fill"); - // To make static executables without libc work, it is our job to provide the TLS accessor - // function for the GD and LD models. This function is unlikely to actually be used, since - // the linker should be able to relax every TLS access to the LE model and therefore - // eliminate all calls to this function, but that isn't guaranteed. - if (native_arch == .s390x) - symbol(&__tls_get_offset, "__tls_get_offset") - else - symbol(&__tls_get_addr, "__tls_get_addr"); - } + // To make static executables without libc work, it is our job to provide the TLS accessor + // function for the GD and LD models. This function is unlikely to actually be used, since the + // linker should be able to relax every TLS access to the LE model and therefore eliminate all + // calls to this function, but that isn't guaranteed. + if (native_arch == .s390x) + symbol(&__tls_get_offset, "__tls_get_offset") + else + symbol(&__tls_get_addr, "__tls_get_addr"); } /// Represents an ELF TLS variant. -- 2.54.0