Commit 1120546f72

mlugg <mlugg@mlugg.co.uk>
2025-09-30 12:06:21
std.debug.SelfInfo: remove shared logic
There were only a few dozen lines of common logic, and they frankly introduced more complexity than they eliminated. Instead, let's accept that the implementations of `SelfInfo` are all pretty different and want to track different state. This probably fixes some synchronization and memory bugs by simplifying a bunch of stuff. It also improves the DWARF unwind cache, making it around twice as fast in a debug build with the self-hosted x86_64 backend, because we no longer have to redundantly go through the hashmap lookup logic to find the module. Unwinding on Windows will also see a slight performance boost from this change, because `RtlVirtualUnwind` does not need to know the module whatsoever, so the old `SelfInfo` implementation was doing redundant work. Lastly, this makes it even easier to implement `SelfInfo` on freestanding targets; there is no longer a need to emulate a real module system, since the user controls the whole implementation! There are various other small refactors here in the `SelfInfo` implementations as well as in the DWARF unwinding logic. This change turned out to make a lot of stuff simpler!
1 parent 12ceb89
lib/std/debug/Dwarf/expression.zig
@@ -10,7 +10,7 @@ const assert = std.debug.assert;
 const testing = std.testing;
 const Writer = std.Io.Writer;
 
-const regNative = std.debug.SelfInfo.DwarfUnwindContext.regNative;
+const regNative = std.debug.Dwarf.SelfUnwinder.regNative;
 
 const ip_reg_num = std.debug.Dwarf.ipRegNum(native_arch).?;
 const fp_reg_num = std.debug.Dwarf.fpRegNum(native_arch);
lib/std/debug/Dwarf/SelfUnwinder.zig
@@ -0,0 +1,334 @@
+//! Implements stack unwinding based on `Dwarf.Unwind`. The caller is responsible for providing the
+//! initialized `Dwarf.Unwind` from the `.debug_frame` (or equivalent) section; this type handles
+//! computing and applying the CFI register rules to evolve a `std.debug.cpu_context.Native` through
+//! stack frames, hence performing the virtual unwind.
+//!
+//! Notably, this type is a valid implementation of `std.debug.SelfInfo.UnwindContext`.
+
+/// The state of the CPU in the current stack frame.
+cpu_state: std.debug.cpu_context.Native,
+/// The value of the Program Counter in this frame. This is almost the same as the value of the IP
+/// register in `cpu_state`, but may be off by one because the IP is typically a *return* address.
+pc: usize,
+
+cfi_vm: Dwarf.Unwind.VirtualMachine,
+expr_vm: Dwarf.expression.StackMachine(.{ .call_frame_context = true }),
+
+pub const CacheEntry = struct {
+    const max_regs = 32;
+
+    pc: usize,
+    cie: *const Dwarf.Unwind.CommonInformationEntry,
+    cfa_rule: Dwarf.Unwind.VirtualMachine.CfaRule,
+    num_rules: u8,
+    rules_regs: [max_regs]u16,
+    rules: [max_regs]Dwarf.Unwind.VirtualMachine.RegisterRule,
+
+    pub fn find(entries: []const CacheEntry, pc: usize) ?*const CacheEntry {
+        assert(pc != 0);
+        const idx = std.hash.int(pc) % entries.len;
+        const entry = &entries[idx];
+        return if (entry.pc == pc) entry else null;
+    }
+
+    pub fn populate(entry: *const CacheEntry, entries: []CacheEntry) void {
+        const idx = std.hash.int(entry.pc) % entries.len;
+        entries[idx] = entry.*;
+    }
+
+    pub const empty: CacheEntry = .{
+        .pc = 0,
+        .cie = undefined,
+        .cfa_rule = undefined,
+        .num_rules = undefined,
+        .rules_regs = undefined,
+        .rules = undefined,
+    };
+};
+
+pub fn init(cpu_context: *const std.debug.cpu_context.Native) SelfUnwinder {
+    // `@constCast` is safe because we aren't going to store to the resulting pointer.
+    const raw_pc_ptr = regNative(@constCast(cpu_context), ip_reg_num) catch |err| switch (err) {
+        error.InvalidRegister => unreachable, // `ip_reg_num` is definitely valid
+        error.UnsupportedRegister => unreachable, // the implementation needs to support ip
+        error.IncompatibleRegisterSize => unreachable, // ip is definitely `usize`-sized
+    };
+    const pc = stripInstructionPtrAuthCode(raw_pc_ptr.*);
+    return .{
+        .cpu_state = cpu_context.*,
+        .pc = pc,
+        .cfi_vm = .{},
+        .expr_vm = .{},
+    };
+}
+
+pub fn deinit(unwinder: *SelfUnwinder, gpa: Allocator) void {
+    unwinder.cfi_vm.deinit(gpa);
+    unwinder.expr_vm.deinit(gpa);
+    unwinder.* = undefined;
+}
+
+pub fn getFp(unwinder: *const SelfUnwinder) usize {
+    // `@constCast` is safe because we aren't going to store to the resulting pointer.
+    const ptr = regNative(@constCast(&unwinder.cpu_state), fp_reg_num) catch |err| switch (err) {
+        error.InvalidRegister => unreachable, // `fp_reg_num` is definitely valid
+        error.UnsupportedRegister => unreachable, // the implementation needs to support fp
+        error.IncompatibleRegisterSize => unreachable, // fp is a pointer so is `usize`-sized
+    };
+    return ptr.*;
+}
+
+/// Compute the rule set for the address `unwinder.pc` from the information in `unwind`. The caller
+/// may store the returned rule set in a simple fixed-size cache keyed on the `pc` field to avoid
+/// frequently recomputing register rules when unwinding many times.
+///
+/// To actually apply the computed rules, see `next`.
+pub fn computeRules(
+    unwinder: *SelfUnwinder,
+    gpa: Allocator,
+    unwind: *const Dwarf.Unwind,
+    load_offset: usize,
+    explicit_fde_offset: ?usize,
+) !CacheEntry {
+    assert(unwinder.pc != 0);
+
+    const pc_vaddr = unwinder.pc - load_offset;
+
+    const fde_offset = explicit_fde_offset orelse try unwind.lookupPc(
+        pc_vaddr,
+        @sizeOf(usize),
+        native_endian,
+    ) orelse return error.MissingDebugInfo;
+    const cie, const fde = try unwind.getFde(fde_offset, native_endian);
+
+    // `lookupPc` can return false positives, so check if the FDE *actually* includes the pc
+    if (pc_vaddr < fde.pc_begin or pc_vaddr >= fde.pc_begin + fde.pc_range) {
+        return error.MissingDebugInfo;
+    }
+
+    unwinder.cfi_vm.reset();
+    const row = try unwinder.cfi_vm.runTo(gpa, pc_vaddr, cie, &fde, @sizeOf(usize), native_endian);
+    const cols = unwinder.cfi_vm.rowColumns(&row);
+
+    if (cols.len > CacheEntry.max_regs) return error.UnsupportedDebugInfo;
+
+    var entry: CacheEntry = .{
+        .pc = unwinder.pc,
+        .cie = cie,
+        .cfa_rule = row.cfa,
+        .num_rules = @intCast(cols.len),
+        .rules_regs = undefined,
+        .rules = undefined,
+    };
+    for (cols, 0..) |col, i| {
+        entry.rules_regs[i] = col.register;
+        entry.rules[i] = col.rule;
+    }
+    return entry;
+}
+
+/// Applies the register rules given in `cache_entry` to the current state of `unwinder`. The caller
+/// is responsible for ensuring that `cache_entry` contains the correct rule set for `unwinder.pc`.
+///
+/// `unwinder.cpu_state` and `unwinder.pc` are updated to refer to the next frame, and this frame's
+/// return address is returned as a `usize`.
+pub fn next(unwinder: *SelfUnwinder, gpa: Allocator, cache_entry: *const CacheEntry) std.debug.SelfInfoError!usize {
+    return unwinder.nextInner(gpa, cache_entry) catch |err| switch (err) {
+        error.OutOfMemory,
+        error.InvalidDebugInfo,
+        => |e| return e,
+
+        error.UnsupportedRegister,
+        error.UnimplementedExpressionCall,
+        error.UnimplementedOpcode,
+        error.UnimplementedUserOpcode,
+        error.UnimplementedTypedComparison,
+        error.UnimplementedTypeConversion,
+        error.UnknownExpressionOpcode,
+        => return error.UnsupportedDebugInfo,
+
+        error.ReadFailed,
+        error.EndOfStream,
+        error.Overflow,
+        error.IncompatibleRegisterSize,
+        error.InvalidRegister,
+        error.IncompleteExpressionContext,
+        error.InvalidCFAOpcode,
+        error.InvalidExpression,
+        error.InvalidFrameBase,
+        error.InvalidIntegralTypeSize,
+        error.InvalidSubExpression,
+        error.InvalidTypeLength,
+        error.TruncatedIntegralType,
+        error.DivisionByZero,
+        => return error.InvalidDebugInfo,
+    };
+}
+
+fn nextInner(unwinder: *SelfUnwinder, gpa: Allocator, cache_entry: *const CacheEntry) !usize {
+    const format = cache_entry.cie.format;
+    const return_address_register = cache_entry.cie.return_address_register;
+
+    const cfa = switch (cache_entry.cfa_rule) {
+        .none => return error.InvalidDebugInfo,
+        .reg_off => |ro| cfa: {
+            const ptr = try regNative(&unwinder.cpu_state, ro.register);
+            break :cfa try applyOffset(ptr.*, ro.offset);
+        },
+        .expression => |expr| cfa: {
+            // On all implemented architectures, the CFA is defined to be the previous frame's SP
+            const prev_cfa_val = (try regNative(&unwinder.cpu_state, sp_reg_num)).*;
+            unwinder.expr_vm.reset();
+            const value = try unwinder.expr_vm.run(expr, gpa, .{
+                .format = format,
+                .cpu_context = &unwinder.cpu_state,
+            }, prev_cfa_val) orelse return error.InvalidDebugInfo;
+            switch (value) {
+                .generic => |g| break :cfa g,
+                else => return error.InvalidDebugInfo,
+            }
+        },
+    };
+
+    // If unspecified, we'll use the default rule for the return address register, which is
+    // typically equivalent to `.undefined` (meaning there is no return address), but may be
+    // overriden by ABIs.
+    var has_return_address: bool = builtin.cpu.arch.isAARCH64() and
+        return_address_register >= 19 and
+        return_address_register <= 28;
+
+    // Create a copy of the CPU state, to which we will apply the new rules.
+    var new_cpu_state = unwinder.cpu_state;
+
+    // On all implemented architectures, the CFA is defined to be the previous frame's SP
+    (try regNative(&new_cpu_state, sp_reg_num)).* = cfa;
+
+    const rules_len = cache_entry.num_rules;
+    for (cache_entry.rules_regs[0..rules_len], cache_entry.rules[0..rules_len]) |register, rule| {
+        const new_val: union(enum) {
+            same,
+            undefined,
+            val: usize,
+            bytes: []const u8,
+        } = switch (rule) {
+            .default => val: {
+                // The default rule is typically equivalent to `.undefined`, but ABIs may override it.
+                if (builtin.cpu.arch.isAARCH64() and register >= 19 and register <= 28) {
+                    break :val .same;
+                }
+                break :val .undefined;
+            },
+            .undefined => .undefined,
+            .same_value => .same,
+            .offset => |offset| val: {
+                const ptr: *const usize = @ptrFromInt(try applyOffset(cfa, offset));
+                break :val .{ .val = ptr.* };
+            },
+            .val_offset => |offset| .{ .val = try applyOffset(cfa, offset) },
+            .register => |r| .{ .bytes = try unwinder.cpu_state.dwarfRegisterBytes(r) },
+            .expression => |expr| val: {
+                unwinder.expr_vm.reset();
+                const value = try unwinder.expr_vm.run(expr, gpa, .{
+                    .format = format,
+                    .cpu_context = &unwinder.cpu_state,
+                }, cfa) orelse return error.InvalidDebugInfo;
+                const ptr: *const usize = switch (value) {
+                    .generic => |addr| @ptrFromInt(addr),
+                    else => return error.InvalidDebugInfo,
+                };
+                break :val .{ .val = ptr.* };
+            },
+            .val_expression => |expr| val: {
+                unwinder.expr_vm.reset();
+                const value = try unwinder.expr_vm.run(expr, gpa, .{
+                    .format = format,
+                    .cpu_context = &unwinder.cpu_state,
+                }, cfa) orelse return error.InvalidDebugInfo;
+                switch (value) {
+                    .generic => |val| break :val .{ .val = val },
+                    else => return error.InvalidDebugInfo,
+                }
+            },
+        };
+        switch (new_val) {
+            .same => {},
+            .undefined => {
+                const dest = try new_cpu_state.dwarfRegisterBytes(@intCast(register));
+                @memset(dest, undefined);
+            },
+            .val => |val| {
+                const dest = try new_cpu_state.dwarfRegisterBytes(@intCast(register));
+                if (dest.len != @sizeOf(usize)) return error.InvalidDebugInfo;
+                const dest_ptr: *align(1) usize = @ptrCast(dest);
+                dest_ptr.* = val;
+            },
+            .bytes => |src| {
+                const dest = try new_cpu_state.dwarfRegisterBytes(@intCast(register));
+                if (dest.len != src.len) return error.InvalidDebugInfo;
+                @memcpy(dest, src);
+            },
+        }
+        if (register == return_address_register) {
+            has_return_address = new_val != .undefined;
+        }
+    }
+
+    const return_address: usize = if (has_return_address) pc: {
+        const raw_ptr = try regNative(&new_cpu_state, return_address_register);
+        break :pc stripInstructionPtrAuthCode(raw_ptr.*);
+    } else 0;
+
+    (try regNative(&new_cpu_state, ip_reg_num)).* = return_address;
+
+    // The new CPU state is complete; flush changes.
+    unwinder.cpu_state = new_cpu_state;
+
+    // The caller will subtract 1 from the return address to get an address corresponding to the
+    // function call. However, if this is a signal frame, that's actually incorrect, because the
+    // "return address" we have is the instruction which triggered the signal (if the signal
+    // handler returned, the instruction would be re-run). Compensate for this by incrementing
+    // the address in that case.
+    const adjusted_ret_addr = if (cache_entry.cie.is_signal_frame) return_address +| 1 else return_address;
+
+    // We also want to do that same subtraction here to get the PC for the next frame's FDE.
+    // This is because if the callee was noreturn, then the function call might be the caller's
+    // last instruction, so `return_address` might actually point outside of it!
+    unwinder.pc = adjusted_ret_addr -| 1;
+
+    return adjusted_ret_addr;
+}
+
+pub fn regNative(ctx: *std.debug.cpu_context.Native, num: u16) error{
+    InvalidRegister,
+    UnsupportedRegister,
+    IncompatibleRegisterSize,
+}!*align(1) usize {
+    const bytes = try ctx.dwarfRegisterBytes(num);
+    if (bytes.len != @sizeOf(usize)) return error.IncompatibleRegisterSize;
+    return @ptrCast(bytes);
+}
+
+/// Since register rules are applied (usually) during a panic,
+/// checked addition / subtraction is used so that we can return
+/// an error and fall back to FP-based unwinding.
+fn applyOffset(base: usize, offset: i64) !usize {
+    return if (offset >= 0)
+        try std.math.add(usize, base, @as(usize, @intCast(offset)))
+    else
+        try std.math.sub(usize, base, @as(usize, @intCast(-offset)));
+}
+
+const ip_reg_num = Dwarf.ipRegNum(builtin.target.cpu.arch).?;
+const fp_reg_num = Dwarf.fpRegNum(builtin.target.cpu.arch);
+const sp_reg_num = Dwarf.spRegNum(builtin.target.cpu.arch);
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Dwarf = std.debug.Dwarf;
+const assert = std.debug.assert;
+const stripInstructionPtrAuthCode = std.debug.stripInstructionPtrAuthCode;
+
+const builtin = @import("builtin");
+const native_endian = builtin.target.cpu.arch.endian();
+
+const SelfUnwinder = @This();
lib/std/debug/Dwarf/Unwind.zig
@@ -530,16 +530,18 @@ pub fn prepare(
     };
     if (saw_terminator != expect_terminator) return bad();
 
