Commit 0bf8617d96

Jacob Young <jacobly0@users.noreply.github.com>
2025-06-05 03:45:31
x86_64: add support for pie executables
1 parent 178ee8a
lib/std/zig/llvm/Builder.zig
@@ -1823,6 +1823,14 @@ pub const Visibility = enum(u2) {
     hidden = 1,
     protected = 2,
 
+    pub fn fromSymbolVisibility(sv: std.builtin.SymbolVisibility) Visibility {
+        return switch (sv) {
+            .default => .default,
+            .hidden => .hidden,
+            .protected => .protected,
+        };
+    }
+
     pub fn format(
         self: Visibility,
         comptime _: []const u8,
@@ -2555,6 +2563,10 @@ pub const Variable = struct {
             return self.ptrConst(builder).global.setLinkage(linkage, builder);
         }
 
+        pub fn setVisibility(self: Index, visibility: Visibility, builder: *Builder) void {
+            return self.ptrConst(builder).global.setVisibility(visibility, builder);
+        }
+
         pub fn setDllStorageClass(self: Index, class: DllStorageClass, builder: *Builder) void {
             return self.ptrConst(builder).global.setDllStorageClass(class, builder);
         }
lib/std/builtin.zig
@@ -61,7 +61,7 @@ pub const StackTrace = struct {
 
 /// This data structure is used by the Zig language code generation and
 /// therefore must be kept in sync with the compiler implementation.
-pub const GlobalLinkage = enum {
+pub const GlobalLinkage = enum(u2) {
     internal,
     strong,
     weak,
@@ -70,7 +70,7 @@ pub const GlobalLinkage = enum {
 
 /// This data structure is used by the Zig language code generation and
 /// therefore must be kept in sync with the compiler implementation.
-pub const SymbolVisibility = enum {
+pub const SymbolVisibility = enum(u2) {
     default,
     hidden,
     protected,
@@ -1030,8 +1030,19 @@ pub const ExternOptions = struct {
     name: []const u8,
     library_name: ?[]const u8 = null,
     linkage: GlobalLinkage = .strong,
+    visibility: SymbolVisibility = .default,
+    /// Setting this to `true` makes the `@extern` a runtime value.
     is_thread_local: bool = false,
     is_dll_import: bool = false,
+    relocation: Relocation = .any,
+
+    pub const Relocation = enum(u1) {
+        /// Any type of relocation is allowed.
+        any,
+        /// A program-counter-relative relocation is required.
+        /// Using this value makes the `@extern` a runtime value.
+        pcrel,
+    };
 };
 
 /// This data structure is used by the Zig language code generation and
lib/std/dynamic_library.zig
@@ -83,13 +83,16 @@ const RDebug = extern struct {
     r_ldbase: usize,
 };
 
-/// TODO make it possible to reference this same external symbol 2x so we don't need this
-/// helper function.
-pub fn get_DYNAMIC() ?[*]elf.Dyn {
-    return @extern([*]elf.Dyn, .{ .name = "_DYNAMIC", .linkage = .weak });
+/// TODO fix comparisons of extern symbol pointers so we don't need this helper function.
+pub fn get_DYNAMIC() ?[*]const elf.Dyn {
+    return @extern([*]const elf.Dyn, .{
+        .name = "_DYNAMIC",
+        .linkage = .weak,
+        .visibility = .hidden,
+    });
 }
 
-pub fn linkmap_iterator(phdrs: []elf.Phdr) error{InvalidExe}!LinkMap.Iterator {
+pub fn linkmap_iterator(phdrs: []const elf.Phdr) error{InvalidExe}!LinkMap.Iterator {
     _ = phdrs;
     const _DYNAMIC = get_DYNAMIC() orelse {
         // No PT_DYNAMIC means this is either a statically-linked program or a
lib/std/pie.zig
@@ -39,167 +39,175 @@ const R_RELATIVE = switch (builtin.cpu.arch) {
 // Obtain a pointer to the _DYNAMIC array.
 // We have to compute its address as a PC-relative quantity not to require a
 // relocation that, at this point, is not yet applied.
-inline fn getDynamicSymbol() [*]elf.Dyn {
-    return switch (builtin.cpu.arch) {
-        .x86 => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ call 1f
-            \\ 1: pop %[ret]
-            \\ lea _DYNAMIC-1b(%[ret]), %[ret]
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        .x86_64 => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ lea _DYNAMIC(%%rip), %[ret]
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        .arc => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ add %[ret], pcl, _DYNAMIC@pcl
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        // Work around the limited offset range of `ldr`
-        .arm, .armeb, .thumb, .thumbeb => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ ldr %[ret], 1f
-            \\ add %[ret], pc
-            \\ b 2f
-            \\ 1: .word _DYNAMIC-1b
-            \\ 2:
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        // A simple `adr` is not enough as it has a limited offset range
-        .aarch64, .aarch64_be => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ adrp %[ret], _DYNAMIC
-            \\ add %[ret], %[ret], #:lo12:_DYNAMIC
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        // The CSKY ABI requires the gb register to point to the GOT. Additionally, the first
-        // entry in the GOT is defined to hold the address of _DYNAMIC.
-        .csky => asm volatile (
-            \\ mov %[ret], gb
-            \\ ldw %[ret], %[ret]
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        .hexagon => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ jump 1f
-            \\ .word _DYNAMIC - .
-            \\ 1:
-            \\ r1 = pc
-            \\ r1 = add(r1, #-4)
-            \\ %[ret] = memw(r1)
-            \\ %[ret] = add(r1, %[ret])
-            : [ret] "=r" (-> [*]elf.Dyn),
-            :
-            : "r1"
-        ),
-        .loongarch32, .loongarch64 => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ la.local %[ret], _DYNAMIC
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        // Note that the - 8 is needed because pc in the second lea instruction points into the
-        // middle of that instruction. (The first lea is 6 bytes, the second is 4 bytes.)
-        .m68k => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ lea _DYNAMIC - . - 8, %[ret]
-            \\ lea (%[ret], %%pc), %[ret]
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        .mips, .mipsel => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ bal 1f
-            \\ .gpword _DYNAMIC
-            \\ 1:
-            \\ lw %[ret], 0($ra)
-            \\ addu %[ret], %[ret], $gp
-            : [ret] "=r" (-> [*]elf.Dyn),
-            :
-            : "lr"
-        ),
-        .mips64, .mips64el => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ .balign 8
-            \\ bal 1f
-            \\ .gpdword _DYNAMIC
-            \\ 1:
-            \\ ld %[ret], 0($ra)
-            \\ daddu %[ret], %[ret], $gp
-            : [ret] "=r" (-> [*]elf.Dyn),
-            :
-            : "lr"
-        ),
-        .powerpc, .powerpcle => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ bl 1f
-            \\ .long _DYNAMIC - .
-            \\ 1:
-            \\ mflr %[ret]
-            \\ lwz 4, 0(%[ret])
-            \\ add %[ret], 4, %[ret]
-            : [ret] "=r" (-> [*]elf.Dyn),
-            :
-            : "lr", "r4"
-        ),
-        .powerpc64, .powerpc64le => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ bl 1f
-            \\ .quad _DYNAMIC - .
-            \\ 1:
-            \\ mflr %[ret]
-            \\ ld 4, 0(%[ret])
-            \\ add %[ret], 4, %[ret]
-            : [ret] "=r" (-> [*]elf.Dyn),
-            :
-            : "lr", "r4"
-        ),
-        .riscv32, .riscv64 => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ lla %[ret], _DYNAMIC
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        .s390x => asm volatile (
-            \\ .weak _DYNAMIC
-            \\ .hidden _DYNAMIC
-            \\ larl %[ret], 1f
-            \\ ag %[ret], 0(%[ret])
-            \\ jg 2f
-            \\ 1: .quad _DYNAMIC - .
-            \\ 2:
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        // The compiler does not necessarily have any obligation to load the `l7` register (pointing
-        // to the GOT), so do it ourselves just in case.
-        .sparc, .sparc64 => asm volatile (
-            \\ sethi %%hi(_GLOBAL_OFFSET_TABLE_ - 4), %%l7
-            \\ call 1f
-            \\ add %%l7, %%lo(_GLOBAL_OFFSET_TABLE_ + 4), %%l7
-            \\ 1:
-            \\ add %%l7, %%o7, %[ret]
-            : [ret] "=r" (-> [*]elf.Dyn),
-        ),
-        else => {
-            @compileError("PIE startup is not yet supported for this target!");
+inline fn getDynamicSymbol() [*]const elf.Dyn {
+    return switch (builtin.zig_backend) {
+        else => switch (builtin.cpu.arch) {
+            .x86 => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ call 1f
+                \\ 1: pop %[ret]
+                \\ lea _DYNAMIC-1b(%[ret]), %[ret]
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            .x86_64 => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ lea _DYNAMIC(%%rip), %[ret]
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            .arc => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ add %[ret], pcl, _DYNAMIC@pcl
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            // Work around the limited offset range of `ldr`
+            .arm, .armeb, .thumb, .thumbeb => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ ldr %[ret], 1f
+                \\ add %[ret], pc
+                \\ b 2f
+                \\ 1: .word _DYNAMIC-1b
+                \\ 2:
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            // A simple `adr` is not enough as it has a limited offset range
+            .aarch64, .aarch64_be => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ adrp %[ret], _DYNAMIC
+                \\ add %[ret], %[ret], #:lo12:_DYNAMIC
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            // The CSKY ABI requires the gb register to point to the GOT. Additionally, the first
+            // entry in the GOT is defined to hold the address of _DYNAMIC.
+            .csky => asm volatile (
+                \\ mov %[ret], gb
+                \\ ldw %[ret], %[ret]
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            .hexagon => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ jump 1f
+                \\ .word _DYNAMIC - .
+                \\ 1:
+                \\ r1 = pc
+                \\ r1 = add(r1, #-4)
+                \\ %[ret] = memw(r1)
+                \\ %[ret] = add(r1, %[ret])
+                : [ret] "=r" (-> [*]const elf.Dyn),
+                :
+                : "r1"
+            ),
+            .loongarch32, .loongarch64 => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ la.local %[ret], _DYNAMIC
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            // Note that the - 8 is needed because pc in the second lea instruction points into the
+            // middle of that instruction. (The first lea is 6 bytes, the second is 4 bytes.)
+            .m68k => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ lea _DYNAMIC - . - 8, %[ret]
+                \\ lea (%[ret], %%pc), %[ret]
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            .mips, .mipsel => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ bal 1f
+                \\ .gpword _DYNAMIC
+                \\ 1:
+                \\ lw %[ret], 0($ra)
+                \\ addu %[ret], %[ret], $gp
+                : [ret] "=r" (-> [*]const elf.Dyn),
+                :
+                : "lr"
+            ),
+            .mips64, .mips64el => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ .balign 8
+                \\ bal 1f
+                \\ .gpdword _DYNAMIC
+                \\ 1:
+                \\ ld %[ret], 0($ra)
+                \\ daddu %[ret], %[ret], $gp
+                : [ret] "=r" (-> [*]const elf.Dyn),
+                :
+                : "lr"
+            ),
+            .powerpc, .powerpcle => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ bl 1f
+                \\ .long _DYNAMIC - .
+                \\ 1:
+                \\ mflr %[ret]
+                \\ lwz 4, 0(%[ret])
+                \\ add %[ret], 4, %[ret]
+                : [ret] "=r" (-> [*]const elf.Dyn),
+                :
+                : "lr", "r4"
+            ),
+            .powerpc64, .powerpc64le => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ bl 1f
+                \\ .quad _DYNAMIC - .
+                \\ 1:
+                \\ mflr %[ret]
+                \\ ld 4, 0(%[ret])
+                \\ add %[ret], 4, %[ret]
+                : [ret] "=r" (-> [*]const elf.Dyn),
+                :
+                : "lr", "r4"
+            ),
+            .riscv32, .riscv64 => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ lla %[ret], _DYNAMIC
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            .s390x => asm volatile (
+                \\ .weak _DYNAMIC
+                \\ .hidden _DYNAMIC
+                \\ larl %[ret], 1f
+                \\ ag %[ret], 0(%[ret])
+                \\ jg 2f
+                \\ 1: .quad _DYNAMIC - .
+                \\ 2:
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            // The compiler does not necessarily have any obligation to load the `l7` register (pointing
+            // to the GOT), so do it ourselves just in case.
+            .sparc, .sparc64 => asm volatile (
+                \\ sethi %%hi(_GLOBAL_OFFSET_TABLE_ - 4), %%l7
+                \\ call 1f
+                \\ add %%l7, %%lo(_GLOBAL_OFFSET_TABLE_ + 4), %%l7
+                \\ 1:
+                \\ add %%l7, %%o7, %[ret]
+                : [ret] "=r" (-> [*]const elf.Dyn),
+            ),
+            else => {
+                @compileError("PIE startup is not yet supported for this target!");
+            },
         },
+        .stage2_x86_64 => @extern([*]const elf.Dyn, .{
+            .name = "_DYNAMIC",
+            .linkage = .weak,
+            .visibility = .hidden,
+            .relocation = .pcrel,
+        }).?,
     };
 }
 
-pub fn relocate(phdrs: []elf.Phdr) void {
+pub fn relocate(phdrs: []const elf.Phdr) void {
     @setRuntimeSafety(false);
     @disableInstrumentation();
 
@@ -256,10 +264,9 @@ pub fn relocate(phdrs: []elf.Phdr) void {
 
     const rel = sorted_dynv[elf.DT_REL];
     if (rel != 0) {
-        const rels = @call(.always_inline, std.mem.bytesAsSlice, .{
-            elf.Rel,
-            @as([*]u8, @ptrFromInt(base_addr + rel))[0..sorted_dynv[elf.DT_RELSZ]],
-        });
+        const rels: []const elf.Rel = @alignCast(@ptrCast(
+            @as([*]align(@alignOf(elf.Rel)) const u8, @ptrFromInt(base_addr + rel))[0..sorted_dynv[elf.DT_RELSZ]],
+        ));
         for (rels) |r| {
             if (r.r_type() != R_RELATIVE) continue;
             @as(*usize, @ptrFromInt(base_addr + r.r_offset)).* += base_addr;
@@ -268,10 +275,9 @@ pub fn relocate(phdrs: []elf.Phdr) void {
 
     const rela = sorted_dynv[elf.DT_RELA];
     if (rela != 0) {
-        const relas = @call(.always_inline, std.mem.bytesAsSlice, .{
-            elf.Rela,
-            @as([*]u8, @ptrFromInt(base_addr + rela))[0..sorted_dynv[elf.DT_RELASZ]],
-        });
+        const relas: []const elf.Rela = @alignCast(@ptrCast(
+            @as([*]align(@alignOf(elf.Rela)) const u8, @ptrFromInt(base_addr + rela))[0..sorted_dynv[elf.DT_RELASZ]],
+        ));
         for (relas) |r| {
             if (r.r_type() != R_RELATIVE) continue;
             @as(*usize, @ptrFromInt(base_addr + r.r_offset)).* = base_addr + @as(usize, @bitCast(r.r_addend));
@@ -280,10 +286,9 @@ pub fn relocate(phdrs: []elf.Phdr) void {
 
     const relr = sorted_dynv[elf.DT_RELR];
     if (relr != 0) {
-        const relrs = @call(.always_inline, std.mem.bytesAsSlice, .{
-            elf.Relr,
-            @as([*]u8, @ptrFromInt(base_addr + relr))[0..sorted_dynv[elf.DT_RELRSZ]],
-        });
+        const relrs: []const elf.Relr = @ptrCast(
+            @as([*]align(@alignOf(elf.Relr)) const u8, @ptrFromInt(base_addr + relr))[0..sorted_dynv[elf.DT_RELRSZ]],
+        );
         var current: [*]usize = undefined;
         for (relrs) |r| {
             if ((r & 1) == 0) {
lib/std/start.zig
@@ -163,7 +163,7 @@ fn exit2(code: usize) noreturn {
         // exits(0)
         .plan9 => std.os.plan9.exits(null),
         .windows => {
-            std.os.windows.ntdll.RtlExitUserProcess(@as(u32, @truncate(code)));
+            std.os.windows.ntdll.RtlExitUserProcess(@truncate(code));
         },
         else => @compileError("TODO"),
     }
@@ -511,7 +511,7 @@ fn posixCallMainAndExit(argc_argv_ptr: [*]usize) callconv(.c) noreturn {
     // Code coverage instrumentation might try to use thread local variables.
     @disableInstrumentation();
     const argc = argc_argv_ptr[0];
-    const argv = @as([*][*:0]u8, @ptrCast(argc_argv_ptr + 1));
+    const argv: [*][*:0]u8 = @ptrCast(argc_argv_ptr + 1);
 
     const envp_optional: [*:null]?[*:0]u8 = @ptrCast(@alignCast(argv + argc + 1));
     var envp_count: usize = 0;
@@ -573,11 +573,11 @@ fn posixCallMainAndExit(argc_argv_ptr: [*]usize) callconv(.c) noreturn {
         expandStackSize(phdrs);
     }
 
-    const opt_init_array_start = @extern([*]*const fn () callconv(.c) void, .{
+    const opt_init_array_start = @extern([*]const *const fn () callconv(.c) void, .{
         .name = "__init_array_start",
         .linkage = .weak,
     });
-    const opt_init_array_end = @extern([*]*const fn () callconv(.c) void, .{
+    const opt_init_array_end = @extern([*]const *const fn () callconv(.c) void, .{
         .name = "__init_array_end",
         .linkage = .weak,
     });
@@ -651,7 +651,7 @@ fn main(c_argc: c_int, c_argv: [*][*:0]c_char, c_envp: [*:null]?[*:0]c_char) cal
 }
 
 fn mainWithoutEnv(c_argc: c_int, c_argv: [*][*:0]c_char) callconv(.c) c_int {
-    std.os.argv = @as([*][*:0]u8, @ptrCast(c_argv))[0..@as(usize, @intCast(c_argc))];
+    std.os.argv = @as([*][*:0]u8, @ptrCast(c_argv))[0..@intCast(c_argc)];
     return callMain();
 }
 
@@ -701,7 +701,7 @@ pub inline fn callMain() u8 {
 pub fn call_wWinMain() std.os.windows.INT {
     const peb = std.os.windows.peb();
     const MAIN_HINSTANCE = @typeInfo(@TypeOf(root.wWinMain)).@"fn".params[0].type.?;
-    const hInstance = @as(MAIN_HINSTANCE, @ptrCast(peb.ImageBaseAddress));
+    const hInstance: MAIN_HINSTANCE = @ptrCast(peb.ImageBaseAddress);
     const lpCmdLine: [*:0]u16 = @ptrCast(peb.ProcessParameters.CommandLine.Buffer);
 
     // There are various types used for the 'show window' variable through the Win32 APIs:
lib/zig.h
@@ -272,6 +272,15 @@
 #define zig_linksection_fn zig_linksection
 #endif
 
+#if zig_has_attribute(visibility)
+#define zig_visibility(name) __attribute__((visibility(#name)))
+#else
+#define zig_visibility(name) zig_visibility_##name
+#define zig_visibility_default
+#define zig_visibility_hidden zig_visibility_hidden_unavailable
+#define zig_visibility_protected zig_visibility_protected_unavailable
+#endif
+
 #if zig_has_builtin(unreachable) || defined(zig_gcc) || defined(zig_tinyc)
 #define zig_unreachable() __builtin_unreachable()
 #elif defined(zig_msvc)
src/Air/Liveness/Verify.zig
@@ -63,7 +63,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
             .wasm_memory_size,
             .err_return_trace,
             .save_err_return_trace_index,
-            .tlv_dllimport_ptr,
+            .runtime_nav_ptr,
             .c_va_start,
             .work_item_id,
             .work_group_size,
src/Air/Legalize.zig
@@ -622,7 +622,7 @@ fn legalizeBody(l: *Legalize, body_start: usize, body_len: usize) Error!void {
             .addrspace_cast,
             .save_err_return_trace_index,
             .vector_store_elem,
-            .tlv_dllimport_ptr,
+            .runtime_nav_ptr,
             .c_va_arg,
             .c_va_copy,
             .c_va_end,
src/Air/Liveness.zig
@@ -339,7 +339,7 @@ pub fn categorizeOperand(
         .wasm_memory_size,
         .err_return_trace,
         .save_err_return_trace_index,
-        .tlv_dllimport_ptr,
+        .runtime_nav_ptr,
         .c_va_start,
         .work_item_id,
         .work_group_size,
@@ -972,7 +972,7 @@ fn analyzeInst(
         .wasm_memory_size,
         .err_return_trace,
         .save_err_return_trace_index,
-        .tlv_dllimport_ptr,
+        .runtime_nav_ptr,
         .c_va_start,
         .work_item_id,
         .work_group_size,
src/print_air.zig โ†’ src/Air/print.zig
@@ -2,13 +2,15 @@ const std = @import("std");
 const Allocator = std.mem.Allocator;
 const fmtIntSizeBin = std.fmt.fmtIntSizeBin;
 
-const Zcu = @import("Zcu.zig");
-const Value = @import("Value.zig");
-const Type = @import("Type.zig");
-const Air = @import("Air.zig");
-const InternPool = @import("InternPool.zig");
-
-pub fn write(stream: anytype, pt: Zcu.PerThread, air: Air, liveness: ?Air.Liveness) void {
+const build_options = @import("build_options");
+const Zcu = @import("../Zcu.zig");
+const Value = @import("../Value.zig");
+const Type = @import("../Type.zig");
+const Air = @import("../Air.zig");
+const InternPool = @import("../InternPool.zig");
+
+pub fn write(air: Air, stream: anytype, pt: Zcu.PerThread, liveness: ?Air.Liveness) void {
+    comptime std.debug.assert(build_options.enable_debug_extensions);
     const instruction_bytes = air.instructions.len *
         // Here we don't use @sizeOf(Air.Inst.Data) because it would include
         // the debug safety tag but we want to measure release size.
@@ -52,12 +54,13 @@ pub fn write(stream: anytype, pt: Zcu.PerThread, air: Air, liveness: ?Air.Livene
 }
 
 pub fn writeInst(
+    air: Air,
     stream: anytype,
     inst: Air.Inst.Index,
     pt: Zcu.PerThread,
-    air: Air,
     liveness: ?Air.Liveness,
 ) void {
+    comptime std.debug.assert(build_options.enable_debug_extensions);
     var writer: Writer = .{
         .pt = pt,
         .gpa = pt.zcu.gpa,
@@ -69,12 +72,12 @@ pub fn writeInst(
     writer.writeInst(stream, inst) catch return;
 }
 
-pub fn dump(pt: Zcu.PerThread, air: Air, liveness: ?Air.Liveness) void {
-    write(std.io.getStdErr().writer(), pt, air, liveness);
+pub fn dump(air: Air, pt: Zcu.PerThread, liveness: ?Air.Liveness) void {
+    air.write(std.io.getStdErr().writer(), pt, liveness);
 }
 
-pub fn dumpInst(inst: Air.Inst.Index, pt: Zcu.PerThread, air: Air, liveness: ?Air.Liveness) void {
-    writeInst(std.io.getStdErr().writer(), inst, pt, air, liveness);
+pub fn dumpInst(air: Air, inst: Air.Inst.Index, pt: Zcu.PerThread, liveness: ?Air.Liveness) void {
+    air.writeInst(std.io.getStdErr().writer(), inst, pt, liveness);
 }
 
 const Writer = struct {
@@ -320,7 +323,7 @@ const Writer = struct {
             .reduce, .reduce_optimized => try w.writeReduce(s, inst),
             .cmp_vector, .cmp_vector_optimized => try w.writeCmpVector(s, inst),
             .vector_store_elem => try w.writeVectorStoreElem(s, inst),
-            .tlv_dllimport_ptr => try w.writeTlvDllimportPtr(s, inst),
+            .runtime_nav_ptr => try w.writeRuntimeNavPtr(s, inst),
 
             .work_item_id,
             .work_group_size,
@@ -578,7 +581,7 @@ const Writer = struct {
         try w.writeOperand(s, inst, 2, extra.rhs);
     }
 
-    fn writeTlvDllimportPtr(w: *Writer, s: anytype, inst: Air.Inst.Index) @TypeOf(s).Error!void {
+    fn writeRuntimeNavPtr(w: *Writer, s: anytype, inst: Air.Inst.Index) @TypeOf(s).Error!void {
         const ip = &w.pt.zcu.intern_pool;
         const ty_nav = w.air.instructions.items(.data)[@intFromEnum(inst)].ty_nav;
         try w.writeType(s, .fromInterned(ty_nav.ty));
src/Air/types_resolved.zig
@@ -321,7 +321,7 @@ fn checkBody(air: Air, body: []const Air.Inst.Index, zcu: *Zcu) bool {
                 if (!checkRef(bin.rhs, zcu)) return false;
             },
 
-            .tlv_dllimport_ptr => {
+            .runtime_nav_ptr => {
                 if (!checkType(.fromInterned(data.ty_nav.ty), zcu)) return false;
             },
 
src/arch/aarch64/CodeGen.zig
@@ -880,7 +880,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .is_named_enum_value => return self.fail("TODO implement is_named_enum_value", .{}),
             .error_set_has_value => return self.fail("TODO implement error_set_has_value", .{}),
             .vector_store_elem => return self.fail("TODO implement vector_store_elem", .{}),
-            .tlv_dllimport_ptr => return self.fail("TODO implement tlv_dllimport_ptr", .{}),
+            .runtime_nav_ptr => return self.fail("TODO implement runtime_nav_ptr", .{}),
 
             .c_va_arg => return self.fail("TODO implement c_va_arg", .{}),
             .c_va_copy => return self.fail("TODO implement c_va_copy", .{}),
src/arch/arm/CodeGen.zig
@@ -869,7 +869,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .is_named_enum_value => return self.fail("TODO implement is_named_enum_value", .{}),
             .error_set_has_value => return self.fail("TODO implement error_set_has_value", .{}),
             .vector_store_elem => return self.fail("TODO implement vector_store_elem", .{}),
-            .tlv_dllimport_ptr => return self.fail("TODO implement tlv_dllimport_ptr", .{}),
+            .runtime_nav_ptr => return self.fail("TODO implement runtime_nav_ptr", .{}),
 
             .c_va_arg => return self.fail("TODO implement c_va_arg", .{}),
             .c_va_copy => return self.fail("TODO implement c_va_copy", .{}),
src/arch/riscv64/CodeGen.zig
@@ -1041,12 +1041,7 @@ fn formatAir(
     _: std.fmt.FormatOptions,
     writer: anytype,
 ) @TypeOf(writer).Error!void {
-    @import("../../print_air.zig").dumpInst(
-        data.inst,
-        data.func.pt,
-        data.func.air,
-        data.func.liveness,
-    );
+    data.func.air.dumpInst(data.inst, data.func.pt, data.func.liveness);
 }
 fn fmtAir(func: *Func, inst: Air.Inst.Index) std.fmt.Formatter(formatAir) {
     return .{ .data = .{ .func = func, .inst = inst } };
@@ -1656,7 +1651,7 @@ fn genBody(func: *Func, body: []const Air.Inst.Index) InnerError!void {
             .wrap_errunion_payload => try func.airWrapErrUnionPayload(inst),
             .wrap_errunion_err     => try func.airWrapErrUnionErr(inst),
 
-            .tlv_dllimport_ptr => try func.airTlvDllimportPtr(inst),
+            .runtime_nav_ptr => try func.airRuntimeNavPtr(inst),
 
             .add_optimized,
             .sub_optimized,
@@ -3626,7 +3621,7 @@ fn airWrapErrUnionErr(func: *Func, inst: Air.Inst.Index) !void {
     return func.finishAir(inst, result, .{ ty_op.operand, .none, .none });
 }
 
-fn airTlvDllimportPtr(func: *Func, inst: Air.Inst.Index) !void {
+fn airRuntimeNavPtr(func: *Func, inst: Air.Inst.Index) !void {
     const zcu = func.pt.zcu;
     const ip = &zcu.intern_pool;
     const ty_nav = func.air.instructions.items(.data)[@intFromEnum(inst)].ty_nav;
@@ -3641,7 +3636,7 @@ fn airTlvDllimportPtr(func: *Func, inst: Air.Inst.Index) !void {
             break :sym sym;
         }
         break :sym try zo.getOrCreateMetadataForNav(zcu, ty_nav.nav);
-    } else return func.fail("TODO tlv_dllimport_ptr on {}", .{func.bin_file.tag});
+    } else return func.fail("TODO runtime_nav_ptr on {}", .{func.bin_file.tag});
 
     const dest_mcv = try func.allocRegOrMem(ptr_ty, inst, true);
     if (dest_mcv.isRegister()) {
src/arch/sparc64/CodeGen.zig
@@ -723,7 +723,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .is_named_enum_value => @panic("TODO implement is_named_enum_value"),
             .error_set_has_value => @panic("TODO implement error_set_has_value"),
             .vector_store_elem => @panic("TODO implement vector_store_elem"),
-            .tlv_dllimport_ptr => @panic("TODO implement tlv_dllimport_ptr"),
+            .runtime_nav_ptr => @panic("TODO implement runtime_nav_ptr"),
 
             .c_va_arg => return self.fail("TODO implement c_va_arg", .{}),
             .c_va_copy => return self.fail("TODO implement c_va_copy", .{}),
src/arch/wasm/CodeGen.zig
@@ -2057,7 +2057,7 @@ fn genInst(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void {
         .error_set_has_value => cg.airErrorSetHasValue(inst),
         .frame_addr => cg.airFrameAddress(inst),
 
-        .tlv_dllimport_ptr => cg.airTlvDllimportPtr(inst),
+        .runtime_nav_ptr => cg.airRuntimeNavPtr(inst),
 
         .assembly,
         .is_err_ptr,
@@ -7616,7 +7616,7 @@ fn airFrameAddress(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void {
     return cg.finishAir(inst, .stack, &.{});
 }
 
-fn airTlvDllimportPtr(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void {
+fn airRuntimeNavPtr(cg: *CodeGen, inst: Air.Inst.Index) InnerError!void {
     const ty_nav = cg.air.instructions.items(.data)[@intFromEnum(inst)].ty_nav;
     const mod = cg.pt.zcu.navFileScope(cg.owner_nav).mod.?;
     if (mod.single_threaded) {
src/arch/x86_64/bits.zig
@@ -762,6 +762,7 @@ pub const Memory = struct {
         frame: FrameIndex,
         table,
         reloc: u32,
+        pcrel: u32,
         rip_inst: Mir.Inst.Index,
 
         pub const Tag = @typeInfo(Base).@"union".tag_type.?;
src/arch/x86_64/CodeGen.zig
@@ -274,6 +274,12 @@ pub const MCValue = union(enum) {
     load_symbol: bits.SymbolOffset,
     /// The address of the memory location not-yet-allocated by the linker.
     lea_symbol: bits.SymbolOffset,
+    /// The value is in memory at an address not-yet-allocated by the linker.
+    /// This must use a non-got pc-relative relocation.
+    load_pcrel: bits.SymbolOffset,
+    /// The address of the memory location not-yet-allocated by the linker.
+    /// This must use a non-got pc-relative relocation.
+    lea_pcrel: bits.SymbolOffset,
     /// The value is in memory at a constant offset from the address in a register.
     indirect: bits.RegisterOffset,
     /// The value is in memory.
@@ -314,6 +320,7 @@ pub const MCValue = union(enum) {
             .eflags,
             .register_overflow,
             .lea_symbol,
+            .lea_pcrel,
             .lea_direct,
             .lea_got,
             .lea_frame,
@@ -327,6 +334,7 @@ pub const MCValue = union(enum) {
             .register_quadruple,
             .memory,
             .load_symbol,
+            .load_pcrel,
             .load_got,
             .load_direct,
             .indirect,
@@ -429,6 +437,7 @@ pub const MCValue = union(enum) {
             .register_overflow,
             .register_mask,
             .lea_symbol,
+            .lea_pcrel,
             .lea_direct,
             .lea_got,
             .lea_frame,
@@ -445,6 +454,7 @@ pub const MCValue = union(enum) {
             .load_got => |sym_index| .{ .lea_got = sym_index },
             .load_frame => |frame_addr| .{ .lea_frame = frame_addr },
             .load_symbol => |sym_off| .{ .lea_symbol = sym_off },
+            .load_pcrel => |sym_off| .{ .lea_pcrel = sym_off },
         };
     }
 
@@ -466,6 +476,7 @@ pub const MCValue = union(enum) {
             .load_got,
             .load_frame,
             .load_symbol,
+            .load_pcrel,
             .elementwise_args,
             .reserved_frame,
             .air_ref,
@@ -477,6 +488,7 @@ pub const MCValue = union(enum) {
             .lea_got => |sym_index| .{ .load_got = sym_index },
             .lea_frame => |frame_addr| .{ .load_frame = frame_addr },
             .lea_symbol => |sym_index| .{ .load_symbol = sym_index },
+            .lea_pcrel => |sym_index| .{ .load_pcrel = sym_index },
         };
     }
 
@@ -505,6 +517,8 @@ pub const MCValue = union(enum) {
             .load_frame,
             .load_symbol,
             .lea_symbol,
+            .load_pcrel,
+            .lea_pcrel,
             => switch (off) {
                 0 => mcv,
                 else => unreachable, // not offsettable
@@ -543,6 +557,7 @@ pub const MCValue = union(enum) {
             .elementwise_args,
             .reserved_frame,
             .lea_symbol,
+            .lea_pcrel,
             => unreachable,
             .memory => |addr| if (std.math.cast(i32, @as(i64, @bitCast(addr)))) |small_addr| .{
                 .base = .{ .reg = .ds },
@@ -583,6 +598,18 @@ pub const MCValue = union(enum) {
                     } },
                 };
             },
+            .load_pcrel => |sym_off| {
+                assert(sym_off.off == 0);
+                return .{
+                    .base = .{ .pcrel = sym_off.sym_index },
+                    .mod = .{ .rm = .{
+                        .size = mod_rm.size,
+                        .index = mod_rm.index,
+                        .scale = mod_rm.scale,
+                        .disp = sym_off.off + mod_rm.disp,
+                    } },
+                };
+            },
             .air_ref => |ref| (try function.resolveInst(ref)).mem(function, mod_rm),
         };
     }
@@ -618,6 +645,8 @@ pub const MCValue = union(enum) {
             }),
             .load_symbol => |pl| try writer.print("[sym:{} + 0x{x}]", .{ pl.sym_index, pl.off }),
             .lea_symbol => |pl| try writer.print("sym:{} + 0x{x}", .{ pl.sym_index, pl.off }),
+            .load_pcrel => |pl| try writer.print("[sym@pcrel:{} + 0x{x}]", .{ pl.sym_index, pl.off }),
+            .lea_pcrel => |pl| try writer.print("sym@pcrel:{} + 0x{x}", .{ pl.sym_index, pl.off }),
             .indirect => |pl| try writer.print("[{s} + 0x{x}]", .{ @tagName(pl.reg), pl.off }),
             .load_direct => |pl| try writer.print("[direct:{d}]", .{pl}),
             .lea_direct => |pl| try writer.print("direct:{d}", .{pl}),
@@ -655,6 +684,8 @@ const InstTracking = struct {
             .lea_frame,
             .load_symbol,
             .lea_symbol,
+            .load_pcrel,
+            .lea_pcrel,
             => result,
             .dead,
             .elementwise_args,
@@ -755,6 +786,8 @@ const InstTracking = struct {
             .lea_frame,
             .load_symbol,
             .lea_symbol,
+            .load_pcrel,
+            .lea_pcrel,
             => assert(std.meta.eql(self.long, target.long)),
             .dead,
             .eflags,
@@ -1228,12 +1261,7 @@ fn formatAir(
     _: std.fmt.FormatOptions,
     writer: anytype,
 ) @TypeOf(writer).Error!void {
-    @import("../../print_air.zig").dumpInst(
-        data.inst,
-        data.self.pt,
-        data.self.air,
-        data.self.liveness,
-    );
+    data.self.air.dumpInst(data.inst, data.self.pt, data.self.liveness);
 }
 fn fmtAir(self: *CodeGen, inst: Air.Inst.Index) std.fmt.Formatter(formatAir) {
     return .{ .data = .{ .self = self, .inst = inst } };
@@ -163487,31 +163515,49 @@ fn genBody(cg: *CodeGen, body: []const Air.Inst.Index) InnerError!void {
                 };
                 for (ops) |op| try op.die(cg);
             },
-            .tlv_dllimport_ptr => switch (cg.bin_file.tag) {
+            .runtime_nav_ptr => switch (cg.bin_file.tag) {
                 .elf, .macho => {
                     const ty_nav = air_datas[@intFromEnum(inst)].ty_nav;
 
                     const nav = ip.getNav(ty_nav.nav);
-                    const tlv_sym_index = sym: {
+                    const sym_index, const relocation = sym: {
                         if (cg.bin_file.cast(.elf)) |elf_file| {
                             const zo = elf_file.zigObjectPtr().?;
                             if (nav.getExtern(ip)) |e| {
                                 const sym = try elf_file.getGlobalSymbol(nav.name.toSlice(ip), e.lib_name.toSlice(ip));
-                                zo.symbol(sym).flags.is_extern_ptr = true;
-                                break :sym sym;
-                            }
-                            break :sym try zo.getOrCreateMetadataForNav(zcu, ty_nav.nav);
-                        }
-                        if (cg.bin_file.cast(.macho)) |macho_file| {
+                                linkage: switch (e.linkage) {
+                                    .internal => {},
+                                    .strong => switch (e.visibility) {
+                                        .default => zo.symbol(sym).flags.is_extern_ptr = true,
+                                        .hidden, .protected => {},
+                                    },
+                                    .weak => {
+                                        zo.symbol(sym).flags.weak = true;
+                                        continue :linkage .strong;
+                                    },
+                                    .link_once => unreachable,
+                                }
+                                break :sym .{ sym, e.relocation };
+                            } else break :sym .{ try zo.getOrCreateMetadataForNav(zcu, ty_nav.nav), .any };
+                        } else if (cg.bin_file.cast(.macho)) |macho_file| {
                             const zo = macho_file.getZigObject().?;
                             if (nav.getExtern(ip)) |e| {
                                 const sym = try macho_file.getGlobalSymbol(nav.name.toSlice(ip), e.lib_name.toSlice(ip));
-                                zo.symbols.items[sym].flags.is_extern_ptr = true;
-                                break :sym sym;
-                            }
-                            break :sym try zo.getOrCreateMetadataForNav(macho_file, ty_nav.nav);
-                        }
-                        unreachable;
+                                linkage: switch (e.linkage) {
+                                    .internal => {},
+                                    .strong => switch (e.visibility) {
+                                        .default => zo.symbols.items[sym].flags.is_extern_ptr = true,
+                                        .hidden, .protected => {},
+                                    },
+                                    .weak => {
+                                        zo.symbols.items[sym].flags.weak = true;
+                                        continue :linkage .strong;
+                                    },
+                                    .link_once => unreachable,
+                                }
+                                break :sym .{ sym, e.relocation };
+                            } else break :sym .{ try zo.getOrCreateMetadataForNav(macho_file, ty_nav.nav), .any };
+                        } else unreachable;
                     };
 
                     if (cg.mod.pic) {
@@ -163520,13 +163566,14 @@ fn genBody(cg: *CodeGen, body: []const Air.Inst.Index) InnerError!void {
                         try cg.spillRegisters(&.{.rax});
                     }
 
-                    var slot = try cg.tempInit(.usize, .{ .lea_symbol = .{
-                        .sym_index = tlv_sym_index,
-                    } });
+                    var slot = try cg.tempInit(.usize, switch (relocation) {
+                        .any => .{ .lea_symbol = .{ .sym_index = sym_index } },
+                        .pcrel => .{ .lea_pcrel = .{ .sym_index = sym_index } },
+                    });
                     while (try slot.toRegClass(true, .general_purpose, cg)) {}
                     try slot.finish(inst, &.{}, &.{}, cg);
                 },
-                else => return cg.fail("TODO implement tlv/dllimport on {}", .{cg.bin_file.tag}),
+                else => return cg.fail("TODO implement runtime_nav_ptr on {}", .{cg.bin_file.tag}),
             },
             .c_va_arg => try cg.airVaArg(inst),
             .c_va_copy => try cg.airVaCopy(inst),
@@ -169189,6 +169236,7 @@ fn load(self: *CodeGen, dst_mcv: MCValue, ptr_ty: Type, ptr_mcv: MCValue) InnerE
         .register,
         .register_offset,
         .lea_symbol,
+        .lea_pcrel,
         .lea_direct,
         .lea_got,
         .lea_frame,
@@ -169196,6 +169244,7 @@ fn load(self: *CodeGen, dst_mcv: MCValue, ptr_ty: Type, ptr_mcv: MCValue) InnerE
         .memory,
         .indirect,
         .load_symbol,
+        .load_pcrel,
         .load_direct,
         .load_got,
         .load_frame,
@@ -169407,6 +169456,7 @@ fn store(
         .register,
         .register_offset,
         .lea_symbol,
+        .lea_pcrel,
         .lea_direct,
         .lea_got,
         .lea_frame,
@@ -169414,6 +169464,7 @@ fn store(
         .memory,
         .indirect,
         .load_symbol,
+        .load_pcrel,
         .load_direct,
         .load_got,
         .load_frame,
@@ -169883,6 +169934,7 @@ fn genUnOpMir(self: *CodeGen, mir_tag: Mir.Inst.FixedTag, dst_ty: Type, dst_mcv:
         .register_overflow,
         .register_mask,
         .lea_symbol,
+        .lea_pcrel,
         .lea_direct,
         .lea_got,
         .lea_frame,
@@ -169892,7 +169944,7 @@ fn genUnOpMir(self: *CodeGen, mir_tag: Mir.Inst.FixedTag, dst_ty: Type, dst_mcv:
         => unreachable, // unmodifiable destination
         .register => |dst_reg| try self.asmRegister(mir_tag, registerAlias(dst_reg, abi_size)),
         .register_pair, .register_triple, .register_quadruple => unreachable, // unimplemented
-        .memory, .load_symbol, .load_got, .load_direct => {
+        .memory, .load_symbol, .load_pcrel, .load_got, .load_direct => {
             const addr_reg = try self.register_manager.allocReg(null, abi.RegisterClass.gp);
             const addr_reg_lock = self.register_manager.lockRegAssumeUnused(addr_reg);
             defer self.register_manager.unlockReg(addr_reg_lock);
@@ -171552,6 +171604,8 @@ fn genBinOp(
                         .register_mask,
                         .load_symbol,
                         .lea_symbol,
+                        .load_pcrel,
+                        .lea_pcrel,
                         .load_direct,
                         .lea_direct,
                         .load_got,
@@ -172740,6 +172794,7 @@ fn genBinOpMir(
         .lea_got,
         .lea_frame,
         .lea_symbol,
+        .lea_pcrel,
         .elementwise_args,
         .reserved_frame,
         .air_ref,
@@ -172831,6 +172886,8 @@ fn genBinOpMir(
                     .indirect,
                     .load_symbol,
                     .lea_symbol,
+                    .load_pcrel,
+                    .lea_pcrel,
                     .load_direct,
                     .lea_direct,
                     .load_got,
@@ -172906,7 +172963,7 @@ fn genBinOpMir(
                 }
             }
         },
-        .memory, .indirect, .load_symbol, .load_got, .load_direct, .load_frame => {
+        .memory, .indirect, .load_symbol, .load_pcrel, .load_got, .load_direct, .load_frame => {
             const OpInfo = ?struct { addr_reg: Register, addr_lock: RegisterLock };
             const limb_abi_size: u32 = @min(abi_size, 8);
 
@@ -172953,8 +173010,9 @@ fn genBinOpMir(
                 .load_frame,
                 .lea_frame,
                 .lea_symbol,
+                .lea_pcrel,
                 => null,
-                .memory, .load_symbol, .load_got, .load_direct => src: {
+                .memory, .load_symbol, .load_pcrel, .load_got, .load_direct => src: {
                     switch (resolved_src_mcv) {
                         .memory => |addr| if (std.math.cast(i32, @as(i64, @bitCast(addr))) != null and
                             std.math.cast(i32, @as(i64, @bitCast(addr)) + abi_size - limb_abi_size) != null)
@@ -173093,6 +173151,8 @@ fn genBinOpMir(
                     .indirect,
                     .load_symbol,
                     .lea_symbol,
+                    .load_pcrel,
+                    .lea_pcrel,
                     .load_direct,
                     .lea_direct,
                     .load_got,
@@ -173160,6 +173220,7 @@ fn genIntMulComplexOpMir(self: *CodeGen, dst_ty: Type, dst_mcv: MCValue, src_mcv
         .register_overflow,
         .register_mask,
         .lea_symbol,
+        .lea_pcrel,
         .lea_direct,
         .lea_got,
         .lea_frame,
@@ -173222,6 +173283,8 @@ fn genIntMulComplexOpMir(self: *CodeGen, dst_ty: Type, dst_mcv: MCValue, src_mcv
                 .eflags,
                 .load_symbol,
                 .lea_symbol,
+                .load_pcrel,
+                .lea_pcrel,
                 .load_direct,
                 .lea_direct,
                 .load_got,
@@ -173281,7 +173344,7 @@ fn genIntMulComplexOpMir(self: *CodeGen, dst_ty: Type, dst_mcv: MCValue, src_mcv
             }
         },
         .register_pair, .register_triple, .register_quadruple => unreachable, // unimplemented
-        .memory, .indirect, .load_symbol, .load_direct, .load_got, .load_frame => {
+        .memory, .indirect, .load_symbol, .load_pcrel, .load_direct, .load_got, .load_frame => {
             const tmp_reg = try self.copyToTmpRegister(dst_ty, dst_mcv);
             const tmp_mcv = MCValue{ .register = tmp_reg };
             const tmp_lock = self.register_manager.lockRegAssumeUnused(tmp_reg);
@@ -173450,7 +173513,8 @@ fn genLocalDebugInfo(
                     .disp = frame_addr.off,
                 } },
             }),
-            .lea_symbol => |sym_off| try self.asmAirMemory(.dbg_local, inst, .{
+            // debug info should explicitly ignore pcrel requirements
+            .lea_symbol, .lea_pcrel => |sym_off| try self.asmAirMemory(.dbg_local, inst, .{
                 .base = .{ .reloc = sym_off.sym_index },
                 .mod = .{ .rm = .{
                     .size = .qword,
@@ -174108,12 +174172,13 @@ fn airCmp(self: *CodeGen, inst: Air.Inst.Index, op: std.math.CompareOperator) !v
                                     .lea_got,
                                     .lea_frame,
                                     .lea_symbol,
+                                    .lea_pcrel,
                                     .elementwise_args,
                                     .reserved_frame,
                                     .air_ref,
                                     => unreachable,
                                     .register, .register_pair, .register_triple, .register_quadruple, .load_frame => null,
-                                    .memory, .load_symbol, .load_got, .load_direct => dst: {
+                                    .memory, .load_symbol, .load_pcrel, .load_got, .load_direct => dst: {
                                         switch (resolved_dst_mcv) {
                                             .memory => |addr| if (std.math.cast(
                                                 i32,
@@ -174122,7 +174187,7 @@ fn airCmp(self: *CodeGen, inst: Air.Inst.Index, op: std.math.CompareOperator) !v
                                                 i32,
                                                 @as(i64, @bitCast(addr)) + abi_size - 8,
                                             ) != null) break :dst null,
-                                            .load_symbol, .load_got, .load_direct => {},
+                                            .load_symbol, .load_pcrel, .load_got, .load_direct => {},
                                             else => unreachable,
                                         }
 
@@ -174160,6 +174225,7 @@ fn airCmp(self: *CodeGen, inst: Air.Inst.Index, op: std.math.CompareOperator) !v
                                     .register_mask,
                                     .indirect,
                                     .lea_symbol,
+                                    .lea_pcrel,
                                     .lea_direct,
                                     .lea_got,
                                     .lea_frame,
@@ -174168,7 +174234,7 @@ fn airCmp(self: *CodeGen, inst: Air.Inst.Index, op: std.math.CompareOperator) !v
                                     .air_ref,
                                     => unreachable,
                                     .register_pair, .register_triple, .register_quadruple, .load_frame => null,
-                                    .memory, .load_symbol, .load_got, .load_direct => src: {
+                                    .memory, .load_symbol, .load_pcrel, .load_got, .load_direct => src: {
                                         switch (resolved_src_mcv) {
                                             .memory => |addr| if (std.math.cast(
                                                 i32,
@@ -174177,7 +174243,7 @@ fn airCmp(self: *CodeGen, inst: Air.Inst.Index, op: std.math.CompareOperator) !v
                                                 i32,
                                                 @as(i64, @bitCast(addr)) + abi_size - 8,
                                             ) != null) break :src null,
-                                            .load_symbol, .load_got, .load_direct => {},
+                                            .load_symbol, .load_pcrel, .load_got, .load_direct => {},
                                             else => unreachable,
                                         }
 
@@ -174568,6 +174634,7 @@ fn isNull(self: *CodeGen, inst: Air.Inst.Index, opt_ty: Type, opt_mcv: MCValue)
         .lea_direct,
         .lea_got,
         .lea_symbol,
+        .lea_pcrel,
         .elementwise_args,
         .reserved_frame,
         .air_ref,
@@ -174616,6 +174683,7 @@ fn isNull(self: *CodeGen, inst: Air.Inst.Index, opt_ty: Type, opt_mcv: MCValue)
 
         .memory,
         .load_symbol,
+        .load_pcrel,
         .load_got,
         .load_direct,
         => {
@@ -176625,6 +176693,7 @@ fn genCopy(self: *CodeGen, ty: Type, dst_mcv: MCValue, src_mcv: MCValue, opts: C
         .lea_got,
         .lea_frame,
         .lea_symbol,
+        .lea_pcrel,
         .elementwise_args,
         .reserved_frame,
         .air_ref,
@@ -176719,7 +176788,7 @@ fn genCopy(self: *CodeGen, ty: Type, dst_mcv: MCValue, src_mcv: MCValue, opts: C
                     }
                     return;
                 },
-                .load_symbol, .load_direct, .load_got => {
+                .load_symbol, .load_pcrel, .load_direct, .load_got => {
                     const src_addr_reg =
                         (try self.register_manager.allocReg(null, abi.RegisterClass.gp)).to64();
                     const src_addr_lock = self.register_manager.lockRegAssumeUnused(src_addr_reg);
@@ -176752,7 +176821,7 @@ fn genCopy(self: *CodeGen, ty: Type, dst_mcv: MCValue, src_mcv: MCValue, opts: C
                         .undef => if (opts.safety and part_i > 0) .{ .register = dst_regs[0] } else .undef,
                         dst_tag => |src_regs| .{ .register = src_regs[part_i] },
                         .memory, .indirect, .load_frame => src_mcv.address().offset(part_disp).deref(),
-                        .load_symbol, .load_direct, .load_got => .{ .indirect = .{
+                        .load_symbol, .load_pcrel, .load_direct, .load_got => .{ .indirect = .{
                             .reg = src_info.?.addr_reg,
                             .off = part_disp,
                         } },
@@ -176773,11 +176842,11 @@ fn genCopy(self: *CodeGen, ty: Type, dst_mcv: MCValue, src_mcv: MCValue, opts: C
             src_mcv,
             opts,
         ),
-        .memory, .load_symbol, .load_direct, .load_got => {
+        .memory, .load_symbol, .load_pcrel, .load_direct, .load_got => {
             switch (dst_mcv) {
                 .memory => |addr| if (std.math.cast(i32, @as(i64, @bitCast(addr)))) |small_addr|
                     return self.genSetMem(.{ .reg = .ds }, small_addr, ty, src_mcv, opts),
-                .load_symbol, .load_direct, .load_got => {},
+                .load_symbol, .load_pcrel, .load_direct, .load_got => {},
                 else => unreachable,
             }
 
@@ -177234,7 +177303,7 @@ fn genSetReg(
             if (src_reg_mask.info.inverted) try self.asmRegister(.{ ._, .not }, registerAlias(bits_reg, abi_size));
             try self.genSetReg(dst_reg, ty, .{ .register = bits_reg }, .{});
         },
-        .memory, .load_symbol, .load_direct, .load_got => {
+        .memory, .load_symbol, .load_pcrel, .load_direct, .load_got => {
             switch (src_mcv) {
                 .memory => |addr| if (std.math.cast(i32, @as(i64, @bitCast(addr)))) |small_addr|
                     return (try self.moveStrategy(
@@ -177263,6 +177332,21 @@ fn genSetReg(
                     .segment, .mmx, .ip, .cr, .dr => unreachable,
                     .x87, .sse => {},
                 },
+                .load_pcrel => |sym_off| switch (dst_reg.class()) {
+                    .general_purpose, .gphi => {
+                        assert(sym_off.off == 0);
+                        try self.asmRegisterMemory(.{ ._, .mov }, dst_alias, .{
+                            .base = .{ .pcrel = sym_off.sym_index },
+                            .mod = .{ .rm = .{
+                                .size = self.memSize(ty),
+                                .disp = sym_off.off,
+                            } },
+                        });
+                        return;
+                    },
+                    .segment, .mmx, .ip, .cr, .dr => unreachable,
+                    .x87, .sse => {},
+                },
                 .load_direct => |sym_index| switch (dst_reg.class()) {
                     .general_purpose, .gphi => {
                         _ = try self.addInst(.{
@@ -177313,6 +177397,28 @@ fn genSetReg(
                 @tagName(self.bin_file.tag),
             }),
         },
+        .lea_pcrel => |sym_off| switch (self.bin_file.tag) {
+            .elf, .macho => {
+                try self.asmRegisterMemory(
+                    .{ ._, .lea },
+                    dst_reg.to64(),
+                    .{
+                        .base = .{ .pcrel = sym_off.sym_index },
+                    },
+                );
+                if (sym_off.off != 0) try self.asmRegisterMemory(
+                    .{ ._, .lea },
+                    dst_reg.to64(),
+                    .{
+                        .base = .{ .reg = dst_reg.to64() },
+                        .mod = .{ .rm = .{ .disp = sym_off.off } },
+                    },
+                );
+            },
+            else => return self.fail("TODO emit symbol sequence on {s}", .{
+                @tagName(self.bin_file.tag),
+            }),
+        },
         .lea_direct, .lea_got => |sym_index| _ = try self.addInst(.{
             .tag = switch (src_mcv) {
                 .lea_direct => .lea,
@@ -177350,6 +177456,7 @@ fn genSetMem(
         .frame => |base_frame_index| .{ .lea_frame = .{ .index = base_frame_index, .off = disp } },
         .table, .rip_inst => unreachable,
         .reloc => |sym_index| .{ .lea_symbol = .{ .sym_index = sym_index, .off = disp } },
+        .pcrel => |sym_index| .{ .lea_pcrel = .{ .sym_index = sym_index, .off = disp } },
     };
     switch (src_mcv) {
         .none,
@@ -177466,7 +177573,7 @@ fn genSetMem(
                     .off = disp,
                 }).compare(.gte, src_align),
                 .table, .rip_inst => unreachable,
-                .reloc => false,
+                .reloc, .pcrel => false,
             })).write(
                 self,
                 .{ .base = base, .mod = .{ .rm = .{
@@ -177557,6 +177664,8 @@ fn genSetMem(
         .lea_frame,
         .load_symbol,
         .lea_symbol,
+        .load_pcrel,
+        .lea_pcrel,
         => switch (abi_size) {
             0 => {},
             1, 2, 4, 8 => {
@@ -178110,7 +178219,7 @@ fn airCmpxchg(self: *CodeGen, inst: Air.Inst.Index) !void {
         .off => return self.fail("TODO airCmpxchg with {s}", .{@tagName(ptr_mcv)}),
     }
     const ptr_lock = switch (ptr_mem.base) {
-        .none, .frame, .reloc => null,
+        .none, .frame, .reloc, .pcrel => null,
         .reg => |reg| self.register_manager.lockReg(reg),
         .table, .rip_inst => unreachable,
     };
@@ -178193,7 +178302,7 @@ fn atomicOp(
         .off => return self.fail("TODO airCmpxchg with {s}", .{@tagName(ptr_mcv)}),
     }
     const mem_lock = switch (ptr_mem.base) {
-        .none, .frame, .reloc => null,
+        .none, .frame, .reloc, .pcrel => null,
         .reg => |reg| self.register_manager.lockReg(reg),
         .table, .rip_inst => unreachable,
     };
@@ -182266,6 +182375,8 @@ const Temp = struct {
                 .memory,
                 .load_symbol,
                 .lea_symbol,
+                .load_pcrel,
+                .lea_pcrel,
                 .indirect,
                 .load_direct,
                 .lea_direct,
@@ -182427,6 +182538,22 @@ const Temp = struct {
                 assert(limb_index == 0);
                 new_temp_index.tracking(cg).* = .init(.{ .lea_symbol = sym_off });
             },
+            .load_pcrel => |sym_off| {
+                const new_reg =
+                    try cg.register_manager.allocReg(new_temp_index.toIndex(), abi.RegisterClass.gp);
+                new_temp_index.tracking(cg).* = .init(.{ .register = new_reg });
+                try cg.asmRegisterMemory(.{ ._, .mov }, new_reg.to64(), .{
+                    .base = .{ .pcrel = sym_off.sym_index },
+                    .mod = .{ .rm = .{
+                        .size = .qword,
+                        .disp = sym_off.off + @as(u31, limb_index) * 8,
+                    } },
+                });
+            },
+            .lea_pcrel => |sym_off| {
+                assert(limb_index == 0);
+                new_temp_index.tracking(cg).* = .init(.{ .lea_pcrel = sym_off });
+            },
             .load_frame => |frame_addr| {
                 const new_reg =
                     try cg.register_manager.allocReg(new_temp_index.toIndex(), abi.RegisterClass.gp);
@@ -182721,11 +182848,12 @@ const Temp = struct {
             .memory,
             .indirect,
             .load_symbol,
+            .load_pcrel,
             .load_direct,
             .load_got,
             .load_frame,
             => return temp.toRegClass(true, .general_purpose, cg),
-            .lea_symbol => |sym_off| {
+            .lea_symbol, .lea_pcrel => |sym_off| {
                 const off = sym_off.off;
                 // hack around linker relocation bugs
                 if (false and off == 0) return false;
@@ -187464,6 +187592,8 @@ const Temp = struct {
                         .memory,
                         .load_symbol,
                         .lea_symbol,
+                        .load_pcrel,
+                        .lea_pcrel,
                         .indirect,
                         .load_direct,
                         .lea_direct,
@@ -190044,6 +190174,7 @@ const Select = struct {
                         .register => |base_reg| .{ .reg = base_reg.toSize(.ptr, s.cg.target) },
                         .register_offset => |base_reg_off| .{ .reg = base_reg_off.reg.toSize(.ptr, s.cg.target) },
                         .lea_symbol => |base_sym_off| .{ .reloc = base_sym_off.sym_index },
+                        .lea_pcrel => |base_sym_off| .{ .pcrel = base_sym_off.sym_index },
                     },
                     .mod = .{ .rm = .{
                         .size = op.flags.base.size,
src/arch/x86_64/Emit.zig
@@ -189,12 +189,12 @@ pub fn emitMir(emit: *Emit) Error!void {
                         .r_addend = lowered_relocs[0].off,
                     }, zo);
                 },
-                .linker_reloc => |sym_index| if (emit.lower.bin_file.cast(.elf)) |elf_file| {
+                .linker_reloc, .linker_pcrel => |sym_index| if (emit.lower.bin_file.cast(.elf)) |elf_file| {
                     const zo = elf_file.zigObjectPtr().?;
                     const atom = zo.symbol(emit.atom_index).atom(elf_file).?;
                     const sym = zo.symbol(sym_index);
                     if (emit.lower.pic) {
-                        const r_type: u32 = if (sym.flags.is_extern_ptr)
+                        const r_type: u32 = if (sym.flags.is_extern_ptr and lowered_relocs[0].target != .linker_pcrel)
                             @intFromEnum(std.elf.R_X86_64.GOTPCREL)
                         else
                             @intFromEnum(std.elf.R_X86_64.PC32);
@@ -218,7 +218,7 @@ pub fn emitMir(emit: *Emit) Error!void {
                     const zo = macho_file.getZigObject().?;
                     const atom = zo.symbols.items[emit.atom_index].getAtom(macho_file).?;
                     const sym = &zo.symbols.items[sym_index];
-                    const @"type": link.File.MachO.Relocation.Type = if (sym.flags.is_extern_ptr)
+                    const @"type": link.File.MachO.Relocation.Type = if (sym.flags.is_extern_ptr and lowered_relocs[0].target != .linker_pcrel)
                         .got_load
                     else if (sym.flags.tlv)
                         .tlv
@@ -438,6 +438,7 @@ pub fn emitMir(emit: *Emit) Error!void {
                                                 .reg => |reg| .{ .breg = reg.dwarfNum() },
                                                 .frame, .table, .rip_inst => unreachable,
                                                 .reloc => |sym_index| .{ .addr_reloc = sym_index },
+                                                .pcrel => unreachable,
                                             };
                                             break :base &loc_buf[0];
                                         },
src/arch/x86_64/encoder.zig
@@ -138,7 +138,7 @@ pub const Instruction = struct {
                 .moffs => true,
                 .rip => false,
                 .sib => |s| switch (s.base) {
-                    .none, .frame, .table, .reloc, .rip_inst => false,
+                    .none, .frame, .table, .reloc, .pcrel, .rip_inst => false,
                     .reg => |reg| reg.isClass(.segment),
                 },
             };
@@ -211,7 +211,7 @@ pub const Instruction = struct {
                 .none, .imm => 0b00,
                 .reg => |reg| @truncate(reg.enc() >> 3),
                 .mem => |mem| switch (mem.base()) {
-                    .none, .frame, .table, .reloc, .rip_inst => 0b00, // rsp, rbp, and rip are not extended
+                    .none, .frame, .table, .reloc, .pcrel, .rip_inst => 0b00, // rsp, rbp, and rip are not extended
                     .reg => |reg| @truncate(reg.enc() >> 3),
                 },
                 .bytes => unreachable,
@@ -282,6 +282,7 @@ pub const Instruction = struct {
                             .frame => |frame_index| try writer.print("{}", .{frame_index}),
                             .table => try writer.print("Table", .{}),
                             .reloc => |sym_index| try writer.print("Symbol({d})", .{sym_index}),
+                            .pcrel => |sym_index| try writer.print("PcRelSymbol({d})", .{sym_index}),
                             .rip_inst => |inst_index| try writer.print("RipInst({d})", .{inst_index}),
                         }
                         if (mem.scaleIndex()) |si| {
@@ -721,7 +722,7 @@ pub const Instruction = struct {
                     try encoder.modRm_indirectDisp32(operand_enc, 0);
                     try encoder.disp32(undefined);
                 } else return error.CannotEncode,
-                .rip_inst => {
+                .pcrel, .rip_inst => {
                     try encoder.modRm_RIPDisp32(operand_enc);
                     try encoder.disp32(sib.disp);
                 },
src/arch/x86_64/Lower.zig
@@ -66,6 +66,7 @@ pub const Reloc = struct {
         inst: Mir.Inst.Index,
         table,
         linker_reloc: u32,
+        linker_pcrel: u32,
         linker_tlsld: u32,
         linker_dtpoff: u32,
         linker_extern_fn: u32,
@@ -421,9 +422,9 @@ fn emit(lower: *Lower, prefix: Prefix, mnemonic: Mnemonic, ops: []const Operand)
     for (emit_ops, ops, 0..) |*emit_op, op, op_index| {
         emit_op.* = switch (op) {
             else => op,
-            .mem => |mem_op| switch (mem_op.base()) {
+            .mem => |mem_op| op: switch (mem_op.base()) {
                 else => op,
-                .reloc => |sym_index| op: {
+                .reloc => |sym_index| {
                     assert(prefix == .none);
                     assert(mem_op.sib.disp == 0);
                     assert(mem_op.sib.scale_index.scale == 0);
@@ -559,6 +560,22 @@ fn emit(lower: *Lower, prefix: Prefix, mnemonic: Mnemonic, ops: []const Operand)
                         return lower.fail("TODO: bin format '{s}'", .{@tagName(lower.bin_file.tag)});
                     }
                 },
+                .pcrel => |sym_index| {
+                    assert(prefix == .none);
+                    assert(mem_op.sib.disp == 0);
+                    assert(mem_op.sib.scale_index.scale == 0);
+
+                    _ = lower.reloc(@intCast(op_index), .{ .linker_pcrel = sym_index }, 0);
+                    break :op switch (lower.bin_file.tag) {
+                        .elf => op,
+                        .macho => switch (mnemonic) {
+                            .lea => .{ .mem = Memory.initRip(.none, 0) },
+                            .mov => .{ .mem = Memory.initRip(mem_op.sib.ptr_size, 0) },
+                            else => unreachable,
+                        },
+                        else => |tag| return lower.fail("TODO: bin format '{s}'", .{@tagName(tag)}),
+                    };
+                },
             },
         };
     }
src/arch/x86_64/Mir.zig
@@ -1866,7 +1866,7 @@ pub const Memory = struct {
                 .none, .table => undefined,
                 .reg => |reg| @intFromEnum(reg),
                 .frame => |frame_index| @intFromEnum(frame_index),
-                .reloc => |sym_index| sym_index,
+                .reloc, .pcrel => |sym_index| sym_index,
                 .rip_inst => |inst_index| inst_index,
             },
             .off = switch (mem.mod) {
@@ -1895,6 +1895,7 @@ pub const Memory = struct {
                         .frame => .{ .frame = @enumFromInt(mem.base) },
                         .table => .table,
                         .reloc => .{ .reloc = mem.base },
+                        .pcrel => .{ .pcrel = mem.base },
                         .rip_inst => .{ .rip_inst = mem.base },
                     },
                     .scale_index = switch (mem.info.index) {
@@ -1959,7 +1960,7 @@ pub fn resolveFrameAddr(mir: Mir, frame_addr: bits.FrameAddr) bits.RegisterOffse
 
 pub fn resolveFrameLoc(mir: Mir, mem: Memory) Memory {
     return switch (mem.info.base) {
-        .none, .reg, .table, .reloc, .rip_inst => mem,
+        .none, .reg, .table, .reloc, .pcrel, .rip_inst => mem,
         .frame => if (mir.frame_locs.len > 0) .{
             .info = .{
                 .base = .reg,
src/codegen/c.zig
@@ -2255,19 +2255,30 @@ pub const DeclGen = struct {
     fn renderFwdDecl(
         dg: *DeclGen,
         nav_index: InternPool.Nav.Index,
-        flags: struct {
-            is_extern: bool,
+        flags: packed struct {
             is_const: bool,
             is_threadlocal: bool,
-            is_weak_linkage: bool,
+            linkage: std.builtin.GlobalLinkage,
+            visibility: std.builtin.SymbolVisibility,
         },
     ) !void {
         const zcu = dg.pt.zcu;
         const ip = &zcu.intern_pool;
         const nav = ip.getNav(nav_index);
         const fwd = dg.fwdDeclWriter();
-        try fwd.writeAll(if (flags.is_extern) "zig_extern " else "static ");
-        if (flags.is_weak_linkage) try fwd.writeAll("zig_weak_linkage ");
+        try fwd.writeAll(switch (flags.linkage) {
+            .internal => "static ",
+            .strong, .weak, .link_once => "zig_extern ",
+        });
+        switch (flags.linkage) {
+            .internal, .strong => {},
+            .weak => try fwd.writeAll("zig_weak_linkage "),
+            .link_once => return dg.fail("TODO: CBE: implement linkonce linkage?", .{}),
+        }
+        switch (flags.linkage) {
+            .internal => {},
+            .strong, .weak, .link_once => try fwd.print("zig_visibility({s}) ", .{@tagName(flags.visibility)}),
+        }
         if (flags.is_threadlocal and !dg.mod.single_threaded) try fwd.writeAll("zig_threadlocal ");
         try dg.renderTypeAndName(
             fwd,
@@ -2994,10 +3005,10 @@ pub fn genDecl(o: *Object) !void {
     switch (ip.indexToKey(nav.status.fully_resolved.val)) {
         .@"extern" => |@"extern"| {
             if (!ip.isFunctionType(nav_ty.toIntern())) return o.dg.renderFwdDecl(o.dg.pass.nav, .{
-                .is_extern = true,
                 .is_const = @"extern".is_const,
                 .is_threadlocal = @"extern".is_threadlocal,
-                .is_weak_linkage = @"extern".is_weak_linkage,
+                .linkage = @"extern".linkage,
+                .visibility = @"extern".visibility,
             });
 
             const fwd = o.dg.fwdDeclWriter();
@@ -3016,13 +3027,12 @@ pub fn genDecl(o: *Object) !void {
         },
         .variable => |variable| {
             try o.dg.renderFwdDecl(o.dg.pass.nav, .{
-                .is_extern = false,
                 .is_const = false,
                 .is_threadlocal = variable.is_threadlocal,
-                .is_weak_linkage = variable.is_weak_linkage,
+                .linkage = .internal,
+                .visibility = .default,
             });
             const w = o.writer();
-            if (variable.is_weak_linkage) try w.writeAll("zig_weak_linkage ");
             if (variable.is_threadlocal and !o.dg.mod.single_threaded) try w.writeAll("zig_threadlocal ");
             if (nav.status.fully_resolved.@"linksection".toSlice(&zcu.intern_pool)) |s|
                 try w.print("zig_linksection({s}) ", .{fmtStringLiteral(s, null)});
@@ -3467,7 +3477,7 @@ fn genBodyInner(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail,
             .error_set_has_value => return f.fail("TODO: C backend: implement error_set_has_value", .{}),
             .vector_store_elem => return f.fail("TODO: C backend: implement vector_store_elem", .{}),
 
-            .tlv_dllimport_ptr => try airTlvDllimportPtr(f, inst),
+            .runtime_nav_ptr => try airRuntimeNavPtr(f, inst),
 
             .c_va_start => try airCVaStart(f, inst),
             .c_va_arg => try airCVaArg(f, inst),
@@ -7672,7 +7682,7 @@ fn airMulAdd(f: *Function, inst: Air.Inst.Index) !CValue {
     return local;
 }
 
-fn airTlvDllimportPtr(f: *Function, inst: Air.Inst.Index) !CValue {
+fn airRuntimeNavPtr(f: *Function, inst: Air.Inst.Index) !CValue {
     const ty_nav = f.air.instructions.items(.data)[@intFromEnum(inst)].ty_nav;
     const writer = f.object.writer();
     const local = try f.allocLocal(inst, .fromInterned(ty_nav.ty));
src/codegen/llvm.zig
@@ -2979,36 +2979,49 @@ pub const Object = struct {
         const zcu = pt.zcu;
         const ip = &zcu.intern_pool;
         const nav = ip.getNav(nav_index);
-        const is_extern, const is_threadlocal, const is_weak_linkage, const is_dll_import = switch (nav.status) {
+        const linkage: std.builtin.GlobalLinkage, const visibility: Builder.Visibility, const is_threadlocal, const is_dll_import = switch (nav.status) {
             .unresolved => unreachable,
             .fully_resolved => |r| switch (ip.indexToKey(r.val)) {
-                .variable => |variable| .{ false, variable.is_threadlocal, variable.is_weak_linkage, false },
-                .@"extern" => |@"extern"| .{ true, @"extern".is_threadlocal, @"extern".is_weak_linkage, @"extern".is_dll_import },
-                else => .{ false, false, false, false },
+                .variable => |variable| .{ .internal, .default, variable.is_threadlocal, false },
+                .@"extern" => |@"extern"| .{ @"extern".linkage, .fromSymbolVisibility(@"extern".visibility), @"extern".is_threadlocal, @"extern".is_dll_import },
+                else => .{ .internal, .default, false, false },
             },
             // This means it's a source declaration which is not `extern`!
-            .type_resolved => |r| .{ false, r.is_threadlocal, false, false },
+            .type_resolved => |r| .{ .internal, .default, r.is_threadlocal, false },
         };
 
         const variable_index = try o.builder.addVariable(
-            try o.builder.strtabString((if (is_extern) nav.name else nav.fqn).toSlice(ip)),
+            try o.builder.strtabString(switch (linkage) {
+                .internal => nav.fqn,
+                .strong, .weak => nav.name,
+                .link_once => unreachable,
+            }.toSlice(ip)),
             try o.lowerType(Type.fromInterned(nav.typeOf(ip))),
             toLlvmGlobalAddressSpace(nav.getAddrspace(), zcu.getTarget()),
         );
         gop.value_ptr.* = variable_index.ptrConst(&o.builder).global;
 
         // This is needed for declarations created by `@extern`.
-        if (is_extern) {
-            variable_index.setLinkage(.external, &o.builder);
-            variable_index.setUnnamedAddr(.default, &o.builder);
-            if (is_threadlocal and !zcu.navFileScope(nav_index).mod.?.single_threaded)
-                variable_index.setThreadLocal(.generaldynamic, &o.builder);
-            if (is_weak_linkage) variable_index.setLinkage(.extern_weak, &o.builder);
-            if (is_dll_import) variable_index.setDllStorageClass(.dllimport, &o.builder);
-        } else {
-            variable_index.setLinkage(.internal, &o.builder);
-            variable_index.setUnnamedAddr(.unnamed_addr, &o.builder);
-        }
+        switch (linkage) {
+            .internal => {
+                variable_index.setLinkage(.internal, &o.builder);
+                variable_index.setUnnamedAddr(.unnamed_addr, &o.builder);
+            },
+            .strong, .weak => {
+                variable_index.setLinkage(switch (linkage) {
+                    .internal => unreachable,
+                    .strong => .external,
+                    .weak => .extern_weak,
+                    .link_once => unreachable,
+                }, &o.builder);
+                variable_index.setUnnamedAddr(.default, &o.builder);
+                if (is_threadlocal and !zcu.navFileScope(nav_index).mod.?.single_threaded)
+                    variable_index.setThreadLocal(.generaldynamic, &o.builder);
+                if (is_dll_import) variable_index.setDllStorageClass(.dllimport, &o.builder);
+            },
+            .link_once => unreachable,
+        }
+        variable_index.setVisibility(visibility, &o.builder);
         return variable_index;
     }
 
@@ -4530,14 +4543,14 @@ pub const NavGen = struct {
         const nav = ip.getNav(nav_index);
         const resolved = nav.status.fully_resolved;
 
-        const is_extern, const lib_name, const is_threadlocal, const is_weak_linkage, const is_dll_import, const is_const, const init_val, const owner_nav = switch (ip.indexToKey(resolved.val)) {
-            .variable => |variable| .{ false, .none, variable.is_threadlocal, variable.is_weak_linkage, false, false, variable.init, variable.owner_nav },
-            .@"extern" => |@"extern"| .{ true, @"extern".lib_name, @"extern".is_threadlocal, @"extern".is_weak_linkage, @"extern".is_dll_import, @"extern".is_const, .none, @"extern".owner_nav },
-            else => .{ false, .none, false, false, false, true, resolved.val, nav_index },
+        const lib_name, const linkage, const visibility: Builder.Visibility, const is_threadlocal, const is_dll_import, const is_const, const init_val, const owner_nav = switch (ip.indexToKey(resolved.val)) {
+            .variable => |variable| .{ .none, .internal, .default, variable.is_threadlocal, false, false, variable.init, variable.owner_nav },
+            .@"extern" => |@"extern"| .{ @"extern".lib_name, @"extern".linkage, .fromSymbolVisibility(@"extern".visibility), @"extern".is_threadlocal, @"extern".is_dll_import, @"extern".is_const, .none, @"extern".owner_nav },
+            else => .{ .none, .internal, .default, false, false, true, resolved.val, nav_index },
         };
         const ty = Type.fromInterned(nav.typeOf(ip));
 
-        if (is_extern and ip.isFunctionType(ty.toIntern())) {
+        if (linkage != .internal and ip.isFunctionType(ty.toIntern())) {
             _ = try o.resolveLlvmFunction(owner_nav);
         } else {
             const variable_index = try o.resolveGlobalNav(nav_index);
@@ -4549,6 +4562,7 @@ pub const NavGen = struct {
                 .none => .no_init,
                 else => try o.lowerValue(init_val),
             }, &o.builder);
+            variable_index.setVisibility(visibility, &o.builder);
 
             const file_scope = zcu.navFileScopeIndex(nav_index);
             const mod = zcu.fileByIndex(file_scope).mod.?;
@@ -4568,7 +4582,7 @@ pub const NavGen = struct {
                     line_number,
                     try o.lowerDebugType(ty),
                     variable_index,
-                    .{ .local = !is_extern },
+                    .{ .local = linkage == .internal },
                 );
 
                 const debug_expression = try o.builder.debugExpression(&.{});
@@ -4583,38 +4597,47 @@ pub const NavGen = struct {
             }
         }
 
-        if (is_extern) {
-            const global_index = o.nav_map.get(nav_index).?;
+        switch (linkage) {
+            .internal => {},
+            .strong, .weak => {
+                const global_index = o.nav_map.get(nav_index).?;
 
-            const decl_name = decl_name: {
-                if (zcu.getTarget().cpu.arch.isWasm() and ty.zigTypeTag(zcu) == .@"fn") {
-                    if (lib_name.toSlice(ip)) |lib_name_slice| {
-                        if (!std.mem.eql(u8, lib_name_slice, "c")) {
-                            break :decl_name try o.builder.strtabStringFmt("{}|{s}", .{ nav.name.fmt(ip), lib_name_slice });
+                const decl_name = decl_name: {
+                    if (zcu.getTarget().cpu.arch.isWasm() and ty.zigTypeTag(zcu) == .@"fn") {
+                        if (lib_name.toSlice(ip)) |lib_name_slice| {
+                            if (!std.mem.eql(u8, lib_name_slice, "c")) {
+                                break :decl_name try o.builder.strtabStringFmt("{}|{s}", .{ nav.name.fmt(ip), lib_name_slice });
+                            }
                         }
                     }
-                }
-                break :decl_name try o.builder.strtabString(nav.name.toSlice(ip));
-            };
+                    break :decl_name try o.builder.strtabString(nav.name.toSlice(ip));
+                };
 
-            if (o.builder.getGlobal(decl_name)) |other_global| {
-                if (other_global != global_index) {
-                    // Another global already has this name; just use it in place of this global.
-                    try global_index.replace(other_global, &o.builder);
-                    return;
+                if (o.builder.getGlobal(decl_name)) |other_global| {
+                    if (other_global != global_index) {
+                        // Another global already has this name; just use it in place of this global.
+                        try global_index.replace(other_global, &o.builder);
+                        return;
+                    }
                 }
-            }
 
-            try global_index.rename(decl_name, &o.builder);
-            global_index.setLinkage(.external, &o.builder);
-            global_index.setUnnamedAddr(.default, &o.builder);
-            if (is_dll_import) {
-                global_index.setDllStorageClass(.dllimport, &o.builder);
-            } else if (zcu.comp.config.dll_export_fns) {
-                global_index.setDllStorageClass(.default, &o.builder);
-            }
+                try global_index.rename(decl_name, &o.builder);
+                global_index.setUnnamedAddr(.default, &o.builder);
+                if (is_dll_import) {
+                    global_index.setDllStorageClass(.dllimport, &o.builder);
+                } else if (zcu.comp.config.dll_export_fns) {
+                    global_index.setDllStorageClass(.default, &o.builder);
+                }
 
-            if (is_weak_linkage) global_index.setLinkage(.extern_weak, &o.builder);
+                global_index.setLinkage(switch (linkage) {
+                    .internal => unreachable,
+                    .strong => .external,
+                    .weak => .extern_weak,
+                    .link_once => unreachable,
+                }, &o.builder);
+                global_index.setVisibility(visibility, &o.builder);
+            },
+            .link_once => unreachable,
         }
     }
 };
@@ -5023,7 +5046,7 @@ pub const FuncGen = struct {
 
                 .vector_store_elem => try self.airVectorStoreElem(inst),
 
-                .tlv_dllimport_ptr => try self.airTlvDllimportPtr(inst),
+                .runtime_nav_ptr => try self.airRuntimeNavPtr(inst),
 
                 .inferred_alloc, .inferred_alloc_comptime => unreachable,
 
@@ -8122,7 +8145,7 @@ pub const FuncGen = struct {
         return .none;
     }
 
-    fn airTlvDllimportPtr(fg: *FuncGen, inst: Air.Inst.Index) !Builder.Value {
+    fn airRuntimeNavPtr(fg: *FuncGen, inst: Air.Inst.Index) !Builder.Value {
         const o = fg.ng.object;
         const ty_nav = fg.air.instructions.items(.data)[@intFromEnum(inst)].ty_nav;
         const llvm_ptr_const = try o.lowerNavRefValue(ty_nav.nav);
src/Compilation/Config.zig
@@ -345,11 +345,19 @@ pub fn resolve(options: Options) ResolveError!Config {
         } else false;
     };
 
+    const is_dyn_lib = switch (options.output_mode) {
+        .Obj, .Exe => false,
+        .Lib => link_mode == .dynamic,
+    };
+
     // Make a decision on whether to use LLVM backend for machine code generation.
     // Note that using the LLVM backend does not necessarily mean using LLVM libraries.
     // For example, Zig can emit .bc and .ll files directly, and this is still considered
     // using "the LLVM backend".
-    const prefer_llvm = b: {
+    const use_llvm = b: {
+        // If we have no zig code to compile, no need for LLVM.
+        if (!options.have_zcu) break :b false;
+
         // If emitting to LLVM bitcode object format, must use LLVM backend.
         if (options.emit_llvm_ir or options.emit_llvm_bc) {
             if (options.use_llvm == false)
@@ -382,9 +390,9 @@ pub fn resolve(options: Options) ResolveError!Config {
         // Prefer LLVM for release builds.
         if (root_optimize_mode != .Debug) break :b true;
 
-        // Self-hosted backends can't handle the inline assembly in std.pie yet
-        // https://github.com/ziglang/zig/issues/24046
-        if (pie) break :b true;
+        // load_dynamic_library standalone test not passing on this combination
+        // https://github.com/ziglang/zig/issues/24080
+        if (target.os.tag == .macos and is_dyn_lib) break :b true;
 
         // At this point we would prefer to use our own self-hosted backend,
         // because the compilation speed is better than LLVM. But only do it if
@@ -392,13 +400,6 @@ pub fn resolve(options: Options) ResolveError!Config {
         break :b !target_util.selfHostedBackendIsAsRobustAsLlvm(target);
     };
 
-    const use_llvm = b: {
-        // If we have no zig code to compile, no need for LLVM.
-        if (!options.have_zcu) break :b false;
-
-        break :b prefer_llvm;
-    };
-
     if (options.emit_bin and options.have_zcu) {
         if (!use_lib_llvm and use_llvm) {
             // Explicit request to use LLVM to produce an object file, but without
@@ -435,7 +436,13 @@ pub fn resolve(options: Options) ResolveError!Config {
         }
 
         if (options.use_lld) |x| break :b x;
-        break :b prefer_llvm;
+
+        // If we have no zig code to compile, no need for the self-hosted linker.
+        if (!options.have_zcu) break :b true;
+
+        // If we do have zig code, match the decision for whether to use the llvm backend,
+        // so that the llvm backend defaults to lld and the self-hosted backends do not.
+        break :b use_llvm;
     };
 
     const lto: std.zig.LtoMode = b: {
src/link/Elf/Atom.zig
@@ -497,14 +497,14 @@ fn dynAbsRelocAction(symbol: *const Symbol, elf_file: *Elf) RelocAction {
 }
 
 fn outputType(elf_file: *Elf) u2 {
-    const comp = elf_file.base.comp;
     assert(!elf_file.base.isRelocatable());
-    return switch (elf_file.base.comp.config.output_mode) {
+    const config = &elf_file.base.comp.config;
+    return switch (config.output_mode) {
         .Obj => unreachable,
         .Lib => 0,
         .Exe => switch (elf_file.getTarget().os.tag) {
             .haiku => 0,
-            else => if (comp.config.pie) 1 else 2,
+            else => if (config.pie) 1 else 2,
         },
     };
 }
src/link/Elf/relocation.zig
@@ -108,13 +108,12 @@ pub const dwarf = struct {
 
     pub fn externalRelocType(
         target: Symbol,
-        is_got: bool,
         source_section: Dwarf.Section.Index,
         address_size: Dwarf.AddressSize,
         cpu_arch: std.Target.Cpu.Arch,
     ) u32 {
         return switch (cpu_arch) {
-            .x86_64 => @intFromEnum(@as(elf.R_X86_64, if (is_got) .GOT32 else switch (source_section) {
+            .x86_64 => @intFromEnum(@as(elf.R_X86_64, switch (source_section) {
                 else => switch (address_size) {
                     .@"32" => if (target.flags.is_tls) .DTPOFF32 else .@"32",
                     .@"64" => if (target.flags.is_tls) .DTPOFF64 else .@"64",
src/link/Elf/ZigObject.zig
@@ -463,11 +463,8 @@ pub fn flush(self: *ZigObject, elf_file: *Elf, tid: Zcu.PerThread.Id) !void {
                     for (entry.external_relocs.items) |reloc| {
                         const target_sym = self.symbol(reloc.target_sym);
                         const r_offset = entry_off + reloc.source_off;
-                        const r_addend: i64 = switch (reloc.target_off) {
-                            .none, .got => 0,
-                            else => |off| @intCast(@intFromEnum(off)),
-                        };
-                        const r_type = relocation.dwarf.externalRelocType(target_sym.*, reloc.target_off == .got, sect_index, dwarf.address_size, cpu_arch);
+                        const r_addend: i64 = @intCast(reloc.target_off);
+                        const r_type = relocation.dwarf.externalRelocType(target_sym.*, sect_index, dwarf.address_size, cpu_arch);
                         atom_ptr.addRelocAssumeCapacity(.{
                             .r_offset = r_offset,
                             .r_addend = r_addend,
@@ -660,6 +657,7 @@ pub fn scanRelocs(self: *ZigObject, elf_file: *Elf, undefs: anytype) !void {
         const atom_ptr = self.atom(atom_index) orelse continue;
         if (!atom_ptr.alive) continue;
         const shdr = atom_ptr.inputShdr(elf_file);
+        if (shdr.sh_flags & elf.SHF_ALLOC == 0) continue;
         if (shdr.sh_type == elf.SHT_NOBITS) continue;
         if (atom_ptr.scanRelocsRequiresCode(elf_file)) {
             // TODO ideally we don't have to fetch the code here.
@@ -950,7 +948,7 @@ pub fn getNavVAddr(
             .dwarf => |wip_nav| try wip_nav.infoExternalReloc(.{
                 .source_off = @intCast(reloc_info.offset),
                 .target_sym = this_sym_index,
-                .target_off = .rel(reloc_info.addend),
+                .target_off = reloc_info.addend,
             }),
             .plan9 => unreachable,
             .none => unreachable,
@@ -983,7 +981,7 @@ pub fn getUavVAddr(
             .dwarf => |wip_nav| try wip_nav.infoExternalReloc(.{
                 .source_off = @intCast(reloc_info.offset),
                 .target_sym = sym_index,
-                .target_off = .rel(reloc_info.addend),
+                .target_off = reloc_info.addend,
             }),
             .plan9 => unreachable,
             .none => unreachable,
src/link/MachO/ZigObject.zig
@@ -648,7 +648,7 @@ pub fn getNavVAddr(
             .dwarf => |wip_nav| try wip_nav.infoExternalReloc(.{
                 .source_off = @intCast(reloc_info.offset),
                 .target_sym = sym_index,
-                .target_off = .rel(reloc_info.addend),
+                .target_off = reloc_info.addend,
             }),
             .plan9 => unreachable,
             .none => unreachable,
@@ -688,7 +688,7 @@ pub fn getUavVAddr(
             .dwarf => |wip_nav| try wip_nav.infoExternalReloc(.{
                 .source_off = @intCast(reloc_info.offset),
                 .target_sym = sym_index,
-                .target_off = .rel(reloc_info.addend),
+                .target_off = reloc_info.addend,
             }),
             .plan9 => unreachable,
             .none => unreachable,
src/link/Dwarf.zig
@@ -785,7 +785,6 @@ const Entry = struct {
     }
 
     const Index = enum(u32) {
-        //got_proc,
         _,
 
         const Optional = enum(u32) {
@@ -1041,7 +1040,7 @@ const Entry = struct {
                 const symbol = zo.symbol(reloc.target_sym);
                 try dwarf.resolveReloc(
                     entry_off + reloc.source_off,
-                    @bitCast(symbol.address(.{}, elf_file) + @as(i64, @intCast(@intFromEnum(reloc.target_off))) -
+                    @bitCast(symbol.address(.{}, elf_file) + @as(i64, @intCast(reloc.target_off)) -
                         if (symbol.flags.is_tls) elf_file.dtpAddress() else 0),
                     @intFromEnum(dwarf.address_size),
                 );
@@ -1052,7 +1051,7 @@ const Entry = struct {
                 const ref = zo.getSymbolRef(reloc.target_sym, macho_file);
                 try dwarf.resolveReloc(
                     entry_off + reloc.source_off,
-                    ref.getSymbol(macho_file).?.getAddress(.{}, macho_file) + @as(i64, @intCast(@intFromEnum(reloc.target_off))),
+                    ref.getSymbol(macho_file).?.getAddress(.{}, macho_file) + @as(i64, @intCast(reloc.target_off)),
                     @intFromEnum(dwarf.address_size),
                 );
             }
@@ -1081,27 +1080,12 @@ const CrossSectionReloc = struct {
 const ExternalReloc = struct {
     source_off: u32 = 0,
     target_sym: u32,
-    target_off: enum(u64) {
-        none = 0,
-        got = std.math.maxInt(i64) + 1,
-        _,
-
-        pub fn rel(off: u64) @This() {
-            const res: @This() = @enumFromInt(off);
-            switch (res) {
-                .none => {},
-                _ => {},
-                .got => unreachable, // assertion failure
-            }
-            return res;
-        }
-    } = .none,
+    target_off: u64 = 0,
 };
 
 pub const Loc = union(enum) {
     empty,
     addr_reloc: u32,
-    got_reloc: u32,
     deref: *const Loc,
     constu: u64,
     consts: i64,
@@ -1166,10 +1150,6 @@ pub const Loc = union(enum) {
                 try addr.write(adapter);
                 try writer.writeByte(DW.OP.deref);
             },
-            .got_reloc => |sym_index| {
-                try writer.writeByte(DW.OP.const4s);
-                try adapter.gotSym(sym_index);
-            },
             .constu => |constu| if (std.math.cast(u5, constu)) |lit| {
                 try writer.writeByte(@as(u8, DW.OP.lit0) + lit);
             } else if (std.math.cast(u8, constu)) |const1u| {
@@ -1766,9 +1746,6 @@ pub const WipNav = struct {
         fn endian(_: ExprLocCounter) std.builtin.Endian {
             return @import("builtin").cpu.arch.endian();
         }
-        fn gotSym(counter: *ExprLocCounter, _: u32) error{}!void {
-            counter.stream.bytes_written += 4;
-        }
         fn addrSym(counter: *ExprLocCounter, _: u32) error{}!void {
             counter.stream.bytes_written += @intFromEnum(counter.address_size);
         }
@@ -1789,14 +1766,6 @@ pub const WipNav = struct {
             fn endian(ctx: @This()) std.builtin.Endian {
                 return ctx.wip_nav.dwarf.endian;
             }
-            fn gotSym(ctx: @This(), sym_index: u32) UpdateError!void {
-                try ctx.wip_nav.infoExternalReloc(.{
-                    .source_off = @intCast(ctx.wip_nav.debug_info.items.len),
-                    .target_sym = sym_index,
-                    .target_off = .got,
-                });
-                try ctx.wip_nav.debug_info.appendNTimes(ctx.wip_nav.dwarf.gpa, 0, 4);
-            }
             fn addrSym(ctx: @This(), sym_index: u32) UpdateError!void {
                 try ctx.wip_nav.infoAddrSym(sym_index, 0);
             }
@@ -1812,7 +1781,7 @@ pub const WipNav = struct {
         try wip_nav.infoExternalReloc(.{
             .source_off = @intCast(wip_nav.debug_info.items.len),
             .target_sym = sym_index,
-            .target_off = .rel(sym_off),
+            .target_off = sym_off,
         });
         try wip_nav.debug_info.appendNTimes(wip_nav.dwarf.gpa, 0, @intFromEnum(wip_nav.dwarf.address_size));
     }
@@ -1829,14 +1798,6 @@ pub const WipNav = struct {
             fn endian(ctx: @This()) std.builtin.Endian {
                 return ctx.wip_nav.dwarf.endian;
             }
-            fn gotSym(ctx: @This(), sym_index: u32) UpdateError!void {
-                try ctx.wip_nav.frameExternalReloc(.{
-                    .source_off = @intCast(ctx.wip_nav.debug_frame.items.len),
-                    .target_sym = sym_index,
-                    .target_off = .got,
-                });
-                try ctx.wip_nav.debug_frame.appendNTimes(ctx.wip_nav.dwarf.gpa, 0, 4);
-            }
             fn addrSym(ctx: @This(), sym_index: u32) UpdateError!void {
                 try ctx.wip_nav.frameAddrSym(sym_index, 0);
             }
@@ -1852,7 +1813,7 @@ pub const WipNav = struct {
         try wip_nav.frameExternalReloc(.{
             .source_off = @intCast(wip_nav.debug_frame.items.len),
             .target_sym = sym_index,
-            .target_off = .rel(sym_off),
+            .target_off = sym_off,
         });
         try wip_nav.debug_frame.appendNTimes(wip_nav.dwarf.gpa, 0, @intFromEnum(wip_nav.dwarf.address_size));
     }
@@ -2338,81 +2299,50 @@ fn getUnit(dwarf: *Dwarf, mod: *Module) !Unit.Index {
     const mod_gop = try dwarf.mods.getOrPut(dwarf.gpa, mod);
     const unit: Unit.Index = @enumFromInt(mod_gop.index);
     if (!mod_gop.found_existing) {
-        {
-            errdefer _ = dwarf.mods.pop();
-            mod_gop.value_ptr.* = .{
-                .root_dir_path = undefined,
-                .dirs = .empty,
-                .files = .empty,
-            };
-            errdefer mod_gop.value_ptr.dirs.deinit(dwarf.gpa);
-            try mod_gop.value_ptr.dirs.putNoClobber(dwarf.gpa, unit, {});
-            assert(try dwarf.debug_aranges.section.addUnit(
-                DebugAranges.headerBytes(dwarf),
-                DebugAranges.trailerBytes(dwarf),
-                dwarf,
-            ) == unit);
-            errdefer dwarf.debug_aranges.section.popUnit(dwarf.gpa);
-            assert(try dwarf.debug_frame.section.addUnit(
-                DebugFrame.headerBytes(dwarf),
-                DebugFrame.trailerBytes(dwarf),
-                dwarf,
-            ) == unit);
-            errdefer dwarf.debug_frame.section.popUnit(dwarf.gpa);
-            assert(try dwarf.debug_info.section.addUnit(
-                DebugInfo.headerBytes(dwarf),
-                DebugInfo.trailer_bytes,
-                dwarf,
-            ) == unit);
-            errdefer dwarf.debug_info.section.popUnit(dwarf.gpa);
-            assert(try dwarf.debug_line.section.addUnit(
-                DebugLine.headerBytes(dwarf, 5, 25),
-                DebugLine.trailer_bytes,
-                dwarf,
-            ) == unit);
-            errdefer dwarf.debug_line.section.popUnit(dwarf.gpa);
-            assert(try dwarf.debug_loclists.section.addUnit(
-                DebugLocLists.headerBytes(dwarf),
-                DebugLocLists.trailer_bytes,
-                dwarf,
-            ) == unit);
-            errdefer dwarf.debug_loclists.section.popUnit(dwarf.gpa);
-            assert(try dwarf.debug_rnglists.section.addUnit(
-                DebugRngLists.headerBytes(dwarf),
-                DebugRngLists.trailer_bytes,
-                dwarf,
-            ) == unit);
-            errdefer dwarf.debug_rnglists.section.popUnit(dwarf.gpa);
-        }
-        //if (dwarf.bin_file.cast(.elf)) |elf_file| {
-        //    if (unit == .main) assert(try dwarf.addCommonEntry(unit) == .got_proc);
-        //    if (mod.pic and dwarf.debug_info.section.getUnit(.main).getEntry(.got_proc).len == 0) {
-        //        var wip_nav: WipNav = .{
-        //            .dwarf = dwarf,
-        //            .pt = undefined,
-        //            .unit = .main,
-        //            .entry = .got_proc,
-        //            .any_children = false,
-        //            .func = .none,
-        //            .func_sym_index = undefined,
-        //            .func_high_pc = undefined,
-        //            .blocks = undefined,
-        //            .cfi = undefined,
-        //            .debug_frame = .empty,
-        //            .debug_info = .empty,
-        //            .debug_line = .empty,
-        //            .debug_loclists = .empty,
-        //            .pending_lazy = .empty,
-        //        };
-        //        defer wip_nav.deinit();
-        //        try wip_nav.abbrevCode(.proc);
-        //        try wip_nav.infoExprLoc(.{ .deref = &.{ .plus = .{
-        //            &.empty,
-        //            &.{ .addr_reloc = try elf_file.zigObjectPtr().?.getGlobalSymbol(elf_file, "_GLOBAL_OFFSET_TABLE_", null) },
-        //        } } });
-        //        try dwarf.debug_info.section.replaceEntry(wip_nav.unit, wip_nav.entry, dwarf, wip_nav.debug_info.items);
-        //    }
-        //}
+        errdefer _ = dwarf.mods.pop();
+        mod_gop.value_ptr.* = .{
+            .root_dir_path = undefined,
+            .dirs = .empty,
+            .files = .empty,
+        };
+        errdefer mod_gop.value_ptr.dirs.deinit(dwarf.gpa);
+        try mod_gop.value_ptr.dirs.putNoClobber(dwarf.gpa, unit, {});
+        assert(try dwarf.debug_aranges.section.addUnit(
+            DebugAranges.headerBytes(dwarf),
+            DebugAranges.trailerBytes(dwarf),
+            dwarf,
+        ) == unit);
+        errdefer dwarf.debug_aranges.section.popUnit(dwarf.gpa);
+        assert(try dwarf.debug_frame.section.addUnit(
+            DebugFrame.headerBytes(dwarf),
+            DebugFrame.trailerBytes(dwarf),
+            dwarf,
+        ) == unit);
+        errdefer dwarf.debug_frame.section.popUnit(dwarf.gpa);
+        assert(try dwarf.debug_info.section.addUnit(
+            DebugInfo.headerBytes(dwarf),
+            DebugInfo.trailer_bytes,
+            dwarf,
+        ) == unit);
+        errdefer dwarf.debug_info.section.popUnit(dwarf.gpa);
+        assert(try dwarf.debug_line.section.addUnit(
+            DebugLine.headerBytes(dwarf, 5, 25),
+            DebugLine.trailer_bytes,
+            dwarf,
+        ) == unit);
+        errdefer dwarf.debug_line.section.popUnit(dwarf.gpa);
+        assert(try dwarf.debug_loclists.section.addUnit(
+            DebugLocLists.headerBytes(dwarf),
+            DebugLocLists.trailer_bytes,
+            dwarf,
+        ) == unit);
+        errdefer dwarf.debug_loclists.section.popUnit(dwarf.gpa);
+        assert(try dwarf.debug_rnglists.section.addUnit(
+            DebugRngLists.headerBytes(dwarf),
+            DebugRngLists.trailer_bytes,
+            dwarf,
+        ) == unit);
+        errdefer dwarf.debug_rnglists.section.popUnit(dwarf.gpa);
     }
     return unit;
 }
@@ -2510,16 +2440,7 @@ fn initWipNavInner(
             try wip_nav.strp(nav.fqn.toSlice(ip));
             const ty: Type = nav_val.typeOf(zcu);
             const addr: Loc = .{ .addr_reloc = sym_index };
-            const loc: Loc = loc: {
-                if (dwarf.bin_file.cast(.elf)) |elf_file| if (decl.linkage == .@"extern" and mod.pic)
-                    // TODO: lldb doesn't support call :(
-                    //.{ .call = .{ .args = &.{.{ .got_reloc = sym_index }}, .unit = .main, .entry = .got_proc } }
-                    break :loc .{ .deref = &.{ .plus = .{
-                        &.{ .addr_reloc = try elf_file.zigObjectPtr().?.getGlobalSymbol(elf_file, "_GLOBAL_OFFSET_TABLE_", null) },
-                        &.{ .got_reloc = sym_index },
-                    } } };
-                break :loc if (decl.is_threadlocal) .{ .form_tls_address = &addr } else addr;
-            };
+            const loc: Loc = if (decl.is_threadlocal) .{ .form_tls_address = &addr } else addr;
             switch (decl.kind) {
                 .unnamed_test, .@"test", .decltest, .@"comptime", .@"usingnamespace" => unreachable,
                 .@"const" => {
@@ -2605,7 +2526,7 @@ fn initWipNavInner(
             try wip_nav.infoAddrSym(sym_index, 0);
             wip_nav.func_high_pc = @intCast(wip_nav.debug_info.items.len);
             try diw.writeInt(u32, 0, dwarf.endian);
-            const target = file.mod.?.resolved_target.result;
+            const target = mod.resolved_target.result;
             try uleb128(diw, switch (nav.status.fully_resolved.alignment) {
                 .none => target_info.defaultFunctionAlignment(target),
                 else => |a| a.maxStrict(target_info.minFunctionAlignment(target)),
@@ -2742,7 +2663,7 @@ pub fn finishWipNavFunc(
             .{
                 .source_off = 1 + @intFromEnum(dwarf.address_size),
                 .target_sym = wip_nav.func_sym_index,
-                .target_off = .rel(code_size),
+                .target_off = code_size,
             },
         });
         try dwarf.debug_rnglists.section.replaceEntry(
@@ -4981,7 +4902,6 @@ const AbbrevCode = enum {
     comptime_value_field_comptime_state,
     comptime_value_elem_runtime_bits,
     comptime_value_elem_comptime_state,
-    //proc,
 
     const decl_bytes = uleb128Bytes(@intFromEnum(AbbrevCode.decl_instance_func_generic));
     comptime {
@@ -5851,12 +5771,6 @@ const AbbrevCode = enum {
                 .{ .ZIG_comptime_value, .ref_addr },
             },
         },
-        //.proc = .{
-        //    .tag = .dwarf_procedure,
-        //    .attrs = &.{
-        //        .{ .location, .exprloc },
-        //    },
-        //},
         .null = undefined,
     });
 };
src/link/Elf.zig
@@ -959,6 +959,12 @@ fn flushModuleInner(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id) !void {
     self.rela_plt.clearRetainingCapacity();
 
     if (self.zigObjectPtr()) |zo| {
+        var undefs: std.AutoArrayHashMap(SymbolResolver.Index, std.ArrayList(Ref)) = .init(gpa);
+        defer {
+            for (undefs.values()) |*refs| refs.deinit();
+            undefs.deinit();
+        }
+
         var has_reloc_errors = false;
         for (zo.atoms_indexes.items) |atom_index| {
             const atom_ptr = zo.atom(atom_index) orelse continue;
@@ -969,7 +975,10 @@ fn flushModuleInner(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id) !void {
             const code = try zo.codeAlloc(self, atom_index);
             defer gpa.free(code);
             const file_offset = atom_ptr.offset(self);
-            atom_ptr.resolveRelocsAlloc(self, code) catch |err| switch (err) {
+            (if (shdr.sh_flags & elf.SHF_ALLOC == 0)
+                atom_ptr.resolveRelocsNonAlloc(self, code, &undefs)
+            else
+                atom_ptr.resolveRelocsAlloc(self, code)) catch |err| switch (err) {
                 error.RelocFailure, error.RelaxFailure => has_reloc_errors = true,
                 error.UnsupportedCpuArch => {
                     try self.reportUnsupportedCpuArch();
@@ -980,6 +989,8 @@ fn flushModuleInner(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id) !void {
             try self.pwriteAll(code, file_offset);
         }
 
+        try self.reportUndefinedSymbols(&undefs);
+
         if (has_reloc_errors) return error.LinkFailure;
     }
 
@@ -1392,11 +1403,9 @@ fn scanRelocs(self: *Elf) !void {
     const gpa = self.base.comp.gpa;
     const shared_objects = self.shared_objects.values();
 
-    var undefs = std.AutoArrayHashMap(SymbolResolver.Index, std.ArrayList(Ref)).init(gpa);
+    var undefs: std.AutoArrayHashMap(SymbolResolver.Index, std.ArrayList(Ref)) = .init(gpa);
     defer {
-        for (undefs.values()) |*refs| {
-            refs.deinit();
-        }
+        for (undefs.values()) |*refs| refs.deinit();
         undefs.deinit();
     }
 
@@ -2702,15 +2711,16 @@ fn initSyntheticSections(self: *Elf) !void {
         });
     }
 
-    const needs_interp = blk: {
-        // On Ubuntu with musl-gcc, we get a weird combo of options looking like this:
-        // -dynamic-linker=<path> -static
-        // In this case, if we do generate .interp section and segment, we will get
-        // a segfault in the dynamic linker trying to load a binary that is static
-        // and doesn't contain .dynamic section.
-        if (self.base.isStatic() and !comp.config.pie) break :blk false;
-        break :blk target.dynamic_linker.get() != null;
+    const is_exe_or_dyn_lib = switch (comp.config.output_mode) {
+        .Exe => true,
+        .Lib => comp.config.link_mode == .dynamic,
+        .Obj => false,
     };
+    const have_dynamic_linker = comp.config.link_mode == .dynamic and is_exe_or_dyn_lib and !target.dynamic_linker.eql(.none);
+
+    const needs_interp = have_dynamic_linker and
+        (comp.config.link_libc or comp.root_mod.resolved_target.is_explicit_dynamic_linker);
+
     if (needs_interp and self.section_indexes.interp == null) {
         self.section_indexes.interp = try self.addSection(.{
             .name = try self.insertShString(".interp"),
@@ -3707,11 +3717,9 @@ fn allocateSpecialPhdrs(self: *Elf) void {
 fn writeAtoms(self: *Elf) !void {
     const gpa = self.base.comp.gpa;
 
-    var undefs = std.AutoArrayHashMap(SymbolResolver.Index, std.ArrayList(Ref)).init(gpa);
+    var undefs: std.AutoArrayHashMap(SymbolResolver.Index, std.ArrayList(Ref)) = .init(gpa);
     defer {
-        for (undefs.values()) |*refs| {
-            refs.deinit();
-        }
+        for (undefs.values()) |*refs| refs.deinit();
         undefs.deinit();
     }
 
src/link/Wasm.zig
@@ -2376,7 +2376,7 @@ pub const FunctionImportId = enum(u32) {
                 const zcu = wasm.base.comp.zcu.?;
                 const ip = &zcu.intern_pool;
                 const ext = ip.getNav(i.ptr(wasm).*).getResolvedExtern(ip).?;
-                return !ext.is_weak_linkage and ext.lib_name != .none;
+                return ext.linkage != .weak and ext.lib_name != .none;
             },
         };
     }
src/Zcu/PerThread.zig
@@ -1153,18 +1153,23 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
     // First, we must resolve the declaration's type. To do this, we analyze the type body if available,
     // or otherwise, we analyze the value body, populating `early_val` in the process.
 
-    switch (zir_decl.kind) {
+    const is_const = is_const: switch (zir_decl.kind) {
         .@"comptime" => unreachable, // this is not a Nav
-        .unnamed_test, .@"test", .decltest => assert(nav_ty.zigTypeTag(zcu) == .@"fn"),
-        .@"usingnamespace" => {},
-        .@"const" => {},
-        .@"var" => try sema.validateVarType(
-            &block,
-            if (zir_decl.type_body != null) ty_src else init_src,
-            nav_ty,
-            zir_decl.linkage == .@"extern",
-        ),
-    }
+        .unnamed_test, .@"test", .decltest => {
+            assert(nav_ty.zigTypeTag(zcu) == .@"fn");
+            break :is_const true;
+        },
+        .@"usingnamespace", .@"const" => true,
+        .@"var" => {
+            try sema.validateVarType(
+                &block,
+                if (zir_decl.type_body != null) ty_src else init_src,
+                nav_ty,
+                zir_decl.linkage == .@"extern",
+            );
+            break :is_const false;
+        },
+    };
 
     // Now that we know the type, we can evaluate the alignment, linksection, and addrspace, to determine
     // the full pointer type of this declaration.
@@ -1195,7 +1200,6 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
                 .init = final_val.?.toIntern(),
                 .owner_nav = nav_id,
                 .is_threadlocal = zir_decl.is_threadlocal,
-                .is_weak_linkage = false,
             } })),
             else => final_val.?,
         },
@@ -1212,10 +1216,12 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
                 .name = old_nav.name,
                 .ty = nav_ty.toIntern(),
                 .lib_name = try ip.getOrPutStringOpt(gpa, pt.tid, lib_name, .no_embedded_nulls),
-                .is_const = zir_decl.kind == .@"const",
                 .is_threadlocal = zir_decl.is_threadlocal,
-                .is_weak_linkage = false,
+                .linkage = .strong,
+                .visibility = .default,
                 .is_dll_import = false,
+                .relocation = .any,
+                .is_const = is_const,
                 .alignment = modifiers.alignment,
                 .@"addrspace" = modifiers.@"addrspace",
                 .zir_index = old_nav.analysis.?.zir_index, // `declaration` instruction
@@ -1243,6 +1249,7 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
         }
         ip.resolveNavValue(nav_id, .{
             .val = nav_val.toIntern(),
+            .is_const = is_const,
             .alignment = .none,
             .@"linksection" = .none,
             .@"addrspace" = .generic,
@@ -1286,6 +1293,7 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
 
     ip.resolveNavValue(nav_id, .{
         .val = nav_val.toIntern(),
+        .is_const = is_const,
         .alignment = modifiers.alignment,
         .@"linksection" = modifiers.@"linksection",
         .@"addrspace" = modifiers.@"addrspace",
@@ -1515,8 +1523,6 @@ fn analyzeNavType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileEr
     // the pointer modifiers, i.e. alignment, linksection, addrspace.
     const modifiers = try sema.resolveNavPtrModifiers(&block, zir_decl, inst_resolved.inst, resolved_ty);
 
-    // Usually, we can infer this information from the resolved `Nav` value; see `Zcu.navValIsConst`.
-    // However, since we don't have one, we need to quickly check the ZIR to figure this out.
     const is_const = switch (zir_decl.kind) {
         .@"comptime" => unreachable,
         .unnamed_test, .@"test", .decltest, .@"usingnamespace", .@"const" => true,
@@ -1542,7 +1548,7 @@ fn analyzeNavType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileEr
             r.alignment != modifiers.alignment or
             r.@"linksection" != modifiers.@"linksection" or
             r.@"addrspace" != modifiers.@"addrspace" or
-            zcu.navValIsConst(r.val) != is_const or
+            r.is_const != is_const or
             (old_nav.getExtern(ip) != null) != is_extern_decl,
     };
 
@@ -1550,10 +1556,10 @@ fn analyzeNavType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileEr
 
     ip.resolveNavType(nav_id, .{
         .type = resolved_ty.toIntern(),
+        .is_const = is_const,
         .alignment = modifiers.alignment,
         .@"linksection" = modifiers.@"linksection",
         .@"addrspace" = modifiers.@"addrspace",
-        .is_const = is_const,
         .is_threadlocal = zir_decl.is_threadlocal,
         .is_extern_decl = is_extern_decl,
     });
@@ -1750,7 +1756,7 @@ pub fn linkerUpdateFunc(pt: Zcu.PerThread, func_index: InternPool.Index, air: *A
 
     if (build_options.enable_debug_extensions and comp.verbose_air) {
         std.debug.print("# Begin Function AIR: {}:\n", .{nav.fqn.fmt(ip)});
-        @import("../print_air.zig").dump(pt, air.*, liveness);
+        air.dump(pt, liveness);
         std.debug.print("# End Function AIR: {}\n\n", .{nav.fqn.fmt(ip)});
     }
 
@@ -3577,8 +3583,10 @@ pub fn getCoerced(pt: Zcu.PerThread, val: Value, new_ty: Type) Allocator.Error!V
                 .lib_name = e.lib_name,
                 .is_const = e.is_const,
                 .is_threadlocal = e.is_threadlocal,
-                .is_weak_linkage = e.is_weak_linkage,
+                .linkage = e.linkage,
+                .visibility = e.visibility,
                 .is_dll_import = e.is_dll_import,
+                .relocation = e.relocation,
                 .alignment = e.alignment,
                 .@"addrspace" = e.@"addrspace",
                 .zir_index = e.zir_index,
@@ -3954,7 +3962,7 @@ pub fn navPtrType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Allocator.Err
     const ty, const alignment, const @"addrspace", const is_const = switch (ip.getNav(nav_id).status) {
         .unresolved => unreachable,
         .type_resolved => |r| .{ r.type, r.alignment, r.@"addrspace", r.is_const },
-        .fully_resolved => |r| .{ ip.typeOf(r.val), r.alignment, r.@"addrspace", zcu.navValIsConst(r.val) },
+        .fully_resolved => |r| .{ ip.typeOf(r.val), r.alignment, r.@"addrspace", r.is_const },
     };
     return pt.ptrType(.{
         .child = ty,
src/Air.zig
@@ -13,6 +13,7 @@ const InternPool = @import("InternPool.zig");
 const Type = @import("Type.zig");
 const Value = @import("Value.zig");
 const Zcu = @import("Zcu.zig");
+const print = @import("Air/print.zig");
 const types_resolved = @import("Air/types_resolved.zig");
 
 pub const Legalize = @import("Air/Legalize.zig");
@@ -863,16 +864,17 @@ pub const Inst = struct {
         /// Uses the `vector_store_elem` field.
         vector_store_elem,
 
-        /// Compute a pointer to a threadlocal or dllimport `Nav`, meaning one of:
+        /// Compute a pointer to a `Nav` at runtime, always one of:
         ///
         /// * `threadlocal var`
         /// * `extern threadlocal var` (or corresponding `@extern`)
         /// * `@extern` with `.is_dll_import = true`
+        /// * `@extern` with `.relocation = .pcrel`
         ///
         /// Such pointers are runtime values, so cannot be represented with an InternPool index.
         ///
         /// Uses the `ty_nav` field.
-        tlv_dllimport_ptr,
+        runtime_nav_ptr,
 
         /// Implements @cVaArg builtin.
         /// Uses the `ty_op` field.
@@ -1708,7 +1710,7 @@ pub fn typeOfIndex(air: *const Air, inst: Air.Inst.Index, ip: *const InternPool)
             return .fromInterned(ip.indexToKey(err_union_ty.ip_index).error_union_type.payload_type);
         },
 
-        .tlv_dllimport_ptr => return .fromInterned(datas[@intFromEnum(inst)].ty_nav.ty),
+        .runtime_nav_ptr => return .fromInterned(datas[@intFromEnum(inst)].ty_nav.ty),
 
         .work_item_id,
         .work_group_size,
@@ -1983,7 +1985,7 @@ pub fn mustLower(air: Air, inst: Air.Inst.Index, ip: *const InternPool) bool {
         .err_return_trace,
         .addrspace_cast,
         .save_err_return_trace_index,
-        .tlv_dllimport_ptr,
+        .runtime_nav_ptr,
         .work_item_id,
         .work_group_size,
         .work_group_id,
@@ -2141,6 +2143,10 @@ pub const typesFullyResolved = types_resolved.typesFullyResolved;
 pub const typeFullyResolved = types_resolved.checkType;
 pub const valFullyResolved = types_resolved.checkVal;
 pub const legalize = Legalize.legalize;
+pub const write = print.write;
+pub const writeInst = print.writeInst;
+pub const dump = print.dump;
+pub const dumpInst = print.dumpInst;
 
 pub const CoveragePoint = enum(u1) {
     /// Indicates the block is not a place of interest corresponding to
src/codegen.zig
@@ -921,41 +921,74 @@ fn genNavRef(
     const nav = ip.getNav(nav_index);
     assert(!nav.isThreadlocal(ip));
 
-    const is_extern, const lib_name = if (nav.getExtern(ip)) |e|
-        .{ true, e.lib_name }
+    const lib_name, const linkage, const visibility = if (nav.getExtern(ip)) |e|
+        .{ e.lib_name, e.linkage, e.visibility }
     else
-        .{ false, .none };
+        .{ .none, .internal, .default };
 
     const name = nav.name;
     if (lf.cast(.elf)) |elf_file| {
         const zo = elf_file.zigObjectPtr().?;
-        if (is_extern) {
-            const sym_index = try elf_file.getGlobalSymbol(name.toSlice(ip), lib_name.toSlice(ip));
-            zo.symbol(sym_index).flags.is_extern_ptr = true;
-            return .{ .mcv = .{ .lea_symbol = sym_index } };
+        switch (linkage) {
+            .internal => {
+                const sym_index = try zo.getOrCreateMetadataForNav(zcu, nav_index);
+                return .{ .mcv = .{ .lea_symbol = sym_index } };
+            },
+            .strong, .weak => {
+                const sym_index = try elf_file.getGlobalSymbol(name.toSlice(ip), lib_name.toSlice(ip));
+                switch (linkage) {
+                    .internal => unreachable,
+                    .strong => {},
+                    .weak => zo.symbol(sym_index).flags.weak = true,
+                    .link_once => unreachable,
+                }
+                switch (visibility) {
+                    .default => zo.symbol(sym_index).flags.is_extern_ptr = true,
+                    .hidden, .protected => {},
+                }
+                return .{ .mcv = .{ .lea_symbol = sym_index } };
+            },
+            .link_once => unreachable,
         }
-        const sym_index = try zo.getOrCreateMetadataForNav(zcu, nav_index);
-        return .{ .mcv = .{ .lea_symbol = sym_index } };
     } else if (lf.cast(.macho)) |macho_file| {
         const zo = macho_file.getZigObject().?;
-        if (is_extern) {
-            const sym_index = try macho_file.getGlobalSymbol(name.toSlice(ip), lib_name.toSlice(ip));
-            zo.symbols.items[sym_index].flags.is_extern_ptr = true;
-            return .{ .mcv = .{ .lea_symbol = sym_index } };
+        switch (linkage) {
+            .internal => {
+                const sym_index = try zo.getOrCreateMetadataForNav(macho_file, nav_index);
+                const sym = zo.symbols.items[sym_index];
+                return .{ .mcv = .{ .lea_symbol = sym.nlist_idx } };
+            },
+            .strong, .weak => {
+                const sym_index = try macho_file.getGlobalSymbol(name.toSlice(ip), lib_name.toSlice(ip));
+                switch (linkage) {
+                    .internal => unreachable,
+                    .strong => {},
+                    .weak => zo.symbols.items[sym_index].flags.weak = true,
+                    .link_once => unreachable,
+                }
+                switch (visibility) {
+                    .default => zo.symbols.items[sym_index].flags.is_extern_ptr = true,
+                    .hidden, .protected => {},
+                }
+                return .{ .mcv = .{ .lea_symbol = sym_index } };
+            },
+            .link_once => unreachable,
         }
-        const sym_index = try zo.getOrCreateMetadataForNav(macho_file, nav_index);
-        const sym = zo.symbols.items[sym_index];
-        return .{ .mcv = .{ .lea_symbol = sym.nlist_idx } };
     } else if (lf.cast(.coff)) |coff_file| {
-        if (is_extern) {
-            // TODO audit this
-            const global_index = try coff_file.getGlobalSymbol(name.toSlice(ip), lib_name.toSlice(ip));
-            try coff_file.need_got_table.put(gpa, global_index, {}); // needs GOT
-            return .{ .mcv = .{ .load_got = link.File.Coff.global_symbol_bit | global_index } };
+        // TODO audit this
+        switch (linkage) {
+            .internal => {
+                const atom_index = try coff_file.getOrCreateAtomForNav(nav_index);
+                const sym_index = coff_file.getAtom(atom_index).getSymbolIndex().?;
+                return .{ .mcv = .{ .load_got = sym_index } };
+            },
+            .strong, .weak => {
+                const global_index = try coff_file.getGlobalSymbol(name.toSlice(ip), lib_name.toSlice(ip));
+                try coff_file.need_got_table.put(gpa, global_index, {}); // needs GOT
+                return .{ .mcv = .{ .load_got = link.File.Coff.global_symbol_bit | global_index } };
+            },
+            .link_once => unreachable,
         }
-        const atom_index = try coff_file.getOrCreateAtomForNav(nav_index);
-        const sym_index = coff_file.getAtom(atom_index).getSymbolIndex().?;
-        return .{ .mcv = .{ .load_got = sym_index } };
     } else if (lf.cast(.plan9)) |p9| {
         const atom_index = try p9.seeNav(pt, nav_index);
         const atom = p9.getAtom(atom_index);
src/InternPool.zig
@@ -526,10 +526,10 @@ pub const Nav = struct {
         /// The type of this `Nav` is resolved; the value is queued for resolution.
         type_resolved: struct {
             type: InternPool.Index,
+            is_const: bool,
             alignment: Alignment,
             @"linksection": OptionalNullTerminatedString,
             @"addrspace": std.builtin.AddressSpace,
-            is_const: bool,
             is_threadlocal: bool,
             /// This field is whether this `Nav` is a literal `extern` definition.
             /// It does *not* tell you whether this might alias an extern fn (see #21027).
@@ -538,6 +538,7 @@ pub const Nav = struct {
         /// The value of this `Nav` is resolved.
         fully_resolved: struct {
             val: InternPool.Index,
+            is_const: bool,
             alignment: Alignment,
             @"linksection": OptionalNullTerminatedString,
             @"addrspace": std.builtin.AddressSpace,
@@ -727,12 +728,12 @@ pub const Nav = struct {
         const Bits = packed struct(u16) {
             status: enum(u2) { unresolved, type_resolved, fully_resolved, type_resolved_extern_decl },
             /// Populated only if `bits.status != .unresolved`.
+            is_const: bool,
+            /// Populated only if `bits.status != .unresolved`.
             alignment: Alignment,
             /// Populated only if `bits.status != .unresolved`.
             @"addrspace": std.builtin.AddressSpace,
             /// Populated only if `bits.status == .type_resolved`.
-            is_const: bool,
-            /// Populated only if `bits.status == .type_resolved`.
             is_threadlocal: bool,
             is_usingnamespace: bool,
         };
@@ -753,15 +754,16 @@ pub const Nav = struct {
                     .unresolved => .unresolved,
                     .type_resolved, .type_resolved_extern_decl => .{ .type_resolved = .{
                         .type = repr.type_or_val,
+                        .is_const = repr.bits.is_const,
                         .alignment = repr.bits.alignment,
                         .@"linksection" = repr.@"linksection",
                         .@"addrspace" = repr.bits.@"addrspace",
-                        .is_const = repr.bits.is_const,
                         .is_threadlocal = repr.bits.is_threadlocal,
                         .is_extern_decl = repr.bits.status == .type_resolved_extern_decl,
                     } },
                     .fully_resolved => .{ .fully_resolved = .{
                         .val = repr.type_or_val,
+                        .is_const = repr.bits.is_const,
                         .alignment = repr.bits.alignment,
                         .@"linksection" = repr.@"linksection",
                         .@"addrspace" = repr.bits.@"addrspace",
@@ -792,26 +794,26 @@ pub const Nav = struct {
             .bits = switch (nav.status) {
                 .unresolved => .{
                     .status = .unresolved,
+                    .is_const = false,
                     .alignment = .none,
                     .@"addrspace" = .generic,
                     .is_usingnamespace = nav.is_usingnamespace,
-                    .is_const = false,
                     .is_threadlocal = false,
                 },
                 .type_resolved => |r| .{
                     .status = if (r.is_extern_decl) .type_resolved_extern_decl else .type_resolved,
+                    .is_const = r.is_const,
                     .alignment = r.alignment,
                     .@"addrspace" = r.@"addrspace",
                     .is_usingnamespace = nav.is_usingnamespace,
-                    .is_const = r.is_const,
                     .is_threadlocal = r.is_threadlocal,
                 },
                 .fully_resolved => |r| .{
                     .status = .fully_resolved,
+                    .is_const = r.is_const,
                     .alignment = r.alignment,
                     .@"addrspace" = r.@"addrspace",
                     .is_usingnamespace = nav.is_usingnamespace,
-                    .is_const = false,
                     .is_threadlocal = false,
                 },
             },
@@ -2221,7 +2223,6 @@ pub const Key = union(enum) {
         init: Index,
         owner_nav: Nav.Index,
         is_threadlocal: bool,
-        is_weak_linkage: bool,
     };
 
     pub const Extern = struct {
@@ -2234,10 +2235,12 @@ pub const Key = union(enum) {
         /// For example `extern "c" fn write(...) usize` would have 'c' as library name.
         /// Index into the string table bytes.
         lib_name: OptionalNullTerminatedString,
-        is_const: bool,
+        linkage: std.builtin.GlobalLinkage,
+        visibility: std.builtin.SymbolVisibility,
         is_threadlocal: bool,
-        is_weak_linkage: bool,
         is_dll_import: bool,
+        relocation: std.builtin.ExternOptions.Relocation,
+        is_const: bool,
         alignment: Alignment,
         @"addrspace": std.builtin.AddressSpace,
         /// The ZIR instruction which created this extern; used only for source locations.
@@ -2844,9 +2847,10 @@ pub const Key = union(enum) {
 
             .@"extern" => |e| Hash.hash(seed, asBytes(&e.name) ++
                 asBytes(&e.ty) ++ asBytes(&e.lib_name) ++
-                asBytes(&e.is_const) ++ asBytes(&e.is_threadlocal) ++
-                asBytes(&e.is_weak_linkage) ++ asBytes(&e.alignment) ++
-                asBytes(&e.is_dll_import) ++ asBytes(&e.@"addrspace") ++
+                asBytes(&e.linkage) ++ asBytes(&e.visibility) ++
+                asBytes(&e.is_threadlocal) ++ asBytes(&e.is_dll_import) ++
+                asBytes(&e.relocation) ++
+                asBytes(&e.is_const) ++ asBytes(&e.alignment) ++ asBytes(&e.@"addrspace") ++
                 asBytes(&e.zir_index)),
         };
     }
@@ -2928,21 +2932,22 @@ pub const Key = union(enum) {
 
             .variable => |a_info| {
                 const b_info = b.variable;
-                return a_info.owner_nav == b_info.owner_nav and
-                    a_info.ty == b_info.ty and
+                return a_info.ty == b_info.ty and
                     a_info.init == b_info.init and
-                    a_info.is_threadlocal == b_info.is_threadlocal and
-                    a_info.is_weak_linkage == b_info.is_weak_linkage;
+                    a_info.owner_nav == b_info.owner_nav and
+                    a_info.is_threadlocal == b_info.is_threadlocal;
             },
             .@"extern" => |a_info| {
                 const b_info = b.@"extern";
                 return a_info.name == b_info.name and
                     a_info.ty == b_info.ty and
                     a_info.lib_name == b_info.lib_name and
-                    a_info.is_const == b_info.is_const and
+                    a_info.linkage == b_info.linkage and
+                    a_info.visibility == b_info.visibility and
                     a_info.is_threadlocal == b_info.is_threadlocal and
-                    a_info.is_weak_linkage == b_info.is_weak_linkage and
                     a_info.is_dll_import == b_info.is_dll_import and
+                    a_info.relocation == b_info.relocation and
+                    a_info.is_const == b_info.is_const and
                     a_info.alignment == b_info.alignment and
                     a_info.@"addrspace" == b_info.@"addrspace" and
                     a_info.zir_index == b_info.zir_index;
@@ -4889,6 +4894,7 @@ pub const Index = enum(u32) {
         float_c_longdouble_f128: struct { data: *Float128 },
         float_comptime_float: struct { data: *Float128 },
         variable: struct { data: *Tag.Variable },
+        threadlocal_variable: struct { data: *Tag.Variable },
         @"extern": struct { data: *Tag.Extern },
         func_decl: struct {
             const @"data.analysis.inferred_error_set" = opaque {};
@@ -5548,6 +5554,9 @@ pub const Tag = enum(u8) {
     /// A global variable.
     /// data is extra index to Variable.
     variable,
+    /// A global threadlocal variable.
+    /// data is extra index to Variable.
+    threadlocal_variable,
     /// An extern function or variable.
     /// data is extra index to Extern.
     /// Some parts of the key are stored in `owner_nav`.
@@ -5863,6 +5872,7 @@ pub const Tag = enum(u8) {
         .float_c_longdouble_f128 = .{ .summary = .@"@as(c_longdouble, {.payload%value})", .payload = f128 },
         .float_comptime_float = .{ .summary = .@"{.payload%value}", .payload = f128 },
         .variable = .{ .summary = .@"{.payload.owner_nav.fqn%summary#\"}", .payload = Variable },
+        .threadlocal_variable = .{ .summary = .@"{.payload.owner_nav.fqn%summary#\"}", .payload = Variable },
         .@"extern" = .{ .summary = .@"{.payload.owner_nav.fqn%summary#\"}", .payload = Extern },
         .func_decl = .{
             .summary = .@"{.payload.owner_nav.fqn%summary#\"}",
@@ -5913,24 +5923,24 @@ pub const Tag = enum(u8) {
         /// May be `none`.
         init: Index,
         owner_nav: Nav.Index,
-        flags: Flags,
-
-        pub const Flags = packed struct(u32) {
-            is_const: bool,
-            is_threadlocal: bool,
-            is_weak_linkage: bool,
-            is_dll_import: bool,
-            _: u28 = 0,
-        };
     };
 
     pub const Extern = struct {
-        // name, alignment, addrspace come from `owner_nav`.
+        // name, is_const, alignment, addrspace come from `owner_nav`.
         ty: Index,
         lib_name: OptionalNullTerminatedString,
-        flags: Variable.Flags,
+        flags: Flags,
         owner_nav: Nav.Index,
         zir_index: TrackedInst.Index,
+
+        pub const Flags = packed struct(u32) {
+            linkage: std.builtin.GlobalLinkage,
+            visibility: std.builtin.SymbolVisibility,
+            is_threadlocal: bool,
+            is_dll_import: bool,
+            relocation: std.builtin.ExternOptions.Relocation,
+            _: u25 = 0,
+        };
     };
 
     /// Trailing:
@@ -7248,14 +7258,17 @@ pub fn indexToKey(ip: *const InternPool, index: Index) Key {
             .ty = .comptime_float_type,
             .storage = .{ .f128 = extraData(unwrapped_index.getExtra(ip), Float128, data).get() },
         } },
-        .variable => {
+        .variable, .threadlocal_variable => {
             const extra = extraData(unwrapped_index.getExtra(ip), Tag.Variable, data);
             return .{ .variable = .{
                 .ty = extra.ty,
                 .init = extra.init,
                 .owner_nav = extra.owner_nav,
-                .is_threadlocal = extra.flags.is_threadlocal,
-                .is_weak_linkage = extra.flags.is_weak_linkage,
+                .is_threadlocal = switch (item.tag) {
+                    else => unreachable,
+                    .variable => false,
+                    .threadlocal_variable => true,
+                },
             } };
         },
         .@"extern" => {
@@ -7265,10 +7278,12 @@ pub fn indexToKey(ip: *const InternPool, index: Index) Key {
                 .name = nav.name,
                 .ty = extra.ty,
                 .lib_name = extra.lib_name,
-                .is_const = extra.flags.is_const,
+                .linkage = extra.flags.linkage,
+                .visibility = extra.flags.visibility,
                 .is_threadlocal = extra.flags.is_threadlocal,
-                .is_weak_linkage = extra.flags.is_weak_linkage,
                 .is_dll_import = extra.flags.is_dll_import,
+                .relocation = extra.flags.relocation,
+                .is_const = nav.status.fully_resolved.is_const,
                 .alignment = nav.status.fully_resolved.alignment,
                 .@"addrspace" = nav.status.fully_resolved.@"addrspace",
                 .zir_index = extra.zir_index,
@@ -7895,17 +7910,14 @@ pub fn get(ip: *InternPool, gpa: Allocator, tid: Zcu.PerThread.Id, key: Key) All
             const has_init = variable.init != .none;
             if (has_init) assert(variable.ty == ip.typeOf(variable.init));
             items.appendAssumeCapacity(.{
-                .tag = .variable,
+                .tag = switch (variable.is_threadlocal) {
+                    false => .variable,
+                    true => .threadlocal_variable,
+                },
                 .data = try addExtra(extra, Tag.Variable{
                     .ty = variable.ty,
                     .init = variable.init,
                     .owner_nav = variable.owner_nav,
-                    .flags = .{
-                        .is_const = false,
-                        .is_threadlocal = variable.is_threadlocal,
-                        .is_weak_linkage = variable.is_weak_linkage,
-                        .is_dll_import = false,
-                    },
                 }),
             });
         },
@@ -9128,6 +9140,7 @@ pub fn getExtern(
         .name = key.name,
         .fqn = key.name,
         .val = extern_index,
+        .is_const = key.is_const,
         .alignment = key.alignment,
         .@"linksection" = .none,
         .@"addrspace" = key.@"addrspace",
@@ -9136,10 +9149,11 @@ pub fn getExtern(
         .ty = key.ty,
         .lib_name = key.lib_name,
         .flags = .{
-            .is_const = key.is_const,
+            .linkage = key.linkage,
+            .visibility = key.visibility,
             .is_threadlocal = key.is_threadlocal,
-            .is_weak_linkage = key.is_weak_linkage,
             .is_dll_import = key.is_dll_import,
+            .relocation = key.relocation,
         },
         .zir_index = key.zir_index,
         .owner_nav = owner_nav,
@@ -9714,6 +9728,7 @@ fn finishFuncInstance(
         .name = nav_name,
         .fqn = try ip.namespacePtr(fn_namespace).internFullyQualifiedName(ip, gpa, tid, nav_name),
         .val = func_index,
+        .is_const = fn_owner_nav.status.fully_resolved.is_const,
         .alignment = fn_owner_nav.status.fully_resolved.alignment,
         .@"linksection" = fn_owner_nav.status.fully_resolved.@"linksection",
         .@"addrspace" = fn_owner_nav.status.fully_resolved.@"addrspace",
@@ -10300,13 +10315,13 @@ fn addExtraAssumeCapacity(extra: Local.Extra.Mutable, item: anytype) u32 {
             u32,
             i32,
             FuncAnalysis,
+            Tag.Extern.Flags,
             Tag.TypePointer.Flags,
             Tag.TypeFunction.Flags,
             Tag.TypePointer.PackedOffset,
             Tag.TypeUnion.Flags,
             Tag.TypeStruct.Flags,
             Tag.TypeStructPacked.Flags,
-            Tag.Variable.Flags,
             => @bitCast(@field(item, field.name)),
 
             else => @compileError("bad field type: " ++ @typeName(field.type)),
@@ -10361,13 +10376,13 @@ fn extraDataTrail(extra: Local.Extra, comptime T: type, index: u32) struct { dat
 
             u32,
             i32,
+            Tag.Extern.Flags,
             Tag.TypePointer.Flags,
             Tag.TypeFunction.Flags,
             Tag.TypePointer.PackedOffset,
             Tag.TypeUnion.Flags,
             Tag.TypeStruct.Flags,
             Tag.TypeStructPacked.Flags,
-            Tag.Variable.Flags,
             FuncAnalysis,
             => @bitCast(extra_item),
 
@@ -11162,7 +11177,7 @@ fn dumpStatsFallible(ip: *const InternPool, arena: Allocator) anyerror!void {
                 .float_c_longdouble_f80 => @sizeOf(Float80),
                 .float_c_longdouble_f128 => @sizeOf(Float128),
                 .float_comptime_float => @sizeOf(Float128),
-                .variable => @sizeOf(Tag.Variable),
+                .variable, .threadlocal_variable => @sizeOf(Tag.Variable),
                 .@"extern" => @sizeOf(Tag.Extern),
                 .func_decl => @sizeOf(Tag.FuncDecl),
                 .func_instance => b: {
@@ -11282,6 +11297,7 @@ fn dumpAllFallible(ip: *const InternPool) anyerror!void {
                 .float_c_longdouble_f128,
                 .float_comptime_float,
                 .variable,
+                .threadlocal_variable,
                 .@"extern",
                 .func_decl,
                 .func_instance,
@@ -11414,6 +11430,7 @@ pub fn createNav(
         name: NullTerminatedString,
         fqn: NullTerminatedString,
         val: InternPool.Index,
+        is_const: bool,
         alignment: Alignment,
         @"linksection": OptionalNullTerminatedString,
         @"addrspace": std.builtin.AddressSpace,
@@ -11430,6 +11447,7 @@ pub fn createNav(
         .analysis = null,
         .status = .{ .fully_resolved = .{
             .val = opts.val,
+            .is_const = opts.is_const,
             .alignment = opts.alignment,
             .@"linksection" = opts.@"linksection",
             .@"addrspace" = opts.@"addrspace",
@@ -11482,10 +11500,10 @@ pub fn resolveNavType(
     nav: Nav.Index,
     resolved: struct {
         type: InternPool.Index,
+        is_const: bool,
         alignment: Alignment,
         @"linksection": OptionalNullTerminatedString,
         @"addrspace": std.builtin.AddressSpace,
-        is_const: bool,
         is_threadlocal: bool,
         is_extern_decl: bool,
     },
@@ -11512,9 +11530,9 @@ pub fn resolveNavType(
 
     var bits = nav_bits[unwrapped.index];
     bits.status = if (resolved.is_extern_decl) .type_resolved_extern_decl else .type_resolved;
+    bits.is_const = resolved.is_const;
     bits.alignment = resolved.alignment;
     bits.@"addrspace" = resolved.@"addrspace";
-    bits.is_const = resolved.is_const;
     bits.is_threadlocal = resolved.is_threadlocal;
     @atomicStore(Nav.Repr.Bits, &nav_bits[unwrapped.index], bits, .release);
 }
@@ -11526,6 +11544,7 @@ pub fn resolveNavValue(
     nav: Nav.Index,
     resolved: struct {
         val: InternPool.Index,
+        is_const: bool,
         alignment: Alignment,
         @"linksection": OptionalNullTerminatedString,
         @"addrspace": std.builtin.AddressSpace,
@@ -11553,6 +11572,7 @@ pub fn resolveNavValue(
 
     var bits = nav_bits[unwrapped.index];
     bits.status = .fully_resolved;
+    bits.is_const = resolved.is_const;
     bits.alignment = resolved.alignment;
     bits.@"addrspace" = resolved.@"addrspace";
     @atomicStore(Nav.Repr.Bits, &nav_bits[unwrapped.index], bits, .release);
@@ -12007,6 +12027,7 @@ pub fn typeOf(ip: *const InternPool, index: Index) Index {
                 .error_union_error,
                 .enum_tag,
                 .variable,
+                .threadlocal_variable,
                 .@"extern",
                 .func_decl,
                 .func_instance,
@@ -12391,6 +12412,7 @@ pub fn zigTypeTag(ip: *const InternPool, index: Index) std.builtin.TypeId {
             .float_c_longdouble_f128,
             .float_comptime_float,
             .variable,
+            .threadlocal_variable,
             .@"extern",
             .func_decl,
             .func_instance,
src/Sema.zig
@@ -26089,10 +26089,12 @@ fn resolveExternOptions(
     zir_ref: Zir.Inst.Ref,
 ) CompileError!struct {
     name: InternPool.NullTerminatedString,
-    library_name: InternPool.OptionalNullTerminatedString = .none,
-    linkage: std.builtin.GlobalLinkage = .strong,
-    is_thread_local: bool = false,
-    is_dll_import: bool = false,
+    library_name: InternPool.OptionalNullTerminatedString,
+    linkage: std.builtin.GlobalLinkage,
+    visibility: std.builtin.SymbolVisibility,
+    is_thread_local: bool,
+    is_dll_import: bool,
+    relocation: std.builtin.ExternOptions.Relocation,
 } {
     const pt = sema.pt;
     const zcu = pt.zcu;
@@ -26105,8 +26107,10 @@ fn resolveExternOptions(
     const name_src = block.src(.{ .init_field_name = src.offset.node_offset_builtin_call_arg.builtin_call_node });
     const library_src = block.src(.{ .init_field_library = src.offset.node_offset_builtin_call_arg.builtin_call_node });
     const linkage_src = block.src(.{ .init_field_linkage = src.offset.node_offset_builtin_call_arg.builtin_call_node });
+    const visibility_src = block.src(.{ .init_field_visibility = src.offset.node_offset_builtin_call_arg.builtin_call_node });
     const thread_local_src = block.src(.{ .init_field_thread_local = src.offset.node_offset_builtin_call_arg.builtin_call_node });
     const dll_import_src = block.src(.{ .init_field_dll_import = src.offset.node_offset_builtin_call_arg.builtin_call_node });
+    const relocation_src = block.src(.{ .init_field_relocation = src.offset.node_offset_builtin_call_arg.builtin_call_node });
 
     const name_ref = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "name", .no_embedded_nulls), name_src);
     const name = try sema.toConstString(block, name_src, name_ref, .{ .simple = .extern_options });
@@ -26118,6 +26122,10 @@ fn resolveExternOptions(
     const linkage_val = try sema.resolveConstDefinedValue(block, linkage_src, linkage_ref, .{ .simple = .extern_options });
     const linkage = try sema.interpretBuiltinType(block, linkage_src, linkage_val, std.builtin.GlobalLinkage);
 
+    const visibility_ref = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "visibility", .no_embedded_nulls), visibility_src);
+    const visibility_val = try sema.resolveConstDefinedValue(block, visibility_src, visibility_ref, .{ .simple = .extern_options });
+    const visibility = try sema.interpretBuiltinType(block, visibility_src, visibility_val, std.builtin.SymbolVisibility);
+
     const is_thread_local = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "is_thread_local", .no_embedded_nulls), thread_local_src);
     const is_thread_local_val = try sema.resolveConstDefinedValue(block, thread_local_src, is_thread_local, .{ .simple = .extern_options });
 
@@ -26133,6 +26141,10 @@ fn resolveExternOptions(
     const is_dll_import_ref = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "is_dll_import", .no_embedded_nulls), dll_import_src);
     const is_dll_import_val = try sema.resolveConstDefinedValue(block, dll_import_src, is_dll_import_ref, .{ .simple = .extern_options });
 
+    const relocation_ref = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "relocation", .no_embedded_nulls), relocation_src);
+    const relocation_val = try sema.resolveConstDefinedValue(block, relocation_src, relocation_ref, .{ .simple = .extern_options });
+    const relocation = try sema.interpretBuiltinType(block, relocation_src, relocation_val, std.builtin.ExternOptions.Relocation);
+
     if (name.len == 0) {
         return sema.fail(block, name_src, "extern symbol name cannot be empty", .{});
     }
@@ -26145,8 +26157,10 @@ fn resolveExternOptions(
         .name = try ip.getOrPutString(gpa, pt.tid, name, .no_embedded_nulls),
         .library_name = try ip.getOrPutStringOpt(gpa, pt.tid, library_name, .no_embedded_nulls),
         .linkage = linkage,
+        .visibility = visibility,
         .is_thread_local = is_thread_local_val.toBool(),
         .is_dll_import = is_dll_import_val.toBool(),
+        .relocation = relocation,
     };
 }
 
@@ -26178,6 +26192,17 @@ fn zirBuiltinExtern(
     }
 
     const options = try sema.resolveExternOptions(block, options_src, extra.rhs);
+    switch (options.linkage) {
+        .internal => if (options.visibility != .default) {
+            return sema.fail(block, options_src, "internal symbol cannot have non-default visibility", .{});
+        },
+        .strong, .weak => {},
+        .link_once => return sema.fail(block, options_src, "external symbol cannot have link once linkage", .{}),
+    }
+    switch (options.relocation) {
+        .any => {},
+        .pcrel => if (options.visibility == .default) return sema.fail(block, options_src, "cannot require a pc-relative relocation to a symbol with default visibility", .{}),
+    }
 
     // TODO: error for threadlocal functions, non-const functions, etc
 
@@ -26190,10 +26215,12 @@ fn zirBuiltinExtern(
         .name = options.name,
         .ty = ptr_info.child,
         .lib_name = options.library_name,
-        .is_const = ptr_info.flags.is_const,
+        .linkage = options.linkage,
+        .visibility = options.visibility,
         .is_threadlocal = options.is_thread_local,
-        .is_weak_linkage = options.linkage == .weak,
         .is_dll_import = options.is_dll_import,
+        .relocation = options.relocation,
+        .is_const = ptr_info.flags.is_const,
         .alignment = ptr_info.flags.alignment,
         .@"addrspace" = ptr_info.flags.address_space,
         // This instruction is just for source locations.
@@ -31685,12 +31712,15 @@ fn analyzeNavRefInner(sema: *Sema, block: *Block, src: LazySrcLoc, orig_nav_inde
 
     const nav_status = ip.getNav(nav_index).status;
 
-    const is_tlv_or_dllimport = switch (nav_status) {
+    const is_runtime = switch (nav_status) {
         .unresolved => unreachable,
         // dllimports go straight to `fully_resolved`; the only option is threadlocal
         .type_resolved => |r| r.is_threadlocal,
         .fully_resolved => |r| switch (ip.indexToKey(r.val)) {
-            .@"extern" => |e| e.is_threadlocal or e.is_dll_import,
+            .@"extern" => |e| e.is_threadlocal or e.is_dll_import or switch (e.relocation) {
+                .any => false,
+                .pcrel => true,
+            },
             .variable => |v| v.is_threadlocal,
             else => false,
         },
@@ -31699,7 +31729,7 @@ fn analyzeNavRefInner(sema: *Sema, block: *Block, src: LazySrcLoc, orig_nav_inde
     const ty, const alignment, const @"addrspace", const is_const = switch (nav_status) {
         .unresolved => unreachable,
         .type_resolved => |r| .{ r.type, r.alignment, r.@"addrspace", r.is_const },
-        .fully_resolved => |r| .{ ip.typeOf(r.val), r.alignment, r.@"addrspace", zcu.navValIsConst(r.val) },
+        .fully_resolved => |r| .{ ip.typeOf(r.val), r.alignment, r.@"addrspace", r.is_const },
     };
     const ptr_ty = try pt.ptrTypeSema(.{
         .child = ty,
@@ -31710,10 +31740,10 @@ fn analyzeNavRefInner(sema: *Sema, block: *Block, src: LazySrcLoc, orig_nav_inde
         },
     });
 
-    if (is_tlv_or_dllimport) {
+    if (is_runtime) {
         // This pointer is runtime-known; we need to emit an AIR instruction to create it.
         return block.addInst(.{
-            .tag = .tlv_dllimport_ptr,
+            .tag = .runtime_nav_ptr,
             .data = .{ .ty_nav = .{
                 .ty = ptr_ty.toIntern(),
                 .nav = nav_index,
@@ -36432,6 +36462,7 @@ pub fn typeHasOnePossibleValue(sema: *Sema, ty: Type) CompileError!?Value {
             .float_c_longdouble_f128,
             .float_comptime_float,
             .variable,
+            .threadlocal_variable,
             .@"extern",
             .func_decl,
             .func_instance,
src/Zcu.zig
@@ -2047,6 +2047,7 @@ pub const SrcLoc = struct {
             .init_field_library,
             .init_field_thread_local,
             .init_field_dll_import,
+            .init_field_relocation,
             => |builtin_call_node| {
                 const wanted = switch (src_loc.lazy) {
                     .init_field_name => "name",
@@ -2059,6 +2060,7 @@ pub const SrcLoc = struct {
                     .init_field_library => "library",
                     .init_field_thread_local => "thread_local",
                     .init_field_dll_import => "dll_import",
+                    .init_field_relocation => "relocation",
                     else => unreachable,
                 };
                 const tree = try src_loc.file_scope.getTree(zcu);
@@ -2506,6 +2508,7 @@ pub const LazySrcLoc = struct {
         init_field_library: Ast.Node.Offset,
         init_field_thread_local: Ast.Node.Offset,
         init_field_dll_import: Ast.Node.Offset,
+        init_field_relocation: Ast.Node.Offset,
         /// The source location points to the value of an item in a specific
         /// case of a `switch`.
         switch_case_item: SwitchItem,
@@ -4562,15 +4565,6 @@ pub fn callconvSupported(zcu: *Zcu, cc: std.builtin.CallingConvention) union(enu
     return .ok;
 }
 
-/// Given that a `Nav` has value `val`, determine if a ref of that `Nav` gives a `const` pointer.
-pub fn navValIsConst(zcu: *const Zcu, val: InternPool.Index) bool {
-    return switch (zcu.intern_pool.indexToKey(val)) {
-        .variable => false,
-        .@"extern" => |e| e.is_const,
-        else => true,
-    };
-}
-
 pub const CodegenFailError = error{
     /// Indicates the error message has been already stored at `Zcu.failed_codegen`.
     CodegenFail,
test/cases/compile_errors/@import_zon_bad_type.zig
@@ -117,9 +117,9 @@ export fn testMutablePointer() void {
 // tmp.zig:37:38: note: imported here
 // neg_inf.zon:1:1: error: expected type '?u8'
 // tmp.zig:57:28: note: imported here
-// neg_inf.zon:1:1: error: expected type 'tmp.testNonExhaustiveEnum__enum_522'
+// neg_inf.zon:1:1: error: expected type 'tmp.testNonExhaustiveEnum__enum_525'
 // tmp.zig:62:39: note: imported here
-// neg_inf.zon:1:1: error: expected type 'tmp.testUntaggedUnion__union_524'
+// neg_inf.zon:1:1: error: expected type 'tmp.testUntaggedUnion__union_527'
 // tmp.zig:67:44: note: imported here
-// neg_inf.zon:1:1: error: expected type 'tmp.testTaggedUnionVoid__union_527'
+// neg_inf.zon:1:1: error: expected type 'tmp.testTaggedUnionVoid__union_530'
 // tmp.zig:72:50: note: imported here
test/cases/compile_errors/anytype_param_requires_comptime.zig
@@ -15,6 +15,6 @@ pub export fn entry() void {
 // error
 //
 // :7:25: error: unable to resolve comptime value
-// :7:25: note: initializer of comptime-only struct 'tmp.S.foo__anon_496.C' must be comptime-known
+// :7:25: note: initializer of comptime-only struct 'tmp.S.foo__anon_499.C' must be comptime-known
 // :4:16: note: struct requires comptime because of this field
 // :4:16: note: types are not available at runtime
test/cases/compile_errors/bogus_method_call_on_slice.zig
@@ -16,5 +16,5 @@ pub export fn entry2() void {
 //
 // :3:6: error: no field or member function named 'copy' in '[]const u8'
 // :9:8: error: no field or member function named 'bar' in '@TypeOf(.{})'
-// :12:18: error: no field or member function named 'bar' in 'tmp.entry2__struct_500'
+// :12:18: error: no field or member function named 'bar' in 'tmp.entry2__struct_503'
 // :12:6: note: struct declared here
test/cases/compile_errors/coerce_anon_struct.zig
@@ -6,6 +6,6 @@ export fn foo() void {
 
 // error
 //
-// :4:16: error: expected type 'tmp.T', found 'tmp.foo__struct_489'
+// :4:16: error: expected type 'tmp.T', found 'tmp.foo__struct_492'
 // :3:16: note: struct declared here
 // :1:11: note: struct declared here
test/cases/compile_errors/redundant_try.zig
@@ -44,9 +44,9 @@ comptime {
 //
 // :5:23: error: expected error union type, found 'comptime_int'
 // :10:23: error: expected error union type, found '@TypeOf(.{})'
-// :15:23: error: expected error union type, found 'tmp.test2__struct_526'
+// :15:23: error: expected error union type, found 'tmp.test2__struct_529'
 // :15:23: note: struct declared here
-// :20:27: error: expected error union type, found 'tmp.test3__struct_528'
+// :20:27: error: expected error union type, found 'tmp.test3__struct_531'
 // :20:27: note: struct declared here
 // :25:23: error: expected error union type, found 'struct { comptime *const [5:0]u8 = "hello" }'
 // :31:13: error: expected error union type, found 'u32'
test/tests.zig
@@ -1598,7 +1598,9 @@ const c_abi_targets = blk: {
     break :blk [_]CAbiTarget{
         // Native Targets
 
-        .{},
+        .{
+            .use_llvm = true,
+        },
 
         // Linux Targets
 
@@ -1837,7 +1839,6 @@ const c_abi_targets = blk: {
                 .abi = .musl,
             },
             .use_llvm = false,
-            .use_lld = false,
             .c_defines = &.{"ZIG_BACKEND_STAGE2_X86_64"},
         },
         .{
@@ -1848,7 +1849,6 @@ const c_abi_targets = blk: {
                 .abi = .musl,
             },
             .use_llvm = false,
-            .use_lld = false,
             .strip = true,
             .c_defines = &.{"ZIG_BACKEND_STAGE2_X86_64"},
         },
@@ -1860,7 +1860,6 @@ const c_abi_targets = blk: {
                 .abi = .musl,
             },
             .use_llvm = false,
-            .use_lld = false,
             .pic = true,
             .c_defines = &.{"ZIG_BACKEND_STAGE2_X86_64"},
         },
@@ -1870,6 +1869,7 @@ const c_abi_targets = blk: {
                 .os_tag = .linux,
                 .abi = .musl,
             },
+            .use_llvm = true,
         },
         .{
             .target = .{
@@ -1877,6 +1877,7 @@ const c_abi_targets = blk: {
                 .os_tag = .linux,
                 .abi = .muslx32,
             },
+            .use_llvm = true,
         },
 
         // WASI Targets
CMakeLists.txt
@@ -519,6 +519,7 @@ set(ZIG_STAGE2_SOURCES
     src/Air/Legalize.zig
     src/Air/Liveness.zig
     src/Air/Liveness/Verify.zig
+    src/Air/print.zig
     src/Air/types_resolved.zig
     src/Builtin.zig
     src/Compilation.zig
@@ -675,7 +676,6 @@ set(ZIG_STAGE2_SOURCES
     src/libs/mingw.zig
     src/libs/musl.zig
     src/mutable_value.zig
-    src/print_air.zig
     src/print_env.zig
     src/print_targets.zig
     src/print_value.zig