Commit 180db2bf23

Alex Rønne Petersen <alex@alexrp.com>
2025-01-23 20:55:20
std.debug: Fall back to .eh_frame/.debug_frame if .eh_frame_hdr is incomplete.
When using the self-hosted backends, especially in incremental mode, the .eh_frame_hdr section may be incomplete, so we can't treat it as authoritative. Instead, if we started out intending to use .eh_frame_hdr but find that it's incomplete, load .eh_frame/.debug_frame on demand and use that info going forward.
1 parent 41185d2
Changed files (3)
lib/std/debug/Dwarf.zig
@@ -48,6 +48,8 @@ compile_unit_list: std.ArrayListUnmanaged(CompileUnit) = .empty,
 /// Filled later by the initializer
 func_list: std.ArrayListUnmanaged(Func) = .empty,
 
+/// Starts out non-`null` if the `.eh_frame_hdr` section is present. May become `null` later if we
+/// find that `.eh_frame_hdr` is incomplete.
 eh_frame_hdr: ?ExceptionFrameHeader = null,
 /// These lookup tables are only used if `eh_frame_hdr` is null
 cie_map: std.AutoArrayHashMapUnmanaged(u64, CommonInformationEntry) = .empty,
@@ -1754,10 +1756,12 @@ fn readDebugAddr(di: Dwarf, compile_unit: CompileUnit, index: u64) !u64 {
     };
 }
 
-/// If .eh_frame_hdr is present, then only the header needs to be parsed.
+/// If `.eh_frame_hdr` is present, then only the header needs to be parsed. Otherwise, `.eh_frame`
+/// and `.debug_frame` are scanned and a sorted list of FDEs is built for binary searching during
+/// unwinding. Even if `.eh_frame_hdr` is used, we may find during unwinding that it's incomplete,
+/// in which case we build the sorted list of FDEs at that point.
 ///