-    std.mem.sortUnstable(SortedFdeEntry, fde_list.items, {}, struct {
-        fn lessThan(ctx: void, a: SortedFdeEntry, b: SortedFdeEntry) bool {
-            ctx;
-            return a.pc_begin < b.pc_begin;
-        }
-    }.lessThan);
+    if (need_lookup) {
+        std.mem.sortUnstable(SortedFdeEntry, fde_list.items, {}, struct {
+            fn lessThan(ctx: void, a: SortedFdeEntry, b: SortedFdeEntry) bool {
+                ctx;
+                return a.pc_begin < b.pc_begin;
+            }
+        }.lessThan);
 
-    // This temporary is necessary to avoid an RLS footgun where `lookup` ends up non-null `undefined` on OOM.
-    const final_fdes = try fde_list.toOwnedSlice(gpa);
-    unwind.lookup = .{ .sorted_fdes = final_fdes };
+        // This temporary is necessary to avoid an RLS footgun where `lookup` ends up non-null `undefined` on OOM.
+        const final_fdes = try fde_list.toOwnedSlice(gpa);
+        unwind.lookup = .{ .sorted_fdes = final_fdes };
+    }
 }
 
 fn findCie(unwind: *const Unwind, offset: u64) ?*const CommonInformationEntry {
lib/std/debug/SelfInfo/Darwin.zig
@@ -0,0 +1,993 @@
+mutex: std.Thread.Mutex,
+/// Accessed through `Module.Adapter`.
+modules: std.ArrayHashMapUnmanaged(Module, void, Module.Context, false),
+ofiles: std.StringArrayHashMapUnmanaged(?OFile),
+
+pub const init: SelfInfo = .{
+    .mutex = .{},
+    .modules = .empty,
+    .ofiles = .empty,
+};
+pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
+    for (si.modules.keys()) |*module| {
+        unwind: {
+            const u = &(module.unwind orelse break :unwind catch break :unwind);
+            if (u.dwarf) |*dwarf| dwarf.deinit(gpa);
+        }
+        loaded: {
+            const l = &(module.loaded_macho orelse break :loaded catch break :loaded);
+            gpa.free(l.symbols);
+            posix.munmap(l.mapped_memory);
+        }
+    }
+    for (si.ofiles.values()) |*opt_ofile| {
+        const ofile = &(opt_ofile.* orelse continue);
+        ofile.dwarf.deinit(gpa);
+        ofile.symbols_by_name.deinit(gpa);
+        posix.munmap(ofile.mapped_memory);
+    }
+    si.modules.deinit(gpa);
+    si.ofiles.deinit(gpa);
+}
+
+pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol {
+    const module = try si.findModule(gpa, address);
+    defer si.mutex.unlock();
+
+    const loaded_macho = try module.getLoadedMachO(gpa);
+
+    const vaddr = address - loaded_macho.vaddr_offset;
+    const symbol = MachoSymbol.find(loaded_macho.symbols, vaddr) orelse return .unknown;
+
+    // offset of `address` from start of `symbol`
+    const address_symbol_offset = vaddr - symbol.addr;
+
+    // Take the symbol name from the N_FUN STAB entry, we're going to
+    // use it if we fail to find the DWARF infos
+    const stab_symbol = mem.sliceTo(loaded_macho.strings[symbol.strx..], 0);
+
+    // If any information is missing, we can at least return this from now on.
+    const sym_only_result: std.debug.Symbol = .{
+        .name = stab_symbol,
+        .compile_unit_name = null,
+        .source_location = null,
+    };
+
+    if (symbol.ofile == MachoSymbol.unknown_ofile) {
+        // We don't have STAB info, so can't track down the object file; all we can do is the symbol name.
+        return sym_only_result;
+    }
+
+    const o_file: *OFile = of: {
+        const path = mem.sliceTo(loaded_macho.strings[symbol.ofile..], 0);
+        const gop = try si.ofiles.getOrPut(gpa, path);
+        if (!gop.found_existing) {
+            gop.value_ptr.* = loadOFile(gpa, path) catch null;
+        }
+        if (gop.value_ptr.*) |*o_file| {
+            break :of o_file;
+        } else {
+            return sym_only_result;
+        }
+    };
+
+    const symbol_index = o_file.symbols_by_name.getKeyAdapted(
+        @as([]const u8, stab_symbol),
+        @as(OFile.SymbolAdapter, .{ .strtab = o_file.strtab, .symtab = o_file.symtab }),
+    ) orelse return sym_only_result;
+    const symbol_ofile_vaddr = o_file.symtab[symbol_index].n_value;
+
+    const compile_unit = o_file.dwarf.findCompileUnit(native_endian, symbol_ofile_vaddr) catch return sym_only_result;
+
+    return .{
+        .name = o_file.dwarf.getSymbolName(symbol_ofile_vaddr + address_symbol_offset) orelse stab_symbol,
+        .compile_unit_name = compile_unit.die.getAttrString(
+            &o_file.dwarf,
+            native_endian,
+            std.dwarf.AT.name,
+            o_file.dwarf.section(.debug_str),
+            compile_unit,
+        ) catch |err| switch (err) {
+            error.MissingDebugInfo, error.InvalidDebugInfo => null,
+        },
+        .source_location = o_file.dwarf.getLineNumberInfo(
+            gpa,
+            native_endian,
+            compile_unit,
+            symbol_ofile_vaddr + address_symbol_offset,
+        ) catch null,
+    };
+}
+pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
+    const module = try si.findModule(gpa, address);
+    defer si.mutex.unlock();
+    return module.name;
+}
+
+pub const can_unwind: bool = true;
+pub const UnwindContext = std.debug.Dwarf.SelfUnwinder;
+/// Unwind a frame using MachO compact unwind info (from `__unwind_info`).
+/// If the compact encoding can't encode a way to unwind a frame, it will
+/// defer unwinding to DWARF, in which case `__eh_frame` will be used if available.
+pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize {
+    return unwindFrameInner(si, gpa, context) catch |err| switch (err) {
+        error.InvalidDebugInfo,
+        error.MissingDebugInfo,
+        error.UnsupportedDebugInfo,
+        error.ReadFailed,
+        error.OutOfMemory,
+        error.Unexpected,
+        => |e| return e,
+        error.UnsupportedRegister,
+        error.UnsupportedAddrSize,
+        error.UnimplementedUserOpcode,
+        => return error.UnsupportedDebugInfo,
+        error.Overflow,
+        error.EndOfStream,
+        error.StreamTooLong,
+        error.InvalidOpcode,
+        error.InvalidOperation,
+        error.InvalidOperand,
+        error.InvalidRegister,
+        error.IncompatibleRegisterSize,
+        => return error.InvalidDebugInfo,
+    };
+}
+fn unwindFrameInner(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) !usize {
+    const module = try si.findModule(gpa, context.pc);
+    defer si.mutex.unlock();
+
+    const unwind: *Module.Unwind = try module.getUnwindInfo(gpa);
+
+    const ip_reg_num = comptime Dwarf.ipRegNum(builtin.target.cpu.arch).?;
+    const fp_reg_num = comptime Dwarf.fpRegNum(builtin.target.cpu.arch);
+    const sp_reg_num = comptime Dwarf.spRegNum(builtin.target.cpu.arch);
+
+    const unwind_info = unwind.unwind_info orelse return error.MissingDebugInfo;
+    if (unwind_info.len < @sizeOf(macho.unwind_info_section_header)) return error.InvalidDebugInfo;
+    const header: *align(1) const macho.unwind_info_section_header = @ptrCast(unwind_info);
+
+    const index_byte_count = header.indexCount * @sizeOf(macho.unwind_info_section_header_index_entry);
+    if (unwind_info.len < header.indexSectionOffset + index_byte_count) return error.InvalidDebugInfo;
+    const indices: []align(1) const macho.unwind_info_section_header_index_entry = @ptrCast(unwind_info[header.indexSectionOffset..][0..index_byte_count]);
+    if (indices.len == 0) return error.MissingDebugInfo;
+
+    // offset of the PC into the `__TEXT` segment
+    const pc_text_offset = context.pc - module.text_base;
+
+    const start_offset: u32, const first_level_offset: u32 = index: {
+        var left: usize = 0;
+        var len: usize = indices.len;
+        while (len > 1) {
+            const mid = left + len / 2;
+            if (pc_text_offset < indices[mid].functionOffset) {
+                len /= 2;
+            } else {
+                left = mid;
+                len -= len / 2;
+            }
+        }
+        break :index .{ indices[left].secondLevelPagesSectionOffset, indices[left].functionOffset };
+    };
+    // An offset of 0 is a sentinel indicating a range does not have unwind info.
+    if (start_offset == 0) return error.MissingDebugInfo;
+
+    const common_encodings_byte_count = header.commonEncodingsArrayCount * @sizeOf(macho.compact_unwind_encoding_t);
+    if (unwind_info.len < header.commonEncodingsArraySectionOffset + common_encodings_byte_count) return error.InvalidDebugInfo;
+    const common_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
+        unwind_info[header.commonEncodingsArraySectionOffset..][0..common_encodings_byte_count],
+    );
+
+    if (unwind_info.len < start_offset + @sizeOf(macho.UNWIND_SECOND_LEVEL)) return error.InvalidDebugInfo;
+    const kind: *align(1) const macho.UNWIND_SECOND_LEVEL = @ptrCast(unwind_info[start_offset..]);
+
+    const entry: struct {
+        function_offset: usize,
+        raw_encoding: u32,
+    } = switch (kind.*) {
+        .REGULAR => entry: {
+            if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_regular_second_level_page_header)) return error.InvalidDebugInfo;
+            const page_header: *align(1) const macho.unwind_info_regular_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
+
+            const entries_byte_count = page_header.entryCount * @sizeOf(macho.unwind_info_regular_second_level_entry);
+            if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
+            const entries: []align(1) const macho.unwind_info_regular_second_level_entry = @ptrCast(
+                unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
+            );
+            if (entries.len == 0) return error.InvalidDebugInfo;
+
+            var left: usize = 0;
+            var len: usize = entries.len;
+            while (len > 1) {
+                const mid = left + len / 2;
+                if (pc_text_offset < entries[mid].functionOffset) {
+                    len /= 2;
+                } else {
+                    left = mid;
+                    len -= len / 2;
+                }
+            }
+            break :entry .{
+                .function_offset = entries[left].functionOffset,
+                .raw_encoding = entries[left].encoding,
+            };
+        },
+        .COMPRESSED => entry: {
+            if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_compressed_second_level_page_header)) return error.InvalidDebugInfo;
+            const page_header: *align(1) const macho.unwind_info_compressed_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
+
+            const entries_byte_count = page_header.entryCount * @sizeOf(macho.UnwindInfoCompressedEntry);
+            if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
+            const entries: []align(1) const macho.UnwindInfoCompressedEntry = @ptrCast(
+                unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
+            );
+            if (entries.len == 0) return error.InvalidDebugInfo;
+
+            var left: usize = 0;
+            var len: usize = entries.len;
+            while (len > 1) {
+                const mid = left + len / 2;
+                if (pc_text_offset < first_level_offset + entries[mid].funcOffset) {
+                    len /= 2;
+                } else {
+                    left = mid;
+                    len -= len / 2;
+                }
+            }
+            const entry = entries[left];
+
+            const function_offset = first_level_offset + entry.funcOffset;
+            if (entry.encodingIndex < common_encodings.len) {
+                break :entry .{
+                    .function_offset = function_offset,
+                    .raw_encoding = common_encodings[entry.encodingIndex],
+                };
+            }
+
+            const local_index = entry.encodingIndex - common_encodings.len;
+            const local_encodings_byte_count = page_header.encodingsCount * @sizeOf(macho.compact_unwind_encoding_t);
+            if (unwind_info.len < start_offset + page_header.encodingsPageOffset + local_encodings_byte_count) return error.InvalidDebugInfo;
+            const local_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
+                unwind_info[start_offset + page_header.encodingsPageOffset ..][0..local_encodings_byte_count],
+            );
+            if (local_index >= local_encodings.len) return error.InvalidDebugInfo;
+            break :entry .{
+                .function_offset = function_offset,
+                .raw_encoding = local_encodings[local_index],
+            };
+        },
+        else => return error.InvalidDebugInfo,
+    };
+
+    if (entry.raw_encoding == 0) return error.MissingDebugInfo;
+
+    const encoding: macho.CompactUnwindEncoding = @bitCast(entry.raw_encoding);
+    const new_ip = switch (builtin.cpu.arch) {
+        .x86_64 => switch (encoding.mode.x86_64) {
+            .OLD => return error.UnsupportedDebugInfo,
+            .RBP_FRAME => ip: {
+                const frame = encoding.value.x86_64.frame;
+
+                const fp = (try dwarfRegNative(&context.cpu_state, fp_reg_num)).*;
+                const new_sp = fp + 2 * @sizeOf(usize);
+
+                const ip_ptr = fp + @sizeOf(usize);
+                const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
+                const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
+
+                (try dwarfRegNative(&context.cpu_state, fp_reg_num)).* = new_fp;
+                (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
+                (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
+
+                const regs: [5]u3 = .{
+                    frame.reg0,
+                    frame.reg1,
+                    frame.reg2,
+                    frame.reg3,
+                    frame.reg4,
+                };
+                for (regs, 0..) |reg, i| {
+                    if (reg == 0) continue;
+                    const addr = fp - frame.frame_offset * @sizeOf(usize) + i * @sizeOf(usize);
+                    const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(reg);
+                    (try dwarfRegNative(&context.cpu_state, reg_number)).* = @as(*const usize, @ptrFromInt(addr)).*;
+                }
+
+                break :ip new_ip;
+            },
+            .STACK_IMMD,
+            .STACK_IND,
+            => ip: {
+                const frameless = encoding.value.x86_64.frameless;
+
+                const sp = (try dwarfRegNative(&context.cpu_state, sp_reg_num)).*;
+                const stack_size: usize = stack_size: {
+                    if (encoding.mode.x86_64 == .STACK_IMMD) {
+                        break :stack_size @as(usize, frameless.stack.direct.stack_size) * @sizeOf(usize);
+                    }
+                    // In .STACK_IND, the stack size is inferred from the subq instruction at the beginning of the function.
+                    const sub_offset_addr =
+                        module.text_base +
+                        entry.function_offset +
+                        frameless.stack.indirect.sub_offset;
+                    // `sub_offset_addr` points to the offset of the literal within the instruction
+                    const sub_operand = @as(*align(1) const u32, @ptrFromInt(sub_offset_addr)).*;
+                    break :stack_size sub_operand + @sizeOf(usize) * @as(usize, frameless.stack.indirect.stack_adjust);
+                };
+
+                // Decode the Lehmer-coded sequence of registers.
+                // For a description of the encoding see lib/libc/include/any-macos.13-any/mach-o/compact_unwind_encoding.h
+
+                // Decode the variable-based permutation number into its digits. Each digit represents
+                // an index into the list of register numbers that weren't yet used in the sequence at
+                // the time the digit was added.
+                const reg_count = frameless.stack_reg_count;
+                const ip_ptr = ip_ptr: {
+                    var digits: [6]u3 = undefined;
+                    var accumulator: usize = frameless.stack_reg_permutation;
+                    var base: usize = 2;
+                    for (0..reg_count) |i| {
+                        const div = accumulator / base;
+                        digits[digits.len - 1 - i] = @intCast(accumulator - base * div);
+                        accumulator = div;
+                        base += 1;
+                    }
+
+                    var registers: [6]u3 = undefined;
+                    var used_indices: [6]bool = @splat(false);
+                    for (digits[digits.len - reg_count ..], 0..) |target_unused_index, i| {
+                        var unused_count: u8 = 0;
+                        const unused_index = for (used_indices, 0..) |used, index| {
+                            if (!used) {
+                                if (target_unused_index == unused_count) break index;
+                                unused_count += 1;
+                            }
+                        } else unreachable;
+                        registers[i] = @intCast(unused_index + 1);
+                        used_indices[unused_index] = true;
+                    }
+
+                    var reg_addr = sp + stack_size - @sizeOf(usize) * @as(usize, reg_count + 1);
+                    for (0..reg_count) |i| {
+                        const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(registers[i]);
+                        (try dwarfRegNative(&context.cpu_state, reg_number)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
+                        reg_addr += @sizeOf(usize);
+                    }
+
+                    break :ip_ptr reg_addr;
+                };
+
+                const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
+                const new_sp = ip_ptr + @sizeOf(usize);
+
+                (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
+                (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
+
+                break :ip new_ip;
+            },
+            .DWARF => {
+                const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
+                const rules = try context.computeRules(gpa, dwarf, unwind.vmaddr_slide, encoding.value.x86_64.dwarf);
+                return context.next(gpa, &rules);
+            },
+        },
+        .aarch64, .aarch64_be => switch (encoding.mode.arm64) {
+            .OLD => return error.UnsupportedDebugInfo,
+            .FRAMELESS => ip: {
+                const sp = (try dwarfRegNative(&context.cpu_state, sp_reg_num)).*;
+                const new_sp = sp + encoding.value.arm64.frameless.stack_size * 16;
+                const new_ip = (try dwarfRegNative(&context.cpu_state, 30)).*;
+                (try dwarfRegNative(&context.cpu_state, sp_reg_num)).* = new_sp;
+                break :ip new_ip;
+            },
+            .DWARF => {
+                const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
+                const rules = try context.computeRules(gpa, dwarf, unwind.vmaddr_slide, encoding.value.arm64.dwarf);
+                return context.next(gpa, &rules);
+            },
+            .FRAME => ip: {
+                const frame = encoding.value.arm64.frame;
+
+                const fp = (try dwarfRegNative(&context.cpu_state, fp_reg_num)).*;
+                const ip_ptr = fp + @sizeOf(usize);
+
+                var reg_addr = fp - @sizeOf(usize);
+                inline for (@typeInfo(@TypeOf(frame.x_reg_pairs)).@"struct".fields, 0..) |field, i| {
+                    if (@field(frame.x_reg_pairs, field.name) != 0) {
+                        (try dwarfRegNative(&context.cpu_state, 19 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
+                        reg_addr += @sizeOf(usize);
+                        (try dwarfRegNative(&context.cpu_state, 20 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
+                        reg_addr += @sizeOf(usize);
+                    }
+                }
+
+                inline for (@typeInfo(@TypeOf(frame.d_reg_pairs)).@"struct".fields, 0..) |field, i| {
+                    if (@field(frame.d_reg_pairs, field.name) != 0) {
+                        // Only the lower half of the 128-bit V registers are restored during unwinding
+                        {
+                            const dest: *align(1) usize = @ptrCast(try context.cpu_state.dwarfRegisterBytes(64 + 8 + i));
+                            dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
+                        }
+                        reg_addr += @sizeOf(usize);
+                        {
+                            const dest: *align(1) usize = @ptrCast(try context.cpu_state.dwarfRegisterBytes(64 + 9 + i));
+                            dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
+                        }
+                        reg_addr += @sizeOf(usize);
+                    }
+                }
+
+                const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
+                const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
+
+                (try dwarfRegNative(&context.cpu_state, fp_reg_num)).* = new_fp;
+                (try dwarfRegNative(&context.cpu_state, ip_reg_num)).* = new_ip;
+
+                break :ip new_ip;
+            },
+        },
+        else => comptime unreachable, // unimplemented
+    };
+
+    const ret_addr = std.debug.stripInstructionPtrAuthCode(new_ip);
+
+    // Like `Dwarf.SelfUnwinder.next`, adjust our next lookup pc in case the `call` was this
+    // function's last instruction making `ret_addr` one byte past its end.
+    context.pc = ret_addr -| 1;
+
+    return ret_addr;
+}
+
+/// Acquires the mutex on success.
+fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) Error!*Module {
+    var info: std.c.dl_info = undefined;
+    if (std.c.dladdr(@ptrFromInt(address), &info) == 0) {
+        return error.MissingDebugInfo;
+    }
+    si.mutex.lock();
+    errdefer si.mutex.unlock();
+    const gop = try si.modules.getOrPutAdapted(gpa, @intFromPtr(info.fbase), Module.Adapter{});
+    errdefer comptime unreachable;
+    if (!gop.found_existing) {
+        gop.key_ptr.* = .{
+            .text_base = @intFromPtr(info.fbase),
+            .name = std.mem.span(info.fname),
+            .unwind = null,
+            .loaded_macho = null,
+        };
+    }
+    return gop.key_ptr;
+}
+
+const Module = struct {
+    text_base: usize,
+    name: []const u8,
+    unwind: ?(Error!Unwind),
+    loaded_macho: ?(Error!LoadedMachO),
+
+    const Adapter = struct {
+        pub fn hash(_: Adapter, text_base: usize) u32 {
+            return @truncate(std.hash.int(text_base));
+        }
+        pub fn eql(_: Adapter, a_text_base: usize, b_module: Module, b_index: usize) bool {
+            _ = b_index;
+            return a_text_base == b_module.text_base;
+        }
+    };
+    const Context = struct {
+        pub fn hash(_: Context, module: Module) u32 {
+            return @truncate(std.hash.int(module.text_base));
+        }
+        pub fn eql(_: Context, a_module: Module, b_module: Module, b_index: usize) bool {
+            _ = b_index;
+            return a_module.text_base == b_module.text_base;
+        }
+    };
+
+    const Unwind = struct {
+        /// The slide applied to the `__unwind_info` and `__eh_frame` sections.
+        /// So, `unwind_info.ptr` is this many bytes higher than the section's vmaddr.
+        vmaddr_slide: u64,
+        /// Backed by the in-memory section mapped by the loader.
+        unwind_info: ?[]const u8,
+        /// Backed by the in-memory `__eh_frame` section mapped by the loader.
+        dwarf: ?Dwarf.Unwind,
+    };
+
+    const LoadedMachO = struct {
+        mapped_memory: []align(std.heap.page_size_min) const u8,
+        symbols: []const MachoSymbol,
+        strings: []const u8,
+        /// This is not necessarily the same as the vmaddr_slide that dyld would report. This is
+        /// because the segments in the file on disk might differ from the ones in memory. Normally
+        /// we wouldn't necessarily expect that to work, but /usr/lib/dyld is incredibly annoying:
+        /// it exists on disk (necessarily, because the kernel needs to load it!), but is also in
+        /// the dyld cache (dyld actually restart itself from cache after loading it), and the two
+        /// versions have (very) different segment base addresses. It's sort of like a large slide
+        /// has been applied to all addresses in memory. For an optimal experience, we consider the
+        /// on-disk vmaddr instead of the in-memory one.
+        vaddr_offset: usize,
+    };
+
+    fn getUnwindInfo(module: *Module, gpa: Allocator) Error!*Unwind {
+        if (module.unwind == null) module.unwind = loadUnwindInfo(module, gpa);
+        return if (module.unwind.?) |*unwind| unwind else |err| err;
+    }
+    fn loadUnwindInfo(module: *const Module, gpa: Allocator) Error!Unwind {
+        const header: *std.macho.mach_header = @ptrFromInt(module.text_base);
+
+        var it: macho.LoadCommandIterator = .{
+            .ncmds = header.ncmds,
+            .buffer = @as([*]u8, @ptrCast(header))[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds],
+        };
+        const sections, const text_vmaddr = while (it.next()) |load_cmd| {
+            if (load_cmd.cmd() != .SEGMENT_64) continue;
+            const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
+            if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
+            break .{ load_cmd.getSections(), segment_cmd.vmaddr };
+        } else unreachable;
+
+        const vmaddr_slide = module.text_base - text_vmaddr;
+
+        var opt_unwind_info: ?[]const u8 = null;
+        var opt_eh_frame: ?[]const u8 = null;
+        for (sections) |sect| {
+            if (mem.eql(u8, sect.sectName(), "__unwind_info")) {
+                const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
+                opt_unwind_info = sect_ptr[0..@intCast(sect.size)];
+            } else if (mem.eql(u8, sect.sectName(), "__eh_frame")) {
+                const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
+                opt_eh_frame = sect_ptr[0..@intCast(sect.size)];
+            }
+        }
+        const eh_frame = opt_eh_frame orelse return .{
+            .vmaddr_slide = vmaddr_slide,
+            .unwind_info = opt_unwind_info,
+            .dwarf = null,
+        };
+        var dwarf: Dwarf.Unwind = .initSection(.eh_frame, @intFromPtr(eh_frame.ptr) - vmaddr_slide, eh_frame);
+        errdefer dwarf.deinit(gpa);
+        // We don't need lookups, so this call is just for scanning CIEs.
+        dwarf.prepare(gpa, @sizeOf(usize), native_endian, false, true) catch |err| switch (err) {
+            error.ReadFailed => unreachable, // it's all fixed buffers
+            error.InvalidDebugInfo,
+            error.MissingDebugInfo,
+            error.OutOfMemory,
+            => |e| return e,
+            error.EndOfStream,
+            error.Overflow,
+            error.StreamTooLong,
+            error.InvalidOperand,
+            error.InvalidOpcode,
+            error.InvalidOperation,
+            => return error.InvalidDebugInfo,
+            error.UnsupportedAddrSize,
+            error.UnsupportedDwarfVersion,
+            error.UnimplementedUserOpcode,
+            => return error.UnsupportedDebugInfo,
+        };
+
+        return .{
+            .vmaddr_slide = vmaddr_slide,
+            .unwind_info = opt_unwind_info,
+            .dwarf = dwarf,
+        };
+    }
+
+    fn getLoadedMachO(module: *Module, gpa: Allocator) Error!*LoadedMachO {
+        if (module.loaded_macho == null) module.loaded_macho = loadMachO(module, gpa) catch |err| switch (err) {
+            error.InvalidDebugInfo, error.MissingDebugInfo, error.OutOfMemory, error.Unexpected => |e| e,
+            else => error.ReadFailed,
+        };
+        return if (module.loaded_macho.?) |*lm| lm else |err| err;
+    }
+    fn loadMachO(module: *const Module, gpa: Allocator) Error!LoadedMachO {
+        const all_mapped_memory = try mapDebugInfoFile(module.name);
+        errdefer posix.munmap(all_mapped_memory);
+
+        // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal
+        // binary": a simple file format which contains Mach-O binaries for multiple targets. For
+        // instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
+        // for both ARM64 macOS and x86_64 macOS.
+        if (all_mapped_memory.len < 4) return error.InvalidDebugInfo;
+        const magic = @as(*const u32, @ptrCast(all_mapped_memory.ptr)).*;
+        // The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
+        const mapped_macho = switch (magic) {
+            macho.MH_MAGIC_64 => all_mapped_memory,
+
+            macho.FAT_CIGAM => mapped_macho: {
+                // This is the universal binary format (aka a "fat binary"). Annoyingly, the whole thing
+                // is big-endian, so we'll be swapping some bytes.
+                if (all_mapped_memory.len < @sizeOf(macho.fat_header)) return error.InvalidDebugInfo;
+                const hdr: *const macho.fat_header = @ptrCast(all_mapped_memory.ptr);
+                const archs_ptr: [*]const macho.fat_arch = @ptrCast(all_mapped_memory.ptr + @sizeOf(macho.fat_header));
+                const archs: []const macho.fat_arch = archs_ptr[0..@byteSwap(hdr.nfat_arch)];
+                const native_cpu_type = switch (builtin.cpu.arch) {
+                    .x86_64 => macho.CPU_TYPE_X86_64,
+                    .aarch64 => macho.CPU_TYPE_ARM64,
+                    else => comptime unreachable,
+                };
+                for (archs) |*arch| {
+                    if (@byteSwap(arch.cputype) != native_cpu_type) continue;
+                    const offset = @byteSwap(arch.offset);
+                    const size = @byteSwap(arch.size);
+                    break :mapped_macho all_mapped_memory[offset..][0..size];
+                }
+                // Our native architecture was not present in the fat binary.
+                return error.MissingDebugInfo;
+            },
+
+            // Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
+            // will be fairly easy to add support here if necessary; it's very similar to above.
+            macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
+
+            else => return error.InvalidDebugInfo,
+        };
+
+        const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_macho.ptr));
+        if (hdr.magic != macho.MH_MAGIC_64)
+            return error.InvalidDebugInfo;
+
+        const symtab: macho.symtab_command, const text_vmaddr: u64 = lc_iter: {
+            var it: macho.LoadCommandIterator = .{
+                .ncmds = hdr.ncmds,
+                .buffer = mapped_macho[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
+            };
+            var symtab: ?macho.symtab_command = null;
+            var text_vmaddr: ?u64 = null;
+            while (it.next()) |cmd| switch (cmd.cmd()) {
+                .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
+                .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
+                    if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
+                    text_vmaddr = seg_cmd.vmaddr;
+                },
+                else => {},
+            };
+            break :lc_iter .{
+                symtab orelse return error.MissingDebugInfo,
+                text_vmaddr orelse return error.MissingDebugInfo,
+            };
+        };
+
+        const syms_ptr: [*]align(1) const macho.nlist_64 = @ptrCast(mapped_macho[symtab.symoff..]);
+        const syms = syms_ptr[0..symtab.nsyms];
+        const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1];
+
+        var symbols: std.ArrayList(MachoSymbol) = try .initCapacity(gpa, syms.len);
+        defer symbols.deinit(gpa);
+
+        // This map is temporary; it is used only to detect duplicates here. This is
+        // necessary because we prefer to use STAB ("symbolic debugging table") symbols,
+        // but they might not be present, so we track normal symbols too.
+        // Indices match 1-1 with those of `symbols`.
+        var symbol_names: std.StringArrayHashMapUnmanaged(void) = .empty;
+        defer symbol_names.deinit(gpa);
+        try symbol_names.ensureUnusedCapacity(gpa, syms.len);
+
+        var ofile: u32 = undefined;
+        var last_sym: MachoSymbol = undefined;
+        var state: enum {
+            init,
+            oso_open,
+            oso_close,
+            bnsym,
+            fun_strx,
+            fun_size,
+            ensym,
+        } = .init;
+
+        for (syms) |*sym| {
+            if (sym.n_type.bits.is_stab == 0) {
+                if (sym.n_strx == 0) continue;
+                switch (sym.n_type.bits.type) {
+                    .undf, .pbud, .indr, .abs, _ => continue,
+                    .sect => {
+                        const name = std.mem.sliceTo(strings[sym.n_strx..], 0);
+                        const gop = symbol_names.getOrPutAssumeCapacity(name);
+                        if (!gop.found_existing) {
+                            assert(gop.index == symbols.items.len);
+                            symbols.appendAssumeCapacity(.{
+                                .strx = sym.n_strx,
+                                .addr = sym.n_value,
+                                .ofile = MachoSymbol.unknown_ofile,
+                            });
+                        }
+                    },
+                }
+                continue;
+            }
+
+            // TODO handle globals N_GSYM, and statics N_STSYM
+            switch (sym.n_type.stab) {
+                .oso => switch (state) {
+                    .init, .oso_close => {
+                        state = .oso_open;
+                        ofile = sym.n_strx;
+                    },
+                    else => return error.InvalidDebugInfo,
+                },
+                .bnsym => switch (state) {
+                    .oso_open, .ensym => {
+                        state = .bnsym;
+                        last_sym = .{
+                            .strx = 0,
+                            .addr = sym.n_value,
+                            .ofile = ofile,
+                        };
+                    },
+                    else => return error.InvalidDebugInfo,
+                },
+                .fun => switch (state) {
+                    .bnsym => {
+                        state = .fun_strx;
+                        last_sym.strx = sym.n_strx;
+                    },
+                    .fun_strx => {
+                        state = .fun_size;
+                    },
+                    else => return error.InvalidDebugInfo,
+                },
+                .ensym => switch (state) {
+                    .fun_size => {
+                        state = .ensym;
+                        if (last_sym.strx != 0) {
+                            const name = std.mem.sliceTo(strings[last_sym.strx..], 0);
+                            const gop = symbol_names.getOrPutAssumeCapacity(name);
+                            if (!gop.found_existing) {
+                                assert(gop.index == symbols.items.len);
+                                symbols.appendAssumeCapacity(last_sym);
+                            } else {
+                                symbols.items[gop.index] = last_sym;
+                            }
+                        }
+                    },
+                    else => return error.InvalidDebugInfo,
+                },
+                .so => switch (state) {
+                    .init, .oso_close => {},
+                    .oso_open, .ensym => {
+                        state = .oso_close;
+                    },
+                    else => return error.InvalidDebugInfo,
+                },
+                else => {},
+            }
+        }
+
+        switch (state) {
+            .init => {
+                // Missing STAB symtab entries is still okay, unless there were also no normal symbols.
+                if (symbols.items.len == 0) return error.MissingDebugInfo;
+            },
+            .oso_close => {},
+            else => return error.InvalidDebugInfo, // corrupted STAB entries in symtab
+        }
+
+        const symbols_slice = try symbols.toOwnedSlice(gpa);
+        errdefer gpa.free(symbols_slice);
+
+        // Even though lld emits symbols in ascending order, this debug code
+        // should work for programs linked in any valid way.
+        // This sort is so that we can binary search later.
+        mem.sort(MachoSymbol, symbols_slice, {}, MachoSymbol.addressLessThan);
+
+        return .{
+            .mapped_memory = all_mapped_memory,
+            .symbols = symbols_slice,
+            .strings = strings,
+            .vaddr_offset = module.text_base - text_vmaddr,
+        };
+    }
+};
+
+const OFile = struct {
+    mapped_memory: []align(std.heap.page_size_min) const u8,
+    dwarf: Dwarf,
+    strtab: []const u8,
+    symtab: []align(1) const macho.nlist_64,
+    /// All named symbols in `symtab`. Stored `u32` key is the index into `symtab`. Accessed
+    /// through `SymbolAdapter`, so that the symbol name is used as the logical key.
+    symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true),
+
+    const SymbolAdapter = struct {
+        strtab: []const u8,
+        symtab: []align(1) const macho.nlist_64,
+        pub fn hash(ctx: SymbolAdapter, sym_name: []const u8) u32 {
+            _ = ctx;
+            return @truncate(std.hash.Wyhash.hash(0, sym_name));
+        }
+        pub fn eql(ctx: SymbolAdapter, a_sym_name: []const u8, b_sym_index: u32, b_index: usize) bool {
+            _ = b_index;
+            const b_sym = ctx.symtab[b_sym_index];
+            const b_sym_name = std.mem.sliceTo(ctx.strtab[b_sym.n_strx..], 0);
+            return mem.eql(u8, a_sym_name, b_sym_name);
+        }
+    };
+};
+
+const MachoSymbol = struct {
+    strx: u32,
+    addr: u64,
+    /// Value may be `unknown_ofile`.
+    ofile: u32,
+    const unknown_ofile = std.math.maxInt(u32);
+    fn addressLessThan(context: void, lhs: MachoSymbol, rhs: MachoSymbol) bool {
+        _ = context;
+        return lhs.addr < rhs.addr;
+    }
+    /// Assumes that `symbols` is sorted in order of ascending `addr`.
+    fn find(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
+        if (symbols.len == 0) return null; // no potential match
+        if (address < symbols[0].addr) return null; // address is before the lowest-address symbol
+        var left: usize = 0;
+        var len: usize = symbols.len;
+        while (len > 1) {
+            const mid = left + len / 2;
+            if (address < symbols[mid].addr) {
+                len /= 2;
+            } else {
+                left = mid;
+                len -= len / 2;
+            }
+        }
+        return &symbols[left];
+    }
+
+    test find {
+        const symbols: []const MachoSymbol = &.{
+            .{ .addr = 100, .strx = undefined, .ofile = undefined },
+            .{ .addr = 200, .strx = undefined, .ofile = undefined },
+            .{ .addr = 300, .strx = undefined, .ofile = undefined },
+        };
+
+        try testing.expectEqual(null, find(symbols, 0));
+        try testing.expectEqual(null, find(symbols, 99));
+        try testing.expectEqual(&symbols[0], find(symbols, 100).?);
+        try testing.expectEqual(&symbols[0], find(symbols, 150).?);
+        try testing.expectEqual(&symbols[0], find(symbols, 199).?);
+
+        try testing.expectEqual(&symbols[1], find(symbols, 200).?);
+        try testing.expectEqual(&symbols[1], find(symbols, 250).?);
+        try testing.expectEqual(&symbols[1], find(symbols, 299).?);
+
+        try testing.expectEqual(&symbols[2], find(symbols, 300).?);
+        try testing.expectEqual(&symbols[2], find(symbols, 301).?);
+        try testing.expectEqual(&symbols[2], find(symbols, 5000).?);
+    }
+};
+test {
+    _ = MachoSymbol;
+}
+
+/// Uses `mmap` to map the file at `path` into memory.
+fn mapDebugInfoFile(path: []const u8) ![]align(std.heap.page_size_min) const u8 {
+    const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
+        error.FileNotFound => return error.MissingDebugInfo,
+        else => return error.ReadFailed,
+    };
+    defer file.close();
+
+    const file_end_pos = file.getEndPos() catch |err| switch (err) {
+        error.Unexpected => |e| return e,
+        else => return error.ReadFailed,
+    };
+    const file_len = std.math.cast(usize, file_end_pos) orelse return error.InvalidDebugInfo;
+
+    return posix.mmap(
+        null,
+        file_len,
+        posix.PROT.READ,
+        .{ .TYPE = .SHARED },
+        file.handle,
+        0,
+    ) catch |err| switch (err) {
+        error.Unexpected => |e| return e,
+        else => return error.ReadFailed,
+    };
+}
+
+fn loadOFile(gpa: Allocator, o_file_path: []const u8) !OFile {
+    const mapped_mem = try mapDebugInfoFile(o_file_path);
+    errdefer posix.munmap(mapped_mem);
+
+    if (mapped_mem.len < @sizeOf(macho.mach_header_64)) return error.InvalidDebugInfo;
+    const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_mem.ptr));
+    if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidDebugInfo;
+
+    const seg_cmd: macho.LoadCommandIterator.LoadCommand, const symtab_cmd: macho.symtab_command = cmds: {
+        var seg_cmd: ?macho.LoadCommandIterator.LoadCommand = null;
+        var symtab_cmd: ?macho.symtab_command = null;
+        var it: macho.LoadCommandIterator = .{
+            .ncmds = hdr.ncmds,
+            .buffer = mapped_mem[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
+        };
+        while (it.next()) |cmd| switch (cmd.cmd()) {
+            .SEGMENT_64 => seg_cmd = cmd,
+            .SYMTAB => symtab_cmd = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
+            else => {},
+        };
+        break :cmds .{
+            seg_cmd orelse return error.MissingDebugInfo,
+            symtab_cmd orelse return error.MissingDebugInfo,
+        };
+    };
+
+    if (mapped_mem.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidDebugInfo;
+    if (mapped_mem[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidDebugInfo;
+    const strtab = mapped_mem[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1];
+
+    const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64);
+    if (mapped_mem.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidDebugInfo;
+    const symtab: []align(1) const macho.nlist_64 = @ptrCast(mapped_mem[symtab_cmd.symoff..][0..n_sym_bytes]);
+
+    // TODO handle tentative (common) symbols
+    var symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true) = .empty;
+    defer symbols_by_name.deinit(gpa);
+    try symbols_by_name.ensureUnusedCapacity(gpa, @intCast(symtab.len));
+    for (symtab, 0..) |sym, sym_index| {
+        if (sym.n_strx == 0) continue;
+        switch (sym.n_type.bits.type) {
+            .undf => continue, // includes tentative symbols
+            .abs => continue,
+            else => {},
+        }
+        const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0);
+        const gop = symbols_by_name.getOrPutAssumeCapacityAdapted(
+            @as([]const u8, sym_name),
+            @as(OFile.SymbolAdapter, .{ .strtab = strtab, .symtab = symtab }),
+        );
+        if (gop.found_existing) return error.InvalidDebugInfo;
+        gop.key_ptr.* = @intCast(sym_index);
+    }
+
+    var sections: Dwarf.SectionArray = @splat(null);
+    for (seg_cmd.getSections()) |sect| {
+        if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue;
+
+        const section_index: usize = inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
+            if (mem.eql(u8, "__" ++ section.name, sect.sectName())) break i;
+        } else continue;
+
+        if (mapped_mem.len < sect.offset + sect.size) return error.InvalidDebugInfo;
+        const section_bytes = mapped_mem[sect.offset..][0..sect.size];
+        sections[section_index] = .{
+            .data = section_bytes,
+            .owned = false,
+        };
+    }
+
+    const missing_debug_info =
+        sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
+        sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
+        sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
+        sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
+    if (missing_debug_info) return error.MissingDebugInfo;
+
+    var dwarf: Dwarf = .{ .sections = sections };
+    errdefer dwarf.deinit(gpa);
+    try dwarf.open(gpa, native_endian);
+
+    return .{
+        .mapped_memory = mapped_mem,
+        .dwarf = dwarf,
+        .strtab = strtab,
+        .symtab = symtab,
+        .symbols_by_name = symbols_by_name.move(),
+    };
+}
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Dwarf = std.debug.Dwarf;
+const Error = std.debug.SelfInfoError;
+const assert = std.debug.assert;
+const posix = std.posix;
+const macho = std.macho;
+const mem = std.mem;
+const testing = std.testing;
+const dwarfRegNative = std.debug.Dwarf.SelfUnwinder.regNative;
+
+const builtin = @import("builtin");
+const native_endian = builtin.target.cpu.arch.endian();
+
+const SelfInfo = @This();
lib/std/debug/SelfInfo/DarwinModule.zig
@@ -1,954 +0,0 @@
-/// The runtime address where __TEXT is loaded.
-text_base: usize,
-name: []const u8,
-
-pub fn key(m: *const DarwinModule) usize {
-    return m.text_base;
-}
-
-/// No cache needed, because `_dyld_get_image_header` etc are already fast.
-pub const LookupCache = void;
-pub fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) Error!DarwinModule {
-    _ = cache;
-    _ = gpa;
-    var info: std.c.dl_info = undefined;
-    switch (std.c.dladdr(@ptrFromInt(address), &info)) {
-        0 => return error.MissingDebugInfo,
-        else => return .{
-            .name = std.mem.span(info.fname),
-            .text_base = @intFromPtr(info.fbase),
-        },
-    }
-}
-fn loadUnwindInfo(module: *const DarwinModule, gpa: Allocator, out: *DebugInfo) !void {
-    const header: *std.macho.mach_header = @ptrFromInt(module.text_base);
-
-    var it: macho.LoadCommandIterator = .{
-        .ncmds = header.ncmds,
-        .buffer = @as([*]u8, @ptrCast(header))[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds],
-    };
-    const sections, const text_vmaddr = while (it.next()) |load_cmd| {
-        if (load_cmd.cmd() != .SEGMENT_64) continue;
-        const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
-        if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
-        break .{ load_cmd.getSections(), segment_cmd.vmaddr };
-    } else unreachable;
-
-    const vmaddr_slide = module.text_base - text_vmaddr;
-
-    var opt_unwind_info: ?[]const u8 = null;
-    var opt_eh_frame: ?[]const u8 = null;
-    for (sections) |sect| {
-        if (mem.eql(u8, sect.sectName(), "__unwind_info")) {
-            const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
-            opt_unwind_info = sect_ptr[0..@intCast(sect.size)];
-        } else if (mem.eql(u8, sect.sectName(), "__eh_frame")) {
-            const sect_ptr: [*]u8 = @ptrFromInt(@as(usize, @intCast(vmaddr_slide + sect.addr)));
-            opt_eh_frame = sect_ptr[0..@intCast(sect.size)];
-        }
-    }
-    const eh_frame = opt_eh_frame orelse {
-        out.unwind = .{
-            .vmaddr_slide = vmaddr_slide,
-            .unwind_info = opt_unwind_info,
-            .dwarf = null,
-            .dwarf_cache = undefined,
-        };
-        return;
-    };
-    var dwarf: Dwarf.Unwind = .initSection(.eh_frame, @intFromPtr(eh_frame.ptr) - vmaddr_slide, eh_frame);
-    errdefer dwarf.deinit(gpa);
-    // We don't need lookups, so this call is just for scanning CIEs.
-    dwarf.prepare(gpa, @sizeOf(usize), native_endian, false, true) catch |err| switch (err) {
-        error.ReadFailed => unreachable, // it's all fixed buffers
-        error.InvalidDebugInfo,
-        error.MissingDebugInfo,
-        error.OutOfMemory,
-        => |e| return e,
-        error.EndOfStream,
-        error.Overflow,
-        error.StreamTooLong,
-        error.InvalidOperand,
-        error.InvalidOpcode,
-        error.InvalidOperation,
-        => return error.InvalidDebugInfo,
-        error.UnsupportedAddrSize,
-        error.UnsupportedDwarfVersion,
-        error.UnimplementedUserOpcode,
-        => return error.UnsupportedDebugInfo,
-    };
-
-    const dwarf_cache = try gpa.create(UnwindContext.Cache);
-    errdefer gpa.destroy(dwarf_cache);
-    dwarf_cache.init();
-
-    out.unwind = .{
-        .vmaddr_slide = vmaddr_slide,
-        .unwind_info = opt_unwind_info,
-        .dwarf = dwarf,
-        .dwarf_cache = dwarf_cache,
-    };
-}
-fn loadMachO(module: *const DarwinModule, gpa: Allocator) !DebugInfo.LoadedMachO {
-    const all_mapped_memory = try mapDebugInfoFile(module.name);
-    errdefer posix.munmap(all_mapped_memory);
-
-    // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal
-    // binary": a simple file format which contains Mach-O binaries for multiple targets. For
-    // instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
-    // for both ARM64 Macs and x86_64 Macs.
-    if (all_mapped_memory.len < 4) return error.InvalidDebugInfo;
-    const magic = @as(*const u32, @ptrCast(all_mapped_memory.ptr)).*;
-    // The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
-    const mapped_macho = switch (magic) {
-        macho.MH_MAGIC_64 => all_mapped_memory,
-
-        macho.FAT_CIGAM => mapped_macho: {
-            // This is the universal binary format (aka a "fat binary"). Annoyingly, the whole thing
-            // is big-endian, so we'll be swapping some bytes.
-            if (all_mapped_memory.len < @sizeOf(macho.fat_header)) return error.InvalidDebugInfo;
-            const hdr: *const macho.fat_header = @ptrCast(all_mapped_memory.ptr);
-            const archs_ptr: [*]const macho.fat_arch = @ptrCast(all_mapped_memory.ptr + @sizeOf(macho.fat_header));
-            const archs: []const macho.fat_arch = archs_ptr[0..@byteSwap(hdr.nfat_arch)];
-            const native_cpu_type = switch (builtin.cpu.arch) {
-                .x86_64 => macho.CPU_TYPE_X86_64,
-                .aarch64 => macho.CPU_TYPE_ARM64,
-                else => comptime unreachable,
-            };
-            for (archs) |*arch| {
-                if (@byteSwap(arch.cputype) != native_cpu_type) continue;
-                const offset = @byteSwap(arch.offset);
-                const size = @byteSwap(arch.size);
-                break :mapped_macho all_mapped_memory[offset..][0..size];
-            }
-            // Our native architecture was not present in the fat binary.
-            return error.MissingDebugInfo;
-        },
-
-        // Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
-        // will be fairly easy to add support here if necessary; it's very similar to above.
-        macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
-
-        else => return error.InvalidDebugInfo,
-    };
-
-    const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_macho.ptr));
-    if (hdr.magic != macho.MH_MAGIC_64)
-        return error.InvalidDebugInfo;
-
-    const symtab: macho.symtab_command, const text_vmaddr: u64 = lc_iter: {
-        var it: macho.LoadCommandIterator = .{
-            .ncmds = hdr.ncmds,
-            .buffer = mapped_macho[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
-        };
-        var symtab: ?macho.symtab_command = null;
-        var text_vmaddr: ?u64 = null;
-        while (it.next()) |cmd| switch (cmd.cmd()) {
-            .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
-            .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
-                if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
-                text_vmaddr = seg_cmd.vmaddr;
-            },
-            else => {},
-        };
-        break :lc_iter .{
-            symtab orelse return error.MissingDebugInfo,
-            text_vmaddr orelse return error.MissingDebugInfo,
-        };
-    };
-
-    const syms_ptr: [*]align(1) const macho.nlist_64 = @ptrCast(mapped_macho[symtab.symoff..]);
-    const syms = syms_ptr[0..symtab.nsyms];
-    const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1];
-
-    var symbols: std.ArrayList(MachoSymbol) = try .initCapacity(gpa, syms.len);
-    defer symbols.deinit(gpa);
-
-    // This map is temporary; it is used only to detect duplicates here. This is
-    // necessary because we prefer to use STAB ("symbolic debugging table") symbols,
-    // but they might not be present, so we track normal symbols too.
-    // Indices match 1-1 with those of `symbols`.
-    var symbol_names: std.StringArrayHashMapUnmanaged(void) = .empty;
-    defer symbol_names.deinit(gpa);
-    try symbol_names.ensureUnusedCapacity(gpa, syms.len);
-
-    var ofile: u32 = undefined;
-    var last_sym: MachoSymbol = undefined;
-    var state: enum {
-        init,
-        oso_open,
-        oso_close,
-        bnsym,
-        fun_strx,
-        fun_size,
-        ensym,
-    } = .init;
-
-    for (syms) |*sym| {
-        if (sym.n_type.bits.is_stab == 0) {
-            if (sym.n_strx == 0) continue;
-            switch (sym.n_type.bits.type) {
-                .undf, .pbud, .indr, .abs, _ => continue,
-                .sect => {
-                    const name = std.mem.sliceTo(strings[sym.n_strx..], 0);
-                    const gop = symbol_names.getOrPutAssumeCapacity(name);
-                    if (!gop.found_existing) {
-                        assert(gop.index == symbols.items.len);
-                        symbols.appendAssumeCapacity(.{
-                            .strx = sym.n_strx,
-                            .addr = sym.n_value,
-                            .ofile = MachoSymbol.unknown_ofile,
-                        });
-                    }
-                },
-            }
-            continue;
-        }
-
-        // TODO handle globals N_GSYM, and statics N_STSYM
-        switch (sym.n_type.stab) {
-            .oso => switch (state) {
-                .init, .oso_close => {
-                    state = .oso_open;
-                    ofile = sym.n_strx;
-                },
-                else => return error.InvalidDebugInfo,
-            },
-            .bnsym => switch (state) {
-                .oso_open, .ensym => {
-                    state = .bnsym;
-                    last_sym = .{
-                        .strx = 0,
-                        .addr = sym.n_value,
-                        .ofile = ofile,
-                    };
-                },
-                else => return error.InvalidDebugInfo,
-            },
-            .fun => switch (state) {
-                .bnsym => {
-                    state = .fun_strx;
-                    last_sym.strx = sym.n_strx;
-                },
-                .fun_strx => {
-                    state = .fun_size;
-                },
-                else => return error.InvalidDebugInfo,
-            },
-            .ensym => switch (state) {
-                .fun_size => {
-                    state = .ensym;
-                    if (last_sym.strx != 0) {
-                        const name = std.mem.sliceTo(strings[last_sym.strx..], 0);
-                        const gop = symbol_names.getOrPutAssumeCapacity(name);
-                        if (!gop.found_existing) {
-                            assert(gop.index == symbols.items.len);
-                            symbols.appendAssumeCapacity(last_sym);
-                        } else {
-                            symbols.items[gop.index] = last_sym;
-                        }
-                    }
-                },
-                else => return error.InvalidDebugInfo,
-            },
-            .so => switch (state) {
-                .init, .oso_close => {},
-                .oso_open, .ensym => {
-                    state = .oso_close;
-                },
-                else => return error.InvalidDebugInfo,
-            },
-            else => {},
-        }
-    }
-
-    switch (state) {
-        .init => {
-            // Missing STAB symtab entries is still okay, unless there were also no normal symbols.
-            if (symbols.items.len == 0) return error.MissingDebugInfo;
-        },
-        .oso_close => {},
-        else => return error.InvalidDebugInfo, // corrupted STAB entries in symtab
-    }
-
-    const symbols_slice = try symbols.toOwnedSlice(gpa);
-    errdefer gpa.free(symbols_slice);
-
-    // Even though lld emits symbols in ascending order, this debug code
-    // should work for programs linked in any valid way.
-    // This sort is so that we can binary search later.
-    mem.sort(MachoSymbol, symbols_slice, {}, MachoSymbol.addressLessThan);
-
-    return .{
-        .mapped_memory = all_mapped_memory,
-        .symbols = symbols_slice,
-        .strings = strings,
-        .ofiles = .empty,
-        .vaddr_offset = module.text_base - text_vmaddr,
-    };
-}
-pub fn getSymbolAtAddress(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, address: usize) Error!std.debug.Symbol {
-    // We need the lock for a few things:
-    // * loading the Mach-O module
-    // * loading the referenced object file
-    // * scanning the DWARF of that object file
-    // * building the line number table of that object file
-    // That's enough that it doesn't really seem worth scoping the lock more tightly than the whole function..
-    di.mutex.lock();
-    defer di.mutex.unlock();
-
-    if (di.loaded_macho == null) di.loaded_macho = module.loadMachO(gpa) catch |err| switch (err) {
-        error.InvalidDebugInfo, error.MissingDebugInfo, error.OutOfMemory, error.Unexpected => |e| return e,
-        else => return error.ReadFailed,
-    };
-    const loaded_macho = &di.loaded_macho.?;
-
-    const vaddr = address - loaded_macho.vaddr_offset;
-    const symbol = MachoSymbol.find(loaded_macho.symbols, vaddr) orelse return .unknown;
-
-    // offset of `address` from start of `symbol`
-    const address_symbol_offset = vaddr - symbol.addr;
-
-    // Take the symbol name from the N_FUN STAB entry, we're going to
-    // use it if we fail to find the DWARF infos
-    const stab_symbol = mem.sliceTo(loaded_macho.strings[symbol.strx..], 0);
-
-    // If any information is missing, we can at least return this from now on.
-    const sym_only_result: std.debug.Symbol = .{
-        .name = stab_symbol,
-        .compile_unit_name = null,
-        .source_location = null,
-    };
-
-    if (symbol.ofile == MachoSymbol.unknown_ofile) {
-        // We don't have STAB info, so can't track down the object file; all we can do is the symbol name.
-        return sym_only_result;
-    }
-
-    const o_file: *DebugInfo.OFile = of: {
-        const gop = try loaded_macho.ofiles.getOrPut(gpa, symbol.ofile);
-        if (!gop.found_existing) {
-            const o_file_path = mem.sliceTo(loaded_macho.strings[symbol.ofile..], 0);
-            gop.value_ptr.* = DebugInfo.loadOFile(gpa, o_file_path) catch {
-                _ = loaded_macho.ofiles.pop().?;
-                return sym_only_result;
-            };
-        }
-        break :of gop.value_ptr;
-    };
-
-    const symbol_index = o_file.symbols_by_name.getKeyAdapted(
-        @as([]const u8, stab_symbol),
-        @as(DebugInfo.OFile.SymbolAdapter, .{ .strtab = o_file.strtab, .symtab = o_file.symtab }),
-    ) orelse return sym_only_result;
-    const symbol_ofile_vaddr = o_file.symtab[symbol_index].n_value;
-
-    const compile_unit = o_file.dwarf.findCompileUnit(native_endian, symbol_ofile_vaddr) catch return sym_only_result;
-
-    return .{
-        .name = o_file.dwarf.getSymbolName(symbol_ofile_vaddr + address_symbol_offset) orelse stab_symbol,
-        .compile_unit_name = compile_unit.die.getAttrString(
-            &o_file.dwarf,
-            native_endian,
-            std.dwarf.AT.name,
-            o_file.dwarf.section(.debug_str),
-            compile_unit,
-        ) catch |err| switch (err) {
-            error.MissingDebugInfo, error.InvalidDebugInfo => null,
-        },
-        .source_location = o_file.dwarf.getLineNumberInfo(
-            gpa,
-            native_endian,
-            compile_unit,
-            symbol_ofile_vaddr + address_symbol_offset,
-        ) catch null,
-    };
-}
-pub const supports_unwinding: bool = true;
-pub const UnwindContext = std.debug.SelfInfo.DwarfUnwindContext;
-/// Unwind a frame using MachO compact unwind info (from __unwind_info).
-/// If the compact encoding can't encode a way to unwind a frame, it will
-/// defer unwinding to DWARF, in which case `.eh_frame` will be used if available.
-pub fn unwindFrame(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) Error!usize {
-    return unwindFrameInner(module, gpa, di, context) catch |err| switch (err) {
-        error.InvalidDebugInfo,
-        error.MissingDebugInfo,
-        error.UnsupportedDebugInfo,
-        error.ReadFailed,
-        error.OutOfMemory,
-        error.Unexpected,
-        => |e| return e,
-        error.UnsupportedRegister,
-        => return error.UnsupportedDebugInfo,
-        error.InvalidRegister,
-        error.IncompatibleRegisterSize,
-        => return error.InvalidDebugInfo,
-    };
-}
-fn unwindFrameInner(module: *const DarwinModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) !usize {
-    const unwind: *DebugInfo.Unwind = u: {
-        di.mutex.lock();
-        defer di.mutex.unlock();
-        if (di.unwind == null) try module.loadUnwindInfo(gpa, di);
-        break :u &di.unwind.?;
-    };
-
-    const unwind_info = unwind.unwind_info orelse return error.MissingDebugInfo;
-    if (unwind_info.len < @sizeOf(macho.unwind_info_section_header)) return error.InvalidDebugInfo;
-    const header: *align(1) const macho.unwind_info_section_header = @ptrCast(unwind_info);
-
-    const index_byte_count = header.indexCount * @sizeOf(macho.unwind_info_section_header_index_entry);
-    if (unwind_info.len < header.indexSectionOffset + index_byte_count) return error.InvalidDebugInfo;
-    const indices: []align(1) const macho.unwind_info_section_header_index_entry = @ptrCast(unwind_info[header.indexSectionOffset..][0..index_byte_count]);
-    if (indices.len == 0) return error.MissingDebugInfo;
-
-    // offset of the PC into the `__TEXT` segment
-    const pc_text_offset = context.pc - module.text_base;
-
-    const start_offset: u32, const first_level_offset: u32 = index: {
-        var left: usize = 0;
-        var len: usize = indices.len;
-        while (len > 1) {
-            const mid = left + len / 2;
-            if (pc_text_offset < indices[mid].functionOffset) {
-                len /= 2;
-            } else {
-                left = mid;
-                len -= len / 2;
-            }
-        }
-        break :index .{ indices[left].secondLevelPagesSectionOffset, indices[left].functionOffset };
-    };
-    // An offset of 0 is a sentinel indicating a range does not have unwind info.
-    if (start_offset == 0) return error.MissingDebugInfo;
-
-    const common_encodings_byte_count = header.commonEncodingsArrayCount * @sizeOf(macho.compact_unwind_encoding_t);
-    if (unwind_info.len < header.commonEncodingsArraySectionOffset + common_encodings_byte_count) return error.InvalidDebugInfo;
-    const common_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
-        unwind_info[header.commonEncodingsArraySectionOffset..][0..common_encodings_byte_count],
-    );
-
-    if (unwind_info.len < start_offset + @sizeOf(macho.UNWIND_SECOND_LEVEL)) return error.InvalidDebugInfo;
-    const kind: *align(1) const macho.UNWIND_SECOND_LEVEL = @ptrCast(unwind_info[start_offset..]);
-
-    const entry: struct {
-        function_offset: usize,
-        raw_encoding: u32,
-    } = switch (kind.*) {
-        .REGULAR => entry: {
-            if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_regular_second_level_page_header)) return error.InvalidDebugInfo;
-            const page_header: *align(1) const macho.unwind_info_regular_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
-
-            const entries_byte_count = page_header.entryCount * @sizeOf(macho.unwind_info_regular_second_level_entry);
-            if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
-            const entries: []align(1) const macho.unwind_info_regular_second_level_entry = @ptrCast(
-                unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
-            );
-            if (entries.len == 0) return error.InvalidDebugInfo;
-
-            var left: usize = 0;
-            var len: usize = entries.len;
-            while (len > 1) {
-                const mid = left + len / 2;
-                if (pc_text_offset < entries[mid].functionOffset) {
-                    len /= 2;
-                } else {
-                    left = mid;
-                    len -= len / 2;
-                }
-            }
-            break :entry .{
-                .function_offset = entries[left].functionOffset,
-                .raw_encoding = entries[left].encoding,
-            };
-        },
-        .COMPRESSED => entry: {
-            if (unwind_info.len < start_offset + @sizeOf(macho.unwind_info_compressed_second_level_page_header)) return error.InvalidDebugInfo;
-            const page_header: *align(1) const macho.unwind_info_compressed_second_level_page_header = @ptrCast(unwind_info[start_offset..]);
-
-            const entries_byte_count = page_header.entryCount * @sizeOf(macho.UnwindInfoCompressedEntry);
-            if (unwind_info.len < start_offset + entries_byte_count) return error.InvalidDebugInfo;
-            const entries: []align(1) const macho.UnwindInfoCompressedEntry = @ptrCast(
-                unwind_info[start_offset + page_header.entryPageOffset ..][0..entries_byte_count],
-            );
-            if (entries.len == 0) return error.InvalidDebugInfo;
-
-            var left: usize = 0;
-            var len: usize = entries.len;
-            while (len > 1) {
-                const mid = left + len / 2;
-                if (pc_text_offset < first_level_offset + entries[mid].funcOffset) {
-                    len /= 2;
-                } else {
-                    left = mid;
-                    len -= len / 2;
-                }
-            }
-            const entry = entries[left];
-
-            const function_offset = first_level_offset + entry.funcOffset;
-            if (entry.encodingIndex < common_encodings.len) {
-                break :entry .{
-                    .function_offset = function_offset,
-                    .raw_encoding = common_encodings[entry.encodingIndex],
-                };
-            }
-
-            const local_index = entry.encodingIndex - common_encodings.len;
-            const local_encodings_byte_count = page_header.encodingsCount * @sizeOf(macho.compact_unwind_encoding_t);
-            if (unwind_info.len < start_offset + page_header.encodingsPageOffset + local_encodings_byte_count) return error.InvalidDebugInfo;
-            const local_encodings: []align(1) const macho.compact_unwind_encoding_t = @ptrCast(
-                unwind_info[start_offset + page_header.encodingsPageOffset ..][0..local_encodings_byte_count],
-            );
-            if (local_index >= local_encodings.len) return error.InvalidDebugInfo;
-            break :entry .{
-                .function_offset = function_offset,
-                .raw_encoding = local_encodings[local_index],
-            };
-        },
-        else => return error.InvalidDebugInfo,
-    };
-
-    if (entry.raw_encoding == 0) return error.MissingDebugInfo;
-
-    const encoding: macho.CompactUnwindEncoding = @bitCast(entry.raw_encoding);
-    const new_ip = switch (builtin.cpu.arch) {
-        .x86_64 => switch (encoding.mode.x86_64) {
-            .OLD => return error.UnsupportedDebugInfo,
-            .RBP_FRAME => ip: {
-                const frame = encoding.value.x86_64.frame;
-
-                const fp = (try dwarfRegNative(&context.cpu_context, fp_reg_num)).*;
-                const new_sp = fp + 2 * @sizeOf(usize);
-
-                const ip_ptr = fp + @sizeOf(usize);
-                const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
-                const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
-
-                (try dwarfRegNative(&context.cpu_context, fp_reg_num)).* = new_fp;
-                (try dwarfRegNative(&context.cpu_context, sp_reg_num)).* = new_sp;
-                (try dwarfRegNative(&context.cpu_context, ip_reg_num)).* = new_ip;
-
-                const regs: [5]u3 = .{
-                    frame.reg0,
-                    frame.reg1,
-                    frame.reg2,
-                    frame.reg3,
-                    frame.reg4,
-                };
-                for (regs, 0..) |reg, i| {
-                    if (reg == 0) continue;
-                    const addr = fp - frame.frame_offset * @sizeOf(usize) + i * @sizeOf(usize);
-                    const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(reg);
-                    (try dwarfRegNative(&context.cpu_context, reg_number)).* = @as(*const usize, @ptrFromInt(addr)).*;
-                }
-
-                break :ip new_ip;
-            },
-            .STACK_IMMD,
-            .STACK_IND,
-            => ip: {
-                const frameless = encoding.value.x86_64.frameless;
-
-                const sp = (try dwarfRegNative(&context.cpu_context, sp_reg_num)).*;
-                const stack_size: usize = stack_size: {
-                    if (encoding.mode.x86_64 == .STACK_IMMD) {
-                        break :stack_size @as(usize, frameless.stack.direct.stack_size) * @sizeOf(usize);
-                    }
-                    // In .STACK_IND, the stack size is inferred from the subq instruction at the beginning of the function.
-                    const sub_offset_addr =
-                        module.text_base +
-                        entry.function_offset +
-                        frameless.stack.indirect.sub_offset;
-                    // `sub_offset_addr` points to the offset of the literal within the instruction
-                    const sub_operand = @as(*align(1) const u32, @ptrFromInt(sub_offset_addr)).*;
-                    break :stack_size sub_operand + @sizeOf(usize) * @as(usize, frameless.stack.indirect.stack_adjust);
-                };
-
-                // Decode the Lehmer-coded sequence of registers.
-                // For a description of the encoding see lib/libc/include/any-macos.13-any/mach-o/compact_unwind_encoding.h
-
-                // Decode the variable-based permutation number into its digits. Each digit represents
-                // an index into the list of register numbers that weren't yet used in the sequence at
-                // the time the digit was added.
-                const reg_count = frameless.stack_reg_count;
-                const ip_ptr = ip_ptr: {
-                    var digits: [6]u3 = undefined;
-                    var accumulator: usize = frameless.stack_reg_permutation;
-                    var base: usize = 2;
-                    for (0..reg_count) |i| {
-                        const div = accumulator / base;
-                        digits[digits.len - 1 - i] = @intCast(accumulator - base * div);
-                        accumulator = div;
-                        base += 1;
-                    }
-
-                    var registers: [6]u3 = undefined;
-                    var used_indices: [6]bool = @splat(false);
-                    for (digits[digits.len - reg_count ..], 0..) |target_unused_index, i| {
-                        var unused_count: u8 = 0;
-                        const unused_index = for (used_indices, 0..) |used, index| {
-                            if (!used) {
-                                if (target_unused_index == unused_count) break index;
-                                unused_count += 1;
-                            }
-                        } else unreachable;
-                        registers[i] = @intCast(unused_index + 1);
-                        used_indices[unused_index] = true;
-                    }
-
-                    var reg_addr = sp + stack_size - @sizeOf(usize) * @as(usize, reg_count + 1);
-                    for (0..reg_count) |i| {
-                        const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(registers[i]);
-                        (try dwarfRegNative(&context.cpu_context, reg_number)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
-                        reg_addr += @sizeOf(usize);
-                    }
-
-                    break :ip_ptr reg_addr;
-                };
-
-                const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
-                const new_sp = ip_ptr + @sizeOf(usize);
-
-                (try dwarfRegNative(&context.cpu_context, sp_reg_num)).* = new_sp;
-                (try dwarfRegNative(&context.cpu_context, ip_reg_num)).* = new_ip;
-
-                break :ip new_ip;
-            },
-            .DWARF => {
-                const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
-                return context.unwindFrame(unwind.dwarf_cache, gpa, dwarf, unwind.vmaddr_slide, encoding.value.x86_64.dwarf);
-            },
-        },
-        .aarch64, .aarch64_be => switch (encoding.mode.arm64) {
-            .OLD => return error.UnsupportedDebugInfo,
-            .FRAMELESS => ip: {
-                const sp = (try dwarfRegNative(&context.cpu_context, sp_reg_num)).*;
-                const new_sp = sp + encoding.value.arm64.frameless.stack_size * 16;
-                const new_ip = (try dwarfRegNative(&context.cpu_context, 30)).*;
-                (try dwarfRegNative(&context.cpu_context, sp_reg_num)).* = new_sp;
-                break :ip new_ip;
-            },
-            .DWARF => {
-                const dwarf = &(unwind.dwarf orelse return error.MissingDebugInfo);
-                return context.unwindFrame(unwind.dwarf_cache, gpa, dwarf, unwind.vmaddr_slide, encoding.value.arm64.dwarf);
-            },
-            .FRAME => ip: {
-                const frame = encoding.value.arm64.frame;
-
-                const fp = (try dwarfRegNative(&context.cpu_context, fp_reg_num)).*;
-                const ip_ptr = fp + @sizeOf(usize);
-
-                var reg_addr = fp - @sizeOf(usize);
-                inline for (@typeInfo(@TypeOf(frame.x_reg_pairs)).@"struct".fields, 0..) |field, i| {
-                    if (@field(frame.x_reg_pairs, field.name) != 0) {
-                        (try dwarfRegNative(&context.cpu_context, 19 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
-                        reg_addr += @sizeOf(usize);
-                        (try dwarfRegNative(&context.cpu_context, 20 + i)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
-                        reg_addr += @sizeOf(usize);
-                    }
-                }
-
-                inline for (@typeInfo(@TypeOf(frame.d_reg_pairs)).@"struct".fields, 0..) |field, i| {
-                    if (@field(frame.d_reg_pairs, field.name) != 0) {
-                        // Only the lower half of the 128-bit V registers are restored during unwinding
-                        {
-                            const dest: *align(1) usize = @ptrCast(try context.cpu_context.dwarfRegisterBytes(64 + 8 + i));
-                            dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
-                        }
-                        reg_addr += @sizeOf(usize);
-                        {
-                            const dest: *align(1) usize = @ptrCast(try context.cpu_context.dwarfRegisterBytes(64 + 9 + i));
-                            dest.* = @as(*const usize, @ptrFromInt(reg_addr)).*;
-                        }
-                        reg_addr += @sizeOf(usize);
-                    }
-                }
-
-                const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
-                const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
-
-                (try dwarfRegNative(&context.cpu_context, fp_reg_num)).* = new_fp;
-                (try dwarfRegNative(&context.cpu_context, ip_reg_num)).* = new_ip;
-
-                break :ip new_ip;
-            },
-        },
-        else => comptime unreachable, // unimplemented
-    };
-
-    const ret_addr = std.debug.stripInstructionPtrAuthCode(new_ip);
-
-    // Like `DwarfUnwindContext.unwindFrame`, adjust our next lookup pc in case the `call` was this
-    // function's last instruction making `ret_addr` one byte past its end.
-    context.pc = ret_addr -| 1;
-
-    return ret_addr;
-}
-pub const DebugInfo = struct {
-    /// Held while checking and/or populating `unwind` or `loaded_macho`.
-    /// Once a field is populated and the pointer `&di.loaded_macho.?` or `&di.unwind.?` has been
-    /// gotten, the lock is released; i.e. it is not held while *using* the loaded info.
-    mutex: std.Thread.Mutex,
-
-    unwind: ?Unwind,
-    loaded_macho: ?LoadedMachO,
-
-    pub const init: DebugInfo = .{
-        .mutex = .{},
-
-        .unwind = null,
-        .loaded_macho = null,
-    };
-
-    pub fn deinit(di: *DebugInfo, gpa: Allocator) void {
-        if (di.loaded_macho) |*loaded_macho| {
-            for (loaded_macho.ofiles.values()) |*ofile| {
-                ofile.dwarf.deinit(gpa);
-                ofile.symbols_by_name.deinit(gpa);
-                posix.munmap(ofile.mapped_memory);
-            }
-            loaded_macho.ofiles.deinit(gpa);
-            gpa.free(loaded_macho.symbols);
-            posix.munmap(loaded_macho.mapped_memory);
-        }
-    }
-
-    const Unwind = struct {
-        /// The slide applied to the `__unwind_info` and `__eh_frame` sections.
-        /// So, `unwind_info.ptr` is this many bytes higher than the section's vmaddr.
-        vmaddr_slide: u64,
-        /// Backed by the in-memory section mapped by the loader.
-        unwind_info: ?[]const u8,
-        /// Backed by the in-memory `__eh_frame` section mapped by the loader.
-        dwarf: ?Dwarf.Unwind,
-        /// This is `undefined` if `dwarf == null`.
-        dwarf_cache: *UnwindContext.Cache,
-    };
-
-    const LoadedMachO = struct {
-        mapped_memory: []align(std.heap.page_size_min) const u8,
-        symbols: []const MachoSymbol,
-        strings: []const u8,
-        /// Key is index into `strings` of the file path.
-        ofiles: std.AutoArrayHashMapUnmanaged(u32, OFile),
-        /// This is not necessarily the same as the vmaddr_slide that dyld would report. This is
-        /// because the segments in the file on disk might differ from the ones in memory. Normally
-        /// we wouldn't necessarily expect that to work, but /usr/lib/dyld is incredibly annoying:
-        /// it exists on disk (necessarily, because the kernel needs to load it!), but is also in
-        /// the dyld cache (dyld actually restart itself from cache after loading it), and the two
-        /// versions have (very) different segment base addresses. It's sort of like a large slide
-        /// has been applied to all addresses in memory. For an optimal experience, we consider the
-        /// on-disk vmaddr instead of the in-memory one.
-        vaddr_offset: usize,
-    };
-
-    const OFile = struct {
-        mapped_memory: []align(std.heap.page_size_min) const u8,
-        dwarf: Dwarf,
-        strtab: []const u8,
-        symtab: []align(1) const macho.nlist_64,
-        /// All named symbols in `symtab`. Stored `u32` key is the index into `symtab`. Accessed
-        /// through `SymbolAdapter`, so that the symbol name is used as the logical key.
-        symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true),
-
-        const SymbolAdapter = struct {
-            strtab: []const u8,
-            symtab: []align(1) const macho.nlist_64,
-            pub fn hash(ctx: SymbolAdapter, sym_name: []const u8) u32 {
-                _ = ctx;
-                return @truncate(std.hash.Wyhash.hash(0, sym_name));
-            }
-            pub fn eql(ctx: SymbolAdapter, a_sym_name: []const u8, b_sym_index: u32, b_index: usize) bool {
-                _ = b_index;
-                const b_sym = ctx.symtab[b_sym_index];
-                const b_sym_name = std.mem.sliceTo(ctx.strtab[b_sym.n_strx..], 0);
-                return mem.eql(u8, a_sym_name, b_sym_name);
-            }
-        };
-    };
-
-    fn loadOFile(gpa: Allocator, o_file_path: []const u8) !OFile {
-        const mapped_mem = try mapDebugInfoFile(o_file_path);
-        errdefer posix.munmap(mapped_mem);
-
-        if (mapped_mem.len < @sizeOf(macho.mach_header_64)) return error.InvalidDebugInfo;
-        const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_mem.ptr));
-        if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidDebugInfo;
-
-        const seg_cmd: macho.LoadCommandIterator.LoadCommand, const symtab_cmd: macho.symtab_command = cmds: {
-            var seg_cmd: ?macho.LoadCommandIterator.LoadCommand = null;
-            var symtab_cmd: ?macho.symtab_command = null;
-            var it: macho.LoadCommandIterator = .{
-                .ncmds = hdr.ncmds,
-                .buffer = mapped_mem[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
-            };
-            while (it.next()) |cmd| switch (cmd.cmd()) {
-                .SEGMENT_64 => seg_cmd = cmd,
-                .SYMTAB => symtab_cmd = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
-                else => {},
-            };
-            break :cmds .{
-                seg_cmd orelse return error.MissingDebugInfo,
-                symtab_cmd orelse return error.MissingDebugInfo,
-            };
-        };
-
-        if (mapped_mem.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidDebugInfo;
-        if (mapped_mem[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidDebugInfo;
-        const strtab = mapped_mem[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1];
-
-        const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64);
-        if (mapped_mem.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidDebugInfo;
-        const symtab: []align(1) const macho.nlist_64 = @ptrCast(mapped_mem[symtab_cmd.symoff..][0..n_sym_bytes]);
-
-        // TODO handle tentative (common) symbols
-        var symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true) = .empty;
-        defer symbols_by_name.deinit(gpa);
-        try symbols_by_name.ensureUnusedCapacity(gpa, @intCast(symtab.len));
-        for (symtab, 0..) |sym, sym_index| {
-            if (sym.n_strx == 0) continue;
-            switch (sym.n_type.bits.type) {
-                .undf => continue, // includes tentative symbols
-                .abs => continue,
-                else => {},
-            }
-            const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0);
-            const gop = symbols_by_name.getOrPutAssumeCapacityAdapted(
-                @as([]const u8, sym_name),
-                @as(DebugInfo.OFile.SymbolAdapter, .{ .strtab = strtab, .symtab = symtab }),
-            );
-            if (gop.found_existing) return error.InvalidDebugInfo;
-            gop.key_ptr.* = @intCast(sym_index);
-        }
-
-        var sections: Dwarf.SectionArray = @splat(null);
-        for (seg_cmd.getSections()) |sect| {
-            if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue;
-
-            const section_index: usize = inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
-                if (mem.eql(u8, "__" ++ section.name, sect.sectName())) break i;
-            } else continue;
-
-            if (mapped_mem.len < sect.offset + sect.size) return error.InvalidDebugInfo;
-            const section_bytes = mapped_mem[sect.offset..][0..sect.size];
-            sections[section_index] = .{
-                .data = section_bytes,
-                .owned = false,
-            };
-        }
-
-        const missing_debug_info =
-            sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
-            sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
-            sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
-            sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
-        if (missing_debug_info) return error.MissingDebugInfo;
-
-        var dwarf: Dwarf = .{ .sections = sections };
-        errdefer dwarf.deinit(gpa);
-        try dwarf.open(gpa, native_endian);
-
-        return .{
-            .mapped_memory = mapped_mem,
-            .dwarf = dwarf,
-            .strtab = strtab,
-            .symtab = symtab,
-            .symbols_by_name = symbols_by_name.move(),
-        };
-    }
-};
-
-const MachoSymbol = struct {
-    strx: u32,
-    addr: u64,
-    /// Value may be `unknown_ofile`.
-    ofile: u32,
-    const unknown_ofile = std.math.maxInt(u32);
-    fn addressLessThan(context: void, lhs: MachoSymbol, rhs: MachoSymbol) bool {
-        _ = context;
-        return lhs.addr < rhs.addr;
-    }
-    /// Assumes that `symbols` is sorted in order of ascending `addr`.
-    fn find(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
-        if (symbols.len == 0) return null; // no potential match
-        if (address < symbols[0].addr) return null; // address is before the lowest-address symbol
-        var left: usize = 0;
-        var len: usize = symbols.len;
-        while (len > 1) {
-            const mid = left + len / 2;
-            if (address < symbols[mid].addr) {
-                len /= 2;
-            } else {
-                left = mid;
-                len -= len / 2;
-            }
-        }
-        return &symbols[left];
-    }
-
-    test find {
-        const symbols: []const MachoSymbol = &.{
-            .{ .addr = 100, .strx = undefined, .ofile = undefined },
-            .{ .addr = 200, .strx = undefined, .ofile = undefined },
-            .{ .addr = 300, .strx = undefined, .ofile = undefined },
-        };
-
-        try testing.expectEqual(null, find(symbols, 0));
-        try testing.expectEqual(null, find(symbols, 99));
-        try testing.expectEqual(&symbols[0], find(symbols, 100).?);
-        try testing.expectEqual(&symbols[0], find(symbols, 150).?);
-        try testing.expectEqual(&symbols[0], find(symbols, 199).?);
-
-        try testing.expectEqual(&symbols[1], find(symbols, 200).?);
-        try testing.expectEqual(&symbols[1], find(symbols, 250).?);
-        try testing.expectEqual(&symbols[1], find(symbols, 299).?);
-
-        try testing.expectEqual(&symbols[2], find(symbols, 300).?);
-        try testing.expectEqual(&symbols[2], find(symbols, 301).?);
-        try testing.expectEqual(&symbols[2], find(symbols, 5000).?);
-    }
-};
-test {
-    _ = MachoSymbol;
-}
-
-const ip_reg_num = Dwarf.ipRegNum(builtin.target.cpu.arch).?;
-const fp_reg_num = Dwarf.fpRegNum(builtin.target.cpu.arch);
-const sp_reg_num = Dwarf.spRegNum(builtin.target.cpu.arch);
-
-/// Uses `mmap` to map the file at `path` into memory.
-fn mapDebugInfoFile(path: []const u8) ![]align(std.heap.page_size_min) const u8 {
-    const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
-        error.FileNotFound => return error.MissingDebugInfo,
-        else => return error.ReadFailed,
-    };
-    defer file.close();
-
-    const file_len = std.math.cast(usize, try file.getEndPos()) orelse return error.InvalidDebugInfo;
-
-    return posix.mmap(
-        null,
-        file_len,
-        posix.PROT.READ,
-        .{ .TYPE = .SHARED },
-        file.handle,
-        0,
-    );
-}
-
-const DarwinModule = @This();
-
-const std = @import("../../std.zig");
-const Allocator = std.mem.Allocator;
-const Dwarf = std.debug.Dwarf;
-const assert = std.debug.assert;
-const macho = std.macho;
-const mem = std.mem;
-const posix = std.posix;
-const testing = std.testing;
-const Error = std.debug.SelfInfo.Error;
-const dwarfRegNative = std.debug.SelfInfo.DwarfUnwindContext.regNative;
-
-const builtin = @import("builtin");
-const native_endian = builtin.target.cpu.arch.endian();
lib/std/debug/SelfInfo/Elf.zig
@@ -0,0 +1,427 @@
+rwlock: std.Thread.RwLock,
+
+modules: std.ArrayList(Module),
+ranges: std.ArrayList(Module.Range),
+
+unwind_cache: if (can_unwind) ?[]Dwarf.SelfUnwinder.CacheEntry else ?noreturn,
+
+pub const init: SelfInfo = .{
+    .rwlock = .{},
+    .modules = .empty,
+    .ranges = .empty,
+    .unwind_cache = null,
+};
+pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
+    for (si.modules.items) |*mod| {
+        unwind: {
+            const u = &(mod.unwind orelse break :unwind catch break :unwind);
+            for (u.buf[0..u.len]) |*unwind| unwind.deinit(gpa);
+        }
+        loaded: {
+            const l = &(mod.loaded_elf orelse break :loaded catch break :loaded);
+            l.file.deinit(gpa);
+        }
+    }
+
+    si.modules.deinit(gpa);
+    si.ranges.deinit(gpa);
+    if (si.unwind_cache) |cache| gpa.free(cache);
+}
+
+pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol {
+    const module = try si.findModule(gpa, address, .exclusive);
+    defer si.rwlock.unlock();
+
+    const vaddr = address - module.load_offset;
+
+    const loaded_elf = try module.getLoadedElf(gpa);
+    if (loaded_elf.file.dwarf) |*dwarf| {
+        if (!loaded_elf.scanned_dwarf) {
+            dwarf.open(gpa, native_endian) catch |err| switch (err) {
+                error.InvalidDebugInfo,
+                error.MissingDebugInfo,
+                error.OutOfMemory,
+                => |e| return e,
+                error.EndOfStream,
+                error.Overflow,
+                error.ReadFailed,
+                error.StreamTooLong,
+                => return error.InvalidDebugInfo,
+            };
+            loaded_elf.scanned_dwarf = true;
+        }
+        if (dwarf.getSymbol(gpa, native_endian, vaddr)) |sym| {
+            return sym;
+        } else |err| switch (err) {
+            error.MissingDebugInfo => {},
+
+            error.InvalidDebugInfo,
+            error.OutOfMemory,
+            => |e| return e,
+
+            error.ReadFailed,
+            error.EndOfStream,
+            error.Overflow,
+            error.StreamTooLong,
+            => return error.InvalidDebugInfo,
+        }
+    }
+    // When DWARF is unavailable, fall back to searching the symtab.
+    return loaded_elf.file.searchSymtab(gpa, vaddr) catch |err| switch (err) {
+        error.NoSymtab, error.NoStrtab => return error.MissingDebugInfo,
+        error.BadSymtab => return error.InvalidDebugInfo,
+        error.OutOfMemory => |e| return e,
+    };
+}
+pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
+    const module = try si.findModule(gpa, address, .shared);
+    defer si.rwlock.unlockShared();
+    if (module.name.len == 0) return error.MissingDebugInfo;
+    return module.name;
+}
+
+pub const can_unwind: bool = s: {
+    // Notably, we are yet to support unwinding on ARM. There, unwinding is not done through
+    // `.eh_frame`, but instead with the `.ARM.exidx` section, which has a different format.
+    const archs: []const std.Target.Cpu.Arch = switch (builtin.target.os.tag) {
+        .linux => &.{ .x86, .x86_64, .aarch64, .aarch64_be },
+        .netbsd => &.{ .x86, .x86_64, .aarch64, .aarch64_be },
+        .freebsd => &.{ .x86_64, .aarch64, .aarch64_be },
+        .openbsd => &.{.x86_64},
+        .solaris => &.{ .x86, .x86_64 },
+        .illumos => &.{ .x86, .x86_64 },
+        else => unreachable,
+    };
+    for (archs) |a| {
+        if (builtin.target.cpu.arch == a) break :s true;
+    }
+    break :s false;
+};
+comptime {
+    if (can_unwind) {
+        std.debug.assert(Dwarf.supportsUnwinding(&builtin.target));
+    }
+}
+pub const UnwindContext = Dwarf.SelfUnwinder;
+pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize {
+    comptime assert(can_unwind);
+
+    {
+        si.rwlock.lockShared();
+        defer si.rwlock.unlockShared();
+        if (si.unwind_cache) |cache| {
+            if (Dwarf.SelfUnwinder.CacheEntry.find(cache, context.pc)) |entry| {
+                return context.next(gpa, entry);
+            }
+        }
+    }
+
+    const module = try si.findModule(gpa, context.pc, .exclusive);
+    defer si.rwlock.unlock();
+
+    if (si.unwind_cache == null) {
+        si.unwind_cache = try gpa.alloc(Dwarf.SelfUnwinder.CacheEntry, 2048);
+        @memset(si.unwind_cache.?, .empty);
+    }
+
+    const unwind_sections = try module.getUnwindSections(gpa);
+    for (unwind_sections) |*unwind| {
+        if (context.computeRules(gpa, unwind, module.load_offset, null)) |entry| {
+            entry.populate(si.unwind_cache.?);
+            return context.next(gpa, &entry);
+        } else |err| switch (err) {
+            error.MissingDebugInfo => continue,
+
+            error.InvalidDebugInfo,
+            error.UnsupportedDebugInfo,
+            error.OutOfMemory,
+            => |e| return e,
+
+            error.EndOfStream,
+            error.StreamTooLong,
+            error.ReadFailed,
+            error.Overflow,
+            error.InvalidOpcode,
+            error.InvalidOperation,
+            error.InvalidOperand,
+            => return error.InvalidDebugInfo,
+
+            error.UnimplementedUserOpcode,
+            error.UnsupportedAddrSize,
+            => return error.UnsupportedDebugInfo,
+        }
+    }
+    return error.MissingDebugInfo;
+}
+
+const Module = struct {
+    load_offset: usize,
+    name: []const u8,
+    build_id: ?[]const u8,
+    gnu_eh_frame: ?[]const u8,
+
+    /// `null` means unwind information has not yet been loaded.
+    unwind: ?(Error!UnwindSections),
+
+    /// `null` means the ELF file has not yet been loaded.
+    loaded_elf: ?(Error!LoadedElf),
+
+    const LoadedElf = struct {
+        file: std.debug.ElfFile,
+        scanned_dwarf: bool,
+    };
+
+    const UnwindSections = struct {
+        buf: [2]Dwarf.Unwind,
+        len: usize,
+    };
+
+    const Range = struct {
+        start: usize,
+        len: usize,
+        /// Index into `modules`
+        module_index: usize,
+    };
+
+    /// Assumes we already hold an exclusive lock.
+    fn getUnwindSections(mod: *Module, gpa: Allocator) Error![]Dwarf.Unwind {
+        if (mod.unwind == null) mod.unwind = loadUnwindSections(mod, gpa);
+        const us = &(mod.unwind.? catch |err| return err);
+        return us.buf[0..us.len];
+    }
+    fn loadUnwindSections(mod: *Module, gpa: Allocator) Error!UnwindSections {
+        var us: UnwindSections = .{
+            .buf = undefined,
+            .len = 0,
+        };
+        if (mod.gnu_eh_frame) |section_bytes| {
+            const section_vaddr: u64 = @intFromPtr(section_bytes.ptr) - mod.load_offset;
+            const header = Dwarf.Unwind.EhFrameHeader.parse(section_vaddr, section_bytes, @sizeOf(usize), native_endian) catch |err| switch (err) {
+                error.ReadFailed => unreachable, // it's all fixed buffers
+                error.InvalidDebugInfo => |e| return e,
+                error.EndOfStream, error.Overflow => return error.InvalidDebugInfo,
+                error.UnsupportedAddrSize => return error.UnsupportedDebugInfo,
+            };
+            us.buf[us.len] = .initEhFrameHdr(header, section_vaddr, @ptrFromInt(@as(usize, @intCast(mod.load_offset + header.eh_frame_vaddr))));
+            us.len += 1;
+        } else {
+            // There is no `.eh_frame_hdr` section. There may still be an `.eh_frame` or `.debug_frame`
+            // section, but we'll have to load the binary to get at it.
+            const loaded = try mod.getLoadedElf(gpa);
+            // If both are present, we can't just pick one -- the info could be split between them.
+            // `.debug_frame` is likely to be the more complete section, so we'll prioritize that one.
+            if (loaded.file.debug_frame) |*debug_frame| {
+                us.buf[us.len] = .initSection(.debug_frame, debug_frame.vaddr, debug_frame.bytes);
+                us.len += 1;
+            }
+            if (loaded.file.eh_frame) |*eh_frame| {
+                us.buf[us.len] = .initSection(.eh_frame, eh_frame.vaddr, eh_frame.bytes);
+                us.len += 1;
+            }
+        }
+        errdefer for (us.buf[0..us.len]) |*u| u.deinit(gpa);
+        for (us.buf[0..us.len]) |*u| u.prepare(gpa, @sizeOf(usize), native_endian, true, false) catch |err| switch (err) {
+            error.ReadFailed => unreachable, // it's all fixed buffers
+            error.InvalidDebugInfo,
+            error.MissingDebugInfo,
+            error.OutOfMemory,
+            => |e| return e,
+            error.EndOfStream,
+            error.Overflow,
+            error.StreamTooLong,
+            error.InvalidOperand,
+            error.InvalidOpcode,
+            error.InvalidOperation,
+            => return error.InvalidDebugInfo,
+            error.UnsupportedAddrSize,
+            error.UnsupportedDwarfVersion,
+            error.UnimplementedUserOpcode,
+            => return error.UnsupportedDebugInfo,
+        };
+        return us;
+    }
+
+    /// Assumes we already hold an exclusive lock.
+    fn getLoadedElf(mod: *Module, gpa: Allocator) Error!*LoadedElf {
+        if (mod.loaded_elf == null) mod.loaded_elf = loadElf(mod, gpa);
+        return if (mod.loaded_elf.?) |*elf| elf else |err| err;
+    }
+    fn loadElf(mod: *Module, gpa: Allocator) Error!LoadedElf {
+        const load_result = if (mod.name.len > 0) res: {
+            var file = std.fs.cwd().openFile(mod.name, .{}) catch return error.MissingDebugInfo;
+            defer file.close();
+            break :res std.debug.ElfFile.load(gpa, file, mod.build_id, &.native(mod.name));
+        } else res: {
+            const path = std.fs.selfExePathAlloc(gpa) catch |err| switch (err) {
+                error.OutOfMemory => |e| return e,
+                else => return error.ReadFailed,
+            };
+            defer gpa.free(path);
+            var file = std.fs.cwd().openFile(path, .{}) catch return error.MissingDebugInfo;
+            defer file.close();
+            break :res std.debug.ElfFile.load(gpa, file, mod.build_id, &.native(path));
+        };
+
+        var elf_file = load_result catch |err| switch (err) {
+            error.OutOfMemory,
+            error.Unexpected,
+            => |e| return e,
+
+            error.Overflow,
+            error.TruncatedElfFile,
+            error.InvalidCompressedSection,
+            error.InvalidElfMagic,
+            error.InvalidElfVersion,
+            error.InvalidElfClass,
+            error.InvalidElfEndian,
+            => return error.InvalidDebugInfo,
+
+            error.SystemResources,
+            error.MemoryMappingNotSupported,
+            error.AccessDenied,
+            error.LockedMemoryLimitExceeded,
+            error.ProcessFdQuotaExceeded,
+            error.SystemFdQuotaExceeded,
+            => return error.ReadFailed,
+        };
+        errdefer elf_file.deinit(gpa);
+
+        if (elf_file.endian != native_endian) return error.InvalidDebugInfo;
+        if (elf_file.is_64 != (@sizeOf(usize) == 8)) return error.InvalidDebugInfo;
+
+        return .{
+            .file = elf_file,
+            .scanned_dwarf = false,
+        };
+    }
+};
+
+fn findModule(si: *SelfInfo, gpa: Allocator, address: usize, lock: enum { shared, exclusive }) Error!*Module {
+    // With the requested lock, scan the module ranges looking for `address`.
+    switch (lock) {
+        .shared => si.rwlock.lockShared(),
+        .exclusive => si.rwlock.lock(),
+    }
+    for (si.ranges.items) |*range| {
+        if (address >= range.start and address < range.start + range.len) {
+            return &si.modules.items[range.module_index];
+        }
+    }
+    // The address wasn't in a known range. We will rebuild the module/range lists, since it's possible
+    // a new module was loaded. Upgrade to an exclusive lock if necessary.
+    switch (lock) {
+        .shared => {
+            si.rwlock.unlockShared();
+            si.rwlock.lock();
+        },
+        .exclusive => {},
+    }
+    // Rebuild module list with the exclusive lock.
+    {
+        errdefer si.rwlock.unlock();
+        for (si.modules.items) |*mod| {
+            unwind: {
+                const u = &(mod.unwind orelse break :unwind catch break :unwind);
+                for (u.buf[0..u.len]) |*unwind| unwind.deinit(gpa);
+            }
+            loaded: {
+                const l = &(mod.loaded_elf orelse break :loaded catch break :loaded);
+                l.file.deinit(gpa);
+            }
+        }
+        si.modules.clearRetainingCapacity();
+        si.ranges.clearRetainingCapacity();
+        var ctx: DlIterContext = .{ .si = si, .gpa = gpa };
+        try std.posix.dl_iterate_phdr(&ctx, error{OutOfMemory}, DlIterContext.callback);
+    }
+    // Downgrade the lock back to shared if necessary.
+    switch (lock) {
+        .shared => {
+            si.rwlock.unlock();
+            si.rwlock.lockShared();
+        },
+        .exclusive => {},
+    }
+    // Scan the newly rebuilt module ranges.
+    for (si.ranges.items) |*range| {
+        if (address >= range.start and address < range.start + range.len) {
+            return &si.modules.items[range.module_index];
+        }
+    }
+    // Still nothing; unlock and error.
+    switch (lock) {
+        .shared => si.rwlock.unlockShared(),
+        .exclusive => si.rwlock.unlock(),
+    }
+    return error.MissingDebugInfo;
+}
+const DlIterContext = struct {
+    si: *SelfInfo,
+    gpa: Allocator,
+
+    fn callback(info: *std.posix.dl_phdr_info, size: usize, context: *@This()) !void {
+        _ = size;
+
+        var build_id: ?[]const u8 = null;
+        var gnu_eh_frame: ?[]const u8 = null;
+
+        // Populate `build_id` and `gnu_eh_frame`
+        for (info.phdr[0..info.phnum]) |phdr| {
+            switch (phdr.p_type) {
+                std.elf.PT_NOTE => {
+                    // Look for .note.gnu.build-id
+                    const segment_ptr: [*]const u8 = @ptrFromInt(info.addr + phdr.p_vaddr);
+                    var r: std.Io.Reader = .fixed(segment_ptr[0..phdr.p_memsz]);
+                    const name_size = r.takeInt(u32, native_endian) catch continue;
+                    const desc_size = r.takeInt(u32, native_endian) catch continue;
+                    const note_type = r.takeInt(u32, native_endian) catch continue;
+                    const name = r.take(name_size) catch continue;
+                    if (note_type != std.elf.NT_GNU_BUILD_ID) continue;
+                    if (!std.mem.eql(u8, name, "GNU\x00")) continue;
+                    const desc = r.take(desc_size) catch continue;
+                    build_id = desc;
+                },
+                std.elf.PT_GNU_EH_FRAME => {
+                    const segment_ptr: [*]const u8 = @ptrFromInt(info.addr + phdr.p_vaddr);
+                    gnu_eh_frame = segment_ptr[0..phdr.p_memsz];
+                },
+                else => {},
+            }
+        }
+
+        const gpa = context.gpa;
+        const si = context.si;
+
+        const module_index = si.modules.items.len;
+        try si.modules.append(gpa, .{
+            .load_offset = info.addr,
+            // Android libc uses NULL instead of "" to mark the main program
+            .name = std.mem.sliceTo(info.name, 0) orelse "",
+            .build_id = build_id,
+            .gnu_eh_frame = gnu_eh_frame,
+            .unwind = null,
+            .loaded_elf = null,
+        });
+
+        for (info.phdr[0..info.phnum]) |phdr| {
+            if (phdr.p_type != std.elf.PT_LOAD) continue;
+            try context.si.ranges.append(gpa, .{
+                // Overflowing addition handles VSDOs having p_vaddr = 0xffffffffff700000
+                .start = info.addr +% phdr.p_vaddr,
+                .len = phdr.p_memsz,
+                .module_index = module_index,
+            });
+        }
+    }
+};
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Dwarf = std.debug.Dwarf;
+const Error = std.debug.SelfInfoError;
+const assert = std.debug.assert;
+
+const builtin = @import("builtin");
+const native_endian = builtin.target.cpu.arch.endian();
+
+const SelfInfo = @This();
lib/std/debug/SelfInfo/ElfModule.zig
@@ -1,349 +0,0 @@
-load_offset: usize,
-name: []const u8,
-build_id: ?[]const u8,
-gnu_eh_frame: ?[]const u8,
-
-pub const LookupCache = struct {
-    rwlock: std.Thread.RwLock,
-    ranges: std.ArrayList(Range),
-    const Range = struct {
-        start: usize,
-        len: usize,
-        mod: ElfModule,
-    };
-    pub const init: LookupCache = .{
-        .rwlock = .{},
-        .ranges = .empty,
-    };
-    pub fn deinit(lc: *LookupCache, gpa: Allocator) void {
-        lc.ranges.deinit(gpa);
-    }
-};
-
-pub const DebugInfo = struct {
-    /// Held while checking and/or populating `loaded_elf`/`scanned_dwarf`/`unwind`.
-    /// Once data is populated and a pointer to the field has been gotten, the lock
-    /// is released; i.e. it is not held while *using* the loaded debug info.
-    mutex: std.Thread.Mutex,
-
-    loaded_elf: ?ElfFile,
-    scanned_dwarf: bool,
-    unwind: if (supports_unwinding) [2]?Dwarf.Unwind else void,
-    unwind_cache: if (supports_unwinding) *UnwindContext.Cache else void,
-
-    pub const init: DebugInfo = .{
-        .mutex = .{},
-        .loaded_elf = null,
-        .scanned_dwarf = false,
-        .unwind = if (supports_unwinding) @splat(null),
-        .unwind_cache = undefined,
-    };
-    pub fn deinit(di: *DebugInfo, gpa: Allocator) void {
-        if (di.loaded_elf) |*loaded_elf| loaded_elf.deinit(gpa);
-        if (supports_unwinding) {
-            if (di.unwind[0] != null) gpa.destroy(di.unwind_cache);
-            for (&di.unwind) |*opt_unwind| {
-                const unwind = &(opt_unwind.* orelse continue);
-                unwind.deinit(gpa);
-            }
-        }
-    }
-};
-
-pub fn key(m: ElfModule) usize {
-    return m.load_offset;
-}
-pub fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) Error!ElfModule {
-    if (lookupInCache(cache, address)) |m| return m;
-
-    {
-        // Check a new module hasn't been loaded
-        cache.rwlock.lock();
-        defer cache.rwlock.unlock();
-        const DlIterContext = struct {
-            ranges: *std.ArrayList(LookupCache.Range),
-            gpa: Allocator,
-
-            fn callback(info: *std.posix.dl_phdr_info, size: usize, context: *@This()) !void {
-                _ = size;
-
-                var mod: ElfModule = .{
-                    .load_offset = info.addr,
-                    // Android libc uses NULL instead of "" to mark the main program
-                    .name = mem.sliceTo(info.name, 0) orelse "",
-                    .build_id = null,
-                    .gnu_eh_frame = null,
-                };
-
-                // Populate `build_id` and `gnu_eh_frame`
-                for (info.phdr[0..info.phnum]) |phdr| {
-                    switch (phdr.p_type) {
-                        elf.PT_NOTE => {
-                            // Look for .note.gnu.build-id
-                            const segment_ptr: [*]const u8 = @ptrFromInt(info.addr + phdr.p_vaddr);
-                            var r: std.Io.Reader = .fixed(segment_ptr[0..phdr.p_memsz]);
-                            const name_size = r.takeInt(u32, native_endian) catch continue;
-                            const desc_size = r.takeInt(u32, native_endian) catch continue;
-                            const note_type = r.takeInt(u32, native_endian) catch continue;
-                            const name = r.take(name_size) catch continue;
-                            if (note_type != elf.NT_GNU_BUILD_ID) continue;
-                            if (!mem.eql(u8, name, "GNU\x00")) continue;
-                            const desc = r.take(desc_size) catch continue;
-                            mod.build_id = desc;
-                        },
-                        elf.PT_GNU_EH_FRAME => {
-                            const segment_ptr: [*]const u8 = @ptrFromInt(info.addr + phdr.p_vaddr);
-                            mod.gnu_eh_frame = segment_ptr[0..phdr.p_memsz];
-                        },
-                        else => {},
-                    }
-                }
-
-                // Now that `mod` is populated, create the ranges
-                for (info.phdr[0..info.phnum]) |phdr| {
-                    if (phdr.p_type != elf.PT_LOAD) continue;
-                    try context.ranges.append(context.gpa, .{
-                        // Overflowing addition handles VSDOs having p_vaddr = 0xffffffffff700000
-                        .start = info.addr +% phdr.p_vaddr,
-                        .len = phdr.p_memsz,
-                        .mod = mod,
-                    });
-                }
-            }
-        };
-        cache.ranges.clearRetainingCapacity();
-        var ctx: DlIterContext = .{
-            .ranges = &cache.ranges,
-            .gpa = gpa,
-        };
-        try std.posix.dl_iterate_phdr(&ctx, error{OutOfMemory}, DlIterContext.callback);
-    }
-
-    if (lookupInCache(cache, address)) |m| return m;
-    return error.MissingDebugInfo;
-}
-fn lookupInCache(cache: *LookupCache, address: usize) ?ElfModule {
-    cache.rwlock.lockShared();
-    defer cache.rwlock.unlockShared();
-    for (cache.ranges.items) |*range| {
-        if (address >= range.start and address < range.start + range.len) {
-            return range.mod;
-        }
-    }
-    return null;
-}
-fn loadElf(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Error!void {
-    std.debug.assert(di.loaded_elf == null);
-    std.debug.assert(!di.scanned_dwarf);
-
-    const load_result = if (module.name.len > 0) res: {
-        var file = std.fs.cwd().openFile(module.name, .{}) catch return error.MissingDebugInfo;
-        defer file.close();
-        break :res ElfFile.load(gpa, file, module.build_id, &.native(module.name));
-    } else res: {
-        const path = std.fs.selfExePathAlloc(gpa) catch |err| switch (err) {
-            error.OutOfMemory => |e| return e,
-            else => return error.ReadFailed,
-        };
-        defer gpa.free(path);
-        var file = std.fs.cwd().openFile(path, .{}) catch return error.MissingDebugInfo;
-        defer file.close();
-        break :res ElfFile.load(gpa, file, module.build_id, &.native(path));
-    };
-    di.loaded_elf = load_result catch |err| switch (err) {
-        error.OutOfMemory,
-        error.Unexpected,
-        => |e| return e,
-
-        error.Overflow,
-        error.TruncatedElfFile,
-        error.InvalidCompressedSection,
-        error.InvalidElfMagic,
-        error.InvalidElfVersion,
-        error.InvalidElfClass,
-        error.InvalidElfEndian,
-        => return error.InvalidDebugInfo,
-
-        error.SystemResources,
-        error.MemoryMappingNotSupported,
-        error.AccessDenied,
-        error.LockedMemoryLimitExceeded,
-        error.ProcessFdQuotaExceeded,
-        error.SystemFdQuotaExceeded,
-        => return error.ReadFailed,
-    };
-
-    const matches_native =
-        di.loaded_elf.?.endian == native_endian and
-        di.loaded_elf.?.is_64 == (@sizeOf(usize) == 8);
-
-    if (!matches_native) {
-        di.loaded_elf.?.deinit(gpa);
-        di.loaded_elf = null;
-        return error.InvalidDebugInfo;
-    }
-}
-pub fn getSymbolAtAddress(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, address: usize) Error!std.debug.Symbol {
-    const vaddr = address - module.load_offset;
-    {
-        di.mutex.lock();
-        defer di.mutex.unlock();
-        if (di.loaded_elf == null) try module.loadElf(gpa, di);
-        const loaded_elf = &di.loaded_elf.?;
-        // We need the lock if using DWARF, as we might scan the DWARF or build a line number table.
-        if (loaded_elf.dwarf) |*dwarf| {
-            if (!di.scanned_dwarf) {
-                dwarf.open(gpa, native_endian) catch |err| switch (err) {
-                    error.InvalidDebugInfo,
-                    error.MissingDebugInfo,
-                    error.OutOfMemory,
-                    => |e| return e,
-                    error.EndOfStream,
-                    error.Overflow,
-                    error.ReadFailed,
-                    error.StreamTooLong,
-                    => return error.InvalidDebugInfo,
-                };
-                di.scanned_dwarf = true;
-            }
-            return dwarf.getSymbol(gpa, native_endian, vaddr) catch |err| switch (err) {
-                error.InvalidDebugInfo,
-                error.MissingDebugInfo,
-                error.OutOfMemory,
-                => |e| return e,
-                error.ReadFailed,
-                error.EndOfStream,
-                error.Overflow,
-                error.StreamTooLong,
-                => return error.InvalidDebugInfo,
-            };
-        }
-        // Otherwise, we're just going to scan the symtab, which we don't need the lock for; fall out of this block.
-    }
-    // When there's no DWARF available, fall back to searching the symtab.
-    return di.loaded_elf.?.searchSymtab(gpa, vaddr) catch |err| switch (err) {
-        error.NoSymtab, error.NoStrtab => return error.MissingDebugInfo,
-        error.BadSymtab => return error.InvalidDebugInfo,
-        error.OutOfMemory => |e| return e,
-    };
-}
-fn prepareUnwindLookup(unwind: *Dwarf.Unwind, gpa: Allocator) Error!void {
-    unwind.prepare(gpa, @sizeOf(usize), native_endian, true, false) catch |err| switch (err) {
-        error.ReadFailed => unreachable, // it's all fixed buffers
-        error.InvalidDebugInfo,
-        error.MissingDebugInfo,
-        error.OutOfMemory,
-        => |e| return e,
-        error.EndOfStream,
-        error.Overflow,
-        error.StreamTooLong,
-        error.InvalidOperand,
-        error.InvalidOpcode,
-        error.InvalidOperation,
-        => return error.InvalidDebugInfo,
-        error.UnsupportedAddrSize,
-        error.UnsupportedDwarfVersion,
-        error.UnimplementedUserOpcode,
-        => return error.UnsupportedDebugInfo,
-    };
-}
-fn loadUnwindInfo(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Error!void {
-    var buf: [2]Dwarf.Unwind = undefined;
-    const unwinds: []Dwarf.Unwind = if (module.gnu_eh_frame) |section_bytes| unwinds: {
-        const section_vaddr: u64 = @intFromPtr(section_bytes.ptr) - module.load_offset;
-        const header = Dwarf.Unwind.EhFrameHeader.parse(section_vaddr, section_bytes, @sizeOf(usize), native_endian) catch |err| switch (err) {
-            error.ReadFailed => unreachable, // it's all fixed buffers
-            error.InvalidDebugInfo => |e| return e,
-            error.EndOfStream, error.Overflow => return error.InvalidDebugInfo,
-            error.UnsupportedAddrSize => return error.UnsupportedDebugInfo,
-        };
-        buf[0] = .initEhFrameHdr(header, section_vaddr, @ptrFromInt(@as(usize, @intCast(module.load_offset + header.eh_frame_vaddr))));
-        break :unwinds buf[0..1];
-    } else unwinds: {
-        // There is no `.eh_frame_hdr` section. There may still be an `.eh_frame` or `.debug_frame`
-        // section, but we'll have to load the binary to get at it.
-        if (di.loaded_elf == null) try module.loadElf(gpa, di);
-        const opt_debug_frame = &di.loaded_elf.?.debug_frame;
-        const opt_eh_frame = &di.loaded_elf.?.eh_frame;
-        var i: usize = 0;
-        // If both are present, we can't just pick one -- the info could be split between them.
-        // `.debug_frame` is likely to be the more complete section, so we'll prioritize that one.
-        if (opt_debug_frame.*) |*debug_frame| {
-            buf[i] = .initSection(.debug_frame, debug_frame.vaddr, debug_frame.bytes);
-            i += 1;
-        }
-        if (opt_eh_frame.*) |*eh_frame| {
-            buf[i] = .initSection(.eh_frame, eh_frame.vaddr, eh_frame.bytes);
-            i += 1;
-        }
-        if (i == 0) return error.MissingDebugInfo;
-        break :unwinds buf[0..i];
-    };
-    errdefer for (unwinds) |*u| u.deinit(gpa);
-    for (unwinds) |*u| try prepareUnwindLookup(u, gpa);
-
-    const unwind_cache = try gpa.create(UnwindContext.Cache);
-    errdefer gpa.destroy(unwind_cache);
-    unwind_cache.init();
-
-    switch (unwinds.len) {
-        0 => unreachable,
-        1 => di.unwind = .{ unwinds[0], null },
-        2 => di.unwind = .{ unwinds[0], unwinds[1] },
-        else => unreachable,
-    }
-    di.unwind_cache = unwind_cache;
-}
-pub fn unwindFrame(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) Error!usize {
-    const unwinds: *const [2]?Dwarf.Unwind = u: {
-        di.mutex.lock();
-        defer di.mutex.unlock();
-        if (di.unwind[0] == null) try module.loadUnwindInfo(gpa, di);
-        std.debug.assert(di.unwind[0] != null);
-        break :u &di.unwind;
-    };
-    for (unwinds) |*opt_unwind| {
-        const unwind = &(opt_unwind.* orelse break);
-        return context.unwindFrame(di.unwind_cache, gpa, unwind, module.load_offset, null) catch |err| switch (err) {
-            error.MissingDebugInfo => continue, // try the next one
-            else => |e| return e,
-        };
-    }
-    return error.MissingDebugInfo;
-}
-pub const UnwindContext = std.debug.SelfInfo.DwarfUnwindContext;
-pub const supports_unwinding: bool = s: {
-    // Notably, we are yet to support unwinding on ARM. There, unwinding is not done through
-    // `.eh_frame`, but instead with the `.ARM.exidx` section, which has a different format.
-    const archs: []const std.Target.Cpu.Arch = switch (builtin.target.os.tag) {
-        .linux => &.{ .x86, .x86_64, .aarch64, .aarch64_be },
-        .netbsd => &.{ .x86, .x86_64, .aarch64, .aarch64_be },
-        .freebsd => &.{ .x86_64, .aarch64, .aarch64_be },
-        .openbsd => &.{.x86_64},
-        .solaris => &.{ .x86, .x86_64 },
-        .illumos => &.{ .x86, .x86_64 },
-        else => unreachable,
-    };
-    for (archs) |a| {
-        if (builtin.target.cpu.arch == a) break :s true;
-    }
-    break :s false;
-};
-comptime {
-    if (supports_unwinding) {
-        std.debug.assert(Dwarf.supportsUnwinding(&builtin.target));
-    }
-}
-
-const ElfModule = @This();
-
-const std = @import("../../std.zig");
-const Allocator = std.mem.Allocator;
-const Dwarf = std.debug.Dwarf;
-const ElfFile = std.debug.ElfFile;
-const elf = std.elf;
-const mem = std.mem;
-const Error = std.debug.SelfInfo.Error;
-
-const builtin = @import("builtin");
-const native_endian = builtin.target.cpu.arch.endian();
lib/std/debug/SelfInfo/Windows.zig
@@ -0,0 +1,559 @@
+mutex: std.Thread.Mutex,
+modules: std.ArrayListUnmanaged(Module),
+module_name_arena: std.heap.ArenaAllocator.State,
+
+pub const init: SelfInfo = .{
+    .mutex = .{},
+    .modules = .empty,
+    .module_name_arena = .{},
+};
+pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
+    for (si.modules.items) |*module| {
+        di: {
+            const di = &(module.di orelse break :di catch break :di);
+            di.deinit(gpa);
+        }
+    }
+    si.modules.deinit(gpa);
+
+    var module_name_arena = si.module_name_arena.promote(gpa);
+    module_name_arena.deinit();
+}
+
+pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol {
+    si.mutex.lock();
+    defer si.mutex.unlock();
+    const module = try si.findModule(gpa, address);
+    const di = try module.getDebugInfo(gpa);
+    return di.getSymbol(gpa, address - module.base_address);
+}
+pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
+    si.mutex.lock();
+    defer si.mutex.unlock();
+    const module = try si.findModule(gpa, address);
+    return module.name;
+}
+
+pub const can_unwind: bool = switch (builtin.cpu.arch) {
+    else => true,
+    // On x86, `RtlVirtualUnwind` does not exist. We could in theory use `RtlCaptureStackBackTrace`
+    // instead, but on x86, it turns out that function is just... doing FP unwinding with esp! It's
+    // hard to find implementation details to confirm that, but the most authoritative source I have
+    // is an entry in the LLVM mailing list from 2020/08/16 which contains this quote:
+    //
+    // > x86 doesn't have what most architectures would consider an "unwinder" in the sense of
+    // > restoring registers; there is simply a linked list of frames that participate in SEH and
+    // > that desire to be called for a dynamic unwind operation, so RtlCaptureStackBackTrace
+    // > assumes that EBP-based frames are in use and walks an EBP-based frame chain on x86 - not
+    // > all x86 code is written with EBP-based frames so while even though we generally build the
+    // > OS that way, you might always run the risk of encountering external code that uses EBP as a
+    // > general purpose register for which such an unwind attempt for a stack trace would fail.
+    //
+    // Regardless, it's easy to effectively confirm this hypothesis just by compiling some code with
+    // `-fomit-frame-pointer -OReleaseFast` and observing that `RtlCaptureStackBackTrace` returns an
+    // empty trace when it's called in such an application. Note that without `-OReleaseFast` or
+    // similar, LLVM seems reluctant to ever clobber ebp, so you'll get a trace returned which just
+    // contains all of the kernel32/ntdll frames but none of your own. Don't be deceived---this is
+    // just coincidental!
+    //
+    // Anyway, the point is, the only stack walking primitive on x86-windows is FP unwinding. We
+    // *could* ask Microsoft to do that for us with `RtlCaptureStackBackTrace`... but better to just
+    // use our existing FP unwinder in `std.debug`!
+    .x86 => false,
+};
+pub const UnwindContext = struct {
+    pc: usize,
+    cur: windows.CONTEXT,
+    history_table: windows.UNWIND_HISTORY_TABLE,
+    pub fn init(ctx: *const std.debug.cpu_context.Native) UnwindContext {
+        return .{
+            .pc = @returnAddress(),
+            .cur = switch (builtin.cpu.arch) {
+                .x86_64 => std.mem.zeroInit(windows.CONTEXT, .{
+                    .Rax = ctx.gprs.get(.rax),
+                    .Rcx = ctx.gprs.get(.rcx),
+                    .Rdx = ctx.gprs.get(.rdx),
+                    .Rbx = ctx.gprs.get(.rbx),
+                    .Rsp = ctx.gprs.get(.rsp),
+                    .Rbp = ctx.gprs.get(.rbp),
+                    .Rsi = ctx.gprs.get(.rsi),
+                    .Rdi = ctx.gprs.get(.rdi),
+                    .R8 = ctx.gprs.get(.r8),
+                    .R9 = ctx.gprs.get(.r9),
+                    .R10 = ctx.gprs.get(.r10),
+                    .R11 = ctx.gprs.get(.r11),
+                    .R12 = ctx.gprs.get(.r12),
+                    .R13 = ctx.gprs.get(.r13),
+                    .R14 = ctx.gprs.get(.r14),
+                    .R15 = ctx.gprs.get(.r15),
+                    .Rip = ctx.gprs.get(.rip),
+                }),
+                .aarch64, .aarch64_be => .{
+                    .ContextFlags = 0,
+                    .Cpsr = 0,
+                    .DUMMYUNIONNAME = .{ .X = ctx.x },
+                    .Sp = ctx.sp,
+                    .Pc = ctx.pc,
+                    .V = @splat(.{ .B = @splat(0) }),
+                    .Fpcr = 0,
+                    .Fpsr = 0,
+                    .Bcr = @splat(0),
+                    .Bvr = @splat(0),
+                    .Wcr = @splat(0),
+                    .Wvr = @splat(0),
+                },
+                .thumb => .{
+                    .ContextFlags = 0,
+                    .R0 = ctx.r[0],
+                    .R1 = ctx.r[1],
+                    .R2 = ctx.r[2],
+                    .R3 = ctx.r[3],
+                    .R4 = ctx.r[4],
+                    .R5 = ctx.r[5],
+                    .R6 = ctx.r[6],
+                    .R7 = ctx.r[7],
+                    .R8 = ctx.r[8],
+                    .R9 = ctx.r[9],
+                    .R10 = ctx.r[10],
+                    .R11 = ctx.r[11],
+                    .R12 = ctx.r[12],
+                    .Sp = ctx.r[13],
+                    .Lr = ctx.r[14],
+                    .Pc = ctx.r[15],
+                    .Cpsr = 0,
+                    .Fpcsr = 0,
+                    .Padding = 0,
+                    .DUMMYUNIONNAME = .{ .S = @splat(0) },
+                    .Bvr = @splat(0),
+                    .Bcr = @splat(0),
+                    .Wvr = @splat(0),
+                    .Wcr = @splat(0),
+                    .Padding2 = @splat(0),
+                },
+                else => comptime unreachable,
+            },
+            .history_table = std.mem.zeroes(windows.UNWIND_HISTORY_TABLE),
+        };
+    }
+    pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void {
+        _ = ctx;
+        _ = gpa;
+    }
+    pub fn getFp(ctx: *UnwindContext) usize {
+        return ctx.cur.getRegs().bp;
+    }
+};
+pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize {
+    _ = si;
+    _ = gpa;
+
+    const current_regs = context.cur.getRegs();
+    var image_base: windows.DWORD64 = undefined;
+    if (windows.ntdll.RtlLookupFunctionEntry(current_regs.ip, &image_base, &context.history_table)) |runtime_function| {
+        var handler_data: ?*anyopaque = null;
+        var establisher_frame: u64 = undefined;
+        _ = windows.ntdll.RtlVirtualUnwind(
+            windows.UNW_FLAG_NHANDLER,
+            image_base,
+            current_regs.ip,
+            runtime_function,
+            &context.cur,
+            &handler_data,
+            &establisher_frame,
+            null,
+        );
+    } else {
+        // leaf function
+        context.cur.setIp(@as(*const usize, @ptrFromInt(current_regs.sp)).*);
+        context.cur.setSp(current_regs.sp + @sizeOf(usize));
+    }
+
+    const next_regs = context.cur.getRegs();
+    const tib = &windows.teb().NtTib;
+    if (next_regs.sp < @intFromPtr(tib.StackLimit) or next_regs.sp > @intFromPtr(tib.StackBase)) {
+        context.pc = 0;
+        return 0;
+    }
+    // Like `DwarfUnwindContext.unwindFrame`, adjust our next lookup pc in case the `call` was this
+    // function's last instruction making `next_regs.ip` one byte past its end.
+    context.pc = next_regs.ip -| 1;
+    return next_regs.ip;
+}
+
+const Module = struct {
+    base_address: usize,
+    size: u32,
+    name: []const u8,
+    handle: windows.HMODULE,
+
+    di: ?(Error!DebugInfo),
+
+    const DebugInfo = struct {
+        arena: std.heap.ArenaAllocator.State,
+        coff_image_base: u64,
+        mapped_file: ?MappedFile,
+        dwarf: ?Dwarf,
+        pdb: ?Pdb,
+        coff_section_headers: []coff.SectionHeader,
+
+        const MappedFile = struct {
+            file: fs.File,
+            section_handle: windows.HANDLE,
+            section_view: []const u8,
+            fn deinit(mf: *const MappedFile) void {
+                const process_handle = windows.GetCurrentProcess();
+                assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(mf.section_view.ptr)) == .SUCCESS);
+                windows.CloseHandle(mf.section_handle);
+                mf.file.close();
+            }
+        };
+
+        fn deinit(di: *DebugInfo, gpa: Allocator) void {
+            if (di.dwarf) |*dwarf| dwarf.deinit(gpa);
+            if (di.pdb) |*pdb| {
+                pdb.file_reader.file.close();
+                pdb.deinit();
+            }
+            if (di.mapped_file) |*mf| mf.deinit();
+
+            var arena = di.arena.promote(gpa);
+            arena.deinit();
+        }
+
+        fn getSymbol(di: *DebugInfo, gpa: Allocator, vaddr: usize) Error!std.debug.Symbol {
+            pdb: {
+                const pdb = &(di.pdb orelse break :pdb);
+                var coff_section: *align(1) const coff.SectionHeader = undefined;
+                const mod_index = for (pdb.sect_contribs) |sect_contrib| {
+                    if (sect_contrib.section > di.coff_section_headers.len) continue;
+                    // Remember that SectionContribEntry.Section is 1-based.
+                    coff_section = &di.coff_section_headers[sect_contrib.section - 1];
+
+                    const vaddr_start = coff_section.virtual_address + sect_contrib.offset;
+                    const vaddr_end = vaddr_start + sect_contrib.size;
+                    if (vaddr >= vaddr_start and vaddr < vaddr_end) {
+                        break sect_contrib.module_index;
+                    }
+                } else {
+                    // we have no information to add to the address
+                    break :pdb;
+                };
+                const module = pdb.getModule(mod_index) catch |err| switch (err) {
+                    error.InvalidDebugInfo,
+                    error.MissingDebugInfo,
+                    error.OutOfMemory,
+                    => |e| return e,
+
+                    error.ReadFailed,
+                    error.EndOfStream,
+                    => return error.InvalidDebugInfo,
+                } orelse {
+                    return error.InvalidDebugInfo; // bad module index
+                };
+                return .{
+                    .name = pdb.getSymbolName(module, vaddr - coff_section.virtual_address),
+                    .compile_unit_name = fs.path.basename(module.obj_file_name),
+                    .source_location = pdb.getLineNumberInfo(module, vaddr - coff_section.virtual_address) catch null,
+                };
+            }
+            dwarf: {
+                const dwarf = &(di.dwarf orelse break :dwarf);
+                const dwarf_address = vaddr + di.coff_image_base;
+                return dwarf.getSymbol(gpa, native_endian, dwarf_address) catch |err| switch (err) {
+                    error.MissingDebugInfo => break :dwarf,
+
+                    error.InvalidDebugInfo,
+                    error.OutOfMemory,
+                    => |e| return e,
+
+                    error.ReadFailed,
+                    error.EndOfStream,
+                    error.Overflow,
+                    error.StreamTooLong,
+                    => return error.InvalidDebugInfo,
+                };
+            }
+            return error.MissingDebugInfo;
+        }
+    };
+
+    fn getDebugInfo(module: *Module, gpa: Allocator) Error!*DebugInfo {
+        if (module.di == null) module.di = loadDebugInfo(module, gpa);
+        return if (module.di.?) |*di| di else |err| err;
+    }
+    fn loadDebugInfo(module: *const Module, gpa: Allocator) Error!DebugInfo {
+        const mapped_ptr: [*]const u8 = @ptrFromInt(module.base_address);
+        const mapped = mapped_ptr[0..module.size];
+        var coff_obj = coff.Coff.init(mapped, true) catch return error.InvalidDebugInfo;
+
+        var arena_instance: std.heap.ArenaAllocator = .init(gpa);
+        errdefer arena_instance.deinit();
+        const arena = arena_instance.allocator();
+
+        // The string table is not mapped into memory by the loader, so if a section name is in the
+        // string table then we have to map the full image file from disk. This can happen when
+        // a binary is produced with -gdwarf, since the section names are longer than 8 bytes.
+        const mapped_file: ?DebugInfo.MappedFile = mapped: {
+            if (!coff_obj.strtabRequired()) break :mapped null;
+            var name_buffer: [windows.PATH_MAX_WIDE + 4:0]u16 = undefined;
+            name_buffer[0..4].* = .{ '\\', '?', '?', '\\' }; // openFileAbsoluteW requires the prefix to be present
+            const process_handle = windows.GetCurrentProcess();
+            const len = windows.kernel32.GetModuleFileNameExW(
+                process_handle,
+                module.handle,
+                name_buffer[4..],
+                windows.PATH_MAX_WIDE,
+            );
+            if (len == 0) return error.MissingDebugInfo;
+            const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) {
+                error.Unexpected => |e| return e,
+                error.FileNotFound => return error.MissingDebugInfo,
+
+                error.FileTooBig,
+                error.IsDir,
+                error.NotDir,
+                error.SymLinkLoop,
+                error.NameTooLong,
+                error.InvalidUtf8,
+                error.InvalidWtf8,
+                error.BadPathName,
+                => return error.InvalidDebugInfo,
+
+                error.SystemResources,
+                error.WouldBlock,
+                error.AccessDenied,
+                error.ProcessNotFound,
+                error.PermissionDenied,
+                error.NoSpaceLeft,
+                error.DeviceBusy,
+                error.NoDevice,
+                error.SharingViolation,
+                error.PathAlreadyExists,
+                error.PipeBusy,
+                error.NetworkNotFound,
+                error.AntivirusInterference,
+                error.ProcessFdQuotaExceeded,
+                error.SystemFdQuotaExceeded,
+                error.FileLocksNotSupported,
+                error.FileBusy,
+                => return error.ReadFailed,
+            };
+            errdefer coff_file.close();
+            var section_handle: windows.HANDLE = undefined;
+            const create_section_rc = windows.ntdll.NtCreateSection(
+                &section_handle,
+                windows.STANDARD_RIGHTS_REQUIRED | windows.SECTION_QUERY | windows.SECTION_MAP_READ,
+                null,
+                null,
+                windows.PAGE_READONLY,
+                // The documentation states that if no AllocationAttribute is specified, then SEC_COMMIT is the default.
+                // In practice, this isn't the case and specifying 0 will result in INVALID_PARAMETER_6.
+                windows.SEC_COMMIT,
+                coff_file.handle,
+            );
+            if (create_section_rc != .SUCCESS) return error.MissingDebugInfo;
+            errdefer windows.CloseHandle(section_handle);
+            var coff_len: usize = 0;
+            var section_view_ptr: ?[*]const u8 = null;
+            const map_section_rc = windows.ntdll.NtMapViewOfSection(
+                section_handle,
+                process_handle,
+                @ptrCast(&section_view_ptr),
+                null,
+                0,
+                null,
+                &coff_len,
+                .ViewUnmap,
+                0,
+                windows.PAGE_READONLY,
+            );
+            if (map_section_rc != .SUCCESS) return error.MissingDebugInfo;
+            errdefer assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(section_view_ptr.?)) == .SUCCESS);
+            const section_view = section_view_ptr.?[0..coff_len];
+            coff_obj = coff.Coff.init(section_view, false) catch return error.InvalidDebugInfo;
+            break :mapped .{
+                .file = coff_file,
+                .section_handle = section_handle,
+                .section_view = section_view,
+            };
+        };
+        errdefer if (mapped_file) |*mf| mf.deinit();
+
+        const coff_image_base = coff_obj.getImageBase();
+
+        var opt_dwarf: ?Dwarf = dwarf: {
+            if (coff_obj.getSectionByName(".debug_info") == null) break :dwarf null;
+
+            var sections: Dwarf.SectionArray = undefined;
+            inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
+                sections[i] = if (coff_obj.getSectionByName("." ++ section.name)) |section_header| .{
+                    .data = try coff_obj.getSectionDataAlloc(section_header, arena),
+                    .owned = false,
+                } else null;
+            }
+            break :dwarf .{ .sections = sections };
+        };
+        errdefer if (opt_dwarf) |*dwarf| dwarf.deinit(gpa);
+
+        if (opt_dwarf) |*dwarf| {
+            dwarf.open(gpa, native_endian) catch |err| switch (err) {
+                error.Overflow,
+                error.EndOfStream,
+                error.StreamTooLong,
+                error.ReadFailed,
+                => return error.InvalidDebugInfo,
+
+                error.InvalidDebugInfo,
+                error.MissingDebugInfo,
+                error.OutOfMemory,
+                => |e| return e,
+            };
+        }
+
+        var opt_pdb: ?Pdb = pdb: {
+            const path = coff_obj.getPdbPath() catch {
+                return error.InvalidDebugInfo;
+            } orelse {
+                break :pdb null;
+            };
+            const pdb_file_open_result = if (fs.path.isAbsolute(path)) res: {
+                break :res std.fs.cwd().openFile(path, .{});
+            } else res: {
+                const self_dir = fs.selfExeDirPathAlloc(gpa) catch |err| switch (err) {
+                    error.OutOfMemory, error.Unexpected => |e| return e,
+                    else => return error.ReadFailed,
+                };
+                defer gpa.free(self_dir);
+                const abs_path = try fs.path.join(gpa, &.{ self_dir, path });
+                defer gpa.free(abs_path);
+                break :res std.fs.cwd().openFile(abs_path, .{});
+            };
+            const pdb_file = pdb_file_open_result catch |err| switch (err) {
+                error.FileNotFound, error.IsDir => break :pdb null,
+                else => return error.ReadFailed,
+            };
+            errdefer pdb_file.close();
+
+            const pdb_reader = try arena.create(std.fs.File.Reader);
+            pdb_reader.* = pdb_file.reader(try arena.alloc(u8, 4096));
+
+            var pdb = Pdb.init(gpa, pdb_reader) catch |err| switch (err) {
+                error.OutOfMemory, error.ReadFailed, error.Unexpected => |e| return e,
+                else => return error.InvalidDebugInfo,
+            };
+            errdefer pdb.deinit();
+            pdb.parseInfoStream() catch |err| switch (err) {
+                error.UnknownPDBVersion => return error.UnsupportedDebugInfo,
+                error.EndOfStream => return error.InvalidDebugInfo,
+
+                error.InvalidDebugInfo,
+                error.MissingDebugInfo,
+                error.OutOfMemory,
+                error.ReadFailed,
+                => |e| return e,
+            };
+            pdb.parseDbiStream() catch |err| switch (err) {
+                error.UnknownPDBVersion => return error.UnsupportedDebugInfo,
+
+                error.EndOfStream,
+                error.EOF,
+                error.StreamTooLong,
+                error.WriteFailed,
+                => return error.InvalidDebugInfo,
+
+                error.InvalidDebugInfo,
+                error.OutOfMemory,
+                error.ReadFailed,
+                => |e| return e,
+            };
+
+            if (!std.mem.eql(u8, &coff_obj.guid, &pdb.guid) or coff_obj.age != pdb.age)
+                return error.InvalidDebugInfo;
+
+            break :pdb pdb;
+        };
+        errdefer if (opt_pdb) |*pdb| {
+            pdb.file_reader.file.close();
+            pdb.deinit();
+        };
+
+        const coff_section_headers: []coff.SectionHeader = if (opt_pdb != null) csh: {
+            break :csh try coff_obj.getSectionHeadersAlloc(arena);
+        } else &.{};
+
+        return .{
+            .arena = arena_instance.state,
+            .coff_image_base = coff_image_base,
+            .mapped_file = mapped_file,
+            .dwarf = opt_dwarf,
+            .pdb = opt_pdb,
+            .coff_section_headers = coff_section_headers,
+        };
+    }
+};
+
+/// Assumes we already hold `si.mutex`.
+fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) error{ MissingDebugInfo, OutOfMemory, Unexpected }!*Module {
+    for (si.modules.items) |*mod| {
+        if (address >= mod.base_address and address < mod.base_address + mod.size) {
+            return mod;
+        }
+    }
+
+    // A new module might have been loaded; rebuild the list.
+    {
+        for (si.modules.items) |*mod| {
+            const di = &(mod.di orelse continue catch continue);
+            di.deinit(gpa);
+        }
+        si.modules.clearRetainingCapacity();
+
+        var module_name_arena = si.module_name_arena.promote(gpa);
+        defer si.module_name_arena = module_name_arena.state;
+        _ = module_name_arena.reset(.retain_capacity);
+
+        const handle = windows.kernel32.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE | windows.TH32CS_SNAPMODULE32, 0);
+        if (handle == windows.INVALID_HANDLE_VALUE) {
+            return windows.unexpectedError(windows.GetLastError());
+        }
+        defer windows.CloseHandle(handle);
+        var entry: windows.MODULEENTRY32 = undefined;
+        entry.dwSize = @sizeOf(windows.MODULEENTRY32);
+        var result = windows.kernel32.Module32First(handle, &entry);
+        while (result != 0) : (result = windows.kernel32.Module32Next(handle, &entry)) {
+            try si.modules.append(gpa, .{
+                .base_address = @intFromPtr(entry.modBaseAddr),
+                .size = entry.modBaseSize,
+                .name = try module_name_arena.allocator().dupe(
+                    u8,
+                    std.mem.sliceTo(&entry.szModule, 0),
+                ),
+                .handle = entry.hModule,
+                .di = null,
+            });
+        }
+    }
+
+    for (si.modules.items) |*mod| {
+        if (address >= mod.base_address and address < mod.base_address + mod.size) {
+            return mod;
+        }
+    }
+
+    return error.MissingDebugInfo;
+}
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Dwarf = std.debug.Dwarf;
+const Pdb = std.debug.Pdb;
+const Error = std.debug.SelfInfoError;
+const assert = std.debug.assert;
+const coff = std.coff;
+const fs = std.fs;
+const windows = std.os.windows;
+
+const builtin = @import("builtin");
+const native_endian = builtin.target.cpu.arch.endian();
+
+const SelfInfo = @This();
lib/std/debug/SelfInfo/WindowsModule.zig
@@ -1,442 +0,0 @@
-base_address: usize,
-size: usize,
-name: []const u8,
-handle: windows.HMODULE,
-pub fn key(m: WindowsModule) usize {
-    return m.base_address;
-}
-pub fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) std.debug.SelfInfo.Error!WindowsModule {
-    if (lookupInCache(cache, address)) |m| return m;
-    {
-        // Check a new module hasn't been loaded
-        cache.rwlock.lock();
-        defer cache.rwlock.unlock();
-        cache.modules.clearRetainingCapacity();
-        const handle = windows.kernel32.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE | windows.TH32CS_SNAPMODULE32, 0);
-        if (handle == windows.INVALID_HANDLE_VALUE) {
-            return windows.unexpectedError(windows.GetLastError());
-        }
-        defer windows.CloseHandle(handle);
-        var entry: windows.MODULEENTRY32 = undefined;
-        entry.dwSize = @sizeOf(windows.MODULEENTRY32);
-        if (windows.kernel32.Module32First(handle, &entry) != 0) {
-            try cache.modules.append(gpa, entry);
-            while (windows.kernel32.Module32Next(handle, &entry) != 0) {
-                try cache.modules.append(gpa, entry);
-            }
-        }
-    }
-    if (lookupInCache(cache, address)) |m| return m;
-    return error.MissingDebugInfo;
-}
-pub fn getSymbolAtAddress(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo, address: usize) std.debug.SelfInfo.Error!std.debug.Symbol {
-    // The `Pdb` API doesn't really allow us *any* thread-safe access, and the `Dwarf` API isn't
-    // great for it either; just lock the whole thing.
-    di.mutex.lock();
-    defer di.mutex.unlock();
-
-    if (!di.loaded) module.loadDebugInfo(gpa, di) catch |err| switch (err) {
-        error.OutOfMemory, error.InvalidDebugInfo, error.MissingDebugInfo, error.Unexpected => |e| return e,
-        error.FileNotFound => return error.MissingDebugInfo,
-        error.UnknownPDBVersion => return error.UnsupportedDebugInfo,
-        else => return error.ReadFailed,
-    };
-
-    // Translate the runtime address into a virtual address into the module
-    const vaddr = address - module.base_address;
-
-    if (di.pdb != null) {
-        if (di.getSymbolFromPdb(vaddr) catch return error.InvalidDebugInfo) |symbol| return symbol;
-    }
-
-    if (di.dwarf) |*dwarf| {
-        const dwarf_address = vaddr + di.coff_image_base;
-        return dwarf.getSymbol(gpa, native_endian, dwarf_address) catch return error.InvalidDebugInfo;
-    }
-
-    return error.MissingDebugInfo;
-}
-fn lookupInCache(cache: *LookupCache, address: usize) ?WindowsModule {
-    cache.rwlock.lockShared();
-    defer cache.rwlock.unlockShared();
-    for (cache.modules.items) |*entry| {
-        const base_address = @intFromPtr(entry.modBaseAddr);
-        if (address >= base_address and address < base_address + entry.modBaseSize) {
-            return .{
-                .base_address = base_address,
-                .size = entry.modBaseSize,
-                .name = std.mem.sliceTo(&entry.szModule, 0),
-                .handle = entry.hModule,
-            };
-        }
-    }
-    return null;
-}
-fn loadDebugInfo(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo) !void {
-    const mapped_ptr: [*]const u8 = @ptrFromInt(module.base_address);
-    const mapped = mapped_ptr[0..module.size];
-    var coff_obj = coff.Coff.init(mapped, true) catch return error.InvalidDebugInfo;
-    // The string table is not mapped into memory by the loader, so if a section name is in the
-    // string table then we have to map the full image file from disk. This can happen when
-    // a binary is produced with -gdwarf, since the section names are longer than 8 bytes.
-    if (coff_obj.strtabRequired()) {
-        var name_buffer: [windows.PATH_MAX_WIDE + 4:0]u16 = undefined;
-        name_buffer[0..4].* = .{ '\\', '?', '?', '\\' }; // openFileAbsoluteW requires the prefix to be present
-        const process_handle = windows.GetCurrentProcess();
-        const len = windows.kernel32.GetModuleFileNameExW(
-            process_handle,
-            module.handle,
-            name_buffer[4..],
-            windows.PATH_MAX_WIDE,
-        );
-        if (len == 0) return error.MissingDebugInfo;
-        const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) {
-            error.FileNotFound => return error.MissingDebugInfo,
-            else => |e| return e,
-        };
-        errdefer coff_file.close();
-        var section_handle: windows.HANDLE = undefined;
-        const create_section_rc = windows.ntdll.NtCreateSection(
-            &section_handle,
-            windows.STANDARD_RIGHTS_REQUIRED | windows.SECTION_QUERY | windows.SECTION_MAP_READ,
-            null,
-            null,
-            windows.PAGE_READONLY,
-            // The documentation states that if no AllocationAttribute is specified, then SEC_COMMIT is the default.
-            // In practice, this isn't the case and specifying 0 will result in INVALID_PARAMETER_6.
-            windows.SEC_COMMIT,
-            coff_file.handle,
-        );
-        if (create_section_rc != .SUCCESS) return error.MissingDebugInfo;
-        errdefer windows.CloseHandle(section_handle);
-        var coff_len: usize = 0;
-        var section_view_ptr: ?[*]const u8 = null;
-        const map_section_rc = windows.ntdll.NtMapViewOfSection(
-            section_handle,
-            process_handle,
-            @ptrCast(&section_view_ptr),
-            null,
-            0,
-            null,
-            &coff_len,
-            .ViewUnmap,
-            0,
-            windows.PAGE_READONLY,
-        );
-        if (map_section_rc != .SUCCESS) return error.MissingDebugInfo;
-        errdefer assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(section_view_ptr.?)) == .SUCCESS);
-        const section_view = section_view_ptr.?[0..coff_len];
-        coff_obj = coff.Coff.init(section_view, false) catch return error.InvalidDebugInfo;
-        di.mapped_file = .{
-            .file = coff_file,
-            .section_handle = section_handle,
-            .section_view = section_view,
-        };
-    }
-    di.coff_image_base = coff_obj.getImageBase();
-
-    if (coff_obj.getSectionByName(".debug_info")) |_| {
-        di.dwarf = .{};
-
-        inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
-            di.dwarf.?.sections[i] = if (coff_obj.getSectionByName("." ++ section.name)) |section_header| blk: {
-                break :blk .{
-                    .data = try coff_obj.getSectionDataAlloc(section_header, gpa),
-                    .owned = true,
-                };
-            } else null;
-        }
-
-        try di.dwarf.?.open(gpa, native_endian);
-    }
-
-    if (coff_obj.getPdbPath() catch return error.InvalidDebugInfo) |raw_path| pdb: {
-        const path = blk: {
-            if (fs.path.isAbsolute(raw_path)) {
-                break :blk raw_path;
-            } else {
-                const self_dir = try fs.selfExeDirPathAlloc(gpa);
-                defer gpa.free(self_dir);
-                break :blk try fs.path.join(gpa, &.{ self_dir, raw_path });
-            }
-        };
-        defer if (path.ptr != raw_path.ptr) gpa.free(path);
-
-        const pdb_file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
-            error.FileNotFound, error.IsDir => break :pdb,
-            else => |e| return e,
-        };
-        errdefer pdb_file.close();
-
-        const pdb_reader = try gpa.create(std.fs.File.Reader);
-        errdefer gpa.destroy(pdb_reader);
-
-        pdb_reader.* = pdb_file.reader(try gpa.alloc(u8, 4096));
-        errdefer gpa.free(pdb_reader.interface.buffer);
-
-        var pdb: Pdb = try .init(gpa, pdb_reader);
-        errdefer pdb.deinit();
-        try pdb.parseInfoStream();
-        try pdb.parseDbiStream();
-
-        if (!mem.eql(u8, &coff_obj.guid, &pdb.guid) or coff_obj.age != pdb.age)
-            return error.InvalidDebugInfo;
-
-        di.coff_section_headers = try coff_obj.getSectionHeadersAlloc(gpa);
-
-        di.pdb = pdb;
-    }
-
-    di.loaded = true;
-}
-pub const LookupCache = struct {
-    rwlock: std.Thread.RwLock,
-    modules: std.ArrayListUnmanaged(windows.MODULEENTRY32),
-    pub const init: LookupCache = .{
-        .rwlock = .{},
-        .modules = .empty,
-    };
-    pub fn deinit(lc: *LookupCache, gpa: Allocator) void {
-        lc.modules.deinit(gpa);
-    }
-};
-pub const DebugInfo = struct {
-    mutex: std.Thread.Mutex,
-
-    loaded: bool,
-
-    coff_image_base: u64,
-    mapped_file: ?struct {
-        file: fs.File,
-        section_handle: windows.HANDLE,
-        section_view: []const u8,
-    },
-
-    dwarf: ?Dwarf,
-
-    pdb: ?Pdb,
-    /// Populated iff `pdb != null`; otherwise `&.{}`.
-    coff_section_headers: []coff.SectionHeader,
-
-    pub const init: DebugInfo = .{
-        .mutex = .{},
-        .loaded = false,
-        .coff_image_base = undefined,
-        .mapped_file = null,
-        .dwarf = null,
-        .pdb = null,
-        .coff_section_headers = &.{},
-    };
-
-    pub fn deinit(di: *DebugInfo, gpa: Allocator) void {
-        if (!di.loaded) return;
-        if (di.dwarf) |*dwarf| dwarf.deinit(gpa);
-        if (di.pdb) |*pdb| {
-            pdb.file_reader.file.close();
-            gpa.free(pdb.file_reader.interface.buffer);
-            gpa.destroy(pdb.file_reader);
-            pdb.deinit();
-        }
-        gpa.free(di.coff_section_headers);
-        if (di.mapped_file) |mapped| {
-            const process_handle = windows.GetCurrentProcess();
-            assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(mapped.section_view.ptr)) == .SUCCESS);
-            windows.CloseHandle(mapped.section_handle);
-            mapped.file.close();
-        }
-    }
-
-    fn getSymbolFromPdb(di: *DebugInfo, relocated_address: usize) !?std.debug.Symbol {
-        var coff_section: *align(1) const coff.SectionHeader = undefined;
-        const mod_index = for (di.pdb.?.sect_contribs) |sect_contrib| {
-            if (sect_contrib.section > di.coff_section_headers.len) continue;
-            // Remember that SectionContribEntry.Section is 1-based.
-            coff_section = &di.coff_section_headers[sect_contrib.section - 1];
-
-            const vaddr_start = coff_section.virtual_address + sect_contrib.offset;
-            const vaddr_end = vaddr_start + sect_contrib.size;
-            if (relocated_address >= vaddr_start and relocated_address < vaddr_end) {
-                break sect_contrib.module_index;
-            }
-        } else {
-            // we have no information to add to the address
-            return null;
-        };
-
-        const module = try di.pdb.?.getModule(mod_index) orelse return error.InvalidDebugInfo;
-
-        return .{
-            .name = di.pdb.?.getSymbolName(
-                module,
-                relocated_address - coff_section.virtual_address,
-            ),
-            .compile_unit_name = fs.path.basename(module.obj_file_name),
-            .source_location = try di.pdb.?.getLineNumberInfo(
-                module,
-                relocated_address - coff_section.virtual_address,
-            ),
-        };
-    }
-};
-
-pub const supports_unwinding: bool = switch (builtin.cpu.arch) {
-    else => true,
-    // On x86, `RtlVirtualUnwind` does not exist. We could in theory use `RtlCaptureStackBackTrace`
-    // instead, but on x86, it turns out that function is just... doing FP unwinding with esp! It's
-    // hard to find implementation details to confirm that, but the most authoritative source I have
-    // is an entry in the LLVM mailing list from 2020/08/16 which contains this quote:
-    //
-    // > x86 doesn't have what most architectures would consider an "unwinder" in the sense of
-    // > restoring registers; there is simply a linked list of frames that participate in SEH and
-    // > that desire to be called for a dynamic unwind operation, so RtlCaptureStackBackTrace
-    // > assumes that EBP-based frames are in use and walks an EBP-based frame chain on x86 - not
-    // > all x86 code is written with EBP-based frames so while even though we generally build the
-    // > OS that way, you might always run the risk of encountering external code that uses EBP as a
-    // > general purpose register for which such an unwind attempt for a stack trace would fail.
-    //
-    // Regardless, it's easy to effectively confirm this hypothesis just by compiling some code with
-    // `-fomit-frame-pointer -OReleaseFast` and observing that `RtlCaptureStackBackTrace` returns an
-    // empty trace when it's called in such an application. Note that without `-OReleaseFast` or
-    // similar, LLVM seems reluctant to ever clobber ebp, so you'll get a trace returned which just
-    // contains all of the kernel32/ntdll frames but none of your own. Don't be deceived---this is
-    // just coincidental!
-    //
-    // Anyway, the point is, the only stack walking primitive on x86-windows is FP unwinding. We
-    // *could* ask Microsoft to do that for us with `RtlCaptureStackBackTrace`... but better to just
-    // use our existing FP unwinder in `std.debug`!
-    .x86 => false,
-};
-pub const UnwindContext = struct {
-    pc: usize,
-    cur: windows.CONTEXT,
-    history_table: windows.UNWIND_HISTORY_TABLE,
-    pub fn init(ctx: *const std.debug.cpu_context.Native) UnwindContext {
-        return .{
-            .pc = @returnAddress(),
-            .cur = switch (builtin.cpu.arch) {
-                .x86_64 => std.mem.zeroInit(windows.CONTEXT, .{
-                    .Rax = ctx.gprs.get(.rax),
-                    .Rcx = ctx.gprs.get(.rcx),
-                    .Rdx = ctx.gprs.get(.rdx),
-                    .Rbx = ctx.gprs.get(.rbx),
-                    .Rsp = ctx.gprs.get(.rsp),
-                    .Rbp = ctx.gprs.get(.rbp),
-                    .Rsi = ctx.gprs.get(.rsi),
-                    .Rdi = ctx.gprs.get(.rdi),
-                    .R8 = ctx.gprs.get(.r8),
-                    .R9 = ctx.gprs.get(.r9),
-                    .R10 = ctx.gprs.get(.r10),
-                    .R11 = ctx.gprs.get(.r11),
-                    .R12 = ctx.gprs.get(.r12),
-                    .R13 = ctx.gprs.get(.r13),
-                    .R14 = ctx.gprs.get(.r14),
-                    .R15 = ctx.gprs.get(.r15),
-                    .Rip = ctx.gprs.get(.rip),
-                }),
-                .aarch64, .aarch64_be => .{
-                    .ContextFlags = 0,
-                    .Cpsr = 0,
-                    .DUMMYUNIONNAME = .{ .X = ctx.x },
-                    .Sp = ctx.sp,
-                    .Pc = ctx.pc,
-                    .V = @splat(.{ .B = @splat(0) }),
-                    .Fpcr = 0,
-                    .Fpsr = 0,
-                    .Bcr = @splat(0),
-                    .Bvr = @splat(0),
-                    .Wcr = @splat(0),
-                    .Wvr = @splat(0),
-                },
-                .thumb => .{
-                    .ContextFlags = 0,
-                    .R0 = ctx.r[0],
-                    .R1 = ctx.r[1],
-                    .R2 = ctx.r[2],
-                    .R3 = ctx.r[3],
-                    .R4 = ctx.r[4],
-                    .R5 = ctx.r[5],
-                    .R6 = ctx.r[6],
-                    .R7 = ctx.r[7],
-                    .R8 = ctx.r[8],
-                    .R9 = ctx.r[9],
-                    .R10 = ctx.r[10],
-                    .R11 = ctx.r[11],
-                    .R12 = ctx.r[12],
-                    .Sp = ctx.r[13],
-                    .Lr = ctx.r[14],
-                    .Pc = ctx.r[15],
-                    .Cpsr = 0,
-                    .Fpcsr = 0,
-                    .Padding = 0,
-                    .DUMMYUNIONNAME = .{ .S = @splat(0) },
-                    .Bvr = @splat(0),
-                    .Bcr = @splat(0),
-                    .Wvr = @splat(0),
-                    .Wcr = @splat(0),
-                    .Padding2 = @splat(0),
-                },
-                else => comptime unreachable,
-            },
-            .history_table = std.mem.zeroes(windows.UNWIND_HISTORY_TABLE),
-        };
-    }
-    pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void {
-        _ = ctx;
-        _ = gpa;
-    }
-    pub fn getFp(ctx: *UnwindContext) usize {
-        return ctx.cur.getRegs().bp;
-    }
-};
-pub fn unwindFrame(module: *const WindowsModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) !usize {
-    _ = module;
-    _ = gpa;
-    _ = di;
-
-    const current_regs = context.cur.getRegs();
-    var image_base: windows.DWORD64 = undefined;
-    if (windows.ntdll.RtlLookupFunctionEntry(current_regs.ip, &image_base, &context.history_table)) |runtime_function| {
-        var handler_data: ?*anyopaque = null;
-        var establisher_frame: u64 = undefined;
-        _ = windows.ntdll.RtlVirtualUnwind(
-            windows.UNW_FLAG_NHANDLER,
-            image_base,
-            current_regs.ip,
-            runtime_function,
-            &context.cur,
-            &handler_data,
-            &establisher_frame,
-            null,
-        );
-    } else {
-        // leaf function
-        context.cur.setIp(@as(*const usize, @ptrFromInt(current_regs.sp)).*);
-        context.cur.setSp(current_regs.sp + @sizeOf(usize));
-    }
-
-    const next_regs = context.cur.getRegs();
-    const tib = &windows.teb().NtTib;
-    if (next_regs.sp < @intFromPtr(tib.StackLimit) or next_regs.sp > @intFromPtr(tib.StackBase)) {
-        context.pc = 0;
-        return 0;
-    }
-    // Like `DwarfUnwindContext.unwindFrame`, adjust our next lookup pc in case the `call` was this
-    // function's last instruction making `next_regs.ip` one byte past its end.
-    context.pc = next_regs.ip -| 1;
-    return next_regs.ip;
-}
-
-const WindowsModule = @This();
-
-const std = @import("../../std.zig");
-const Allocator = std.mem.Allocator;
-const Dwarf = std.debug.Dwarf;
-const Pdb = std.debug.Pdb;
-const assert = std.debug.assert;
-const coff = std.coff;
-const fs = std.fs;
-const mem = std.mem;
-const windows = std.os.windows;
-
-const builtin = @import("builtin");
-const native_endian = builtin.target.cpu.arch.endian();
lib/std/debug/Dwarf.zig
@@ -28,6 +28,7 @@ const Dwarf = @This();
 
 pub const expression = @import("Dwarf/expression.zig");
 pub const Unwind = @import("Dwarf/Unwind.zig");
