Commit 0a330d4f94

Matthew Lugg <mlugg@mlugg.co.uk>
2025-11-19 13:19:22
std.debug.Info: basic Mach-O support
1 parent 0caca62
Changed files (3)
lib
std
Build
debug
tools
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));
 }
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, 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();
 
-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();
+            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);
+            return .{
+                .impl = .{ .elf = elf_file },
+                .coverage = coverage,
+            };
+        },
+        .macho => {
+            const path_str = try path.toString(gpa);
+            defer gpa.free(path_str);
 
-    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;
+            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);
+            }
+        },
+    }
 }
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);