-/// Otherwise, .eh_frame and .debug_frame are scanned and a sorted list
-/// of FDEs is built for binary searching during unwinding.
+/// See also `scanCieFdeInfo`.
 pub fn scanAllUnwindInfo(di: *Dwarf, allocator: Allocator, base_address: usize) !void {
     if (di.section(.eh_frame_hdr)) |eh_frame_hdr| blk: {
         var fbr: FixedBufferReader = .{ .buf = eh_frame_hdr, .endian = native_endian };
@@ -1797,6 +1801,12 @@ pub fn scanAllUnwindInfo(di: *Dwarf, allocator: Allocator, base_address: usize)
         return;
     }
 
+    try di.scanCieFdeInfo(allocator, base_address);
+}
+
+/// Scan `.eh_frame` and `.debug_frame` and build a sorted list of FDEs for binary searching during
+/// unwinding.
+pub fn scanCieFdeInfo(di: *Dwarf, allocator: Allocator, base_address: usize) !void {
     const frame_sections = [2]Section.Id{ .eh_frame, .debug_frame };
     for (frame_sections) |frame_section| {
         if (di.section(frame_section)) |section_data| {
@@ -2125,7 +2135,7 @@ pub const ElfModule = struct {
         return self.dwarf.getSymbol(allocator, relocated_address);
     }
 
-    pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
+    pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*Dwarf {
         _ = allocator;
         _ = address;
         return &self.dwarf;
lib/std/debug/SelfInfo.zig
@@ -707,7 +707,7 @@ pub const Module = switch (native_os) {
             }
         }
 
-        pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
+        pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*Dwarf {
             return if ((try self.getOFileInfoForAddress(allocator, address)).o_file_info) |o_file_info| &o_file_info.di else null;
         }
     },
@@ -784,7 +784,7 @@ pub const Module = switch (native_os) {
             return .{};
         }
 
-        pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
+        pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*Dwarf {
             _ = allocator;
             _ = address;
 
@@ -808,7 +808,7 @@ pub const Module = switch (native_os) {
             return .{};
         }
 
-        pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
+        pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*Dwarf {
             _ = self;
             _ = allocator;
             _ = address;
@@ -1156,11 +1156,12 @@ test machoSearchSymbols {
 /// 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 unwindFrameMachO(
+    allocator: Allocator,
+    base_address: usize,
     context: *UnwindContext,
     ma: *std.debug.MemoryAccessor,
     unwind_info: []const u8,
     eh_frame: ?[]const u8,
-    module_base_address: usize,
 ) !usize {
     const header = std.mem.bytesAsValue(
         macho.unwind_info_section_header,
@@ -1172,7 +1173,7 @@ pub fn unwindFrameMachO(
     );
     if (indices.len == 0) return error.MissingUnwindInfo;
 
-    const mapped_pc = context.pc - module_base_address;
+    const mapped_pc = context.pc - base_address;
     const second_level_index = blk: {
         var left: usize = 0;
         var len: usize = indices.len;
@@ -1351,7 +1352,7 @@ pub fn unwindFrameMachO(
                 else stack_size: {
                     // In .STACK_IND, the stack size is inferred from the subq instruction at the beginning of the function.
                     const sub_offset_addr =
-                        module_base_address +
+                        base_address +
                         entry.function_offset +
                         encoding.value.x86_64.frameless.stack.indirect.sub_offset;
                     if (ma.load(usize, sub_offset_addr) == null) return error.InvalidUnwindInfo;
@@ -1416,7 +1417,7 @@ pub fn unwindFrameMachO(
                 break :blk new_ip;
             },
             .DWARF => {
-                return unwindFrameMachODwarf(context, ma, eh_frame orelse return error.MissingEhFrame, @intCast(encoding.value.x86_64.dwarf));
+                return unwindFrameMachODwarf(allocator, base_address, context, ma, eh_frame orelse return error.MissingEhFrame, @intCast(encoding.value.x86_64.dwarf));
             },
         },
         .aarch64, .aarch64_be => switch (encoding.mode.arm64) {
@@ -1430,7 +1431,7 @@ pub fn unwindFrameMachO(
                 break :blk new_ip;
             },
             .DWARF => {
-                return unwindFrameMachODwarf(context, ma, eh_frame orelse return error.MissingEhFrame, @intCast(encoding.value.arm64.dwarf));
+                return unwindFrameMachODwarf(allocator, base_address, context, ma, eh_frame orelse return error.MissingEhFrame, @intCast(encoding.value.arm64.dwarf));
             },
             .FRAME => blk: {
                 const fp = (try regValueNative(context.thread_context, fpRegNum(reg_context), reg_context)).*;
@@ -1555,13 +1556,16 @@ pub inline fn stripInstructionPtrAuthCode(ptr: usize) usize {
 
 /// Unwind a stack frame using DWARF unwinding info, updating the register context.
 ///
-/// If `.eh_frame_hdr` is available, 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.
+/// 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 __unwind_info
 /// defers unwinding to DWARF. This is an offset into the `.eh_frame` section.
 pub fn unwindFrameDwarf(
-    di: *const Dwarf,
+    allocator: Allocator,
+    di: *Dwarf,
+    base_address: usize,
     context: *UnwindContext,
     ma: *std.debug.MemoryAccessor,
     explicit_fde_offset: ?usize,
@@ -1570,10 +1574,7 @@ pub fn unwindFrameDwarf(
     if (context.pc == 0) return 0;
 
     // Find the FDE and CIE
-    var cie: Dwarf.CommonInformationEntry = undefined;
-    var fde: Dwarf.FrameDescriptionEntry = undefined;
-
-    if (explicit_fde_offset) |fde_offset| {
+    const cie, const fde = if (explicit_fde_offset) |fde_offset| blk: {
         const dwarf_section: Dwarf.Section.Id = .eh_frame;
         const frame_section = di.section(dwarf_section) orelse return error.MissingFDE;
         if (fde_offset >= frame_section.len) return error.MissingFDE;
@@ -1594,7 +1595,7 @@ pub fn unwindFrameDwarf(
         const cie_entry_header = try Dwarf.EntryHeader.read(&fbr, null, dwarf_section);
         if (cie_entry_header.type != .cie) return Dwarf.bad();
 
-        cie = try Dwarf.CommonInformationEntry.parse(
+        const cie = try Dwarf.CommonInformationEntry.parse(
             cie_entry_header.entry_bytes,
             0,
             true,
@@ -1604,8 +1605,7 @@ pub fn unwindFrameDwarf(
             @sizeOf(usize),
             native_endian,
         );
-
-        fde = try Dwarf.FrameDescriptionEntry.parse(
+        const fde = try Dwarf.FrameDescriptionEntry.parse(
             fde_entry_header.entry_bytes,
             0,
             true,
@@ -1613,17 +1613,44 @@ pub fn unwindFrameDwarf(
             @sizeOf(usize),
             native_endian,
         );
-    } else if (di.eh_frame_hdr) |header| {
-        const eh_frame_len = if (di.section(.eh_frame)) |eh_frame| eh_frame.len else null;
-        try header.findEntry(
-            ma,
-            eh_frame_len,
-            @intFromPtr(di.section(.eh_frame_hdr).?.ptr),
-            context.pc,
-            &cie,
-            &fde,
-        );
-    } else {
+
+        break :blk .{ cie, fde };
+    } else blk: {
+        // `.eh_frame_hdr` may be incomplete. We'll try it first, but if the lookup fails, we fall
+        // back to loading `.eh_frame`/`.debug_frame` and using those from that point on.
+
+        if (di.eh_frame_hdr) |header| hdr: {
+            const eh_frame_len = if (di.section(.eh_frame)) |eh_frame| eh_frame.len else null;
+
+            var cie: Dwarf.CommonInformationEntry = undefined;
+            var fde: Dwarf.FrameDescriptionEntry = undefined;
+
+            header.findEntry(
+                ma,
+                eh_frame_len,
+                @intFromPtr(di.section(.eh_frame_hdr).?.ptr),
+                context.pc,
+                &cie,
+                &fde,
+            ) catch |err| switch (err) {
+                error.InvalidDebugInfo => {
+                    // `.eh_frame_hdr` appears to be incomplete, so go ahead and populate `cie_map`
+                    // and `fde_list`, and fall back to the binary search logic below.
+                    try di.scanCieFdeInfo(allocator, base_address);
+
+                    // Since `.eh_frame_hdr` is incomplete, we're very likely to get more lookup
+                    // failures using it, and we've just built a complete, sorted list of FDEs
+                    // anyway, so just stop using `.eh_frame_hdr` altogether.
+                    di.eh_frame_hdr = null;
+
+                    break :hdr;
+                },
+                else => return err,
+            };
+
+            break :blk .{ cie, fde };
+        }
+
         const index = std.sort.binarySearch(Dwarf.FrameDescriptionEntry, di.fde_list.items, context.pc, struct {
             pub fn compareFn(pc: usize, item: Dwarf.FrameDescriptionEntry) std.math.Order {
                 if (pc < item.pc_begin) return .lt;
@@ -1635,9 +1662,11 @@ pub fn unwindFrameDwarf(
             }
         }.compareFn);
 
-        fde = if (index) |i| di.fde_list.items[i] else return error.MissingFDE;
-        cie = di.cie_map.get(fde.cie_length_offset) orelse return error.MissingCIE;
-    }
+        const fde = if (index) |i| di.fde_list.items[i] else return error.MissingFDE;
+        const cie = di.cie_map.get(fde.cie_length_offset) orelse return error.MissingCIE;
+
+        break :blk .{ cie, fde };
+    };
 
     var expression_context: Dwarf.expression.Context = .{
         .format = cie.format,
@@ -1802,6 +1831,8 @@ pub fn supportsUnwinding(target: std.Target) bool {
 }
 
 fn unwindFrameMachODwarf(
+    allocator: Allocator,
+    base_address: usize,
     context: *UnwindContext,
     ma: *std.debug.MemoryAccessor,
     eh_frame: []const u8,
@@ -1818,7 +1849,7 @@ fn unwindFrameMachODwarf(
         .owned = false,
     };
 
-    return unwindFrameDwarf(&di, context, ma, fde_offset);
+    return unwindFrameDwarf(allocator, &di, base_address, context, ma, fde_offset);
 }
 
 /// This is a virtual machine that runs DWARF call frame instructions.
lib/std/debug.zig
@@ -732,11 +732,12 @@ pub const StackIterator = struct {
                 // via DWARF before attempting to use the compact unwind info will produce incorrect results.
                 if (module.unwind_info) |unwind_info| {
                     if (SelfInfo.unwindFrameMachO(
+                        unwind_state.debug_info.allocator,
+                        module.base_address,
                         &unwind_state.dwarf_context,
                         &it.ma,
                         unwind_info,
                         module.eh_frame,
-                        module.base_address,
                     )) |return_address| {
                         return return_address;
                     } else |err| {
@@ -748,7 +749,14 @@ pub const StackIterator = struct {
         }
 
         if (try module.getDwarfInfoForAddress(unwind_state.debug_info.allocator, unwind_state.dwarf_context.pc)) |di| {
-            return SelfInfo.unwindFrameDwarf(di, &unwind_state.dwarf_context, &it.ma, null);
+            return SelfInfo.unwindFrameDwarf(
+                unwind_state.debug_info.allocator,
+                di,
+                module.base_address,
+                &unwind_state.dwarf_context,
+                &it.ma,
+                null,
+            );
         } else return error.MissingDebugInfo;
     }