authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-11-20 17:48:35+00:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-11-20 17:48:35+00:00
log8a73fc8d8ee0065e4d07e473ce6ab095cebfaece
tree36bed2955666bdfce7b96e2f33c9409bd26f03c1
parenta9568ed2963298864e5c9a92a3eafe81771128ff
parenta87b5332319b20347916f77e57253e2df4b2a3af
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #25981 from mlugg/macos-fuzz-2

make the fuzzer vaguely work on macOS

21 files changed, 876 insertions(+), 534 deletions(-)

lib/build-web/fuzz.zig+15-14
......@@ -228,20 +228,21 @@ fn unpackSourcesInner(tar_bytes: []u8) !void {
228228 if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
229229 log.debug("found file: '{s}'", .{tar_file.name});
230230 const file_name = try gpa.dupe(u8, tar_file.name);
231 if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
232 const pkg_name = file_name[0..pkg_name_end];
233 const gop = try Walk.modules.getOrPut(gpa, pkg_name);
234 const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
235 if (!gop.found_existing or
236 std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
237 std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
238 {
239 gop.value_ptr.* = file;
240 }
241 const file_bytes = tar_reader.take(@intCast(tar_file.size)) catch unreachable;
242 it.unread_file_bytes = 0; // we have read the whole thing
243 assert(file == try Walk.add_file(file_name, file_bytes));
244 }
231 // This is a hack to guess modules from the tar file contents. To handle modules
232 // properly, the build system will need to change the structure here to have one
233 // directory per module. This in turn requires compiler enhancements to allow
234 // the build system to actually discover the required information.
235 const mod_name, const is_module_root = p: {
236 if (std.mem.find(u8, file_name, "std/")) |i| break :p .{ "std", std.mem.eql(u8, file_name[i + 4 ..], "std.zig") };
237 if (std.mem.endsWith(u8, file_name, "/builtin.zig")) break :p .{ "builtin", true };
238 break :p .{ "root", std.mem.endsWith(u8, file_name, "/root.zig") };
239 };
240 const gop = try Walk.modules.getOrPut(gpa, mod_name);
241 const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
242 if (!gop.found_existing or is_module_root) gop.value_ptr.* = file;
243 const file_bytes = tar_reader.take(@intCast(tar_file.size)) catch unreachable;
244 it.unread_file_bytes = 0; // we have read the whole thing
245 assert(file == try Walk.add_file(file_name, file_bytes));
245246 } else {
246247 log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name});
247248 }
lib/compiler/test_runner.zig+1-1
......@@ -184,7 +184,7 @@ fn mainServer() !void {
184184 const test_fn = builtin.test_functions[index];
185185 const entry_addr = @intFromPtr(test_fn.func);
186186
187 try server.serveU64Message(.fuzz_start_addr, entry_addr);
187 try server.serveU64Message(.fuzz_start_addr, fuzz_abi.fuzzer_unslide_address(entry_addr));
188188 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
189189 is_fuzz_test = false;
190190 fuzz_test_index = index;
lib/fuzzer.zig+28-6
......@@ -116,13 +116,18 @@ const Executable = struct {
116116 "failed to init memory map for coverage file '{s}': {t}",
117117 .{ &coverage_file_name, e },
118118 );
119 map.appendSliceAssumeCapacity(mem.asBytes(&abi.SeenPcsHeader{
119 map.appendSliceAssumeCapacity(@ptrCast(&abi.SeenPcsHeader{
120120 .n_runs = 0,
121121 .unique_runs = 0,
122122 .pcs_len = pcs.len,
123123 }));
124124 map.appendNTimesAssumeCapacity(0, pc_bitset_usizes * @sizeOf(usize));
125 map.appendSliceAssumeCapacity(mem.sliceAsBytes(pcs));
125 // Relocations have been applied to `pcs` so it contains runtime addresses (with slide
126 // applied). We need to translate these to the virtual addresses as on disk.
127 for (pcs) |pc| {
128 const pc_vaddr = fuzzer_unslide_address(pc);
129 map.appendSliceAssumeCapacity(@ptrCast(&pc_vaddr));
130 }
126131 return map;
127132 } else {
128133 const size = coverage_file.getEndPos() catch |e| panic(
......@@ -215,7 +220,16 @@ const Executable = struct {
215220 .{ self.pc_counters.len, pcs.len },
216221 );
217222
218 self.pc_digest = std.hash.Wyhash.hash(0, mem.sliceAsBytes(pcs));
223 self.pc_digest = digest: {
224 // Relocations have been applied to `pcs` so it contains runtime addresses (with slide
225 // applied). We need to translate these to the virtual addresses as on disk.
226 var h: std.hash.Wyhash = .init(0);
227 for (pcs) |pc| {
228 const pc_vaddr = fuzzer_unslide_address(pc);
229 h.update(@ptrCast(&pc_vaddr));
230 }
231 break :digest h.final();
232 };
219233 self.shared_seen_pcs = getCoverageFile(cache_dir, pcs, self.pc_digest);
220234
221235 return self;
......@@ -622,6 +636,14 @@ export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void {
622636 }
623637}
624638
639export fn fuzzer_unslide_address(addr: usize) usize {
640 const si = std.debug.getSelfDebugInfo() catch @compileError("unsupported");
641 const slide = si.getModuleSlide(std.debug.getDebugInfoAllocator(), addr) catch |err| {
642 std.debug.panic("failed to find virtual address slide: {t}", .{err});
643 };
644 return addr - slide;
645}
646
625647/// Helps determine run uniqueness in the face of recursion.
626648/// Currently not used by the fuzzer.
627649export threadlocal var __sancov_lowest_stack: usize = 0;
......@@ -1185,13 +1207,13 @@ const Mutation = enum {
11851207 const j = rng.uintAtMostBiased(usize, corpus[splice_i].len - len);
11861208 out.appendSliceAssumeCapacity(corpus[splice_i][j..][0..len]);
11871209 },
1188 .@"const" => out.appendSliceAssumeCapacity(mem.asBytes(
1210 .@"const" => out.appendSliceAssumeCapacity(@ptrCast(
11891211 &data_ctx[rng.uintLessThanBiased(usize, data_ctx.len)],
11901212 )),
1191 .small => out.appendSliceAssumeCapacity(mem.asBytes(
1213 .small => out.appendSliceAssumeCapacity(@ptrCast(
11921214 &mem.nativeTo(data_ctx[0], rng.int(SmallValue), data_ctx[1]),
11931215 )),
1194 .few => out.appendSliceAssumeCapacity(mem.asBytes(
1216 .few => out.appendSliceAssumeCapacity(@ptrCast(
11951217 &fewValue(rng, data_ctx[0], data_ctx[1]),
11961218 )),
11971219 }
lib/std/Build/Fuzz.zig+25-4
......@@ -383,7 +383,14 @@ fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutO
383383 errdefer gop.value_ptr.coverage.deinit(fuzz.gpa);
384384
385385 const rebuilt_exe_path = run_step.rebuilt_executable.?;
386 var debug_info = std.debug.Info.load(fuzz.gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| {
386 const target = run_step.producer.?.rootModuleTarget();
387 var debug_info = std.debug.Info.load(
388 fuzz.gpa,
389 rebuilt_exe_path,
390 &gop.value_ptr.coverage,
391 target.ofmt,
392 target.cpu.arch,
393 ) catch |err| {
387394 log.err("step '{s}': failed to load debug information for '{f}': {s}", .{
388395 run_step.step.name, rebuilt_exe_path, @errorName(err),
389396 });
......@@ -479,9 +486,23 @@ fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReporte
479486 if (false) {
480487 const sl = coverage_map.source_locations[index];
481488 const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
482 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{
483 addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
484 });
489 if (pcs.len == 1) {
490 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{
491 addr, file_name, sl.line, sl.column,
492 });
493 } else if (index == 0) {
494 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{
495 addr, file_name, sl.line, sl.column, pcs[index + 1],
496 });
497 } else if (index == pcs.len - 1) {
498 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{
499 addr, file_name, sl.line, sl.column, index, pcs[index - 1],
500 });
501 } else {
502 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{
503 addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
504 });
505 }
485506 }
486507 try coverage_map.entry_points.append(fuzz.gpa, @intCast(index));
487508}
lib/std/Build/Step/CheckObject.zig+14-15
......@@ -729,10 +729,10 @@ const MachODumper = struct {
729729 imports: std.ArrayListUnmanaged([]const u8) = .empty,
730730
731731 fn parse(ctx: *ObjectContext) !void {
732 var it = ctx.getLoadCommandIterator();
732 var it = try ctx.getLoadCommandIterator();
733733 var i: usize = 0;
734 while (it.next()) |cmd| {
735 switch (cmd.cmd()) {
734 while (try it.next()) |cmd| {
735 switch (cmd.hdr.cmd) {
736736 .SEGMENT_64 => {
737737 const seg = cmd.cast(macho.segment_command_64).?;
738738 try ctx.segments.append(ctx.gpa, seg);
......@@ -771,14 +771,13 @@ const MachODumper = struct {
771771 return mem.sliceTo(@as([*:0]const u8, @ptrCast(ctx.strtab.items.ptr + off)), 0);
772772 }
773773
774 fn getLoadCommandIterator(ctx: ObjectContext) macho.LoadCommandIterator {
775 const data = ctx.data[@sizeOf(macho.mach_header_64)..][0..ctx.header.sizeofcmds];
776 return .{ .ncmds = ctx.header.ncmds, .buffer = data };
774 fn getLoadCommandIterator(ctx: ObjectContext) !macho.LoadCommandIterator {
775 return .init(&ctx.header, ctx.data[@sizeOf(macho.mach_header_64)..]);
777776 }
778777
779 fn getLoadCommand(ctx: ObjectContext, cmd: macho.LC) ?macho.LoadCommandIterator.LoadCommand {
780 var it = ctx.getLoadCommandIterator();
781 while (it.next()) |lc| if (lc.cmd() == cmd) {
778 fn getLoadCommand(ctx: ObjectContext, cmd: macho.LC) !?macho.LoadCommandIterator.LoadCommand {
779 var it = try ctx.getLoadCommandIterator();
780 while (try it.next()) |lc| if (lc.hdr.cmd == cmd) {
782781 return lc;
783782 };
784783 return null;
......@@ -872,9 +871,9 @@ const MachODumper = struct {
872871 \\LC {d}
873872 \\cmd {s}
874873 \\cmdsize {d}
875 , .{ index, @tagName(lc.cmd()), lc.cmdsize() });
874 , .{ index, @tagName(lc.hdr.cmd), lc.hdr.cmdsize });
876875
877 switch (lc.cmd()) {
876 switch (lc.hdr.cmd) {
878877 .SEGMENT_64 => {
879878 const seg = lc.cast(macho.segment_command_64).?;
880879 try writer.writeByte('\n');
......@@ -1592,9 +1591,9 @@ const MachODumper = struct {
15921591 .headers => {
15931592 try ObjectContext.dumpHeader(ctx.header, writer);
15941593
1595 var it = ctx.getLoadCommandIterator();
1594 var it = try ctx.getLoadCommandIterator();
15961595 var i: usize = 0;
1597 while (it.next()) |cmd| {
1596 while (try it.next()) |cmd| {
15981597 try ObjectContext.dumpLoadCommand(cmd, i, writer);
15991598 try writer.writeByte('\n');
16001599
......@@ -1615,7 +1614,7 @@ const MachODumper = struct {
16151614 .dyld_weak_bind,
16161615 .dyld_lazy_bind,
16171616 => {
1618 const cmd = ctx.getLoadCommand(.DYLD_INFO_ONLY) orelse
1617 const cmd = try ctx.getLoadCommand(.DYLD_INFO_ONLY) orelse
16191618 return step.fail("no dyld info found", .{});
16201619 const lc = cmd.cast(macho.dyld_info_command).?;
16211620
......@@ -1649,7 +1648,7 @@ const MachODumper = struct {
16491648 },
16501649
16511650 .exports => blk: {
1652 if (ctx.getLoadCommand(.DYLD_INFO_ONLY)) |cmd| {
1651 if (try ctx.getLoadCommand(.DYLD_INFO_ONLY)) |cmd| {
16531652 const lc = cmd.cast(macho.dyld_info_command).?;
16541653 if (lc.export_size > 0) {
16551654 const data = ctx.data[lc.export_off..][0..lc.export_size];
lib/std/Build/Step/Compile.zig+5
......@@ -1932,6 +1932,11 @@ pub fn rebuildInFuzzMode(c: *Compile, gpa: Allocator, progress_node: std.Progres
19321932 c.step.result_error_bundle.deinit(gpa);
19331933 c.step.result_error_bundle = std.zig.ErrorBundle.empty;
19341934
1935 if (c.step.result_failed_command) |cmd| {
1936 gpa.free(cmd);
1937 c.step.result_failed_command = null;
1938 }
1939
19351940 const zig_args = try getZigArgs(c, true);
19361941 const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false, null, gpa);
19371942 return maybe_output_bin_path.?;
lib/std/Build/Step/Run.zig+11-2
......@@ -1140,6 +1140,12 @@ pub fn rerunInFuzzMode(
11401140 .output_file, .output_directory => unreachable,
11411141 }
11421142 }
1143
1144 if (run.step.result_failed_command) |cmd| {
1145 fuzz.gpa.free(cmd);
1146 run.step.result_failed_command = null;
1147 }
1148
11431149 const has_side_effects = false;
11441150 const rand_int = std.crypto.random.int(u64);
11451151 const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
......@@ -1150,7 +1156,7 @@ pub fn rerunInFuzzMode(
11501156 .web_server = null, // only needed for time reports
11511157 .ttyconf = fuzz.ttyconf,
11521158 .unit_test_timeout_ns = null, // don't time out fuzz tests for now
1153 .gpa = undefined, // not used by `runCommand`
1159 .gpa = fuzz.gpa,
11541160 }, .{
11551161 .unit_test_index = unit_test_index,
11561162 .fuzz = fuzz,
......@@ -1870,7 +1876,10 @@ fn pollZigTest(
18701876 // test. For instance, if the test runner leaves this much time between us requesting a test to
18711877 // start and it acknowledging the test starting, we terminate the child and raise an error. This
18721878 // *should* never happen, but could in theory be caused by some very unlucky IB in a test.
1873 const response_timeout_ns = @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
1879 const response_timeout_ns: ?u64 = ns: {
1880 if (fuzz_context != null) break :ns null; // don't timeout fuzz tests
1881 break :ns @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
1882 };
18741883
18751884 const stdout = poller.reader(.stdout);
18761885 const stderr = poller.reader(.stderr);
lib/std/Build/abi.zig+1
......@@ -145,6 +145,7 @@ pub const fuzz = struct {
145145 pub extern fn fuzzer_init_test(test_one: TestOne, unit_test_name: Slice) void;
146146 pub extern fn fuzzer_new_input(bytes: Slice) void;
147147 pub extern fn fuzzer_main(limit_kind: LimitKind, amount: u64) void;
148 pub extern fn fuzzer_unslide_address(addr: usize) usize;
148149
149150 pub const Slice = extern struct {
150151 ptr: [*]const u8,
lib/std/Io/Writer.zig+16-6
......@@ -270,16 +270,17 @@ fn writeSplatHeaderLimitFinish(
270270 remaining -= copy_len;
271271 if (remaining == 0) break :v;
272272 }
273 for (data[0 .. data.len - 1]) |buf| if (buf.len != 0) {
274 const copy_len = @min(header.len, remaining);
275 vecs[i] = buf;
273 for (data[0 .. data.len - 1]) |buf| {
274 if (buf.len == 0) continue;
275 const copy_len = @min(buf.len, remaining);
276 vecs[i] = buf[0..copy_len];
276277 i += 1;
277278 remaining -= copy_len;
278279 if (remaining == 0) break :v;
279280 if (vecs.len - i == 0) break :v;
280 };
281 }
281282 const pattern = data[data.len - 1];
282 if (splat == 1) {
283 if (splat == 1 or remaining < pattern.len) {
283284 vecs[i] = pattern[0..@min(remaining, pattern.len)];
284285 i += 1;
285286 break :v;
......@@ -915,7 +916,16 @@ pub fn sendFileHeader(
915916 if (new_end <= w.buffer.len) {
916917 @memcpy(w.buffer[w.end..][0..header.len], header);
917918 w.end = new_end;
918 return header.len + try w.vtable.sendFile(w, file_reader, limit);
919 const file_bytes = w.vtable.sendFile(w, file_reader, limit) catch |err| switch (err) {
920 error.ReadFailed, error.WriteFailed => |e| return e,
921 error.EndOfStream, error.Unimplemented => |e| {
922 // These errors are non-fatal, so if we wrote any header bytes, we will report that
923 // and suppress this error. Only if there was no header may we return the error.
924 if (header.len != 0) return header.len;
925 return e;
926 },
927 };
928 return header.len + file_bytes;
919929 }
920930 const buffered_contents = limit.slice(file_reader.interface.buffered());
921931 const n = try w.vtable.drain(w, &.{ header, buffered_contents }, 1);
lib/std/debug.zig+2-1
......@@ -21,6 +21,7 @@ const root = @import("root");
2121pub const Dwarf = @import("debug/Dwarf.zig");
2222pub const Pdb = @import("debug/Pdb.zig");
2323pub const ElfFile = @import("debug/ElfFile.zig");
24pub const MachOFile = @import("debug/MachOFile.zig");
2425pub const Info = @import("debug/Info.zig");
2526pub const Coverage = @import("debug/Coverage.zig");
2627pub const cpu_context = @import("debug/cpu_context.zig");
......@@ -1366,7 +1367,7 @@ test printLineFromFile {
13661367
13671368/// The returned allocator should be thread-safe if the compilation is multi-threaded, because
13681369/// multiple threads could capture and/or print stack traces simultaneously.
1369fn getDebugInfoAllocator() Allocator {
1370pub fn getDebugInfoAllocator() Allocator {
13701371 // Allow overriding the debug info allocator by exposing `root.debug.getDebugInfoAllocator`.
13711372 if (@hasDecl(root, "debug") and @hasDecl(root.debug, "getDebugInfoAllocator")) {
13721373 return root.debug.getDebugInfoAllocator();
lib/std/debug/Info.zig+65-26
......@@ -9,49 +9,67 @@
99const std = @import("../std.zig");
1010const Allocator = std.mem.Allocator;
1111const Path = std.Build.Cache.Path;
12const ElfFile = std.debug.ElfFile;
1312const assert = std.debug.assert;
1413const Coverage = std.debug.Coverage;
1514const SourceLocation = std.debug.Coverage.SourceLocation;
1615
16const ElfFile = std.debug.ElfFile;
17const MachOFile = std.debug.MachOFile;
18
1719const Info = @This();
1820
19/// Sorted by key, ascending.
20address_map: std.AutoArrayHashMapUnmanaged(u64, ElfFile),
21impl: union(enum) {
22 elf: ElfFile,
23 macho: MachOFile,
24},
2125/// Externally managed, outlives this `Info` instance.
2226coverage: *Coverage,
2327
24pub const LoadError = std.fs.File.OpenError || ElfFile.LoadError || std.debug.Dwarf.ScanError || error{MissingDebugInfo};
28pub const LoadError = std.fs.File.OpenError || ElfFile.LoadError || MachOFile.Error || std.debug.Dwarf.ScanError || error{ MissingDebugInfo, UnsupportedDebugInfo };
29
30pub fn load(gpa: Allocator, path: Path, coverage: *Coverage, format: std.Target.ObjectFormat, arch: std.Target.Cpu.Arch) LoadError!Info {
31 switch (format) {
32 .elf => {
33 var file = try path.root_dir.handle.openFile(path.sub_path, .{});
34 defer file.close();
2535
26pub fn load(gpa: Allocator, path: Path, coverage: *Coverage) LoadError!Info {
27 var file = try path.root_dir.handle.openFile(path.sub_path, .{});
28 defer file.close();
36 var elf_file: ElfFile = try .load(gpa, file, null, &.none);
37 errdefer elf_file.deinit(gpa);
2938
30 var elf_file: ElfFile = try .load(gpa, file, null, &.none);
31 errdefer elf_file.deinit(gpa);
39 if (elf_file.dwarf == null) return error.MissingDebugInfo;
40 try elf_file.dwarf.?.open(gpa, elf_file.endian);
41 try elf_file.dwarf.?.populateRanges(gpa, elf_file.endian);
3242
33 if (elf_file.dwarf == null) return error.MissingDebugInfo;
34 try elf_file.dwarf.?.open(gpa, elf_file.endian);
35 try elf_file.dwarf.?.populateRanges(gpa, elf_file.endian);
43 return .{
44 .impl = .{ .elf = elf_file },
45 .coverage = coverage,
46 };
47 },
48 .macho => {
49 const path_str = try path.toString(gpa);
50 defer gpa.free(path_str);
3651
37 var info: Info = .{
38 .address_map = .{},
39 .coverage = coverage,
40 };
41 try info.address_map.put(gpa, 0, elf_file);
42 errdefer comptime unreachable; // elf_file is owned by the map now
43 return info;
52 var macho_file: MachOFile = try .load(gpa, path_str, arch);
53 errdefer macho_file.deinit(gpa);
54
55 return .{
56 .impl = .{ .macho = macho_file },
57 .coverage = coverage,
58 };
59 },
60 else => return error.UnsupportedDebugInfo,
61 }
4462}
4563
4664pub fn deinit(info: *Info, gpa: Allocator) void {
47 for (info.address_map.values()) |*elf_file| {
48 elf_file.dwarf.?.deinit(gpa);
65 switch (info.impl) {
66 .elf => |*ef| ef.deinit(gpa),
67 .macho => |*mf| mf.deinit(gpa),
4968 }
50 info.address_map.deinit(gpa);
5169 info.* = undefined;
5270}
5371
54pub const ResolveAddressesError = Coverage.ResolveAddressesDwarfError;
72pub const ResolveAddressesError = Coverage.ResolveAddressesDwarfError || error{UnsupportedDebugInfo};
5573
5674/// Given an array of virtual memory addresses, sorted ascending, outputs a
5775/// corresponding array of source locations.
......@@ -64,7 +82,28 @@ pub fn resolveAddresses(
6482 output: []SourceLocation,
6583) ResolveAddressesError!void {
6684 assert(sorted_pc_addrs.len == output.len);
67 if (info.address_map.entries.len != 1) @panic("TODO");
68 const elf_file = &info.address_map.values()[0];
69 return info.coverage.resolveAddressesDwarf(gpa, elf_file.endian, sorted_pc_addrs, output, &elf_file.dwarf.?);
85 switch (info.impl) {
86 .elf => |*ef| return info.coverage.resolveAddressesDwarf(gpa, ef.endian, sorted_pc_addrs, output, &ef.dwarf.?),
87 .macho => |*mf| {
88 // Resolving all of the addresses at once unfortunately isn't so easy in Mach-O binaries
89 // due to split debug information. For now, we'll just resolve the addreses one by one.
90 for (sorted_pc_addrs, output) |pc_addr, *src_loc| {
91 const dwarf, const dwarf_pc_addr = mf.getDwarfForAddress(gpa, pc_addr) catch |err| switch (err) {
92 error.InvalidMachO, error.InvalidDwarf => return error.InvalidDebugInfo,
93 else => |e| return e,
94 };
95 if (dwarf.ranges.items.len == 0) {
96 dwarf.populateRanges(gpa, .little) catch |err| switch (err) {
97 error.EndOfStream,
98 error.Overflow,
99 error.StreamTooLong,
100 error.ReadFailed,
101 => return error.InvalidDebugInfo,
102 else => |e| return e,
103 };
104 }
105 try info.coverage.resolveAddressesDwarf(gpa, .little, &.{dwarf_pc_addr}, src_loc[0..1], dwarf);
106 }
107 },
108 }
70109}
lib/std/debug/MachOFile.zig created+548
......@@ -0,0 +1,548 @@
1mapped_memory: []align(std.heap.page_size_min) const u8,
2symbols: []const Symbol,
3strings: []const u8,
4text_vmaddr: u64,
5
6/// Key is index into `strings` of the file path.
7ofiles: std.AutoArrayHashMapUnmanaged(u32, Error!OFile),
8
9pub const Error = error{
10 InvalidMachO,
11 InvalidDwarf,
12 MissingDebugInfo,
13 UnsupportedDebugInfo,
14 ReadFailed,
15 OutOfMemory,
16};
17
18pub fn deinit(mf: *MachOFile, gpa: Allocator) void {
19 for (mf.ofiles.values()) |*maybe_of| {
20 const of = &(maybe_of.* catch continue);
21 posix.munmap(of.mapped_memory);
22 of.dwarf.deinit(gpa);
23 of.symbols_by_name.deinit(gpa);
24 }
25 mf.ofiles.deinit(gpa);
26 gpa.free(mf.symbols);
27 posix.munmap(mf.mapped_memory);
28}
29
30pub fn load(gpa: Allocator, path: []const u8, arch: std.Target.Cpu.Arch) Error!MachOFile {
31 switch (arch) {
32 .x86_64, .aarch64 => {},
33 else => unreachable,
34 }
35
36 const all_mapped_memory = try mapDebugInfoFile(path);
37 errdefer posix.munmap(all_mapped_memory);
38
39 // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal
40 // binary": a simple file format which contains Mach-O binaries for multiple targets. For
41 // instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
42 // for both ARM64 macOS and x86_64 macOS.
43 if (all_mapped_memory.len < 4) return error.InvalidMachO;
44 const magic = std.mem.readInt(u32, all_mapped_memory.ptr[0..4], .little);
45
46 // The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
47 const mapped_macho = switch (magic) {
48 macho.MH_MAGIC_64 => all_mapped_memory,
49
50 macho.FAT_CIGAM => mapped_macho: {
51 // This is the universal binary format (aka a "fat binary").
52 var fat_r: Io.Reader = .fixed(all_mapped_memory);
53 const hdr = fat_r.takeStruct(macho.fat_header, .big) catch |err| switch (err) {
54 error.ReadFailed => unreachable,
55 error.EndOfStream => return error.InvalidMachO,
56 };
57 const want_cpu_type = switch (arch) {
58 .x86_64 => macho.CPU_TYPE_X86_64,
59 .aarch64 => macho.CPU_TYPE_ARM64,
60 else => unreachable,
61 };
62 for (0..hdr.nfat_arch) |_| {
63 const fat_arch = fat_r.takeStruct(macho.fat_arch, .big) catch |err| switch (err) {
64 error.ReadFailed => unreachable,
65 error.EndOfStream => return error.InvalidMachO,
66 };
67 if (fat_arch.cputype != want_cpu_type) continue;
68 if (fat_arch.offset + fat_arch.size > all_mapped_memory.len) return error.InvalidMachO;
69 break :mapped_macho all_mapped_memory[fat_arch.offset..][0..fat_arch.size];
70 }
71 // `arch` was not present in the fat binary.
72 return error.MissingDebugInfo;
73 },
74
75 // Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
76 // will be fairly easy to add support here if necessary; it's very similar to above.
77 macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
78
79 else => return error.InvalidMachO,
80 };
81
82 var r: Io.Reader = .fixed(mapped_macho);
83 const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) {
84 error.ReadFailed => unreachable,
85 error.EndOfStream => return error.InvalidMachO,
86 };
87
88 if (hdr.magic != macho.MH_MAGIC_64)
89 return error.InvalidMachO;
90
91 const symtab: macho.symtab_command, const text_vmaddr: u64 = lcs: {
92 var it: macho.LoadCommandIterator = try .init(&hdr, mapped_macho[@sizeOf(macho.mach_header_64)..]);
93 var symtab: ?macho.symtab_command = null;
94 var text_vmaddr: ?u64 = null;
95 while (try it.next()) |cmd| switch (cmd.hdr.cmd) {
96 .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidMachO,
97 .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
98 if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
99 text_vmaddr = seg_cmd.vmaddr;
100 },
101 else => {},
102 };
103 break :lcs .{
104 symtab orelse return error.MissingDebugInfo,
105 text_vmaddr orelse return error.MissingDebugInfo,
106 };
107 };
108
109 const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1];
110
111 var symbols: std.ArrayList(Symbol) = try .initCapacity(gpa, symtab.nsyms);
112 defer symbols.deinit(gpa);
113
114 // This map is temporary; it is used only to detect duplicates here. This is
115 // necessary because we prefer to use STAB ("symbolic debugging table") symbols,
116 // but they might not be present, so we track normal symbols too.
117 // Indices match 1-1 with those of `symbols`.
118 var symbol_names: std.StringArrayHashMapUnmanaged(void) = .empty;
119 defer symbol_names.deinit(gpa);
120 try symbol_names.ensureUnusedCapacity(gpa, symtab.nsyms);
121
122 var ofile: u32 = undefined;
123 var last_sym: Symbol = undefined;
124 var state: enum {
125 init,
126 oso_open,
127 oso_close,
128 bnsym,
129 fun_strx,
130 fun_size,
131 ensym,
132 } = .init;
133
134 var sym_r: Io.Reader = .fixed(mapped_macho[symtab.symoff..]);
135 for (0..symtab.nsyms) |_| {
136 const sym = sym_r.takeStruct(macho.nlist_64, .little) catch |err| switch (err) {
137 error.ReadFailed => unreachable,
138 error.EndOfStream => return error.InvalidMachO,
139 };
140 if (sym.n_type.bits.is_stab == 0) {
141 if (sym.n_strx == 0) continue;
142 switch (sym.n_type.bits.type) {
143 .undf, .pbud, .indr, .abs, _ => continue,
144 .sect => {
145 const name = std.mem.sliceTo(strings[sym.n_strx..], 0);
146 const gop = symbol_names.getOrPutAssumeCapacity(name);
147 if (!gop.found_existing) {
148 assert(gop.index == symbols.items.len);
149 symbols.appendAssumeCapacity(.{
150 .strx = sym.n_strx,
151 .addr = sym.n_value,
152 .ofile = Symbol.unknown_ofile,
153 });
154 }
155 },
156 }
157 continue;
158 }
159
160 // TODO handle globals N_GSYM, and statics N_STSYM
161 switch (sym.n_type.stab) {
162 .oso => switch (state) {
163 .init, .oso_close => {
164 state = .oso_open;
165 ofile = sym.n_strx;
166 },
167 else => return error.InvalidMachO,
168 },
169 .bnsym => switch (state) {
170 .oso_open, .ensym => {
171 state = .bnsym;
172 last_sym = .{
173 .strx = 0,
174 .addr = sym.n_value,
175 .ofile = ofile,
176 };
177 },
178 else => return error.InvalidMachO,
179 },
180 .fun => switch (state) {
181 .bnsym => {
182 state = .fun_strx;
183 last_sym.strx = sym.n_strx;
184 },
185 .fun_strx => {
186 state = .fun_size;
187 },
188 else => return error.InvalidMachO,
189 },
190 .ensym => switch (state) {
191 .fun_size => {
192 state = .ensym;
193 if (last_sym.strx != 0) {
194 const name = std.mem.sliceTo(strings[last_sym.strx..], 0);
195 const gop = symbol_names.getOrPutAssumeCapacity(name);
196 if (!gop.found_existing) {
197 assert(gop.index == symbols.items.len);
198 symbols.appendAssumeCapacity(last_sym);
199 } else {
200 symbols.items[gop.index] = last_sym;
201 }
202 }
203 },
204 else => return error.InvalidMachO,
205 },
206 .so => switch (state) {
207 .init, .oso_close => {},
208 .oso_open, .ensym => {
209 state = .oso_close;
210 },
211 else => return error.InvalidMachO,
212 },
213 else => {},
214 }
215 }
216
217 switch (state) {
218 .init => {
219 // Missing STAB symtab entries is still okay, unless there were also no normal symbols.
220 if (symbols.items.len == 0) return error.MissingDebugInfo;
221 },
222 .oso_close => {},
223 else => return error.InvalidMachO, // corrupted STAB entries in symtab
224 }
225
226 const symbols_slice = try symbols.toOwnedSlice(gpa);
227 errdefer gpa.free(symbols_slice);
228
229 // Even though lld emits symbols in ascending order, this debug code
230 // should work for programs linked in any valid way.
231 // This sort is so that we can binary search later.
232 mem.sort(Symbol, symbols_slice, {}, Symbol.addressLessThan);
233
234 return .{
235 .mapped_memory = all_mapped_memory,
236 .symbols = symbols_slice,
237 .strings = strings,
238 .ofiles = .empty,
239 .text_vmaddr = text_vmaddr,
240 };
241}
242pub fn getDwarfForAddress(mf: *MachOFile, gpa: Allocator, vaddr: u64) !struct { *Dwarf, u64 } {
243 const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo;
244
245 if (symbol.ofile == Symbol.unknown_ofile) return error.MissingDebugInfo;
246
247 // offset of `address` from start of `symbol`
248 const address_symbol_offset = vaddr - symbol.addr;
249
250 // Take the symbol name from the N_FUN STAB entry, we're going to
251 // use it if we fail to find the DWARF infos
252 const stab_symbol = mem.sliceTo(mf.strings[symbol.strx..], 0);
253
254 const gop = try mf.ofiles.getOrPut(gpa, symbol.ofile);
255 if (!gop.found_existing) {
256 const name = mem.sliceTo(mf.strings[symbol.ofile..], 0);
257 gop.value_ptr.* = loadOFile(gpa, name);
258 }
259 const of = &(gop.value_ptr.* catch |err| return err);
260
261 const symbol_index = of.symbols_by_name.getKeyAdapted(
262 @as([]const u8, stab_symbol),
263 @as(OFile.SymbolAdapter, .{ .strtab = of.strtab, .symtab_raw = of.symtab_raw }),
264 ) orelse return error.MissingDebugInfo;
265
266 const symbol_ofile_vaddr = vaddr: {
267 var sym = of.symtab_raw[symbol_index];
268 if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &sym);
269 break :vaddr sym.n_value;
270 };
271
272 return .{ &of.dwarf, symbol_ofile_vaddr + address_symbol_offset };
273}
274pub fn lookupSymbolName(mf: *MachOFile, vaddr: u64) error{MissingDebugInfo}![]const u8 {
275 const symbol = Symbol.find(mf.symbols, vaddr) orelse return error.MissingDebugInfo;
276 return mem.sliceTo(mf.strings[symbol.strx..], 0);
277}
278
279const OFile = struct {
280 mapped_memory: []align(std.heap.page_size_min) const u8,
281 dwarf: Dwarf,
282 strtab: []const u8,
283 symtab_raw: []align(1) const macho.nlist_64,
284 /// All named symbols in `symtab_raw`. Stored `u32` key is the index into `symtab_raw`. Accessed
285 /// through `SymbolAdapter`, so that the symbol name is used as the logical key.
286 symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true),
287
288 const SymbolAdapter = struct {
289 strtab: []const u8,
290 symtab_raw: []align(1) const macho.nlist_64,
291 pub fn hash(ctx: SymbolAdapter, sym_name: []const u8) u32 {
292 _ = ctx;
293 return @truncate(std.hash.Wyhash.hash(0, sym_name));
294 }
295 pub fn eql(ctx: SymbolAdapter, a_sym_name: []const u8, b_sym_index: u32, b_index: usize) bool {
296 _ = b_index;
297 var b_sym = ctx.symtab_raw[b_sym_index];
298 if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &b_sym);
299 const b_sym_name = std.mem.sliceTo(ctx.strtab[b_sym.n_strx..], 0);
300 return mem.eql(u8, a_sym_name, b_sym_name);
301 }
302 };
303};
304
305const Symbol = struct {
306 strx: u32,
307 addr: u64,
308 /// Value may be `unknown_ofile`.
309 ofile: u32,
310 const unknown_ofile = std.math.maxInt(u32);
311 fn addressLessThan(context: void, lhs: Symbol, rhs: Symbol) bool {
312 _ = context;
313 return lhs.addr < rhs.addr;
314 }
315 /// Assumes that `symbols` is sorted in order of ascending `addr`.
316 fn find(symbols: []const Symbol, address: usize) ?*const Symbol {
317 if (symbols.len == 0) return null; // no potential match
318 if (address < symbols[0].addr) return null; // address is before the lowest-address symbol
319 var left: usize = 0;
320 var len: usize = symbols.len;
321 while (len > 1) {
322 const mid = left + len / 2;
323 if (address < symbols[mid].addr) {
324 len /= 2;
325 } else {
326 left = mid;
327 len -= len / 2;
328 }
329 }
330 return &symbols[left];
331 }
332
333 test find {
334 const symbols: []const Symbol = &.{
335 .{ .addr = 100, .strx = undefined, .ofile = undefined },
336 .{ .addr = 200, .strx = undefined, .ofile = undefined },
337 .{ .addr = 300, .strx = undefined, .ofile = undefined },
338 };
339
340 try testing.expectEqual(null, find(symbols, 0));
341 try testing.expectEqual(null, find(symbols, 99));
342 try testing.expectEqual(&symbols[0], find(symbols, 100).?);
343 try testing.expectEqual(&symbols[0], find(symbols, 150).?);
344 try testing.expectEqual(&symbols[0], find(symbols, 199).?);
345
346 try testing.expectEqual(&symbols[1], find(symbols, 200).?);
347 try testing.expectEqual(&symbols[1], find(symbols, 250).?);
348 try testing.expectEqual(&symbols[1], find(symbols, 299).?);
349
350 try testing.expectEqual(&symbols[2], find(symbols, 300).?);
351 try testing.expectEqual(&symbols[2], find(symbols, 301).?);
352 try testing.expectEqual(&symbols[2], find(symbols, 5000).?);
353 }
354};
355test {
356 _ = Symbol;
357}
358
359fn loadOFile(gpa: Allocator, o_file_name: []const u8) !OFile {
360 const all_mapped_memory, const mapped_ofile = map: {
361 const open_paren = paren: {
362 if (std.mem.endsWith(u8, o_file_name, ")")) {
363 if (std.mem.findScalarLast(u8, o_file_name, '(')) |i| {
364 break :paren i;
365 }
366 }
367 // Not an archive, just a normal path to a .o file
368 const m = try mapDebugInfoFile(o_file_name);
369 break :map .{ m, m };
370 };
371
372 // We have the form 'path/to/archive.a(entry.o)'. Map the archive and find the object file in question.
373
374 const archive_path = o_file_name[0..open_paren];
375 const target_name_in_archive = o_file_name[open_paren + 1 .. o_file_name.len - 1];
376 const mapped_archive = try mapDebugInfoFile(archive_path);
377 errdefer posix.munmap(mapped_archive);
378
379 var ar_reader: Io.Reader = .fixed(mapped_archive);
380 const ar_magic = ar_reader.take(8) catch return error.InvalidMachO;
381 if (!std.mem.eql(u8, ar_magic, "!<arch>\n")) return error.InvalidMachO;
382 while (true) {
383 if (ar_reader.seek == ar_reader.buffer.len) return error.MissingDebugInfo;
384
385 const raw_name = ar_reader.takeArray(16) catch return error.InvalidMachO;
386 ar_reader.discardAll(12 + 6 + 6 + 8) catch return error.InvalidMachO;
387 const raw_size = ar_reader.takeArray(10) catch return error.InvalidMachO;
388 const file_magic = ar_reader.takeArray(2) catch return error.InvalidMachO;
389 if (!std.mem.eql(u8, file_magic, "`\n")) return error.InvalidMachO;
390
391 const size = std.fmt.parseInt(u32, mem.sliceTo(raw_size, ' '), 10) catch return error.InvalidMachO;
392 const raw_data = ar_reader.take(size) catch return error.InvalidMachO;
393
394 const entry_name: []const u8, const entry_contents: []const u8 = entry: {
395 if (!std.mem.startsWith(u8, raw_name, "#1/")) {
396 break :entry .{ mem.sliceTo(raw_name, '/'), raw_data };
397 }
398 const len = std.fmt.parseInt(u32, mem.sliceTo(raw_name[3..], ' '), 10) catch return error.InvalidMachO;
399 if (len > size) return error.InvalidMachO;
400 break :entry .{ mem.sliceTo(raw_data[0..len], 0), raw_data[len..] };
401 };
402
403 if (std.mem.eql(u8, entry_name, target_name_in_archive)) {
404 break :map .{ mapped_archive, entry_contents };
405 }
406 }
407 };
408 errdefer posix.munmap(all_mapped_memory);
409
410 var r: Io.Reader = .fixed(mapped_ofile);
411 const hdr = r.takeStruct(macho.mach_header_64, .little) catch |err| switch (err) {
412 error.ReadFailed => unreachable,
413 error.EndOfStream => return error.InvalidMachO,
414 };
415 if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidMachO;
416
417 const seg_cmd: macho.LoadCommandIterator.LoadCommand, const symtab_cmd: macho.symtab_command = cmds: {
418 var seg_cmd: ?macho.LoadCommandIterator.LoadCommand = null;
419 var symtab_cmd: ?macho.symtab_command = null;
420 var it: macho.LoadCommandIterator = try .init(&hdr, mapped_ofile[@sizeOf(macho.mach_header_64)..]);
421 while (try it.next()) |lc| switch (lc.hdr.cmd) {
422 .SEGMENT_64 => seg_cmd = lc,
423 .SYMTAB => symtab_cmd = lc.cast(macho.symtab_command) orelse return error.InvalidMachO,
424 else => {},
425 };
426 break :cmds .{
427 seg_cmd orelse return error.MissingDebugInfo,
428 symtab_cmd orelse return error.MissingDebugInfo,
429 };
430 };
431
432 if (mapped_ofile.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidMachO;
433 if (mapped_ofile[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidMachO;
434 const strtab = mapped_ofile[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1];
435
436 const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64);
437 if (mapped_ofile.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidMachO;
438 const symtab_raw: []align(1) const macho.nlist_64 = @ptrCast(mapped_ofile[symtab_cmd.symoff..][0..n_sym_bytes]);
439
440 // TODO handle tentative (common) symbols
441 var symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true) = .empty;
442 defer symbols_by_name.deinit(gpa);
443 try symbols_by_name.ensureUnusedCapacity(gpa, @intCast(symtab_raw.len));
444 for (symtab_raw, 0..) |sym_raw, sym_index| {
445 var sym = sym_raw;
446 if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.nlist_64, &sym);
447 if (sym.n_strx == 0) continue;
448 switch (sym.n_type.bits.type) {
449 .undf => continue, // includes tentative symbols
450 .abs => continue,
451 else => {},
452 }
453 const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0);
454 const gop = symbols_by_name.getOrPutAssumeCapacityAdapted(
455 @as([]const u8, sym_name),
456 @as(OFile.SymbolAdapter, .{ .strtab = strtab, .symtab_raw = symtab_raw }),
457 );
458 if (gop.found_existing) return error.InvalidMachO;
459 gop.key_ptr.* = @intCast(sym_index);
460 }
461
462 var sections: Dwarf.SectionArray = @splat(null);
463 for (seg_cmd.getSections()) |sect_raw| {
464 var sect = sect_raw;
465 if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(macho.section_64, &sect);
466
467 if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue;
468
469 const section_index: usize = inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
470 if (mem.eql(u8, "__" ++ section.name, sect.sectName())) break i;
471 } else continue;
472
473 if (mapped_ofile.len < sect.offset + sect.size) return error.InvalidMachO;
474 const section_bytes = mapped_ofile[sect.offset..][0..sect.size];
475 sections[section_index] = .{
476 .data = section_bytes,
477 .owned = false,
478 };
479 }
480
481 if (sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
482 sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
483 sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
484 sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null)
485 {
486 return error.MissingDebugInfo;
487 }
488
489 var dwarf: Dwarf = .{ .sections = sections };
490 errdefer dwarf.deinit(gpa);
491 dwarf.open(gpa, .little) catch |err| switch (err) {
492 error.InvalidDebugInfo,
493 error.EndOfStream,
494 error.Overflow,
495 error.StreamTooLong,
496 => return error.InvalidDwarf,
497
498 error.MissingDebugInfo,
499 error.ReadFailed,
500 error.OutOfMemory,
501 => |e| return e,
502 };
503
504 return .{
505 .mapped_memory = all_mapped_memory,
506 .dwarf = dwarf,
507 .strtab = strtab,
508 .symtab_raw = symtab_raw,
509 .symbols_by_name = symbols_by_name.move(),
510 };
511}
512
513/// Uses `mmap` to map the file at `path` into memory.
514fn mapDebugInfoFile(path: []const u8) ![]align(std.heap.page_size_min) const u8 {
515 const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
516 error.FileNotFound => return error.MissingDebugInfo,
517 else => return error.ReadFailed,
518 };
519 defer file.close();
520
521 const file_len = std.math.cast(
522 usize,
523 file.getEndPos() catch return error.ReadFailed,
524 ) orelse return error.ReadFailed;
525
526 return posix.mmap(
527 null,
528 file_len,
529 posix.PROT.READ,
530 .{ .TYPE = .SHARED },
531 file.handle,
532 0,
533 ) catch return error.ReadFailed;
534}
535
536const std = @import("std");
537const Allocator = std.mem.Allocator;
538const Dwarf = std.debug.Dwarf;
539const Io = std.Io;
540const assert = std.debug.assert;
541const posix = std.posix;
542const macho = std.macho;
543const mem = std.mem;
544const testing = std.testing;
545
546const builtin = @import("builtin");
547
548const MachOFile = @This();
lib/std/debug/SelfInfo/Elf.zig+5
......@@ -80,6 +80,11 @@ pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]cons
8080 if (module.name.len == 0) return error.MissingDebugInfo;
8181 return module.name;
8282}
83pub fn getModuleSlide(si: *SelfInfo, gpa: Allocator, address: usize) Error!usize {
84 const module = try si.findModule(gpa, address, .shared);
85 defer si.rwlock.unlockShared();
86 return module.load_offset;
87}
8388
8489pub const can_unwind: bool = s: {
8590 // The DWARF code can't deal with ILP32 ABIs yet: https://github.com/ziglang/zig/issues/25447
lib/std/debug/SelfInfo/MachO.zig+61-397
......@@ -1,12 +1,10 @@
11mutex: std.Thread.Mutex,
22/// Accessed through `Module.Adapter`.
33modules: std.ArrayHashMapUnmanaged(Module, void, Module.Context, false),
4ofiles: std.StringArrayHashMapUnmanaged(?OFile),
54
65pub const init: SelfInfo = .{
76 .mutex = .{},
87 .modules = .empty,
9 .ofiles = .empty,
108};
119pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
1210 for (si.modules.keys()) |*module| {
......@@ -14,20 +12,12 @@ pub fn deinit(si: *SelfInfo, gpa: Allocator) void {
1412 const u = &(module.unwind orelse break :unwind catch break :unwind);
1513 if (u.dwarf) |*dwarf| dwarf.deinit(gpa);
1614 }
17 loaded: {
18 const l = &(module.loaded_macho orelse break :loaded catch break :loaded);
19 gpa.free(l.symbols);
20 posix.munmap(l.mapped_memory);
15 file: {
16 const f = &(module.file orelse break :file catch break :file);
17 f.deinit(gpa);
2118 }
2219 }
23 for (si.ofiles.values()) |*opt_ofile| {
24 const ofile = &(opt_ofile.* orelse continue);
25 ofile.dwarf.deinit(gpa);
26 ofile.symbols_by_name.deinit(gpa);
27 posix.munmap(ofile.mapped_memory);
28 }
2920 si.modules.deinit(gpa);
30 si.ofiles.deinit(gpa);
3121}
3222
3323pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!std.debug.Symbol {
......@@ -35,67 +25,55 @@ pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!st
3525 const module = try si.findModule(gpa, address);
3626 defer si.mutex.unlock();
3727
38 const loaded_macho = try module.getLoadedMachO(gpa);
39
40 const vaddr = address - loaded_macho.vaddr_offset;
41 const symbol = MachoSymbol.find(loaded_macho.symbols, vaddr) orelse return .unknown;
28 const file = try module.getFile(gpa);
4229
43 // offset of `address` from start of `symbol`
44 const address_symbol_offset = vaddr - symbol.addr;
30 // This is not necessarily the same as the vmaddr_slide that dyld would report. This is
31 // because the segments in the file on disk might differ from the ones in memory. Normally
32 // we wouldn't necessarily expect that to work, but /usr/lib/dyld is incredibly annoying:
33 // it exists on disk (necessarily, because the kernel needs to load it!), but is also in
34 // the dyld cache (dyld actually restart itself from cache after loading it), and the two
35 // versions have (very) different segment base addresses. It's sort of like a large slide
36 // has been applied to all addresses in memory. For an optimal experience, we consider the
37 // on-disk vmaddr instead of the in-memory one.
38 const vaddr_offset = module.text_base - file.text_vmaddr;
4539
46 // Take the symbol name from the N_FUN STAB entry, we're going to
47 // use it if we fail to find the DWARF infos
48 const stab_symbol = mem.sliceTo(loaded_macho.strings[symbol.strx..], 0);
40 const vaddr = address - vaddr_offset;
4941
50 // If any information is missing, we can at least return this from now on.
51 const sym_only_result: std.debug.Symbol = .{
52 .name = stab_symbol,
53 .compile_unit_name = null,
54 .source_location = null,
42 const ofile_dwarf, const ofile_vaddr = file.getDwarfForAddress(gpa, vaddr) catch {
43 // Return at least the symbol name if available.
44 return .{
45 .name = try file.lookupSymbolName(vaddr),
46 .compile_unit_name = null,
47 .source_location = null,
48 };
5549 };
5650
57 if (symbol.ofile == MachoSymbol.unknown_ofile) {
58 // We don't have STAB info, so can't track down the object file; all we can do is the symbol name.
59 return sym_only_result;
60 }
61
62 const o_file: *OFile = of: {
63 const path = mem.sliceTo(loaded_macho.strings[symbol.ofile..], 0);
64 const gop = try si.ofiles.getOrPut(gpa, path);
65 if (!gop.found_existing) {
66 gop.value_ptr.* = loadOFile(gpa, path) catch null;
67 }
68 if (gop.value_ptr.*) |*o_file| {
69 break :of o_file;
70 } else {
71 return sym_only_result;
72 }
51 const compile_unit = ofile_dwarf.findCompileUnit(native_endian, ofile_vaddr) catch {
52 // Return at least the symbol name if available.
53 return .{
54 .name = try file.lookupSymbolName(vaddr),
55 .compile_unit_name = null,
56 .source_location = null,
57 };
7358 };
7459
75 const symbol_index = o_file.symbols_by_name.getKeyAdapted(
76 @as([]const u8, stab_symbol),
77 @as(OFile.SymbolAdapter, .{ .strtab = o_file.strtab, .symtab = o_file.symtab }),
78 ) orelse return sym_only_result;
79 const symbol_ofile_vaddr = o_file.symtab[symbol_index].n_value;
80
81 const compile_unit = o_file.dwarf.findCompileUnit(native_endian, symbol_ofile_vaddr) catch return sym_only_result;
82
8360 return .{
84 .name = o_file.dwarf.getSymbolName(symbol_ofile_vaddr + address_symbol_offset) orelse stab_symbol,
61 .name = ofile_dwarf.getSymbolName(ofile_vaddr) orelse
62 try file.lookupSymbolName(vaddr),
8563 .compile_unit_name = compile_unit.die.getAttrString(
86 &o_file.dwarf,
64 ofile_dwarf,
8765 native_endian,
8866 std.dwarf.AT.name,
89 o_file.dwarf.section(.debug_str),
67 ofile_dwarf.section(.debug_str),
9068 compile_unit,
9169 ) catch |err| switch (err) {
9270 error.MissingDebugInfo, error.InvalidDebugInfo => null,
9371 },
94 .source_location = o_file.dwarf.getLineNumberInfo(
72 .source_location = ofile_dwarf.getLineNumberInfo(
9573 gpa,
9674 native_endian,
9775 compile_unit,
98 symbol_ofile_vaddr + address_symbol_offset,
76 ofile_vaddr,
9977 ) catch null,
10078 };
10179}
......@@ -104,6 +82,20 @@ pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]cons
10482 defer si.mutex.unlock();
10583 return module.name;
10684}
85pub fn getModuleSlide(si: *SelfInfo, gpa: Allocator, address: usize) Error!usize {
86 const module = try si.findModule(gpa, address);
87 defer si.mutex.unlock();
88 const header: *std.macho.mach_header_64 = @ptrFromInt(module.text_base);
89 const raw_macho: [*]u8 = @ptrCast(header);
90 var it = macho.LoadCommandIterator.init(header, raw_macho[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds]) catch unreachable;
91 const text_vmaddr = while (it.next() catch unreachable) |load_cmd| {
92 if (load_cmd.hdr.cmd != .SEGMENT_64) continue;
93 const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
94 if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
95 break segment_cmd.vmaddr;
96 } else unreachable;
97 return module.text_base - text_vmaddr;
98}
10799
108100pub const can_unwind: bool = true;
109101pub const UnwindContext = std.debug.Dwarf.SelfUnwinder;
......@@ -447,7 +439,7 @@ fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) Error!*Module {
447439 .text_base = @intFromPtr(info.fbase),
448440 .name = std.mem.span(info.fname),
449441 .unwind = null,
450 .loaded_macho = null,
442 .file = null,
451443 };
452444 }
453445 return gop.key_ptr;
......@@ -457,7 +449,7 @@ const Module = struct {
457449 text_base: usize,
458450 name: []const u8,
459451 unwind: ?(Error!Unwind),
460 loaded_macho: ?(Error!LoadedMachO),
452 file: ?(Error!MachOFile),
461453
462454 const Adapter = struct {
463455 pub fn hash(_: Adapter, text_base: usize) u32 {
......@@ -488,34 +480,17 @@ const Module = struct {
488480 dwarf: ?Dwarf.Unwind,
489481 };
490482
491 const LoadedMachO = struct {
492 mapped_memory: []align(std.heap.page_size_min) const u8,
493 symbols: []const MachoSymbol,
494 strings: []const u8,
495 /// This is not necessarily the same as the vmaddr_slide that dyld would report. This is
496 /// because the segments in the file on disk might differ from the ones in memory. Normally
497 /// we wouldn't necessarily expect that to work, but /usr/lib/dyld is incredibly annoying:
498 /// it exists on disk (necessarily, because the kernel needs to load it!), but is also in
499 /// the dyld cache (dyld actually restart itself from cache after loading it), and the two
500 /// versions have (very) different segment base addresses. It's sort of like a large slide
501 /// has been applied to all addresses in memory. For an optimal experience, we consider the
502 /// on-disk vmaddr instead of the in-memory one.
503 vaddr_offset: usize,
504 };
505
506483 fn getUnwindInfo(module: *Module, gpa: Allocator) Error!*Unwind {
507484 if (module.unwind == null) module.unwind = loadUnwindInfo(module, gpa);
508485 return if (module.unwind.?) |*unwind| unwind else |err| err;
509486 }
510487 fn loadUnwindInfo(module: *const Module, gpa: Allocator) Error!Unwind {
511 const header: *std.macho.mach_header = @ptrFromInt(module.text_base);
488 const header: *std.macho.mach_header_64 = @ptrFromInt(module.text_base);
512489
513 var it: macho.LoadCommandIterator = .{
514 .ncmds = header.ncmds,
515 .buffer = @as([*]u8, @ptrCast(header))[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds],
516 };
517 const sections, const text_vmaddr = while (it.next()) |load_cmd| {
518 if (load_cmd.cmd() != .SEGMENT_64) continue;
490 const raw_macho: [*]u8 = @ptrCast(header);
491 var it = macho.LoadCommandIterator.init(header, raw_macho[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds]) catch unreachable;
492 const sections, const text_vmaddr = while (it.next() catch unreachable) |load_cmd| {
493 if (load_cmd.hdr.cmd != .SEGMENT_64) continue;
519494 const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
520495 if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
521496 break .{ load_cmd.getSections(), segment_cmd.vmaddr };
......@@ -568,237 +543,15 @@ const Module = struct {
568543 };
569544 }
570545
571 fn getLoadedMachO(module: *Module, gpa: Allocator) Error!*LoadedMachO {
572 if (module.loaded_macho == null) module.loaded_macho = loadMachO(module, gpa) catch |err| switch (err) {
573 error.InvalidDebugInfo, error.MissingDebugInfo, error.OutOfMemory, error.Unexpected => |e| e,
574 else => error.ReadFailed,
575 };
576 return if (module.loaded_macho.?) |*lm| lm else |err| err;
577 }
578 fn loadMachO(module: *const Module, gpa: Allocator) Error!LoadedMachO {
579 const all_mapped_memory = try mapDebugInfoFile(module.name);
580 errdefer posix.munmap(all_mapped_memory);
581
582 // In most cases, the file we just mapped is a Mach-O binary. However, it could be a "universal
583 // binary": a simple file format which contains Mach-O binaries for multiple targets. For
584 // instance, `/usr/lib/dyld` is currently distributed as a universal binary containing images
585 // for both ARM64 macOS and x86_64 macOS.
586 if (all_mapped_memory.len < 4) return error.InvalidDebugInfo;
587 const magic = @as(*const u32, @ptrCast(all_mapped_memory.ptr)).*;
588 // The contents of a Mach-O file, which may or may not be the whole of `all_mapped_memory`.
589 const mapped_macho = switch (magic) {
590 macho.MH_MAGIC_64 => all_mapped_memory,
591
592 macho.FAT_CIGAM => mapped_macho: {
593 // This is the universal binary format (aka a "fat binary"). Annoyingly, the whole thing
594 // is big-endian, so we'll be swapping some bytes.
595 if (all_mapped_memory.len < @sizeOf(macho.fat_header)) return error.InvalidDebugInfo;
596 const hdr: *const macho.fat_header = @ptrCast(all_mapped_memory.ptr);
597 const archs_ptr: [*]const macho.fat_arch = @ptrCast(all_mapped_memory.ptr + @sizeOf(macho.fat_header));
598 const archs: []const macho.fat_arch = archs_ptr[0..@byteSwap(hdr.nfat_arch)];
599 const native_cpu_type = switch (builtin.cpu.arch) {
600 .x86_64 => macho.CPU_TYPE_X86_64,
601 .aarch64 => macho.CPU_TYPE_ARM64,
602 else => comptime unreachable,
603 };
604 for (archs) |*arch| {
605 if (@byteSwap(arch.cputype) != native_cpu_type) continue;
606 const offset = @byteSwap(arch.offset);
607 const size = @byteSwap(arch.size);
608 break :mapped_macho all_mapped_memory[offset..][0..size];
609 }
610 // Our native architecture was not present in the fat binary.
611 return error.MissingDebugInfo;
612 },
613
614 // Even on modern 64-bit targets, this format doesn't seem to be too extensively used. It
615 // will be fairly easy to add support here if necessary; it's very similar to above.
616 macho.FAT_CIGAM_64 => return error.UnsupportedDebugInfo,
617
618 else => return error.InvalidDebugInfo,
619 };
620
621 const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_macho.ptr));
622 if (hdr.magic != macho.MH_MAGIC_64)
623 return error.InvalidDebugInfo;
624
625 const symtab: macho.symtab_command, const text_vmaddr: u64 = lc_iter: {
626 var it: macho.LoadCommandIterator = .{
627 .ncmds = hdr.ncmds,
628 .buffer = mapped_macho[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
629 };
630 var symtab: ?macho.symtab_command = null;
631 var text_vmaddr: ?u64 = null;
632 while (it.next()) |cmd| switch (cmd.cmd()) {
633 .SYMTAB => symtab = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
634 .SEGMENT_64 => if (cmd.cast(macho.segment_command_64)) |seg_cmd| {
635 if (!mem.eql(u8, seg_cmd.segName(), "__TEXT")) continue;
636 text_vmaddr = seg_cmd.vmaddr;
637 },
638 else => {},
639 };
640 break :lc_iter .{
641 symtab orelse return error.MissingDebugInfo,
642 text_vmaddr orelse return error.MissingDebugInfo,
643 };
644 };
645
646 const syms_ptr: [*]align(1) const macho.nlist_64 = @ptrCast(mapped_macho[symtab.symoff..]);
647 const syms = syms_ptr[0..symtab.nsyms];
648 const strings = mapped_macho[symtab.stroff..][0 .. symtab.strsize - 1];
649
650 var symbols: std.ArrayList(MachoSymbol) = try .initCapacity(gpa, syms.len);
651 defer symbols.deinit(gpa);
652
653 // This map is temporary; it is used only to detect duplicates here. This is
654 // necessary because we prefer to use STAB ("symbolic debugging table") symbols,
655 // but they might not be present, so we track normal symbols too.
656 // Indices match 1-1 with those of `symbols`.
657 var symbol_names: std.StringArrayHashMapUnmanaged(void) = .empty;
658 defer symbol_names.deinit(gpa);
659 try symbol_names.ensureUnusedCapacity(gpa, syms.len);
660
661 var ofile: u32 = undefined;
662 var last_sym: MachoSymbol = undefined;
663 var state: enum {
664 init,
665 oso_open,
666 oso_close,
667 bnsym,
668 fun_strx,
669 fun_size,
670 ensym,
671 } = .init;
672
673 for (syms) |*sym| {
674 if (sym.n_type.bits.is_stab == 0) {
675 if (sym.n_strx == 0) continue;
676 switch (sym.n_type.bits.type) {
677 .undf, .pbud, .indr, .abs, _ => continue,
678 .sect => {
679 const name = std.mem.sliceTo(strings[sym.n_strx..], 0);
680 const gop = symbol_names.getOrPutAssumeCapacity(name);
681 if (!gop.found_existing) {
682 assert(gop.index == symbols.items.len);
683 symbols.appendAssumeCapacity(.{
684 .strx = sym.n_strx,
685 .addr = sym.n_value,
686 .ofile = MachoSymbol.unknown_ofile,
687 });
688 }
689 },
690 }
691 continue;
692 }
693
694 // TODO handle globals N_GSYM, and statics N_STSYM
695 switch (sym.n_type.stab) {
696 .oso => switch (state) {
697 .init, .oso_close => {
698 state = .oso_open;
699 ofile = sym.n_strx;
700 },
701 else => return error.InvalidDebugInfo,
702 },
703 .bnsym => switch (state) {
704 .oso_open, .ensym => {
705 state = .bnsym;
706 last_sym = .{
707 .strx = 0,
708 .addr = sym.n_value,
709 .ofile = ofile,
710 };
711 },
712 else => return error.InvalidDebugInfo,
713 },
714 .fun => switch (state) {
715 .bnsym => {
716 state = .fun_strx;
717 last_sym.strx = sym.n_strx;
718 },
719 .fun_strx => {
720 state = .fun_size;
721 },
722 else => return error.InvalidDebugInfo,
723 },
724 .ensym => switch (state) {
725 .fun_size => {
726 state = .ensym;
727 if (last_sym.strx != 0) {
728 const name = std.mem.sliceTo(strings[last_sym.strx..], 0);
729 const gop = symbol_names.getOrPutAssumeCapacity(name);
730 if (!gop.found_existing) {
731 assert(gop.index == symbols.items.len);
732 symbols.appendAssumeCapacity(last_sym);
733 } else {
734 symbols.items[gop.index] = last_sym;
735 }
736 }
737 },
738 else => return error.InvalidDebugInfo,
739 },
740 .so => switch (state) {
741 .init, .oso_close => {},
742 .oso_open, .ensym => {
743 state = .oso_close;
744 },
745 else => return error.InvalidDebugInfo,
746 },
747 else => {},
748 }
749 }
750
751 switch (state) {
752 .init => {
753 // Missing STAB symtab entries is still okay, unless there were also no normal symbols.
754 if (symbols.items.len == 0) return error.MissingDebugInfo;
755 },
756 .oso_close => {},
757 else => return error.InvalidDebugInfo, // corrupted STAB entries in symtab
758 }
759
760 const symbols_slice = try symbols.toOwnedSlice(gpa);
761 errdefer gpa.free(symbols_slice);
762
763 // Even though lld emits symbols in ascending order, this debug code
764 // should work for programs linked in any valid way.
765 // This sort is so that we can binary search later.
766 mem.sort(MachoSymbol, symbols_slice, {}, MachoSymbol.addressLessThan);
767
768 return .{
769 .mapped_memory = all_mapped_memory,
770 .symbols = symbols_slice,
771 .strings = strings,
772 .vaddr_offset = module.text_base - text_vmaddr,
546 fn getFile(module: *Module, gpa: Allocator) Error!*MachOFile {
547 if (module.file == null) module.file = MachOFile.load(gpa, module.name, builtin.cpu.arch) catch |err| switch (err) {
548 error.InvalidMachO, error.InvalidDwarf => error.InvalidDebugInfo,
549 error.MissingDebugInfo, error.OutOfMemory, error.UnsupportedDebugInfo, error.ReadFailed => |e| e,
773550 };
551 return if (module.file.?) |*f| f else |err| err;
774552 }
775553};
776554
777const OFile = struct {
778 mapped_memory: []align(std.heap.page_size_min) const u8,
779 dwarf: Dwarf,
780 strtab: []const u8,
781 symtab: []align(1) const macho.nlist_64,
782 /// All named symbols in `symtab`. Stored `u32` key is the index into `symtab`. Accessed
783 /// through `SymbolAdapter`, so that the symbol name is used as the logical key.
784 symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true),
785
786 const SymbolAdapter = struct {
787 strtab: []const u8,
788 symtab: []align(1) const macho.nlist_64,
789 pub fn hash(ctx: SymbolAdapter, sym_name: []const u8) u32 {
790 _ = ctx;
791 return @truncate(std.hash.Wyhash.hash(0, sym_name));
792 }
793 pub fn eql(ctx: SymbolAdapter, a_sym_name: []const u8, b_sym_index: u32, b_index: usize) bool {
794 _ = b_index;
795 const b_sym = ctx.symtab[b_sym_index];
796 const b_sym_name = std.mem.sliceTo(ctx.strtab[b_sym.n_strx..], 0);
797 return mem.eql(u8, a_sym_name, b_sym_name);
798 }
799 };
800};
801
802555const MachoSymbol = struct {
803556 strx: u32,
804557 addr: u64,
......@@ -880,101 +633,12 @@ fn mapDebugInfoFile(path: []const u8) ![]align(std.heap.page_size_min) const u8
880633 };
881634}
882635
883fn loadOFile(gpa: Allocator, o_file_path: []const u8) !OFile {
884 const mapped_mem = try mapDebugInfoFile(o_file_path);
885 errdefer posix.munmap(mapped_mem);
886
887 if (mapped_mem.len < @sizeOf(macho.mach_header_64)) return error.InvalidDebugInfo;
888 const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_mem.ptr));
889 if (hdr.magic != std.macho.MH_MAGIC_64) return error.InvalidDebugInfo;
890
891 const seg_cmd: macho.LoadCommandIterator.LoadCommand, const symtab_cmd: macho.symtab_command = cmds: {
892 var seg_cmd: ?macho.LoadCommandIterator.LoadCommand = null;
893 var symtab_cmd: ?macho.symtab_command = null;
894 var it: macho.LoadCommandIterator = .{
895 .ncmds = hdr.ncmds,
896 .buffer = mapped_mem[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
897 };
898 while (it.next()) |cmd| switch (cmd.cmd()) {
899 .SEGMENT_64 => seg_cmd = cmd,
900 .SYMTAB => symtab_cmd = cmd.cast(macho.symtab_command) orelse return error.InvalidDebugInfo,
901 else => {},
902 };
903 break :cmds .{
904 seg_cmd orelse return error.MissingDebugInfo,
905 symtab_cmd orelse return error.MissingDebugInfo,
906 };
907 };
908
909 if (mapped_mem.len < symtab_cmd.stroff + symtab_cmd.strsize) return error.InvalidDebugInfo;
910 if (mapped_mem[symtab_cmd.stroff + symtab_cmd.strsize - 1] != 0) return error.InvalidDebugInfo;
911 const strtab = mapped_mem[symtab_cmd.stroff..][0 .. symtab_cmd.strsize - 1];
912
913 const n_sym_bytes = symtab_cmd.nsyms * @sizeOf(macho.nlist_64);
914 if (mapped_mem.len < symtab_cmd.symoff + n_sym_bytes) return error.InvalidDebugInfo;
915 const symtab: []align(1) const macho.nlist_64 = @ptrCast(mapped_mem[symtab_cmd.symoff..][0..n_sym_bytes]);
916
917 // TODO handle tentative (common) symbols
918 var symbols_by_name: std.ArrayHashMapUnmanaged(u32, void, void, true) = .empty;
919 defer symbols_by_name.deinit(gpa);
920 try symbols_by_name.ensureUnusedCapacity(gpa, @intCast(symtab.len));
921 for (symtab, 0..) |sym, sym_index| {
922 if (sym.n_strx == 0) continue;
923 switch (sym.n_type.bits.type) {
924 .undf => continue, // includes tentative symbols
925 .abs => continue,
926 else => {},
927 }
928 const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0);
929 const gop = symbols_by_name.getOrPutAssumeCapacityAdapted(
930 @as([]const u8, sym_name),
931 @as(OFile.SymbolAdapter, .{ .strtab = strtab, .symtab = symtab }),
932 );
933 if (gop.found_existing) return error.InvalidDebugInfo;
934 gop.key_ptr.* = @intCast(sym_index);
935 }
936
937 var sections: Dwarf.SectionArray = @splat(null);
938 for (seg_cmd.getSections()) |sect| {
939 if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue;
940
941 const section_index: usize = inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |section, i| {
942 if (mem.eql(u8, "__" ++ section.name, sect.sectName())) break i;
943 } else continue;
944
945 if (mapped_mem.len < sect.offset + sect.size) return error.InvalidDebugInfo;
946 const section_bytes = mapped_mem[sect.offset..][0..sect.size];
947 sections[section_index] = .{
948 .data = section_bytes,
949 .owned = false,
950 };
951 }
952
953 const missing_debug_info =
954 sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
955 sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
956 sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
957 sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
958 if (missing_debug_info) return error.MissingDebugInfo;
959
960 var dwarf: Dwarf = .{ .sections = sections };
961 errdefer dwarf.deinit(gpa);
962 try dwarf.open(gpa, native_endian);
963
964 return .{
965 .mapped_memory = mapped_mem,
966 .dwarf = dwarf,
967 .strtab = strtab,
968 .symtab = symtab,
969 .symbols_by_name = symbols_by_name.move(),
970 };
971}
972
973636const std = @import("std");
974637const Io = std.Io;
975638const Allocator = std.mem.Allocator;
976639const Dwarf = std.debug.Dwarf;
977640const Error = std.debug.SelfInfoError;
641const MachOFile = std.debug.MachOFile;
978642const assert = std.debug.assert;
979643const posix = std.posix;
980644const macho = std.macho;
lib/std/debug/SelfInfo/Windows.zig+6
......@@ -33,6 +33,12 @@ pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]cons
3333 const module = try si.findModule(gpa, address);
3434 return module.name;
3535}
36pub fn getModuleSlide(si: *SelfInfo, gpa: Allocator, address: usize) Error!usize {
37 si.mutex.lock();
38 defer si.mutex.unlock();
39 const module = try si.findModule(gpa, address);
40 return module.base_address;
41}
3642
3743pub const can_unwind: bool = switch (builtin.cpu.arch) {
3844 else => true,
lib/std/http.zig+3-4
......@@ -962,6 +962,7 @@ pub const BodyWriter = struct {
962962 // have to flush the chunk header before knowing the chunk length.
963963 return error.Unimplemented;
964964 };
965 if (data_len == 0) return error.EndOfStream;
965966 const out = bw.http_protocol_output;
966967 l: switch (bw.state.chunk_len) {
967968 0 => {
......@@ -975,8 +976,7 @@ pub const BodyWriter = struct {
975976 2 => {
976977 try out.writeAll("\r\n");
977978 bw.state.chunk_len = 0;
978 assert(file_reader.atEnd());
979 return error.EndOfStream;
979 continue :l 0;
980980 },
981981 else => {
982982 const chunk_limit: std.Io.Limit = .limited(bw.state.chunk_len - 2);
......@@ -985,8 +985,7 @@ pub const BodyWriter = struct {
985985 else
986986 try out.write(chunk_limit.slice(w.buffered()));
987987 bw.state.chunk_len -= n;
988 const ret = w.consume(n);
989 return ret;
988 return w.consume(n);
990989 },
991990 }
992991 }
lib/std/macho.zig+35-33
......@@ -1902,74 +1902,76 @@ pub const data_in_code_entry = extern struct {
19021902};
19031903
19041904pub const LoadCommandIterator = struct {
1905 next_index: usize,
19051906 ncmds: usize,
1906 buffer: []const u8,
1907 index: usize = 0,
1907 r: std.Io.Reader,
19081908
19091909 pub const LoadCommand = struct {
19101910 hdr: load_command,
19111911 data: []const u8,
19121912
1913 pub fn cmd(lc: LoadCommand) LC {
1914 return lc.hdr.cmd;
1915 }
1916
1917 pub fn cmdsize(lc: LoadCommand) u32 {
1918 return lc.hdr.cmdsize;
1919 }
1920
19211913 pub fn cast(lc: LoadCommand, comptime Cmd: type) ?Cmd {
19221914 if (lc.data.len < @sizeOf(Cmd)) return null;
1923 return @as(*align(1) const Cmd, @ptrCast(lc.data.ptr)).*;
1915 const ptr: *align(1) const Cmd = @ptrCast(lc.data.ptr);
1916 var cmd = ptr.*;
1917 if (builtin.cpu.arch.endian() != .little) std.mem.byteSwapAllFields(Cmd, &cmd);
1918 return cmd;
19241919 }
19251920
19261921 /// Asserts LoadCommand is of type segment_command_64.
1922 /// If the native endian is not `.little`, the `section_64` values must be byte-swapped by the caller.
19271923 pub fn getSections(lc: LoadCommand) []align(1) const section_64 {
19281924 const segment_lc = lc.cast(segment_command_64).?;
1929 if (segment_lc.nsects == 0) return &[0]section_64{};
1930 const data = lc.data[@sizeOf(segment_command_64)..];
1931 const sections = @as([*]align(1) const section_64, @ptrCast(data.ptr))[0..segment_lc.nsects];
1932 return sections;
1925 const sects_ptr: [*]align(1) const section_64 = @ptrCast(lc.data[@sizeOf(segment_command_64)..]);
1926 return sects_ptr[0..segment_lc.nsects];
19331927 }
19341928
19351929 /// Asserts LoadCommand is of type dylib_command.
19361930 pub fn getDylibPathName(lc: LoadCommand) []const u8 {
19371931 const dylib_lc = lc.cast(dylib_command).?;
1938 const data = lc.data[dylib_lc.dylib.name..];
1939 return mem.sliceTo(data, 0);
1932 return mem.sliceTo(lc.data[dylib_lc.dylib.name..], 0);
19401933 }
19411934
19421935 /// Asserts LoadCommand is of type rpath_command.
19431936 pub fn getRpathPathName(lc: LoadCommand) []const u8 {
19441937 const rpath_lc = lc.cast(rpath_command).?;
1945 const data = lc.data[rpath_lc.path..];
1946 return mem.sliceTo(data, 0);
1938 return mem.sliceTo(lc.data[rpath_lc.path..], 0);
19471939 }
19481940
19491941 /// Asserts LoadCommand is of type build_version_command.
1942 /// If the native endian is not `.little`, the `build_tool_version` values must be byte-swapped by the caller.
19501943 pub fn getBuildVersionTools(lc: LoadCommand) []align(1) const build_tool_version {
19511944 const build_lc = lc.cast(build_version_command).?;
1952 const ntools = build_lc.ntools;
1953 if (ntools == 0) return &[0]build_tool_version{};
1954 const data = lc.data[@sizeOf(build_version_command)..];
1955 const tools = @as([*]align(1) const build_tool_version, @ptrCast(data.ptr))[0..ntools];
1956 return tools;
1945 const tools_ptr: [*]align(1) const build_tool_version = @ptrCast(lc.data[@sizeOf(build_version_command)..]);
1946 return tools_ptr[0..build_lc.ntools];
19571947 }
19581948 };
19591949
1960 pub fn next(it: *LoadCommandIterator) ?LoadCommand {
1961 if (it.index >= it.ncmds) return null;
1950 pub fn next(it: *LoadCommandIterator) error{InvalidMachO}!?LoadCommand {
1951 if (it.next_index >= it.ncmds) return null;
19621952
1963 const hdr = @as(*align(1) const load_command, @ptrCast(it.buffer.ptr)).*;
1964 const cmd = LoadCommand{
1965 .hdr = hdr,
1966 .data = it.buffer[0..hdr.cmdsize],
1953 const hdr = it.r.peekStruct(load_command, .little) catch |err| switch (err) {
1954 error.ReadFailed => unreachable,
1955 error.EndOfStream => return error.InvalidMachO,
1956 };
1957 const data = it.r.take(hdr.cmdsize) catch |err| switch (err) {
1958 error.ReadFailed => unreachable,
1959 error.EndOfStream => return error.InvalidMachO,
19671960 };
19681961
1969 it.buffer = it.buffer[hdr.cmdsize..];
1970 it.index += 1;
1962 it.next_index += 1;
1963 return .{ .hdr = hdr, .data = data };
1964 }
19711965
1972 return cmd;
1966 pub fn init(hdr: *const mach_header_64, cmds_buf_overlong: []const u8) error{InvalidMachO}!LoadCommandIterator {
1967 if (cmds_buf_overlong.len < hdr.sizeofcmds) return error.InvalidMachO;
1968 if (hdr.ncmds > 0 and hdr.sizeofcmds < @sizeOf(load_command)) return error.InvalidMachO;
1969 const cmds_buf = cmds_buf_overlong[0..hdr.sizeofcmds];
1970 return .{
1971 .next_index = 0,
1972 .ncmds = hdr.ncmds,
1973 .r = .fixed(cmds_buf),
1974 };
19731975 }
19741976};
19751977
src/link/MachO.zig+2-2
......@@ -4167,7 +4167,7 @@ pub const Platform = struct {
41674167 /// Using Apple's ld64 as our blueprint, `min_version` as well as `sdk_version` are set to
41684168 /// the extracted minimum platform version.
41694169 pub fn fromLoadCommand(lc: macho.LoadCommandIterator.LoadCommand) Platform {
4170 switch (lc.cmd()) {
4170 switch (lc.hdr.cmd) {
41714171 .BUILD_VERSION => {
41724172 const cmd = lc.cast(macho.build_version_command).?;
41734173 return .{
......@@ -4200,7 +4200,7 @@ pub const Platform = struct {
42004200 // We can't distinguish Mac Catalyst here, but this is legacy stuff anyway.
42014201 const cmd = lc.cast(macho.version_min_command).?;
42024202 return .{
4203 .os_tag = switch (lc.cmd()) {
4203 .os_tag = switch (lc.hdr.cmd) {
42044204 .VERSION_MIN_IPHONEOS => .ios,
42054205 .VERSION_MIN_MACOSX => .macos,
42064206 .VERSION_MIN_TVOS => .tvos,
src/link/MachO/Dylib.zig+2-5
......@@ -90,11 +90,8 @@ fn parseBinary(self: *Dylib, macho_file: *MachO) !void {
9090 if (amt != lc_buffer.len) return error.InputOutput;
9191 }
9292
93 var it = LoadCommandIterator{
94 .ncmds = header.ncmds,
95 .buffer = lc_buffer,
96 };
97 while (it.next()) |cmd| switch (cmd.cmd()) {
93 var it = LoadCommandIterator.init(&header, lc_buffer) catch |err| std.debug.panic("bad dylib: {t}", .{err});
94 while (it.next() catch |err| std.debug.panic("bad dylib: {t}", .{err})) |cmd| switch (cmd.hdr.cmd) {
9895 .ID_DYLIB => {
9996 self.id = try Id.fromLoadCommand(gpa, cmd.cast(macho.dylib_command).?, cmd.getDylibPathName());
10097 },
src/link/MachO/Object.zig+4-10
......@@ -109,11 +109,8 @@ pub fn parse(self: *Object, macho_file: *MachO) !void {
109109 if (amt != self.header.?.sizeofcmds) return error.InputOutput;
110110 }
111111
112 var it = LoadCommandIterator{
113 .ncmds = self.header.?.ncmds,
114 .buffer = lc_buffer,
115 };
116 while (it.next()) |lc| switch (lc.cmd()) {
112 var it = LoadCommandIterator.init(&self.header.?, lc_buffer) catch |err| std.debug.panic("bad object: {t}", .{err});
113 while (it.next() catch |err| std.debug.panic("bad object: {t}", .{err})) |lc| switch (lc.hdr.cmd) {
117114 .SEGMENT_64 => {
118115 const sections = lc.getSections();
119116 try self.sections.ensureUnusedCapacity(gpa, sections.len);
......@@ -1644,11 +1641,8 @@ pub fn parseAr(self: *Object, macho_file: *MachO) !void {
16441641 if (amt != self.header.?.sizeofcmds) return error.InputOutput;
16451642 }
16461643
1647 var it = LoadCommandIterator{
1648 .ncmds = self.header.?.ncmds,
1649 .buffer = lc_buffer,
1650 };
1651 while (it.next()) |lc| switch (lc.cmd()) {
1644 var it = LoadCommandIterator.init(&self.header.?, lc_buffer) catch |err| std.debug.panic("bad object: {t}", .{err});
1645 while (it.next() catch |err| std.debug.panic("bad object: {t}", .{err})) |lc| switch (lc.hdr.cmd) {
16521646 .SYMTAB => {
16531647 const cmd = lc.cast(macho.symtab_command).?;
16541648 try self.strtab.resize(gpa, cmd.strsize);
tools/dump-cov.zig+27-8
......@@ -8,31 +8,50 @@ const assert = std.debug.assert;
88const SeenPcsHeader = std.Build.abi.fuzz.SeenPcsHeader;
99
1010pub fn main() !void {
11 var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init;
12 defer _ = general_purpose_allocator.deinit();
13 const gpa = general_purpose_allocator.allocator();
11 var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
12 defer _ = debug_allocator.deinit();
13 const gpa = debug_allocator.allocator();
1414
15 var arena_instance = std.heap.ArenaAllocator.init(gpa);
15 var arena_instance: std.heap.ArenaAllocator = .init(gpa);
1616 defer arena_instance.deinit();
1717 const arena = arena_instance.allocator();
1818
19 var threaded: std.Io.Threaded = .init(gpa);
20 defer threaded.deinit();
21 const io = threaded.io();
22
1923 const args = try std.process.argsAlloc(arena);
24
25 const target_query_str = switch (args.len) {
26 3 => "native",
27 4 => args[3],
28 else => return fatal(
29 \\usage: {0s} path/to/exe path/to/coverage [target]
30 \\ if omitted, 'target' defaults to 'native'
31 \\ example: {0s} zig-out/test .zig-cache/v/xxxxxxxx x86_64-linux
32 , .{if (args.len == 0) "dump-cov" else args[0]}),
33 };
34
35 const target = std.zig.resolveTargetQueryOrFatal(io, try .parse(.{
36 .arch_os_abi = target_query_str,
37 }));
38
2039 const exe_file_name = args[1];
2140 const cov_file_name = args[2];
2241
2342 const exe_path: Path = .{
24 .root_dir = std.Build.Cache.Directory.cwd(),
43 .root_dir = .cwd(),
2544 .sub_path = exe_file_name,
2645 };
2746 const cov_path: Path = .{
28 .root_dir = std.Build.Cache.Directory.cwd(),
47 .root_dir = .cwd(),
2948 .sub_path = cov_file_name,
3049 };
3150
32 var coverage = std.debug.Coverage.init;
51 var coverage: std.debug.Coverage = .init;
3352 defer coverage.deinit(gpa);
3453
35 var debug_info = std.debug.Info.load(gpa, exe_path, &coverage) catch |err| {
54 var debug_info = std.debug.Info.load(gpa, exe_path, &coverage, target.ofmt, target.cpu.arch) catch |err| {
3655 fatal("failed to load debug info for {f}: {s}", .{ exe_path, @errorName(err) });
3756 };
3857 defer debug_info.deinit(gpa);