From 0a330d4f947c1b05ac9f7d624443e2f80db2912f Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Wed, 19 Nov 2025 12:19:22 +0000 Subject: [PATCH] std.debug.Info: basic Mach-O support --- lib/std/Build/Fuzz.zig | 29 ++++++++++++-- lib/std/debug/Info.zig | 91 ++++++++++++++++++++++++++++++------------ tools/dump-cov.zig | 35 ++++++++++++---- 3 files changed, 117 insertions(+), 38 deletions(-) diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig index 6dd4f70f9f..7a22522d6b 100644 --- a/lib/std/Build/Fuzz.zig +++ b/lib/std/Build/Fuzz.zig @@ -383,7 +383,14 @@ fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutO errdefer gop.value_ptr.coverage.deinit(fuzz.gpa); const rebuilt_exe_path = run_step.rebuilt_executable.?; - var debug_info = std.debug.Info.load(fuzz.gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| { + const target = run_step.producer.?.rootModuleTarget(); + var debug_info = std.debug.Info.load( + fuzz.gpa, + rebuilt_exe_path, + &gop.value_ptr.coverage, + target.ofmt, + target.cpu.arch, + ) catch |err| { log.err("step '{s}': failed to load debug information for '{f}': {s}", .{ run_step.step.name, rebuilt_exe_path, @errorName(err), }); @@ -479,9 +486,23 @@ fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReporte if (false) { const sl = coverage_map.source_locations[index]; const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename); - log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{ - addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1], - }); + if (pcs.len == 1) { + log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{ + addr, file_name, sl.line, sl.column, + }); + } else if (index == 0) { + log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{ + addr, file_name, sl.line, sl.column, pcs[index + 1], + }); + } else if (index == pcs.len - 1) { + log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{ + addr, file_name, sl.line, sl.column, index, pcs[index - 1], + }); + } else { + log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{ + addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1], + }); + } } try coverage_map.entry_points.append(fuzz.gpa, @intCast(index)); } diff --git a/lib/std/debug/Info.zig b/lib/std/debug/Info.zig index 74119a3ea4..921cd36ab8 100644 --- a/lib/std/debug/Info.zig +++ b/lib/std/debug/Info.zig @@ -9,49 +9,67 @@ const std = @import("../std.zig"); const Allocator = std.mem.Allocator; const Path = std.Build.Cache.Path; -const ElfFile = std.debug.ElfFile; const assert = std.debug.assert; const Coverage = std.debug.Coverage; const SourceLocation = std.debug.Coverage.SourceLocation; +const ElfFile = std.debug.ElfFile; +const MachOFile = std.debug.MachOFile; + const Info = @This(); -/// Sorted by key, ascending. -address_map: std.AutoArrayHashMapUnmanaged(u64, ElfFile), +impl: union(enum) { + elf: ElfFile, + macho: MachOFile, +}, /// Externally managed, outlives this `Info` instance. coverage: *Coverage, -pub const LoadError = std.fs.File.OpenError || ElfFile.LoadError || std.debug.Dwarf.ScanError || error{MissingDebugInfo}; +pub const LoadError = std.fs.File.OpenError || ElfFile.LoadError || MachOFile.Error || std.debug.Dwarf.ScanError || error{ MissingDebugInfo, UnsupportedDebugInfo }; -pub fn load(gpa: Allocator, path: Path, coverage: *Coverage) LoadError!Info { - var file = try path.root_dir.handle.openFile(path.sub_path, .{}); - defer file.close(); +pub fn load(gpa: Allocator, path: Path, coverage: *Coverage, format: std.Target.ObjectFormat, arch: std.Target.Cpu.Arch) LoadError!Info { + switch (format) { + .elf => { + var file = try path.root_dir.handle.openFile(path.sub_path, .{}); + defer file.close(); - var elf_file: ElfFile = try .load(gpa, file, null, &.none); - errdefer elf_file.deinit(gpa); + var elf_file: ElfFile = try .load(gpa, file, null, &.none); + errdefer elf_file.deinit(gpa); - if (elf_file.dwarf == null) return error.MissingDebugInfo; - try elf_file.dwarf.?.open(gpa, elf_file.endian); - try elf_file.dwarf.?.populateRanges(gpa, elf_file.endian); + if (elf_file.dwarf == null) return error.MissingDebugInfo; + try elf_file.dwarf.?.open(gpa, elf_file.endian); + try elf_file.dwarf.?.populateRanges(gpa, elf_file.endian); - var info: Info = .{ - .address_map = .{}, - .coverage = coverage, - }; - try info.address_map.put(gpa, 0, elf_file); - errdefer comptime unreachable; // elf_file is owned by the map now - return info; + return .{ + .impl = .{ .elf = elf_file }, + .coverage = coverage, + }; + }, + .macho => { + const path_str = try path.toString(gpa); + defer gpa.free(path_str); + + var macho_file: MachOFile = try .load(gpa, path_str, arch); + errdefer macho_file.deinit(gpa); + + return .{ + .impl = .{ .macho = macho_file }, + .coverage = coverage, + }; + }, + else => return error.UnsupportedDebugInfo, + } } pub fn deinit(info: *Info, gpa: Allocator) void { - for (info.address_map.values()) |*elf_file| { - elf_file.dwarf.?.deinit(gpa); + switch (info.impl) { + .elf => |*ef| ef.deinit(gpa), + .macho => |*mf| mf.deinit(gpa), } - info.address_map.deinit(gpa); info.* = undefined; } -pub const ResolveAddressesError = Coverage.ResolveAddressesDwarfError; +pub const ResolveAddressesError = Coverage.ResolveAddressesDwarfError || error{UnsupportedDebugInfo}; /// Given an array of virtual memory addresses, sorted ascending, outputs a /// corresponding array of source locations. @@ -64,7 +82,28 @@ pub fn resolveAddresses( output: []SourceLocation, ) ResolveAddressesError!void { assert(sorted_pc_addrs.len == output.len); - if (info.address_map.entries.len != 1) @panic("TODO"); - const elf_file = &info.address_map.values()[0]; - return info.coverage.resolveAddressesDwarf(gpa, elf_file.endian, sorted_pc_addrs, output, &elf_file.dwarf.?); + switch (info.impl) { + .elf => |*ef| return info.coverage.resolveAddressesDwarf(gpa, ef.endian, sorted_pc_addrs, output, &ef.dwarf.?), + .macho => |*mf| { + // Resolving all of the addresses at once unfortunately isn't so easy in Mach-O binaries + // due to split debug information. For now, we'll just resolve the addreses one by one. + for (sorted_pc_addrs, output) |pc_addr, *src_loc| { + const dwarf, const dwarf_pc_addr = mf.getDwarfForAddress(gpa, pc_addr) catch |err| switch (err) { + error.InvalidMachO, error.InvalidDwarf => return error.InvalidDebugInfo, + else => |e| return e, + }; + if (dwarf.ranges.items.len == 0) { + dwarf.populateRanges(gpa, .little) catch |err| switch (err) { + error.EndOfStream, + error.Overflow, + error.StreamTooLong, + error.ReadFailed, + => return error.InvalidDebugInfo, + else => |e| return e, + }; + } + try info.coverage.resolveAddressesDwarf(gpa, .little, &.{dwarf_pc_addr}, src_loc[0..1], dwarf); + } + }, + } } diff --git a/tools/dump-cov.zig b/tools/dump-cov.zig index 249783b927..3dd91de612 100644 --- a/tools/dump-cov.zig +++ b/tools/dump-cov.zig @@ -8,31 +8,50 @@ const assert = std.debug.assert; const SeenPcsHeader = std.Build.abi.fuzz.SeenPcsHeader; pub fn main() !void { - var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init; - defer _ = general_purpose_allocator.deinit(); - const gpa = general_purpose_allocator.allocator(); + var debug_allocator: std.heap.DebugAllocator(.{}) = .init; + defer _ = debug_allocator.deinit(); + const gpa = debug_allocator.allocator(); - var arena_instance = std.heap.ArenaAllocator.init(gpa); + var arena_instance: std.heap.ArenaAllocator = .init(gpa); defer arena_instance.deinit(); const arena = arena_instance.allocator(); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const args = try std.process.argsAlloc(arena); + + const target_query_str = switch (args.len) { + 3 => "native", + 4 => args[3], + else => return fatal( + \\usage: {0s} path/to/exe path/to/coverage [target] + \\ if omitted, 'target' defaults to 'native' + \\ example: {0s} zig-out/test .zig-cache/v/xxxxxxxx x86_64-linux + , .{if (args.len == 0) "dump-cov" else args[0]}), + }; + + const target = std.zig.resolveTargetQueryOrFatal(io, try .parse(.{ + .arch_os_abi = target_query_str, + })); + const exe_file_name = args[1]; const cov_file_name = args[2]; const exe_path: Path = .{ - .root_dir = std.Build.Cache.Directory.cwd(), + .root_dir = .cwd(), .sub_path = exe_file_name, }; const cov_path: Path = .{ - .root_dir = std.Build.Cache.Directory.cwd(), + .root_dir = .cwd(), .sub_path = cov_file_name, }; - var coverage = std.debug.Coverage.init; + var coverage: std.debug.Coverage = .init; defer coverage.deinit(gpa); - var debug_info = std.debug.Info.load(gpa, exe_path, &coverage) catch |err| { + var debug_info = std.debug.Info.load(gpa, exe_path, &coverage, target.ofmt, target.cpu.arch) catch |err| { fatal("failed to load debug info for {f}: {s}", .{ exe_path, @errorName(err) }); }; defer debug_info.deinit(gpa);