From 405075f7455492717ac6cba505603eb74e4f54b9 Mon Sep 17 00:00:00 2001 From: mlugg Date: Thu, 4 Sep 2025 00:39:38 +0100 Subject: [PATCH] SelfInfo: load eh_frame/debug_frame from ELF file if eh_frame_hdr omitted --- lib/std/debug/Dwarf/ElfModule.zig | 88 ++++++++++++++++++++++------ lib/std/debug/SelfInfo/ElfModule.zig | 73 +++++++++++++++++------ 2 files changed, 124 insertions(+), 37 deletions(-) diff --git a/lib/std/debug/Dwarf/ElfModule.zig b/lib/std/debug/Dwarf/ElfModule.zig index 02b94e580d..4d425b1718 100644 --- a/lib/std/debug/Dwarf/ElfModule.zig +++ b/lib/std/debug/Dwarf/ElfModule.zig @@ -3,6 +3,13 @@ dwarf: Dwarf, +/// If we encounter a `.eh_frame` section while loading the ELF module, it is stored here and may be +/// used with `Dwarf.Unwind` for call stack unwinding. +eh_frame: ?UnwindSection, +/// If we encounter a `.debug_frame` section while loading the ELF module, it is stored here and may +/// be used with `Dwarf.Unwind` for call stack unwinding. +debug_frame: ?UnwindSection, + /// The memory-mapped ELF file, which is referenced by `dwarf`. This field is here only so that /// this memory can be unmapped by `ElfModule.deinit`. mapped_file: []align(std.heap.page_size_min) const u8, @@ -11,10 +18,18 @@ mapped_file: []align(std.heap.page_size_min) const u8, /// be unmapped by `ElfModule.deinit`. mapped_debug_file: ?[]align(std.heap.page_size_min) const u8, -pub fn deinit(em: *ElfModule, allocator: Allocator) void { - em.dwarf.deinit(allocator); +pub const UnwindSection = struct { + vaddr: u64, + bytes: []const u8, + owned: bool, +}; + +pub fn deinit(em: *ElfModule, gpa: Allocator) void { + em.dwarf.deinit(gpa); std.posix.munmap(em.mapped_file); if (em.mapped_debug_file) |m| std.posix.munmap(m); + if (em.eh_frame) |s| if (s.owned) gpa.free(s.bytes); + if (em.debug_frame) |s| if (s.owned) gpa.free(s.bytes); } pub const LoadError = error{ @@ -98,7 +113,6 @@ pub fn load( )[0..hdr.e_shnum]; var sections: Dwarf.SectionArray = @splat(null); - // Combine section list. This takes ownership over any owned sections from the parent scope. if (parent_sections) |ps| { for (ps, §ions) |*parent, *section_elem| { @@ -110,6 +124,12 @@ pub fn load( } errdefer for (sections) |opt_section| if (opt_section) |s| if (s.owned) gpa.free(s.data); + var eh_frame_section: ?UnwindSection = null; + errdefer if (eh_frame_section) |s| if (s.owned) gpa.free(s.bytes); + + var debug_frame_section: ?UnwindSection = null; + errdefer if (debug_frame_section) |s| if (s.owned) gpa.free(s.bytes); + var separate_debug_filename: ?[]const u8 = null; var separate_debug_crc: ?u32 = null; @@ -128,17 +148,35 @@ pub fn load( continue; } - var section_index: ?usize = null; - inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |sect, i| { - if (mem.eql(u8, "." ++ sect.name, name)) section_index = i; + const section_id: union(enum) { + dwarf: Dwarf.Section.Id, + eh_frame, + debug_frame, + } = s: { + inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields) |s| { + if (mem.eql(u8, "." ++ s.name, name)) { + break :s .{ .dwarf = @enumFromInt(s.value) }; + } + } + if (mem.eql(u8, ".eh_frame", name)) break :s .eh_frame; + if (mem.eql(u8, ".debug_frame", name)) break :s .debug_frame; + continue; + }; + + switch (section_id) { + .dwarf => |i| if (sections[@intFromEnum(i)] != null) continue, + .eh_frame => if (eh_frame_section != null) continue, + .debug_frame => if (debug_frame_section != null) continue, } - if (section_index == null) continue; - if (sections[section_index.?] != null) continue; if (mapped_mem.len < shdr.sh_offset + shdr.sh_size) return error.InvalidDebugInfo; - const section_bytes = mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)]; - sections[section_index.?] = if ((shdr.sh_flags & elf.SHF_COMPRESSED) > 0) blk: { - var section_reader: Reader = .fixed(section_bytes); + const raw_section_bytes = mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)]; + + const section_bytes: []const u8, const section_owned: bool = section: { + if ((shdr.sh_flags & elf.SHF_COMPRESSED) == 0) { + break :section .{ raw_section_bytes, false }; + } + var section_reader: Reader = .fixed(raw_section_bytes); const chdr = section_reader.takeStruct(elf.Chdr, endian) catch continue; if (chdr.ch_type != .ZLIB) continue; @@ -153,14 +191,24 @@ pub fn load( Dwarf.invalidDebugInfoDetected(); continue; } - break :blk .{ - .data = try decompressed_section.toOwnedSlice(gpa), - .owned = true, - }; - } else .{ - .data = section_bytes, - .owned = false, + break :section .{ try decompressed_section.toOwnedSlice(gpa), true }; }; + switch (section_id) { + .dwarf => |id| sections[@intFromEnum(id)] = .{ + .data = section_bytes, + .owned = section_owned, + }, + .eh_frame => eh_frame_section = .{ + .vaddr = shdr.sh_addr, + .bytes = section_bytes, + .owned = section_owned, + }, + .debug_frame => debug_frame_section = .{ + .vaddr = shdr.sh_addr, + .bytes = section_bytes, + .owned = section_owned, + }, + } } const missing_debug_info = @@ -305,9 +353,11 @@ pub fn load( var dwarf: Dwarf = .{ .sections = sections }; try dwarf.open(gpa, endian); return .{ + .dwarf = dwarf, + .eh_frame = eh_frame_section, + .debug_frame = debug_frame_section, .mapped_file = parent_mapped_mem orelse mapped_mem, .mapped_debug_file = if (parent_mapped_mem != null) mapped_mem else null, - .dwarf = dwarf, }; } diff --git a/lib/std/debug/SelfInfo/ElfModule.zig b/lib/std/debug/SelfInfo/ElfModule.zig index 25ce1827b7..8d06be151e 100644 --- a/lib/std/debug/SelfInfo/ElfModule.zig +++ b/lib/std/debug/SelfInfo/ElfModule.zig @@ -8,10 +8,10 @@ pub const LookupCache = void; pub const DebugInfo = struct { loaded_elf: ?Dwarf.ElfModule, - unwind: ?Dwarf.Unwind, + unwind: [2]?Dwarf.Unwind, pub const init: DebugInfo = .{ .loaded_elf = null, - .unwind = null, + .unwind = @splat(null), }; pub fn deinit(di: *DebugInfo, gpa: Allocator) void { if (di.loaded_elf) |*loaded_elf| loaded_elf.deinit(gpa); @@ -143,30 +143,67 @@ pub fn getSymbolAtAddress(module: *const ElfModule, gpa: Allocator, di: *DebugIn => return error.InvalidDebugInfo, }; } -fn loadUnwindInfo(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Error!void { - const section_bytes = module.gnu_eh_frame orelse return error.MissingDebugInfo; // MLUGG TODO: load from file - - 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, - }; - - var unwind: Dwarf.Unwind = .initEhFrameHdr(header, section_vaddr, @ptrFromInt(module.load_offset + header.eh_frame_vaddr)); +fn prepareUnwindLookup(unwind: *Dwarf.Unwind, gpa: Allocator) Error!void { unwind.prepareLookup(gpa, @sizeOf(usize), native_endian) 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 => return error.InvalidDebugInfo, error.UnsupportedAddrSize, error.UnsupportedDwarfVersion => return error.UnsupportedDebugInfo, }; - - di.unwind = unwind; +} +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(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. + try module.loadDwarf(gpa, di); + const opt_debug_frame = &di.loaded_elf.?.debug_frame; + const opt_eh_frame = &di.loaded_elf.?.eh_frame; + // 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[0] = .initSection(.debug_frame, debug_frame.vaddr, debug_frame.bytes); + if (opt_eh_frame.*) |*eh_frame| { + buf[1] = .initSection(.eh_frame, eh_frame.vaddr, eh_frame.bytes); + break :unwinds buf[0..2]; + } + break :unwinds buf[0..1]; + } else if (opt_eh_frame.*) |eh_frame| { + buf[0] = .initSection(.eh_frame, eh_frame.vaddr, eh_frame.bytes); + break :unwinds buf[0..1]; + } + return error.MissingDebugInfo; + }; + errdefer for (unwinds) |*u| u.deinit(gpa); + for (unwinds) |*u| try prepareUnwindLookup(u, gpa); + switch (unwinds.len) { + 0 => unreachable, + 1 => di.unwind = .{ unwinds[0], null }, + 2 => di.unwind = .{ unwinds[0], unwinds[1] }, + else => unreachable, + } } pub fn unwindFrame(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, context: *UnwindContext) Error!usize { - if (di.unwind == null) try module.loadUnwindInfo(gpa, di); - return context.unwindFrameDwarf(&di.unwind.?, module.load_offset, null); + if (di.unwind[0] == null) try module.loadUnwindInfo(gpa, di); + std.debug.assert(di.unwind[0] != null); + for (&di.unwind) |*opt_unwind| { + const unwind = &(opt_unwind.* orelse break); + return context.unwindFrameDwarf(unwind, module.load_offset, null) catch |err| switch (err) { + error.MissingDebugInfo => continue, // try the next one + else => |e| return e, + }; + } + return error.MissingDebugInfo; } const ElfModule = @This();