+pub const SelfUnwinder = @import("Dwarf/SelfUnwinder.zig");
 
 /// Useful to temporarily enable while working on this file.
 const debug_debug_mode = false;
@@ -1458,8 +1459,8 @@ pub fn spRegNum(arch: std.Target.Cpu.Arch) u16 {
 
 /// Tells whether unwinding for this target is supported by the Dwarf standard.
 ///
-/// See also `std.debug.SelfInfo.supports_unwinding` which tells whether the Zig
-/// standard library has a working implementation of unwinding for this target.
+/// See also `std.debug.SelfInfo.can_unwind` which tells whether the Zig standard
+/// library has a working implementation of unwinding for the current target.
 pub fn supportsUnwinding(target: *const std.Target) bool {
     return switch (target.cpu.arch) {
         .amdgcn,
lib/std/debug/SelfInfo.zig
@@ -1,551 +0,0 @@
-//! Cross-platform abstraction for this binary's own debug information, with a
-//! goal of minimal code bloat and compilation speed penalty.
-
-const builtin = @import("builtin");
-const native_endian = native_arch.endian();
-const native_arch = builtin.cpu.arch;
-
-const std = @import("../std.zig");
-const mem = std.mem;
-const Allocator = std.mem.Allocator;
-const assert = std.debug.assert;
-const Dwarf = std.debug.Dwarf;
-const CpuContext = std.debug.cpu_context.Native;
-
-const stripInstructionPtrAuthCode = std.debug.stripInstructionPtrAuthCode;
-
-const root = @import("root");
-
-const SelfInfo = @This();
-
-/// Locks access to `modules`. However, does *not* lock the `Module.DebugInfo`, nor `lookup_cache`
-/// the implementation is responsible for locking as needed in its exposed methods.
-///
-/// TODO: to allow `SelfInfo` to work on freestanding, we currently just don't use this mutex there.
-/// That's a bad solution, but a better one depends on the standard library's general support for
-/// "bring your own OS" being improved.
-modules_mutex: switch (builtin.os.tag) {
-    else => std.Thread.Mutex,
-    .freestanding, .other => struct {
-        fn lock(_: @This()) void {}
-        fn unlock(_: @This()) void {}
-    },
-},
-/// Value is allocated into gpa to give it a stable pointer.
-modules: if (target_supported) std.AutoArrayHashMapUnmanaged(usize, *Module.DebugInfo) else void,
-lookup_cache: if (target_supported) Module.LookupCache else void,
-
-pub const Error = error{
-    /// The required debug info is invalid or corrupted.
-    InvalidDebugInfo,
-    /// The required debug info could not be found.
-    MissingDebugInfo,
-    /// The required debug info was found, and may be valid, but is not supported by this implementation.
-    UnsupportedDebugInfo,
-    /// The required debug info could not be read from disk due to some IO error.
-    ReadFailed,
-    OutOfMemory,
-    Unexpected,
-};
-
-/// Indicates whether the `SelfInfo` implementation has support for this target.
-pub const target_supported: bool = Module != void;
-
-/// Indicates whether the `SelfInfo` implementation has support for unwinding on this target.
-pub const supports_unwinding: bool = target_supported and Module.supports_unwinding;
-
-pub const UnwindContext = if (supports_unwinding) Module.UnwindContext;
-
-pub const init: SelfInfo = .{
-    .modules_mutex = .{},
-    .modules = .empty,
-    .lookup_cache = if (Module.LookupCache != void) .init,
-};
-
-pub fn deinit(self: *SelfInfo, gpa: Allocator) void {
-    for (self.modules.values()) |di| {
-        di.deinit(gpa);
-        gpa.destroy(di);
-    }
-    self.modules.deinit(gpa);
-    if (Module.LookupCache != void) self.lookup_cache.deinit(gpa);
-}
-
-pub fn unwindFrame(self: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error!usize {
-    comptime assert(supports_unwinding);
-    const module: Module = try .lookup(&self.lookup_cache, gpa, context.pc);
-    const di: *Module.DebugInfo = di: {
-        self.modules_mutex.lock();
-        defer self.modules_mutex.unlock();
-        const gop = try self.modules.getOrPut(gpa, module.key());
-        if (gop.found_existing) break :di gop.value_ptr.*;
-        errdefer _ = self.modules.pop().?;
-        const di = try gpa.create(Module.DebugInfo);
-        di.* = .init;
-        gop.value_ptr.* = di;
-        break :di di;
-    };
-    return module.unwindFrame(gpa, di, context);
-}
-
-pub fn getSymbolAtAddress(self: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol {
-    comptime assert(target_supported);
-    const module: Module = try .lookup(&self.lookup_cache, gpa, address);
-    const di: *Module.DebugInfo = di: {
-        self.modules_mutex.lock();
-        defer self.modules_mutex.unlock();
-        const gop = try self.modules.getOrPut(gpa, module.key());
-        if (gop.found_existing) break :di gop.value_ptr.*;
-        errdefer _ = self.modules.pop().?;
-        const di = try gpa.create(Module.DebugInfo);
-        di.* = .init;
-        gop.value_ptr.* = di;
-        break :di di;
-    };
-    return module.getSymbolAtAddress(gpa, di, address);
-}
-
-pub fn getModuleNameForAddress(self: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 {
-    comptime assert(target_supported);
-    const module: Module = try .lookup(&self.lookup_cache, gpa, address);
-    if (module.name.len == 0) return error.MissingDebugInfo;
-    return module.name;
-}
-
-/// `void` indicates that `SelfInfo` is not supported for this target.
-///
-/// This type contains the target-specific implementation. Logically, a `Module` represents a subset
-/// of the executable with its own debug information. This typically corresponds to what ELF calls a
-/// module, i.e. a shared library or executable image, but could be anything. For instance, it would
-/// be valid to consider the entire application one module, or on the other hand to consider each
-/// object file a module.
-///
-/// Because different threads can collect stack traces concurrently, the implementation must be able
-/// to tolerate concurrent calls to any method it implements.
-///
-/// This type must must expose the following declarations:
-///
-/// ```
-/// /// Holds state cached by the implementation between calls to `lookup`.
-/// /// This may be `void`, in which case the inner declarations can be omitted.
-/// pub const LookupCache = struct {
-///     pub const init: LookupCache;
-///     pub fn deinit(lc: *LookupCache, gpa: Allocator) void;
-/// };
-/// /// Holds debug information associated with a particular `Module`.
-/// pub const DebugInfo = struct {
-///     pub const init: DebugInfo;
-/// };
-/// /// Finds the `Module` corresponding to `address`.
-/// pub fn lookup(lc: *LookupCache, gpa: Allocator, address: usize) SelfInfo.Error!Module;
-/// /// Returns a unique identifier for this `Module`, such as a load address.
-/// pub fn key(mod: *const Module) usize;
-/// /// Locates and loads location information for the symbol corresponding to `address`.
-/// pub fn getSymbolAtAddress(
-///     mod: *const Module,
-///     gpa: Allocator,
-///     di: *DebugInfo,
-///     address: usize,
-/// ) SelfInfo.Error!std.debug.Symbol;
-/// /// Whether a reliable stack unwinding strategy, such as DWARF unwinding, is available.
-/// pub const supports_unwinding: bool;
-/// /// Only required if `supports_unwinding == true`.
-/// pub const UnwindContext = struct {
-///     /// A PC value representing the location in the last frame.
-///     pc: usize,
-///     pub fn init(ctx: *std.debug.cpu_context.Native, gpa: Allocator) Allocator.Error!UnwindContext;
-///     pub fn deinit(uc: *UnwindContext, gpa: Allocator) void;
-///     /// Returns the frame pointer associated with the last unwound stack frame. If the frame
-///     /// pointer is unknown, 0 may be returned instead.
-///     pub fn getFp(uc: *UnwindContext) usize;
-/// };
-/// /// Only required if `supports_unwinding == true`. Unwinds a single stack frame, and returns
-/// /// the frame's return address.
-/// pub fn unwindFrame(
-///     mod: *const Module,
-///     gpa: Allocator,
-///     di: *DebugInfo,
-///     ctx: *UnwindContext,
-/// ) SelfInfo.Error!usize;
-/// ```
-const Module: type = Module: {
-    // Allow overriding the target-specific `SelfInfo` implementation by exposing `root.debug.Module`.
-    if (@hasDecl(root, "debug") and @hasDecl(root.debug, "Module")) {
-        break :Module root.debug.Module;
-    }
-    break :Module switch (builtin.os.tag) {
-        .linux,
-        .netbsd,
-        .freebsd,
-        .dragonfly,
-        .openbsd,
-        .solaris,
-        .illumos,
-        => @import("SelfInfo/ElfModule.zig"),
-
-        .macos,
-        .ios,
-        .watchos,
-        .tvos,
-        .visionos,
-        => @import("SelfInfo/DarwinModule.zig"),
-
-        .uefi,
-        .windows,
-        => @import("SelfInfo/WindowsModule.zig"),
-
-        else => void,
-    };
-};
-
-/// An implementation of `UnwindContext` useful for DWARF-based unwinders. The `Module.unwindFrame`
-/// implementation should wrap `DwarfUnwindContext.unwindFrame`.
-pub const DwarfUnwindContext = struct {
-    cfa: ?usize,
-    pc: usize,
-    cpu_context: CpuContext,
-    vm: Dwarf.Unwind.VirtualMachine,
-    stack_machine: Dwarf.expression.StackMachine(.{ .call_frame_context = true }),
-
-    pub const Cache = struct {
-        /// TODO: to allow `DwarfUnwindContext` to work on freestanding, we currently just don't use
-        /// this mutex there. That's a bad solution, but a better one depends on the standard
-        /// library's general support for "bring your own OS" being improved.
-        mutex: switch (builtin.os.tag) {
-            else => std.Thread.Mutex,
-            .freestanding, .other => struct {
-                fn lock(_: @This()) void {}
-                fn unlock(_: @This()) void {}
-            },
-        },
-        buf: [num_slots]Slot,
-        const num_slots = 2048;
-        const Slot = struct {
-            const max_regs = 32;
-            pc: usize,
-            cie: *const Dwarf.Unwind.CommonInformationEntry,
-            cfa_rule: Dwarf.Unwind.VirtualMachine.CfaRule,
-            rules_regs: [max_regs]u16,
-            rules: [max_regs]Dwarf.Unwind.VirtualMachine.RegisterRule,
-            num_rules: u8,
-        };
-        /// This is a function rather than a declaration to avoid lowering a very large struct value
-        /// into the binary when most of it is `undefined`.
-        pub fn init(c: *Cache) void {
-            c.mutex = .{};
-            for (&c.buf) |*slot| slot.pc = 0;
-        }
-    };
-
-    pub fn init(cpu_context: *const CpuContext) DwarfUnwindContext {
-        comptime assert(supports_unwinding);
-
-        // `@constCast` is safe because we aren't going to store to the resulting pointer.
-        const raw_pc_ptr = regNative(@constCast(cpu_context), ip_reg_num) catch |err| switch (err) {
-            error.InvalidRegister => unreachable, // `ip_reg_num` is definitely valid
-            error.UnsupportedRegister => unreachable, // the implementation needs to support ip
-            error.IncompatibleRegisterSize => unreachable, // ip is definitely `usize`-sized
-        };
-        const pc = stripInstructionPtrAuthCode(raw_pc_ptr.*);
-
-        return .{
-            .cfa = null,
-            .pc = pc,
-            .cpu_context = cpu_context.*,
-            .vm = .{},
-            .stack_machine = .{},
-        };
-    }
-
-    pub fn deinit(self: *DwarfUnwindContext, gpa: Allocator) void {
-        self.vm.deinit(gpa);
-        self.stack_machine.deinit(gpa);
-        self.* = undefined;
-    }
-
-    pub fn getFp(self: *const DwarfUnwindContext) usize {
-        // `@constCast` is safe because we aren't going to store to the resulting pointer.
-        const ptr = regNative(@constCast(&self.cpu_context), fp_reg_num) catch |err| switch (err) {
-            error.InvalidRegister => unreachable, // `fp_reg_num` is definitely valid
-            error.UnsupportedRegister => unreachable, // the implementation needs to support fp
-            error.IncompatibleRegisterSize => unreachable, // fp is a pointer so is `usize`-sized
-        };
-        return ptr.*;
-    }
-
-    /// Unwind a stack frame using DWARF unwinding info, updating the register context.
-    ///
-    /// If `.eh_frame_hdr` is available and complete, it will be used to binary search for the FDE.
-    /// Otherwise, a linear scan of `.eh_frame` and `.debug_frame` is done to find the FDE. The latter
-    /// may require lazily loading the data in those sections.
-    ///
-    /// `explicit_fde_offset` is for cases where the FDE offset is known, such as when using macOS'
-    /// `__unwind_info` section.
-    pub fn unwindFrame(
-        context: *DwarfUnwindContext,
-        cache: *Cache,
-        gpa: Allocator,
-        unwind: *const Dwarf.Unwind,
-        load_offset: usize,
-        explicit_fde_offset: ?usize,
-    ) Error!usize {
-        return unwindFrameInner(context, cache, gpa, unwind, load_offset, explicit_fde_offset) catch |err| switch (err) {
-            error.InvalidDebugInfo,
-            error.MissingDebugInfo,
-            error.UnsupportedDebugInfo,
-            error.OutOfMemory,
-            => |e| return e,
-
-            error.UnsupportedAddrSize,
-            error.UnimplementedUserOpcode,
-            error.UnimplementedExpressionCall,
-            error.UnimplementedOpcode,
-            error.UnimplementedTypedComparison,
-            error.UnimplementedTypeConversion,
-            error.UnknownExpressionOpcode,
-            error.UnsupportedRegister,
-            => return error.UnsupportedDebugInfo,
-
-            error.InvalidRegister,
-            error.ReadFailed,
-            error.EndOfStream,
-            error.IncompatibleRegisterSize,
-            error.Overflow,
-            error.StreamTooLong,
-            error.InvalidOperand,
-            error.InvalidOpcode,
-            error.InvalidOperation,
-            error.InvalidCFARule,
-            error.IncompleteExpressionContext,
-            error.InvalidCFAOpcode,
-            error.InvalidExpression,
-            error.InvalidFrameBase,
-            error.InvalidIntegralTypeSize,
-            error.InvalidSubExpression,
-            error.InvalidTypeLength,
-            error.TruncatedIntegralType,
-            error.DivisionByZero,
-            error.InvalidExpressionValue,
-            error.NoExpressionValue,
-            error.RegisterSizeMismatch,
-            => return error.InvalidDebugInfo,
-        };
-    }
-    fn unwindFrameInner(
-        context: *DwarfUnwindContext,
-        cache: *Cache,
-        gpa: Allocator,
-        unwind: *const Dwarf.Unwind,
-        load_offset: usize,
-        explicit_fde_offset: ?usize,
-    ) !usize {
-        comptime assert(supports_unwinding);
-
-        if (context.pc == 0) return 0;
-
-        const pc_vaddr = context.pc - load_offset;
-
-        const cache_slot: Cache.Slot = slot: {
-            const slot_idx = std.hash.int(pc_vaddr) % Cache.num_slots;
-
-            {
-                cache.mutex.lock();
-                defer cache.mutex.unlock();
-                if (cache.buf[slot_idx].pc == pc_vaddr) break :slot cache.buf[slot_idx];
-            }
-
-            const fde_offset = explicit_fde_offset orelse try unwind.lookupPc(
-                pc_vaddr,
-                @sizeOf(usize),
-                native_endian,
-            ) orelse return error.MissingDebugInfo;
-            const cie, const fde = try unwind.getFde(fde_offset, native_endian);
-
-            // Check if the FDE *actually* includes the pc (`lookupPc` can return false positives).
-            if (pc_vaddr < fde.pc_begin or pc_vaddr >= fde.pc_begin + fde.pc_range) {
-                return error.MissingDebugInfo;
-            }
-
-            context.vm.reset();
-
-            const row = try context.vm.runTo(gpa, pc_vaddr, cie, &fde, @sizeOf(usize), native_endian);
-
-            if (row.columns.len > Cache.Slot.max_regs) return error.UnsupportedDebugInfo;
-
-            var slot: Cache.Slot = .{
-                .pc = pc_vaddr,
-                .cie = cie,
-                .cfa_rule = row.cfa,
-                .rules_regs = undefined,
-                .rules = undefined,
-                .num_rules = 0,
-            };
-            for (context.vm.rowColumns(&row)) |col| {
-                const i = slot.num_rules;
-                slot.rules_regs[i] = col.register;
-                slot.rules[i] = col.rule;
-                slot.num_rules += 1;
-            }
-
-            {
-                cache.mutex.lock();
-                defer cache.mutex.unlock();
-                cache.buf[slot_idx] = slot;
-            }
-
-            break :slot slot;
-        };
-
-        const format = cache_slot.cie.format;
-        const return_address_register = cache_slot.cie.return_address_register;
-
-        context.cfa = switch (cache_slot.cfa_rule) {
-            .none => return error.InvalidCFARule,
-            .reg_off => |ro| cfa: {
-                const ptr = try regNative(&context.cpu_context, ro.register);
-                break :cfa try applyOffset(ptr.*, ro.offset);
-            },
-            .expression => |expr| cfa: {
-                context.stack_machine.reset();
-                const value = try context.stack_machine.run(expr, gpa, .{
-                    .format = format,
-                    .cpu_context = &context.cpu_context,
-                }, context.cfa) orelse return error.NoExpressionValue;
-                switch (value) {
-                    .generic => |g| break :cfa g,
-                    else => return error.InvalidExpressionValue,
-                }
-            },
-        };
-
-        // If unspecified, we'll use the default rule for the return address register, which is
-        // typically equivalent to `.undefined` (meaning there is no return address), but may be
-        // overriden by ABIs.
-        var has_return_address: bool = builtin.cpu.arch.isAARCH64() and
-            return_address_register >= 19 and
-            return_address_register <= 28;
-
-        // Create a copy of the CPU context, to which we will apply the new rules.
-        var new_cpu_context = context.cpu_context;
-
-        // On all implemented architectures, the CFA is defined as being the previous frame's SP
-        (try regNative(&new_cpu_context, sp_reg_num)).* = context.cfa.?;
-
-        const rules_len = cache_slot.num_rules;
-        for (cache_slot.rules_regs[0..rules_len], cache_slot.rules[0..rules_len]) |register, rule| {
-            const new_val: union(enum) {
-                same,
-                undefined,
-                val: usize,
-                bytes: []const u8,
-            } = switch (rule) {
-                .default => val: {
-                    // The default rule is typically equivalent to `.undefined`, but ABIs may override it.
-                    if (builtin.cpu.arch.isAARCH64() and register >= 19 and register <= 28) {
-                        break :val .same;
-                    }
-                    break :val .undefined;
-                },
-                .undefined => .undefined,
-                .same_value => .same,
-                .offset => |offset| val: {
-                    const ptr: *const usize = @ptrFromInt(try applyOffset(context.cfa.?, offset));
-                    break :val .{ .val = ptr.* };
-                },
-                .val_offset => |offset| .{ .val = try applyOffset(context.cfa.?, offset) },
-                .register => |r| .{ .bytes = try context.cpu_context.dwarfRegisterBytes(r) },
-                .expression => |expr| val: {
-                    context.stack_machine.reset();
-                    const value = try context.stack_machine.run(expr, gpa, .{
-                        .format = format,
-                        .cpu_context = &context.cpu_context,
-                    }, context.cfa.?) orelse return error.NoExpressionValue;
-                    const ptr: *const usize = switch (value) {
-                        .generic => |addr| @ptrFromInt(addr),
-                        else => return error.InvalidExpressionValue,
-                    };
-                    break :val .{ .val = ptr.* };
-                },
-                .val_expression => |expr| val: {
-                    context.stack_machine.reset();
-                    const value = try context.stack_machine.run(expr, gpa, .{
-                        .format = format,
-                        .cpu_context = &context.cpu_context,
-                    }, context.cfa.?) orelse return error.NoExpressionValue;
-                    switch (value) {
-                        .generic => |val| break :val .{ .val = val },
-                        else => return error.InvalidExpressionValue,
-                    }
-                },
-            };
-            switch (new_val) {
-                .same => {},
-                .undefined => {
-                    const dest = try new_cpu_context.dwarfRegisterBytes(@intCast(register));
-                    @memset(dest, undefined);
-                },
-                .val => |val| {
-                    const dest = try new_cpu_context.dwarfRegisterBytes(@intCast(register));
-                    if (dest.len != @sizeOf(usize)) return error.RegisterSizeMismatch;
-                    const dest_ptr: *align(1) usize = @ptrCast(dest);
-                    dest_ptr.* = val;
-                },
-                .bytes => |src| {
-                    const dest = try new_cpu_context.dwarfRegisterBytes(@intCast(register));
-                    if (dest.len != src.len) return error.RegisterSizeMismatch;
-                    @memcpy(dest, src);
-                },
-            }
-            if (register == return_address_register) {
-                has_return_address = new_val != .undefined;
-            }
-        }
-
-        const return_address: usize = if (has_return_address) pc: {
-            const raw_ptr = try regNative(&new_cpu_context, return_address_register);
-            break :pc stripInstructionPtrAuthCode(raw_ptr.*);
-        } else 0;
-
-        (try regNative(&new_cpu_context, ip_reg_num)).* = return_address;
-
-        // The new CPU context is complete; flush changes.
-        context.cpu_context = new_cpu_context;
-
-        // The caller will subtract 1 from the return address to get an address corresponding to the
-        // function call. However, if this is a signal frame, that's actually incorrect, because the
-        // "return address" we have is the instruction which triggered the signal (if the signal
-        // handler returned, the instruction would be re-run). Compensate for this by incrementing
-        // the address in that case.
-        const adjusted_ret_addr = if (cache_slot.cie.is_signal_frame) return_address +| 1 else return_address;
-
-        // We also want to do that same subtraction here to get the PC for the next frame's FDE.
-        // This is because if the callee was noreturn, then the function call might be the caller's
-        // last instruction, so `return_address` might actually point outside of it!
-        context.pc = adjusted_ret_addr -| 1;
-
-        return adjusted_ret_addr;
-    }
-    /// Since register rules are applied (usually) during a panic,
-    /// checked addition / subtraction is used so that we can return
-    /// an error and fall back to FP-based unwinding.
-    fn applyOffset(base: usize, offset: i64) !usize {
-        return if (offset >= 0)
-            try std.math.add(usize, base, @as(usize, @intCast(offset)))
-        else
-            try std.math.sub(usize, base, @as(usize, @intCast(-offset)));
-    }
-
-    pub fn regNative(ctx: *CpuContext, num: u16) error{
-        InvalidRegister,
-        UnsupportedRegister,
-        IncompatibleRegisterSize,
-    }!*align(1) usize {
-        const bytes = try ctx.dwarfRegisterBytes(num);
-        if (bytes.len != @sizeOf(usize)) return error.IncompatibleRegisterSize;
-        return @ptrCast(bytes);
-    }
-
-    const ip_reg_num = Dwarf.ipRegNum(native_arch).?;
-    const fp_reg_num = Dwarf.fpRegNum(native_arch);
-    const sp_reg_num = Dwarf.spRegNum(native_arch);
-};
lib/std/debug.zig
@@ -19,11 +19,85 @@ const root = @import("root");
 pub const Dwarf = @import("debug/Dwarf.zig");
 pub const Pdb = @import("debug/Pdb.zig");
 pub const ElfFile = @import("debug/ElfFile.zig");
-pub const SelfInfo = @import("debug/SelfInfo.zig");
 pub const Info = @import("debug/Info.zig");
 pub const Coverage = @import("debug/Coverage.zig");
 pub const cpu_context = @import("debug/cpu_context.zig");
 
+/// This type abstracts the target-specific implementation of accessing this process' own debug
+/// information behind a generic interface which supports looking up source locations associated
+/// with addresses, as well as unwinding the stack where a safe mechanism to do so exists.
+///
+/// The Zig Standard Library provides default implementations of `SelfInfo` for common targets, but
+/// the implementation can be overriden by exposing `root.debug.SelfInfo`. Setting `SelfInfo` to
+/// `void` indicates that the `SelfInfo` API is not supported.
+///
+/// This type must expose the following declarations:
+///
+/// ```
+/// pub const init: SelfInfo;
+/// pub fn deinit(si: *SelfInfo, gpa: Allocator) void;
+///
+/// /// Returns the symbol and source location of the instruction at `address`.
+/// pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) SelfInfoError!Symbol;
+/// /// Returns a name for the "module" (e.g. shared library or executable image) containing `address`.
+/// pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) SelfInfoError![]const u8;
+///
+/// /// Whether a reliable stack unwinding strategy, such as DWARF unwinding, is available.
+/// pub const can_unwind: bool;
+/// /// Only required if `can_unwind == true`.
+/// pub const UnwindContext = struct {
+///     /// An address representing the instruction pointer in the last frame.
+///     pc: usize,
+///
+///     pub fn init(ctx: *cpu_context.Native, gpa: Allocator) Allocator.Error!UnwindContext;
+///     pub fn deinit(ctx: *UnwindContext, gpa: Allocator) void;
+///     /// Returns the frame pointer associated with the last unwound stack frame.
+///     /// If the frame pointer is unknown, 0 may be returned instead.
+///     pub fn getFp(uc: *UnwindContext) usize;
+/// };
+/// /// Only required if `can_unwind == true`. Unwinds a single stack frame, returning the frame's
+/// /// return address, or 0 if the end of the stack has been reached.
+/// pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) SelfInfoError!usize;
+/// ```
+pub const SelfInfo = if (@hasDecl(root, "debug") and @hasDecl(root.debug, "SelfInfo"))
+    root.debug.SelfInfo
+else switch (native_os) {
+    .linux,
+    .netbsd,
+    .freebsd,
+    .dragonfly,
+    .openbsd,
+    .solaris,
+    .illumos,
+    => @import("debug/SelfInfo/Elf.zig"),
+
+    .macos,
+    .ios,
+    .watchos,
+    .tvos,
+    .visionos,
+    => @import("debug/SelfInfo/Darwin.zig"),
+
+    .uefi,
+    .windows,
+    => @import("debug/SelfInfo/Windows.zig"),
+
+    else => void,
+};
+
+pub const SelfInfoError = error{
+    /// The required debug info is invalid or corrupted.
+    InvalidDebugInfo,
+    /// The required debug info could not be found.
+    MissingDebugInfo,
+    /// The required debug info was found, and may be valid, but is not supported by this implementation.
+    UnsupportedDebugInfo,
+    /// The required debug info could not be read from disk due to some IO error.
+    ReadFailed,
+    OutOfMemory,
+    Unexpected,
+};
+
 pub const simple_panic = @import("debug/simple_panic.zig");
 pub const no_panic = @import("debug/no_panic.zig");
 
@@ -240,7 +314,7 @@ pub fn print(comptime fmt: []const u8, args: anytype) void {
 
 /// Marked `inline` to propagate a comptime-known error to callers.
 pub inline fn getSelfDebugInfo() !*SelfInfo {
-    if (!SelfInfo.target_supported) return error.UnsupportedTarget;
+    if (SelfInfo == void) return error.UnsupportedTarget;
     const S = struct {
         var self_info: SelfInfo = .init;
     };
@@ -640,7 +714,7 @@ pub fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_
     while (true) switch (it.next()) {
         .switch_to_fp => |unwind_error| {
             if (StackIterator.fp_unwind_is_safe) continue; // no need to even warn
-            const module_name = di.getModuleNameForAddress(di_gpa, unwind_error.address) catch "???";
+            const module_name = di.getModuleName(di_gpa, unwind_error.address) catch "???";
             const caption: []const u8 = switch (unwind_error.err) {
                 error.MissingDebugInfo => "unwind info unavailable",
                 error.InvalidDebugInfo => "unwind info invalid",
@@ -753,9 +827,9 @@ pub fn dumpStackTrace(st: *const std.builtin.StackTrace) void {
 
 const StackIterator = union(enum) {
     /// Unwinding using debug info (e.g. DWARF CFI).
-    di: if (SelfInfo.supports_unwinding) SelfInfo.UnwindContext else noreturn,
+    di: if (SelfInfo != void and SelfInfo.can_unwind) SelfInfo.UnwindContext else noreturn,
     /// We will first report the *current* PC of this `UnwindContext`, then we will switch to `di`.
-    di_first: if (SelfInfo.supports_unwinding) SelfInfo.UnwindContext else noreturn,
+    di_first: if (SelfInfo != void and SelfInfo.can_unwind) SelfInfo.UnwindContext else noreturn,
     /// Naive frame-pointer-based unwinding. Very simple, but typically unreliable.
     fp: usize,
 
@@ -772,7 +846,7 @@ const StackIterator = union(enum) {
             }
         }
         if (opt_context_ptr) |context_ptr| {
-            if (!SelfInfo.supports_unwinding) return error.CannotUnwindFromContext;
+            if (SelfInfo == void or !SelfInfo.can_unwind) return error.CannotUnwindFromContext;
             // Use `di_first` here so we report the PC in the context before unwinding any further.
             return .{ .di_first = .init(context_ptr) };
         }
@@ -780,7 +854,8 @@ const StackIterator = union(enum) {
         // call to `current`. This effectively constrains stack trace collection and dumping to FP
         // unwinding when building with CBE for MSVC.
         if (!(builtin.zig_backend == .stage2_c and builtin.target.abi == .msvc) and
-            SelfInfo.supports_unwinding and
+            SelfInfo != void and
+            SelfInfo.can_unwind and
             cpu_context.Native != noreturn)
         {
             // We don't need `di_first` here, because our PC is in `std.debug`; we're only interested
@@ -820,7 +895,7 @@ const StackIterator = union(enum) {
         /// We were using `SelfInfo.UnwindInfo`, but are now switching to FP unwinding due to this error.
         switch_to_fp: struct {
             address: usize,
-            err: SelfInfo.Error,
+            err: SelfInfoError,
         },
     };
 
@@ -929,7 +1004,7 @@ pub inline fn stripInstructionPtrAuthCode(ptr: usize) usize {
 }
 
 fn printSourceAtAddress(gpa: Allocator, debug_info: *SelfInfo, writer: *Writer, address: usize, tty_config: tty.Config) Writer.Error!void {
-    const symbol: Symbol = debug_info.getSymbolAtAddress(gpa, address) catch |err| switch (err) {
+    const symbol: Symbol = debug_info.getSymbol(gpa, address) catch |err| switch (err) {
         error.MissingDebugInfo,
         error.UnsupportedDebugInfo,
         error.InvalidDebugInfo,
@@ -953,7 +1028,7 @@ fn printSourceAtAddress(gpa: Allocator, debug_info: *SelfInfo, writer: *Writer,
         symbol.source_location,
         address,
         symbol.name orelse "???",
-        symbol.compile_unit_name orelse debug_info.getModuleNameForAddress(gpa, address) catch "???",
+        symbol.compile_unit_name orelse debug_info.getModuleName(gpa, address) catch "???",
         tty_config,
     );
 }
@@ -1386,7 +1461,7 @@ pub fn dumpStackPointerAddr(prefix: []const u8) void {
 }
 
 test "manage resources correctly" {
-    if (!SelfInfo.target_supported) return error.SkipZigTest;
+    if (SelfInfo == void) return error.SkipZigTest;
     const S = struct {
         noinline fn showMyTrace() usize {
             return @returnAddress();
test/standalone/coff_dwarf/main.zig
@@ -14,7 +14,7 @@ pub fn main() void {
     var add_addr: usize = undefined;
     _ = add(1, 2, &add_addr);
 
-    const symbol = di.getSymbolAtAddress(gpa, add_addr) catch |err| fatal("failed to get symbol: {t}", .{err});
+    const symbol = di.getSymbol(gpa, add_addr) catch |err| fatal("failed to get symbol: {t}", .{err});
     defer if (symbol.source_location) |sl| gpa.free(sl.file_name);
 
     if (symbol.name == null) fatal("failed to resolve symbol name", .{});