diff --git a/doc/langref.html.in b/doc/langref.html.in index 9f32e05896..1974d76791 100644 --- a/doc/langref.html.in +++ b/doc/langref.html.in @@ -3263,7 +3263,7 @@ fn createFoo(param: i32) !Foo { {#header_open|Implementation Details#}

diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index fd4ac20eb0..2a5483907e 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -144,7 +144,7 @@ fn mainServer(init: std.process.Init.Minimal) !void { error.SkipZigTest => .skip, else => s: { if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace); + std.debug.dumpErrorReturnTrace(trace); } break :s .fail; }, @@ -312,7 +312,7 @@ fn mainTerminal(init: std.process.Init.Minimal) void { std.debug.print("FAIL ({t})\n", .{err}); } if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace); + std.debug.dumpErrorReturnTrace(trace); } test_node.end(); }, @@ -438,7 +438,7 @@ var fuzz_runner: if (builtin.fuzz) struct { error.SkipZigTest => return, else => { if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace); + std.debug.dumpErrorReturnTrace(trace); } std.debug.print("failed with error.{t}\n", .{err}); std.process.exit(1); diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index 9df1b9d038..ae7ea66974 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -67,7 +67,7 @@ test_results: TestResults, /// The return address associated with creation of this step that can be useful /// to print along with debugging messages. -debug_stack_trace: std.builtin.StackTrace, +debug_stack_trace: std.debug.StackTrace, pub const TestResults = struct { /// The total number of tests in the step. Every test has a "status" from the following: @@ -328,7 +328,7 @@ pub fn cast(step: *Step, comptime T: type) ?*T { /// For debugging purposes, prints identifying information about this Step. pub fn dump(step: *Step, t: Io.Terminal) void { const w = t.writer; - if (step.debug_stack_trace.instruction_addresses.len > 0) { + if (step.debug_stack_trace.return_addresses.len > 0) { w.print("name: '{s}'. creation stack trace:\n", .{step.name}) catch {}; std.debug.writeStackTrace(&step.debug_stack_trace, t) catch {}; } else { diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 8f31add6e2..6e9f7fb540 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -442,7 +442,7 @@ fn callFn(comptime f: anytype, args: anytype) switch (Impl) { @call(.auto, f, args) catch |err| { std.debug.print("error: {s}\n", .{@errorName(err)}); if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace); + std.debug.dumpErrorReturnTrace(trace); } }; @@ -932,7 +932,7 @@ const WasiThreadImpl = struct { @call(.auto, f, w.args) catch |err| { std.debug.print("error: {s}\n", .{@errorName(err)}); if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace); + std.debug.dumpErrorReturnTrace(trace); } }; }, diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 3e05aa49d7..3dac7229f7 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -13,7 +13,6 @@ const windows = std.os.windows; const builtin = @import("builtin"); const native_arch = builtin.cpu.arch; const native_os = builtin.os.tag; -const StackTrace = std.builtin.StackTrace; const root = @import("root"); @@ -39,8 +38,8 @@ pub const cpu_context = @import("debug/cpu_context.zig"); /// pub const init: SelfInfo; /// pub fn deinit(si: *SelfInfo, io: Io) void; /// -/// /// Returns the symbol and source location of the instruction at `address`. -/// pub fn getSymbol(si: *SelfInfo, io: Io, address: usize) SelfInfoError!Symbol; +/// /// Appends the symbols for the instruction at `address` to `symbols`. +/// pub fn getSymbols(si: *SelfInfo, io: Io, symbol_allocator: Allocator, text_arena: Allocator, address: usize, include_inline_callers: bool, symbols: *std.ArrayList(Symbol)) SelfInfoError!void; /// /// Returns a name for the "module" (e.g. shared library or executable image) containing `address`. /// pub fn getModuleName(si: *SelfInfo, io: Io, address: usize) SelfInfoError![]const u8; /// pub fn getModuleSlide(si: *SelfInfo, io: Io, address: usize) SelfInfoError!usize; @@ -563,7 +562,7 @@ pub fn defaultPanic(msg: []const u8, first_trace_addr: ?usize) noreturn { if (@errorReturnTrace()) |t| if (t.index > 0) { writer.writeAll("error return context:\n") catch break :trace; - writeStackTrace(t, stderr) catch break :trace; + writeErrorReturnTrace(t, stderr) catch break :trace; writer.writeAll("\nstack trace:\n") catch break :trace; }; writeCurrentStackTrace(.{ @@ -602,6 +601,35 @@ fn waitForOtherThreadToFinishPanicking() void { } } +pub const StackTrace = struct { + /// Each element is the "return address" of a function call, meaning the instruction address + /// which control flow will return to when the function returns. + /// + /// The first slice element corresponds to the innermost stack frame, and the last element to + /// the outermost. + /// + /// Inlined function calls do not have meaningful return addresses and are therefore not + /// included in this slice. Instead, when printing the stack trace, the source locations of + /// inline calls should be read from debug information and the corresponding "inline frames" + /// printed in the appropriate locations. + return_addresses: []usize, + /// Indicates whether any stack frames were omitted from `return_addresses`. + skipped: SkippedAddresses, +}; + +/// Indicates how many addresses were skipped in a trace. +pub const SkippedAddresses = enum(usize) { + /// No addresses were omitted: `return_addresses` contains all stack frames, including the + /// outermost. + none = 0, + /// It is not known whether any frames were omitted. + unknown = std.math.maxInt(usize), + /// The full stack trace was available, but some frames are not included in + /// `return_addresses` due to buffer size limitations. The enum value is the exact number of + /// addresses which were omitted. + _, +}; + pub const StackUnwindOptions = struct { /// If not `null`, we will ignore all frames up until this return address. This is typically /// used to omit intermediate handling code (for instance, a panic handler and its machinery) @@ -621,7 +649,10 @@ pub const StackUnwindOptions = struct { /// /// See `writeCurrentStackTrace` to immediately print the trace instead of capturing it. pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) StackTrace { - const empty_trace: StackTrace = .{ .index = 0, .instruction_addresses = &.{} }; + const empty_trace: StackTrace = .{ + .return_addresses = &.{}, + .skipped = .none, + }; if (!std.options.allow_stack_tracing) return empty_trace; var it: StackIterator = .init(options.context); defer it.deinit(); @@ -632,17 +663,17 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: var total_frames: usize = 0; var index: usize = 0; var wait_for = options.first_address; - // Ideally, we would iterate the whole stack so that the `index` in the returned trace was + // Ideally, we would iterate the whole stack so that the `index - min(buf.len, index)` would be // indicative of how many frames were skipped. However, this has a significant runtime cost // in some cases, so at least for now, we don't do that. - while (index < addr_buf.len) switch (it.next(io)) { - .switch_to_fp => if (!it.stratOk(options.allow_unsafe_unwind)) break, - .end => break, + const skipped: SkippedAddresses = while (index < addr_buf.len) switch (it.next(io)) { + .switch_to_fp => if (!it.stratOk(options.allow_unsafe_unwind)) break .unknown, + .end => break .none, .frame => |ret_addr| { if (total_frames > 10_000) { // Limit the number of frames in case of (e.g.) broken debug information which is // getting unwinding stuck in a loop. - break; + break .unknown; } total_frames += 1; if (wait_for) |target| { @@ -652,10 +683,10 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: addr_buf[index] = ret_addr; index += 1; }, - }; + } else .unknown; return .{ - .index = index, - .instruction_addresses = addr_buf[0..index], + .return_addresses = addr_buf[0..index], + .skipped = skipped, }; } /// Write the current stack trace to `writer`, annotated with source locations. @@ -663,6 +694,10 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: /// See `captureCurrentStackTrace` to capture the trace addresses into a buffer instead of printing. pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, t: Io.Terminal) Writer.Error!void { const writer = t.writer; + + var text_arena: std.heap.ArenaAllocator = .init(getDebugInfoAllocator()); + defer text_arena.deinit(); + if (!std.options.allow_stack_tracing) { t.setColor(.dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); @@ -740,7 +775,10 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, t: Io.Termin } // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(io, di, t, ret_addr -| StackIterator.ra_call_offset); + try printSourceAtAddress(io, &text_arena, di, t, .{ + .address = ret_addr -| StackIterator.ra_call_offset, + .resolve_inline_callers = true, + }); printed_any_frame = true; }, }; @@ -773,8 +811,29 @@ pub const FormatStackTrace = struct { } }; +/// Write a previously captured error return trace to `writer`, annotated with source locations. +pub fn writeErrorReturnTrace(et: *const std.builtin.StackTrace, t: Io.Terminal) Writer.Error!void { + // We take the slice by value, preventing the length from being mutated if an error occurs while + // writing the stack trace. + const len = @min(et.instruction_addresses.len, et.index); + const skipped = et.index - len; + try writeTrace(et.instruction_addresses[0..len], @enumFromInt(skipped), t, false); +} + /// Write a previously captured stack trace to `writer`, annotated with source locations. pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void { + try writeTrace(st.return_addresses, st.skipped, t, true); +} + +fn writeTrace( + addresses: []const usize, + skipped: SkippedAddresses, + t: Io.Terminal, + resolve_inline_callers: bool, +) Writer.Error!void { + var text_arena: std.heap.ArenaAllocator = .init(getDebugInfoAllocator()); + defer text_arena.deinit(); + const writer = t.writer; if (!std.options.allow_stack_tracing) { t.setColor(.dim) catch {}; @@ -783,10 +842,7 @@ pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void return; } - // Fetch `st.index` straight away. Aside from avoiding redundant loads, this prevents issues if - // `st` is `@errorReturnTrace()` and errors are encountered while writing the stack trace. - const n_frames = st.index; - if (n_frames == 0) return writer.writeAll("(empty stack trace)\n"); + if (addresses.len == 0) return writer.writeAll("(empty stack trace)\n"); const di = getSelfDebugInfo() catch |err| switch (err) { error.UnsupportedTarget => { t.setColor(.dim) catch {}; @@ -796,16 +852,26 @@ pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void }, }; const io = std.Options.debug_io; - const captured_frames = @min(n_frames, st.instruction_addresses.len); - for (st.instruction_addresses[0..captured_frames]) |ret_addr| { - // `ret_addr` is the return address, which is *after* the function call. + for (addresses) |addr| { + // `addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(io, di, t, ret_addr -| StackIterator.ra_call_offset); + try printSourceAtAddress(io, &text_arena, di, t, .{ + .address = addr -| StackIterator.ra_call_offset, + .resolve_inline_callers = resolve_inline_callers, + }); } - if (n_frames > captured_frames) { - t.setColor(.bold) catch {}; - try writer.print("({d} additional stack frames skipped...)\n", .{n_frames - captured_frames}); - t.setColor(.reset) catch {}; + switch (skipped) { + .none => {}, + .unknown => { + t.setColor(.bold) catch {}; + try writer.writeAll("(additional stack frames may have been skipped...)\n"); + t.setColor(.reset) catch {}; + }, + else => |n| { + t.setColor(.bold) catch {}; + try writer.print("({d} additional stack frames skipped due to buffer size limitations...)\n", .{n}); + t.setColor(.reset) catch {}; + }, } } /// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors. @@ -817,6 +883,15 @@ pub fn dumpStackTrace(st: *const StackTrace) void { }; } +/// A thin wrapper around `writeErrorReturnTrace` which writes to stderr and ignores write errors. +pub fn dumpErrorReturnTrace(et: *const std.builtin.StackTrace) void { + const stderr = lockStderr(&.{}).terminal(); + defer unlockStderr(); + writeErrorReturnTrace(et, stderr) catch |err| switch (err) { + error.WriteFailed => {}, + }; +} + const StackIterator = union(enum) { /// We will first report the current PC of this `CpuContextPtr`, then we will switch to a /// different strategy to actually unwind. @@ -1106,48 +1181,77 @@ pub inline fn stripInstructionPtrAuthCode(ptr: usize) usize { return ptr; } -fn printSourceAtAddress(io: Io, debug_info: *SelfInfo, t: Io.Terminal, address: usize) Writer.Error!void { - const symbol: Symbol = debug_info.getSymbol(io, address) catch |err| switch (err) { - error.MissingDebugInfo, - error.UnsupportedDebugInfo, - error.InvalidDebugInfo, - => .unknown, - error.ReadFailed, error.Unexpected, error.Canceled => s: { - t.setColor(.dim) catch {}; - try t.writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{}); - t.setColor(.reset) catch {}; - break :s .unknown; - }, - error.OutOfMemory => s: { - t.setColor(.dim) catch {}; - try t.writer.print("Ran out of memory loading debug info, trace may be incomplete\n\n", .{}); - t.setColor(.reset) catch {}; - break :s .unknown; - }, - }; - defer if (symbol.source_location) |sl| getDebugInfoAllocator().free(sl.file_name); - return printLineInfo( +const PrintSourceAddressOptions = struct { + address: usize, + resolve_inline_callers: bool, +}; + +fn printSourceAtAddress( + io: Io, + text_arena: *std.heap.ArenaAllocator, + debug_info: *SelfInfo, + t: Io.Terminal, + options: PrintSourceAddressOptions, +) Writer.Error!void { + defer _ = text_arena.reset(.retain_capacity); + + // Initialize the symbol array with space for at least one element, allocating this on the stack + // in the common case where only one element is needed + var symbol_fallback_allocator = std.heap.stackFallback(@sizeOf(Symbol) + @alignOf(Symbol) - 1, getDebugInfoAllocator()); + const symbol_allocator = symbol_fallback_allocator.get(); + var symbols = std.ArrayList(Symbol).initCapacity(symbol_allocator, 1) catch unreachable; + defer symbols.deinit(symbol_allocator); + + debug_info.getSymbols( io, - t, - symbol.source_location, - address, - symbol.name orelse "???", - symbol.compile_unit_name orelse debug_info.getModuleName(io, address) catch "???", - ); + symbol_allocator, + text_arena.allocator(), + options.address, + options.resolve_inline_callers, + &symbols, + ) catch |err| { + t.setColor(.dim) catch {}; + defer t.setColor(.reset) catch {}; + switch (err) { + error.MissingDebugInfo, + error.UnsupportedDebugInfo, + error.InvalidDebugInfo, + => {}, + error.ReadFailed, error.Unexpected, error.Canceled => { + try t.writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{}); + }, + error.OutOfMemory => { + t.setColor(.dim) catch {}; + try t.writer.print("Ran out of memory loading debug info, trace may be incomplete\n\n", .{}); + t.setColor(.reset) catch {}; + }, + } + }; + + // If we failed to write any symbols, at least write the unknown symbol. Can't fail since we + // initialized with a capacity of 1. + if (symbols.items.len == 0) symbols.appendAssumeCapacity(.unknown); + + for (symbols.items) |symbol| { + try printLineInfo(io, t, debug_info, options.address, symbol); + } } fn printLineInfo( io: Io, t: Io.Terminal, - source_location: ?SourceLocation, + debug_info: *SelfInfo, address: usize, - symbol_name: []const u8, - compile_unit_name: []const u8, + symbol: Symbol, ) Writer.Error!void { const writer = t.writer; t.setColor(.bold) catch {}; - if (source_location) |*sl| { - try writer.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column }); + if (symbol.source_location) |*sl| { + if (sl.column == 0) { + try writer.print("{s}:{d}", .{ sl.file_name, sl.line }); + } else { + try writer.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column }); + } } else { try writer.writeAll("???:?:?"); } @@ -1155,12 +1259,16 @@ fn printLineInfo( t.setColor(.reset) catch {}; try writer.writeAll(": "); t.setColor(.dim) catch {}; - try writer.print("0x{x} in {s} ({s})", .{ address, symbol_name, compile_unit_name }); + try writer.print("0x{x} in {s} ({s})", .{ + address, + symbol.name orelse "???", + symbol.compile_unit_name orelse debug_info.getModuleName(io, address) catch "???", + }); t.setColor(.reset) catch {}; try writer.writeAll("\n"); // Show the matching source code line if possible - if (source_location) |sl| { + if (symbol.source_location) |sl| { if (printLineFromFile(io, writer, sl)) { if (sl.column > 0) { // The caret already takes one char @@ -1599,7 +1707,12 @@ test "manage resources correctly" { var di: SelfInfo = .init; defer di.deinit(io); const t: Io.Terminal = .{ .writer = &discarding.writer, .mode = .no_color }; - try printSourceAtAddress(io, &di, t, S.showMyTrace()); + var text_arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer text_arena.deinit(); + try printSourceAtAddress(io, &text_arena, &di, t, .{ + .address = S.showMyTrace(), + .resolve_inline_callers = true, + }); } /// This API helps you track where a value originated and where it was mutated, @@ -1648,8 +1761,8 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize t.notes[t.index] = note; const addrs = &t.addrs[t.index]; const st = captureCurrentStackTrace(.{ .first_address = addr }, addrs); - if (st.index < addrs.len) { - @memset(addrs[st.index..], 0); // zero unused frames to indicate end of trace + if (st.return_addresses.len < addrs.len) { + @memset(addrs[st.return_addresses.len..], 0); // zero unused frames to indicate end of trace } } // Keep counting even if the end is reached so that the @@ -1667,9 +1780,10 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize stderr.writer.print("{s}:\n", .{t.notes[i]}) catch return; var frames_array_mutable = frames_array; const frames = mem.sliceTo(frames_array_mutable[0..], 0); + const len = @min(t.index, frames.len); const stack_trace: StackTrace = .{ - .index = frames.len, - .instruction_addresses = frames, + .return_addresses = frames[0..len], + .skipped = if (len < frames.len) .none else .unknown, }; writeStackTrace(&stack_trace, stderr) catch return; } diff --git a/lib/std/debug/Dwarf.zig b/lib/std/debug/Dwarf.zig index f82b4829de..fe28baf8bf 100644 --- a/lib/std/debug/Dwarf.zig +++ b/lib/std/debug/Dwarf.zig @@ -22,7 +22,9 @@ const cast = std.math.cast; const maxInt = std.math.maxInt; const ArrayList = std.ArrayList; const Endian = std.builtin.Endian; -const Reader = std.Io.Reader; +const Io = std.Io; +const Reader = Io.Reader; +const Error = std.debug.SelfInfoError; const Dwarf = @This(); @@ -1218,6 +1220,7 @@ pub fn populateSrcLocCache(d: *Dwarf, gpa: Allocator, endian: Endian, cu: *Compi pub fn getLineNumberInfo( d: *Dwarf, gpa: Allocator, + text_arena: Allocator, endian: Endian, compile_unit: *CompileUnit, target_address: u64, @@ -1230,7 +1233,7 @@ pub fn getLineNumberInfo( const file_entry = &slc.files[file_index]; if (file_entry.dir_index >= slc.directories.len) return bad(); const dir_name = slc.directories[file_entry.dir_index].path; - const file_name = try std.fs.path.join(gpa, &.{ dir_name, file_entry.path }); + const file_name = try std.fs.path.join(text_arena, &.{ dir_name, file_entry.path }); return .{ .line = entry.line, .column = entry.column, @@ -1543,21 +1546,38 @@ fn getStringGeneric(opt_str: ?[]const u8, offset: u64) ![:0]const u8 { return str[casted_offset..last :0]; } -pub fn getSymbol(di: *Dwarf, gpa: Allocator, endian: Endian, address: u64) !std.debug.Symbol { +pub fn getSymbols( + di: *Dwarf, + symbol_allocator: Allocator, + text_arena: Allocator, + endian: Endian, + address: u64, + resolve_inline_callers: bool, + symbols: *std.ArrayList(std.debug.Symbol), +) std.debug.SelfInfoError!void { + _ = resolve_inline_callers; + const gpa = std.debug.getDebugInfoAllocator(); + const compile_unit = di.findCompileUnit(endian, address) catch |err| switch (err) { - error.MissingDebugInfo, error.InvalidDebugInfo => return .unknown, - else => return err, + error.EndOfStream => return error.MissingDebugInfo, + error.Overflow => return error.InvalidDebugInfo, + error.ReadFailed, error.InvalidDebugInfo, error.MissingDebugInfo => |e| return e, }; - return .{ + try symbols.append(symbol_allocator, .{ .name = di.getSymbolName(address), .compile_unit_name = compile_unit.die.getAttrString(di, endian, std.dwarf.AT.name, di.section(.debug_str), compile_unit) catch |err| switch (err) { error.MissingDebugInfo, error.InvalidDebugInfo => null, }, - .source_location = di.getLineNumberInfo(gpa, endian, compile_unit, address) catch |err| switch (err) { + .source_location = di.getLineNumberInfo(gpa, text_arena, endian, compile_unit, address) catch |err| switch (err) { error.MissingDebugInfo, error.InvalidDebugInfo => null, - else => return err, + error.ReadFailed, + error.EndOfStream, + error.Overflow, + error.StreamTooLong, + => return error.InvalidDebugInfo, + else => |e| return e, }, - }; + }); } /// DWARF5 7.4: "In the 32-bit DWARF format, all values that represent lengths of DWARF sections and diff --git a/lib/std/debug/Pdb.zig b/lib/std/debug/Pdb.zig index 3ecfd1b363..6c48d10dcd 100644 --- a/lib/std/debug/Pdb.zig +++ b/lib/std/debug/Pdb.zig @@ -1,5 +1,6 @@ const std = @import("../std.zig"); -const File = std.Io.File; +const Io = std.Io; +const File = Io.File; const Allocator = std.mem.Allocator; const pdb = std.pdb; const assert = std.debug.assert; @@ -10,7 +11,7 @@ file_reader: *File.Reader, msf: Msf, allocator: Allocator, string_table: ?*MsfStream, -dbi: ?*MsfStream, +ipi: ?[]u8, modules: []Module, sect_contribs: []pdb.SectionContribEntry, guid: [16]u8, @@ -25,6 +26,10 @@ pub const Module = struct { symbols: []u8, subsect_info: []u8, checksum_offset: ?usize, + /// The inlinee source lines, sorted by inlinee. This saves us from repeatedly doing linear + /// searches over all inlinees. We prefer binary search over a hashmap as LLVM somtimes outputs + /// multiple entries for a single inlinee ID, see `getInlineeSourceLines` for more info. + inlinee_source_lines: []InlineeSourceLine, pub fn deinit(self: *Module, allocator: Allocator) void { allocator.free(self.module_name); @@ -32,6 +37,7 @@ pub const Module = struct { if (self.populated) { allocator.free(self.symbols); allocator.free(self.subsect_info); + allocator.free(self.inlinee_source_lines); } } }; @@ -41,7 +47,7 @@ pub fn init(gpa: Allocator, file_reader: *File.Reader) !Pdb { .file_reader = file_reader, .allocator = gpa, .string_table = null, - .dbi = null, + .ipi = null, .msf = try Msf.init(gpa, file_reader), .modules = &.{}, .sect_contribs = &.{}, @@ -53,6 +59,7 @@ pub fn init(gpa: Allocator, file_reader: *File.Reader) !Pdb { pub fn deinit(self: *Pdb) void { const gpa = self.allocator; self.msf.deinit(gpa); + if (self.ipi) |ipi| gpa.free(ipi); for (self.modules) |*module| { module.deinit(gpa); } @@ -67,7 +74,7 @@ pub fn parseDbiStream(self: *Pdb) !void { const gpa = self.allocator; const reader = &stream.interface; - const header = try reader.takeStruct(std.pdb.DbiStreamHeader, .little); + const header = try reader.takeStruct(pdb.DbiStreamHeader, .little); if (header.version_header != 19990903) // V70, only value observed by LLVM team return error.UnknownPDBVersion; // if (header.Age != age) @@ -85,14 +92,14 @@ pub fn parseDbiStream(self: *Pdb) !void { const mod_info = try reader.takeStruct(pdb.ModInfo, .little); var this_record_len: usize = @sizeOf(pdb.ModInfo); - var module_name: std.Io.Writer.Allocating = .init(gpa); + var module_name: Io.Writer.Allocating = .init(gpa); defer module_name.deinit(); this_record_len += try reader.streamDelimiterLimit(&module_name.writer, 0, .limited(1024)); assert(reader.buffered()[0] == 0); // TODO change streamDelimiterLimit API reader.toss(1); this_record_len += 1; - var obj_file_name: std.Io.Writer.Allocating = .init(gpa); + var obj_file_name: Io.Writer.Allocating = .init(gpa); defer obj_file_name.deinit(); this_record_len += try reader.streamDelimiterLimit(&obj_file_name.writer, 0, .limited(1024)); assert(reader.buffered()[0] == 0); // TODO change streamDelimiterLimit API @@ -115,6 +122,7 @@ pub fn parseDbiStream(self: *Pdb) !void { .symbols = undefined, .subsect_info = undefined, .checksum_offset = null, + .inlinee_source_lines = undefined, }); mod_info_offset += this_record_len; @@ -128,7 +136,7 @@ pub fn parseDbiStream(self: *Pdb) !void { var sect_cont_offset: usize = 0; if (section_contrib_size != 0) { - const version = reader.takeEnum(std.pdb.SectionContrSubstreamVersion, .little) catch |err| switch (err) { + const version = reader.takeEnum(pdb.SectionContrSubstreamVersion, .little) catch |err| switch (err) { error.InvalidEnumTag, error.EndOfStream => return error.InvalidDebugInfo, error.ReadFailed => return error.ReadFailed, }; @@ -148,6 +156,15 @@ pub fn parseDbiStream(self: *Pdb) !void { self.sect_contribs = try sect_contribs.toOwnedSlice(); } +pub fn parseIpiStream(self: *Pdb) !void { + const gpa = self.allocator; + const stream = self.getStream(.ipi) orelse return; + const header = try stream.interface.peekStruct(pdb.IpiStreamHeader, .little); + if (header.version != .v80) // only value observed by LLVM team + return error.UnknownPDBVersion; + self.ipi = try stream.interface.readAlloc(gpa, @sizeOf(pdb.IpiStreamHeader) + header.type_record_bytes); +} + pub fn parseInfoStream(self: *Pdb) !void { var stream = self.getStream(pdb.StreamType.pdb) orelse return error.InvalidDebugInfo; const reader = &stream.interface; @@ -212,38 +229,500 @@ pub fn parseInfoStream(self: *Pdb) !void { return error.MissingDebugInfo; } -pub fn getSymbolName(self: *Pdb, module: *Module, address: u64) ?[]const u8 { +pub fn getProcSym(self: *Pdb, module: *Module, address: u64) ?*align(1) pdb.ProcSym { _ = self; std.debug.assert(module.populated); - - var symbol_i: usize = 0; - while (symbol_i != module.symbols.len) { - const prefix: *align(1) pdb.RecordPrefix = @ptrCast(&module.symbols[symbol_i]); + var reader: Io.Reader = .fixed(module.symbols); + while (true) { + const prefix = reader.takeStructPointer(pdb.RecordPrefix) catch return null; if (prefix.record_len < 2) return null; + reader.discardAll(prefix.record_len - @sizeOf(u16)) catch return null; switch (prefix.record_kind) { .lproc32, .gproc32 => { - const proc_sym: *align(1) pdb.ProcSym = @ptrCast(&module.symbols[symbol_i + @sizeOf(pdb.RecordPrefix)]); + const proc_sym: *align(1) pdb.ProcSym = @ptrCast(prefix); if (address >= proc_sym.code_offset and address < proc_sym.code_offset + proc_sym.code_size) { - return std.mem.sliceTo(@as([*:0]u8, @ptrCast(&proc_sym.name[0])), 0); + return proc_sym; } }, else => {}, } - symbol_i += prefix.record_len + @sizeOf(u16); } - return null; } -pub fn getLineNumberInfo(self: *Pdb, module: *Module, address: u64) !std.debug.SourceLocation { +pub const InlineSiteSymIterator = struct { + module_index: usize, + offset: usize, + end: usize, + + const empty: InlineSiteSymIterator = .{ + .module_index = 0, + .offset = 0, + .end = 0, + }; + + pub fn next(iter: *InlineSiteSymIterator, module: *Module) ?*align(1) pdb.InlineSiteSym { + while (iter.offset < iter.end) { + const inline_prefix: *align(1) pdb.RecordPrefix = @ptrCast(&module.symbols[iter.offset]); + const end = iter.offset + inline_prefix.record_len + @sizeOf(u16); + if (end > iter.end) return null; + defer iter.offset = end; + switch (inline_prefix.record_kind) { + // Skip nested procedures + .lproc32, + .lproc32_st, + .gproc32, + .gproc32_st, + .lproc32_id, + .gproc32_id, + .lproc32_dpc, + .lproc32_dpc_id, + => { + const skip: *align(1) pdb.ProcSym = @ptrCast(inline_prefix); + iter.offset = skip.end; + }, + .inlinesite, + .inlinesite2, + => return @ptrCast(inline_prefix), + else => {}, + } + } + + return null; + } +}; + +pub const BinaryAnnotation = union(enum) { + code_offset: u32, + change_code_offset_base: u32, + change_code_offset: u32, + change_code_length: u32, + change_file: u32, + change_line_offset: i32, + change_line_end_delta: u32, + change_range_kind: RangeKind, + change_column_start: u32, + change_column_end_delta: i32, + change_code_offset_and_line_offset: struct { code_delta: u32, line_delta: i32 }, + change_code_length_and_code_offset: struct { length: u32, delta: u32 }, + change_column_end: u32, + + pub const RangeKind = enum(u32) { expression = 0, statement = 1 }; + + /// A virtual machine that processed binary annotations. + pub const RangeIterator = struct { + annotations: Iterator, + curr: PartialRange, + /// The previous range is tracked as the code length is sometimes implied by the subsequent + /// range. + prev: ?PartialRange, + + const PartialRange = struct { + line_offset: i32, + file_id: ?u32, + code_offset: u32, + code_length: ?u32, + + /// Resolves a partial range to a range with a definite length, or returns null if this + /// is not possible. + fn resolve(self: PartialRange, next_code_offset: ?u32) ?Range { + return .{ + .line_offset = self.line_offset, + .file_id = self.file_id, + .code_offset = self.code_offset, + .code_length = b: { + if (self.code_length) |l| break :b l; + const end = next_code_offset orelse return null; + break :b end - self.code_offset; + }, + }; + } + }; + + pub fn init(annotations: Iterator) RangeIterator { + return .{ + .annotations = annotations, + .curr = .{ + .line_offset = 0, + .file_id = null, + .code_offset = 0, + .code_length = null, + }, + .prev = null, + }; + } + + pub const Range = struct { + line_offset: i32, + file_id: ?u32, + code_offset: u32, + code_length: u32, + + pub fn contains(self: Range, offset_in_func: usize) bool { + return self.code_offset <= offset_in_func and + offset_in_func < self.code_offset + self.code_length; + } + }; + + pub fn next(self: *RangeIterator) error{InvalidDebugInfo}!?Range { + while (try self.annotations.next()) |annotation| { + switch (annotation) { + .change_code_offset => |delta| { + self.curr.code_offset += delta; + }, + .change_code_length => |length| { + if (self.prev) |*prev| prev.code_length = prev.code_length orelse length; + self.curr.code_offset += length; + }, + // LLVM has code to emit these, but I wasn't able to figure out how trigger it + // so this logic is untested. + .change_file => |file_id| { + self.curr.file_id = file_id; + }, + // LLVM never emits this opcode, but it's clear enough how to interpret it so we + // may as well handle it in case they emit it in the future + .change_code_length_and_code_offset => |info| { + self.curr.code_length = info.length; + self.curr.code_offset += info.delta; + }, + .change_line_offset => |delta| { + self.curr.line_offset += delta; + }, + .change_code_offset_and_line_offset => |info| { + self.curr.code_offset += info.code_delta; + self.curr.line_offset += info.line_delta; + }, + + // Not emitted by LLVM at the time of writing, and we don't want to add support + // without a test case. Safe to ignore since we don't use this info right now. + .change_line_end_delta, + .change_column_start, + .change_column_end_delta, + .change_column_end, + => {}, + + // Not emitted by LLVM at the time of writing. Various sources conflict on how + // these opcodes should be interpreted, so we make no attempt to handle them. + .code_offset, + .change_code_offset_base, + .change_range_kind, + => { + self.annotations = .empty; + self.prev = null; + return null; + }, + } + + // If we have a new code offset, return the previous range if it exists, resolving + // its length if necessary. + switch (annotation) { + .change_code_offset, + .change_code_offset_and_line_offset, + .change_code_length_and_code_offset, + => {}, + else => continue, + } + defer self.prev = self.curr; + const prev = self.prev orelse continue; + return prev.resolve(self.curr.code_offset); + } + + // If we've processed all the binary operations but still have a previous range leftover + // with a known length, return it. + const prev = self.prev orelse return null; + defer self.prev = null; + return prev.resolve(null); + } + }; + + pub const Iterator = struct { + reader: Io.Reader, + + pub const empty: Iterator = .{ .reader = .ending_instance }; + + pub fn next(self: *Iterator) error{InvalidDebugInfo}!?BinaryAnnotation { + return take(&self.reader) catch |err| switch (err) { + error.ReadFailed => return error.InvalidDebugInfo, + error.EndOfStream => return null, + }; + } + }; + + pub fn take(reader: *Io.Reader) Io.Reader.Error!BinaryAnnotation { + const op = std.enums.fromInt( + pdb.BinaryAnnotationOpcode, + try takePackedU32(reader), + ) orelse return error.ReadFailed; + switch (op) { + // Microsoft's docs say that invalid is used as padding, though it is left ambiguous + // whether padding is allowed internally or only after all instructions are complete. + // Empirically, the latter appears to be the case, at least with the output from LLVM + // that I've tested. + .invalid => return error.EndOfStream, + .code_offset => return .{ + .code_offset = try expect(takePackedU32(reader)), + }, + .change_code_offset_base => return .{ + .change_code_offset_base = try expect(takePackedU32(reader)), + }, + .change_code_offset => return .{ + .change_code_offset = try expect(takePackedU32(reader)), + }, + .change_code_length => return .{ + .change_code_length = try expect(takePackedU32(reader)), + }, + .change_file => return .{ + .change_file = try expect(takePackedU32(reader)), + }, + .change_line_offset => return .{ + .change_line_offset = try expect(takePackedI32(reader)), + }, + .change_line_end_delta => return .{ + .change_line_end_delta = try expect(takePackedU32(reader)), + }, + .change_range_kind => return .{ + .change_range_kind = std.enums.fromInt( + RangeKind, + try expect(takePackedU32(reader)), + ) orelse return error.ReadFailed, + }, + .change_column_start => return .{ + .change_column_start = try expect(takePackedU32(reader)), + }, + .change_column_end_delta => return .{ + .change_column_end_delta = try expect(takePackedI32(reader)), + }, + .change_code_offset_and_line_offset => { + const EncodedArgs = packed struct(u32) { + code_delta: u4, + encoded_line_delta: u28, + }; + const args: EncodedArgs = @bitCast(try expect(takePackedU32(reader))); + return .{ + .change_code_offset_and_line_offset = .{ + .code_delta = args.code_delta, + .line_delta = decodeI32(args.encoded_line_delta), + }, + }; + }, + .change_code_length_and_code_offset => return .{ + .change_code_length_and_code_offset = .{ + .length = try expect(takePackedU32(reader)), + .delta = try expect(takePackedU32(reader)), + }, + }, + .change_column_end => return .{ + .change_column_end = try expect(takePackedU32(reader)), + }, + } + } + + // Adapted from: + // https://github.com/microsoft/microsoft-pdb/blob/805655a28bd8198004be2ac27e6e0290121a5e89/include/cvinfo.h#L4942 + pub fn takePackedU32(reader: *Io.Reader) Io.Reader.Error!u32 { + const b0: u32 = try reader.takeByte(); + if (b0 & 0x80 == 0x00) return b0; + + const b1: u32 = try reader.takeByte(); + if (b0 & 0xC0 == 0x80) return ((b0 & 0x3F) << 8) | b1; + + const b2: u32 = try reader.takeByte(); + const b3: u32 = try reader.takeByte(); + if (b0 & 0xE0 == 0xC0) return ((b0 & 0x1f) << 24) | (b1 << 16) | (b2 << 8) | b3; + + return error.ReadFailed; + } + + pub fn takePackedI32(reader: *Io.Reader) Io.Reader.Error!i32 { + return decodeI32(try takePackedU32(reader)); + } + + pub fn decodeI32(u: u32) i32 { + const i: i32 = @bitCast(u); + if (i & 1 != 0) { + return -(i >> 1); + } else { + return i >> 1; + } + } + + fn expect(value: anytype) error{ReadFailed}!@typeInfo(@TypeOf(value)).error_union.payload { + comptime assert(@typeInfo(@TypeOf(value)).error_union.error_set == Io.Reader.Error); + return value catch error.ReadFailed; + } +}; + +pub fn findInlineeName(self: *const Pdb, inlinee: u32) ?[]const u8 { + // According to LLVM, the high bit *can* be used to indicate that a type index comes from the + // ipi stream in which case that bit needs to be cleared. LLVM doesn't generate data in this + // manner, but we may as well handle it since it just involves a single bitwise and. + // https://llvm.org/docs/PDB/TpiStream.html#type-indices + const type_index = inlinee & 0x7FFFFFFF; + + var reader: Io.Reader = .fixed(self.ipi orelse return null); + const header = reader.takeStructPointer(pdb.IpiStreamHeader) catch return null; + for (header.type_index_begin..header.type_index_end) |curr_type_index| { + const prefix = reader.takeStructPointer(pdb.LfRecordPrefix) catch return null; + if (prefix.len < 2) return null; + reader.discardAll(prefix.len - @sizeOf(u16)) catch return null; + + if (curr_type_index == type_index) { + switch (prefix.kind) { + .func_id => { + const func: *align(1) pdb.LfFuncId = @ptrCast(prefix); + return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(&func.name[0])), 0); + }, + .mfunc_id => { + const func: *align(1) pdb.LfMFuncId = @ptrCast(prefix); + return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(&func.name[0])), 0); + }, + else => return null, + } + } + } + return null; +} + +pub fn getInlinees(self: *Pdb, module: *Module, proc_sym: *align(1) const pdb.ProcSym) InlineSiteSymIterator { + const module_index = module - self.modules.ptr; + const offset = @intFromPtr(proc_sym) - + @intFromPtr(module.symbols.ptr) + + proc_sym.record_len + + @sizeOf(u16); + const symbols_end = @intFromPtr(module.symbols.ptr) + module.symbols.len; + if (offset > symbols_end or proc_sym.end > symbols_end) return .empty; + return .{ + .module_index = module_index, + .offset = offset, + .end = proc_sym.end, + }; +} + +pub fn getBinaryAnnotations(self: *Pdb, module: *Module, site: *align(1) const pdb.InlineSiteSym) BinaryAnnotation.Iterator { + _ = self; + var start: usize = @intFromPtr(site) + @sizeOf(pdb.InlineSiteSym); + var end = start + site.record_len + @sizeOf(u16) - @sizeOf(pdb.InlineSiteSym); + switch (site.record_kind) { + .inlinesite => {}, + .inlinesite2 => start += @sizeOf(pdb.InlineSiteSym2) - @sizeOf(pdb.InlineSiteSym), + else => end = start, + } + if (start < @intFromPtr(module.symbols.ptr) or end > @intFromPtr(module.symbols.ptr) + module.symbols.len) return .empty; + const len = end - start; + const ptr: [*]const u8 = @ptrFromInt(start); + const slice = ptr[0..len]; + return .{ .reader = Io.Reader.fixed(slice) }; +} + +pub fn getInlineSiteSourceLocation( + self: *Pdb, + gpa: Allocator, + mod: *Module, + site: *align(1) const pdb.InlineSiteSym, + inlinee_src_line: *align(1) const pdb.InlineeSourceLine, + offset_in_func: usize, +) !?std.debug.SourceLocation { + var ranges: BinaryAnnotation.RangeIterator = .init(self.getBinaryAnnotations(mod, site)); + while (try ranges.next()) |range| { + if (!range.contains(offset_in_func)) continue; + + const file_id = range.file_id orelse inlinee_src_line.file_id; + const file_name = try self.getFileName(gpa, mod, file_id); + errdefer self.allocator.free(file_name); + + return .{ + .line = inlinee_src_line.source_line_num +% @as(u32, @bitCast(range.line_offset)), + // LLVM doesn't currently emit column information for inlined calls in PDBs. + .column = 0, + .file_name = file_name, + }; + } + return null; +} + +pub fn getFileName(self: *Pdb, gpa: Allocator, mod: *Module, file_id: u32) ![]const u8 { + const checksum_offset = mod.checksum_offset orelse return error.MissingDebugInfo; + const subsect_index = checksum_offset + file_id; + const chksum_hdr: *align(1) pdb.FileChecksumEntryHeader = @ptrCast(&mod.subsect_info[subsect_index]); + const strtab_offset = @sizeOf(pdb.StringTableHeader) + chksum_hdr.file_name_offset; + self.string_table.?.seekTo(strtab_offset) catch return error.InvalidDebugInfo; + const string_reader = &self.string_table.?.interface; + var source_file_name: Io.Writer.Allocating = .init(gpa); + defer source_file_name.deinit(); + _ = try string_reader.streamDelimiterLimit(&source_file_name.writer, 0, .limited(1024)); + assert(string_reader.buffered()[0] == 0); // TODO change streamDelimiterLimit API + string_reader.toss(1); + return try source_file_name.toOwnedSlice(); +} + +pub fn getSymbolName(self: *Pdb, proc_sym: *align(1) const pdb.ProcSym) []const u8 { + _ = self; + return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(&proc_sym.name[0])), 0); +} + +pub const InlineeSourceLine = struct { + signature: pdb.InlineeSourceLineSignature, + info: *align(1) const pdb.InlineeSourceLine, + + fn lessThan(_: void, lhs: InlineeSourceLine, rhs: InlineeSourceLine) bool { + return lhs.info.inlinee < rhs.info.inlinee; + } + + fn compare(inlinee: u32, self: InlineeSourceLine) std.math.Order { + return std.math.order(inlinee, self.info.inlinee); + } +}; + +/// Returns all `InlineeSourceLine`s for a given module with the given inlinee. Ideally there would +/// only be one entry per inlinee, but LLVM appears to assign all functions that share a name the +/// same inlinee ID. This appears to be a bug, so the best the caller can do right now is print all +/// the results. +pub fn getInlineeSourceLines( + self: *Pdb, + mod: *Module, + inlinee: u32, +) []const InlineeSourceLine { + _ = self; + + // Binary search to an arbitrary match, if there are other matches they will be adjacent + const any = std.sort.binarySearch( + InlineeSourceLine, + mod.inlinee_source_lines, + inlinee, + InlineeSourceLine.compare, + ) orelse return &.{}; + + // Linearly scan to the first match + const begin = b: { + var begin = any; + while (begin > 0) { + const prev = begin - 1; + if (mod.inlinee_source_lines[prev].info.inlinee != inlinee) break; + begin = prev; + } + break :b begin; + }; + + // Linearly scan to the last match + const end = b: { + var end = any + 1; + while (end < mod.inlinee_source_lines.len and + mod.inlinee_source_lines[end].info.inlinee == inlinee) : (end += 1) + {} + break :b end; + }; + + // Return a slice of all the matches + return mod.inlinee_source_lines[begin..end]; +} + +pub fn getLineNumberInfo(self: *Pdb, gpa: Allocator, module: *Module, address: u64) !std.debug.SourceLocation { std.debug.assert(module.populated); const subsect_info = module.subsect_info; - const gpa = self.allocator; var sect_offset: usize = 0; var skip_len: usize = undefined; - const checksum_offset = module.checksum_offset orelse return error.MissingDebugInfo; while (sect_offset != subsect_info.len) : (sect_offset += skip_len) { const subsect_hdr: *align(1) pdb.DebugSubsectionHeader = @ptrCast(&subsect_info[sect_offset]); skip_len = subsect_hdr.length; @@ -290,20 +769,8 @@ pub fn getLineNumberInfo(self: *Pdb, module: *Module, address: u64) !std.debug.S // line_i == 0 would mean that no matching pdb.LineNumberEntry was found. if (line_i > 0) { - const subsect_index = checksum_offset + block_hdr.name_index; - const chksum_hdr: *align(1) pdb.FileChecksumEntryHeader = @ptrCast(&module.subsect_info[subsect_index]); - const strtab_offset = @sizeOf(pdb.StringTableHeader) + chksum_hdr.file_name_offset; - try self.string_table.?.seekTo(strtab_offset); - const source_file_name = s: { - const string_reader = &self.string_table.?.interface; - var source_file_name: std.Io.Writer.Allocating = .init(gpa); - defer source_file_name.deinit(); - _ = try string_reader.streamDelimiterLimit(&source_file_name.writer, 0, .limited(1024)); - assert(string_reader.buffered()[0] == 0); // TODO change streamDelimiterLimit API - string_reader.toss(1); - break :s try source_file_name.toOwnedSlice(); - }; - errdefer gpa.free(source_file_name); + const file_name = try self.getFileName(gpa, module, block_hdr.name_index); + errdefer gpa.free(file_name); const line_entry_idx = line_i - 1; @@ -318,7 +785,7 @@ pub fn getLineNumberInfo(self: *Pdb, module: *Module, address: u64) !std.debug.S const line_num_entry: *align(1) pdb.LineNumberEntry = @ptrCast(&subsect_info[found_line_index]); return .{ - .file_name = source_file_name, + .file_name = file_name, .line = line_num_entry.flags.start, .column = column, }; @@ -366,7 +833,43 @@ pub fn getModule(self: *Pdb, index: usize) !?*Module { const gpa = self.allocator; mod.symbols = try reader.readAlloc(gpa, mod.mod_info.sym_byte_size - 4); + errdefer gpa.free(mod.symbols); mod.subsect_info = try reader.readAlloc(gpa, mod.mod_info.c13_byte_size); + errdefer gpa.free(mod.subsect_info); + mod.inlinee_source_lines = b: { + var inlinee_source_lines: std.ArrayList(InlineeSourceLine) = .empty; + defer inlinee_source_lines.deinit(gpa); + var subsects: Io.Reader = .fixed(mod.subsect_info); + while (subsects.takeStructPointer(pdb.DebugSubsectionHeader) catch null) |subsect_hdr| { + var subsect: Io.Reader = .fixed(subsects.take(subsect_hdr.length) catch return null); + if (subsect_hdr.kind == .inlinee_lines) { + const inlinee_source_line_signature = subsect.takeEnum(pdb.InlineeSourceLineSignature, .little) catch return error.InvalidDebugInfo; + const has_extra_files = switch (inlinee_source_line_signature) { + .normal => false, + .ex => true, + else => continue, + }; + while (subsect.takeStructPointer(pdb.InlineeSourceLine) catch null) |info| { + if (has_extra_files) { + const file_count = subsect.takeInt(u32, .little) catch + return error.InvalidDebugInfo; + const file_bytes = std.math.mul(usize, file_count, @sizeOf(u32)) catch return error.InvalidDebugInfo; + subsect.discardAll(file_bytes) catch + return error.InvalidDebugInfo; + } + + try inlinee_source_lines.append(gpa, .{ + .signature = inlinee_source_line_signature, + .info = info, + }); + } + } + } + + std.mem.sortUnstable(InlineeSourceLine, inlinee_source_lines.items, {}, InlineeSourceLine.lessThan); + break :b try inlinee_source_lines.toOwnedSlice(gpa); + }; + errdefer gpa.free(mod.inlinee_source_lines); var sect_offset: usize = 0; var skip_len: usize = undefined; @@ -497,7 +1000,7 @@ const MsfStream = struct { next_read_pos: u64, blocks: []u32, block_size: u32, - interface: std.Io.Reader, + interface: Io.Reader, err: ?Error, const Error = File.Reader.SeekError; @@ -527,7 +1030,7 @@ const MsfStream = struct { }; } - fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { + fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { const ms: *MsfStream = @alignCast(@fieldParentPtr("interface", r)); var block_id: usize = @intCast(ms.next_read_pos / ms.block_size); @@ -595,7 +1098,7 @@ const MsfStream = struct { } }; -fn readSparseBitVector(reader: *std.Io.Reader, allocator: Allocator) ![]u32 { +fn readSparseBitVector(reader: *Io.Reader, allocator: Allocator) ![]u32 { const num_words = try reader.takeInt(u32, .little); var list = std.array_list.Managed(u32).init(allocator); errdefer list.deinit(); diff --git a/lib/std/debug/SelfInfo/Elf.zig b/lib/std/debug/SelfInfo/Elf.zig index 263f292e99..b56e32983f 100644 --- a/lib/std/debug/SelfInfo/Elf.zig +++ b/lib/std/debug/SelfInfo/Elf.zig @@ -30,7 +30,15 @@ pub fn deinit(si: *SelfInfo, io: Io) void { if (si.unwind_cache) |cache| gpa.free(cache); } -pub fn getSymbol(si: *SelfInfo, io: Io, address: usize) Error!std.debug.Symbol { +pub fn getSymbols( + si: *SelfInfo, + io: Io, + symbol_allocator: Allocator, + text_arena: Allocator, + address: usize, + resolve_inline_callers: bool, + symbols: *std.ArrayList(std.debug.Symbol), +) Error!void { const gpa = std.debug.getDebugInfoAllocator(); const module = try si.findModule(gpa, io, address, .exclusive); defer si.rwlock.unlock(io); @@ -53,28 +61,21 @@ pub fn getSymbol(si: *SelfInfo, io: Io, address: usize) Error!std.debug.Symbol { }; loaded_elf.scanned_dwarf = true; } - if (dwarf.getSymbol(gpa, native_endian, vaddr)) |sym| { - return sym; - } else |err| switch (err) { - error.MissingDebugInfo => {}, - - error.InvalidDebugInfo, - error.OutOfMemory, - => |e| return e, - - error.ReadFailed, - error.EndOfStream, - error.Overflow, - error.StreamTooLong, - => return error.InvalidDebugInfo, - } + return dwarf.getSymbols( + symbol_allocator, + text_arena, + native_endian, + vaddr, + resolve_inline_callers, + symbols, + ); } // When DWARF is unavailable, fall back to searching the symtab. - return loaded_elf.file.searchSymtab(gpa, vaddr) catch |err| switch (err) { + try symbols.append(symbol_allocator, loaded_elf.file.searchSymtab(gpa, vaddr) catch |err| switch (err) { error.NoSymtab, error.NoStrtab => return error.MissingDebugInfo, error.BadSymtab => return error.InvalidDebugInfo, error.OutOfMemory => |e| return e, - }; + }); } pub fn getModuleName(si: *SelfInfo, io: Io, address: usize) Error![]const u8 { const gpa = std.debug.getDebugInfoAllocator(); diff --git a/lib/std/debug/SelfInfo/MachO.zig b/lib/std/debug/SelfInfo/MachO.zig index 6b184fec7a..91c8cd41dc 100644 --- a/lib/std/debug/SelfInfo/MachO.zig +++ b/lib/std/debug/SelfInfo/MachO.zig @@ -22,8 +22,18 @@ pub fn deinit(si: *SelfInfo, io: Io) void { si.modules.deinit(gpa); } -pub fn getSymbol(si: *SelfInfo, io: Io, address: usize) Error!std.debug.Symbol { +pub fn getSymbols( + si: *SelfInfo, + io: Io, + symbol_allocator: Allocator, + text_arena: Allocator, + address: usize, + resolve_inline_callers: bool, + symbols: *std.ArrayList(std.debug.Symbol), +) Error!void { + _ = resolve_inline_callers; const gpa = std.debug.getDebugInfoAllocator(); + const module = try si.findModule(gpa, io, address); defer si.mutex.unlock(io); @@ -43,23 +53,23 @@ pub fn getSymbol(si: *SelfInfo, io: Io, address: usize) Error!std.debug.Symbol { const ofile_dwarf, const ofile_vaddr = file.getDwarfForAddress(gpa, io, vaddr) catch { // Return at least the symbol name if available. - return .{ + return symbols.append(symbol_allocator, .{ .name = try file.lookupSymbolName(vaddr), .compile_unit_name = null, .source_location = null, - }; + }); }; const compile_unit = ofile_dwarf.findCompileUnit(native_endian, ofile_vaddr) catch { // Return at least the symbol name if available. - return .{ + return symbols.append(symbol_allocator, .{ .name = try file.lookupSymbolName(vaddr), .compile_unit_name = null, .source_location = null, - }; + }); }; - return .{ + try symbols.append(symbol_allocator, .{ .name = ofile_dwarf.getSymbolName(ofile_vaddr) orelse try file.lookupSymbolName(vaddr), .compile_unit_name = compile_unit.die.getAttrString( @@ -73,11 +83,12 @@ pub fn getSymbol(si: *SelfInfo, io: Io, address: usize) Error!std.debug.Symbol { }, .source_location = ofile_dwarf.getLineNumberInfo( gpa, + text_arena, native_endian, compile_unit, ofile_vaddr, ) catch null, - }; + }); } pub fn getModuleName(si: *SelfInfo, io: Io, address: usize) Error![]const u8 { _ = si; diff --git a/lib/std/debug/SelfInfo/Windows.zig b/lib/std/debug/SelfInfo/Windows.zig index c7323c722a..50684e9b2a 100644 --- a/lib/std/debug/SelfInfo/Windows.zig +++ b/lib/std/debug/SelfInfo/Windows.zig @@ -1,10 +1,10 @@ -mutex: Io.Mutex, +lock: Io.RwLock, ntdll_handle: ?if (load_dll_notification_procs) *anyopaque else noreturn, notification_cookie: ?LDR.DLL_NOTIFICATION.COOKIE, modules: std.ArrayList(Module), pub const init: SelfInfo = .{ - .mutex = .init, + .lock = .init, .ntdll_handle = null, .notification_cookie = null, .modules = .empty, @@ -25,18 +25,33 @@ pub fn deinit(si: *SelfInfo, io: Io) void { si.modules.deinit(gpa); } -pub fn getSymbol(si: *SelfInfo, io: Io, address: usize) Error!std.debug.Symbol { +pub fn getSymbols( + si: *SelfInfo, + io: Io, + symbol_allocator: Allocator, + text_arena: Allocator, + address: usize, + resolve_inline_callers: bool, + symbols: *std.ArrayList(std.debug.Symbol), +) Error!void { const gpa = std.debug.getDebugInfoAllocator(); - try si.mutex.lock(io); - defer si.mutex.unlock(io); + try si.lock.lockShared(io); + defer si.lock.unlockShared(io); const module = try si.findModule(gpa, address); const di = try module.getDebugInfo(gpa, io); - return di.getSymbol(gpa, address - @intFromPtr(module.entry.DllBase)); + return di.getSymbols( + symbol_allocator, + text_arena, + address - @intFromPtr(module.entry.DllBase), + resolve_inline_callers, + symbols, + ); } + pub fn getModuleName(si: *SelfInfo, io: Io, address: usize) Error![]const u8 { const gpa = std.debug.getDebugInfoAllocator(); - try si.mutex.lock(io); - defer si.mutex.unlock(io); + try si.lock.lockShared(io); + defer si.lock.unlockShared(io); const module = try si.findModule(gpa, address); return module.name orelse { const name = try std.unicode.wtf16LeToWtf8Alloc(gpa, module.entry.BaseDllName.slice()); @@ -46,8 +61,8 @@ pub fn getModuleName(si: *SelfInfo, io: Io, address: usize) Error![]const u8 { } pub fn getModuleSlide(si: *SelfInfo, io: Io, address: usize) Error!usize { const gpa = std.debug.getDebugInfoAllocator(); - try si.mutex.lock(io); - defer si.mutex.unlock(io); + try si.lock.lockShared(io); + defer si.lock.unlockShared(io); const module = try si.findModule(gpa, address); return module.base_address; } @@ -240,7 +255,14 @@ const Module = struct { arena.deinit(); } - fn getSymbol(di: *DebugInfo, gpa: Allocator, vaddr: usize) Error!std.debug.Symbol { + fn getSymbols( + di: *DebugInfo, + symbol_allocator: Allocator, + text_arena: Allocator, + vaddr: usize, + resolve_inline_callers: bool, + symbols: *std.ArrayList(std.debug.Symbol), + ) Error!void { pdb: { const pdb = &(di.pdb orelse break :pdb); var coff_section: *align(1) const coff.SectionHeader = undefined; @@ -270,32 +292,101 @@ const Module = struct { } orelse { return error.InvalidDebugInfo; // bad module index }; - return .{ - .name = pdb.getSymbolName(module, vaddr - coff_section.virtual_address), - .compile_unit_name = fs.path.basename(module.obj_file_name), - .source_location = pdb.getLineNumberInfo( - module, - vaddr - coff_section.virtual_address, - ) catch null, - }; + + const addr = vaddr - coff_section.virtual_address; + const maybe_proc = pdb.getProcSym(module, addr); + const compile_unit_name = fs.path.basename(module.obj_file_name); + const symbols_top = symbols.items.len; + if (maybe_proc) |proc| { + const offset_in_func = addr - proc.code_offset; + var last_inlinee: ?u32 = null; + var iter = pdb.getInlinees(module, proc); + while (iter.next(module)) |inline_site| { + // Filter out duplicate inline sites. Tools like llvm-addr2line output + // duplicate sites in the same cases as us if we elide this check, + // implying that they exist in the underlying data and are not indicative + // of a parser bug. No useful information is lost here since an inline site + // can't actually reference itself. + if (inline_site.inlinee == last_inlinee) continue; + + // If our address points into this site, get the source location(s) it + // points at + for (pdb.getInlineeSourceLines( + module, + inline_site.inlinee, + )) |inlinee_src_line| { + const maybe_loc = pdb.getInlineSiteSourceLocation( + text_arena, + module, + inline_site, + inlinee_src_line.info, + offset_in_func, + ) catch continue; + const loc = maybe_loc orelse continue; + + // If we aren't trying to resolve inline callers, and we've matched a + // new inline site, we want to overwrite the previously appended + // results. + if (!resolve_inline_callers and inline_site.inlinee != last_inlinee) { + symbols.items.len = symbols_top; + } + + // Only resolve the name if we're resolving inline callers, otherwise + // wait until we're done to avoid duplicated work. + const name = if (resolve_inline_callers) + pdb.findInlineeName(inline_site.inlinee) + else + null; + + try symbols.append(symbol_allocator, .{ + .name = name, + .compile_unit_name = compile_unit_name, + .source_location = loc, + }); + + last_inlinee = inline_site.inlinee; + } + } + + if (resolve_inline_callers) { + // Inline sites are stored in the pdb in reverse order, so we reverse the + // matching sites here. We could alternatively use the parent fields to + // determine the order, but this would introduce seemingly unecessary + // complexity. + std.mem.reverse(std.debug.Symbol, symbols.items); + } else if (last_inlinee) |inlinee| { + // If we aren't resolving inline callers, then all results will have the + // same inline site, and we resolve its name once at the end. + const name = pdb.findInlineeName(inlinee); + for (symbols.items) |*symbol| symbol.name = name; + } + } + + // If there's room for another symbol, add the actual proc + if (resolve_inline_callers or symbols.items.len == 0) { + try symbols.append(symbol_allocator, .{ + .name = if (maybe_proc) |proc| pdb.getSymbolName(proc) else null, + .compile_unit_name = compile_unit_name, + .source_location = pdb.getLineNumberInfo(text_arena, module, addr) catch null, + }); + } + + return; } + dwarf: { const dwarf = &(di.dwarf orelse break :dwarf); - const dwarf_address = vaddr + di.coff_image_base; - return dwarf.getSymbol(gpa, native_endian, dwarf_address) catch |err| switch (err) { - error.MissingDebugInfo => break :dwarf, - - error.InvalidDebugInfo, - error.OutOfMemory, - => |e| return e, - - error.ReadFailed, - error.EndOfStream, - error.Overflow, - error.StreamTooLong, - => return error.InvalidDebugInfo, - }; + const addr = vaddr + di.coff_image_base; + return dwarf.getSymbols( + symbol_allocator, + text_arena, + native_endian, + addr, + resolve_inline_callers, + symbols, + ); } + return error.MissingDebugInfo; } }; @@ -505,6 +596,16 @@ const Module = struct { error.ReadFailed, => |e| return e, }; + pdb.parseIpiStream() catch |err| switch (err) { + error.UnknownPDBVersion => return error.UnsupportedDebugInfo, + + error.EndOfStream, + => return error.InvalidDebugInfo, + + error.OutOfMemory, + error.ReadFailed, + => |e| return e, + }; if (!std.mem.eql(u8, &coff_obj.guid, &pdb.guid) or coff_obj.age != pdb.age) return error.InvalidDebugInfo; @@ -531,7 +632,7 @@ const Module = struct { } }; -/// Assumes we already hold `si.mutex`. +/// Assumes we already hold `si.lock`. fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) error{ MissingDebugInfo, OutOfMemory, Unexpected }!*Module { for (si.modules.items) |*mod| { const base = @intFromPtr(mod.entry.DllBase); @@ -601,8 +702,8 @@ fn dllNotification( .LOADED => {}, .UNLOADED => { const io = std.Options.debug_io; - si.mutex.lockUncancelable(io); - defer si.mutex.unlock(io); + si.lock.lockUncancelable(io); + defer si.lock.unlock(io); for (si.modules.items, 0..) |*mod, mod_index| { if (mod.entry.DllBase != data.Unloaded.DllBase) continue; mod.deinit(std.debug.getDebugInfoAllocator(), io); diff --git a/lib/std/heap/debug_allocator.zig b/lib/std/heap/debug_allocator.zig index d6f2bf1796..30d0bcee0b 100644 --- a/lib/std/heap/debug_allocator.zig +++ b/lib/std/heap/debug_allocator.zig @@ -81,7 +81,7 @@ //! Resizing and remapping are forwarded directly to the backing allocator, //! except where such operations would change the category from large to small. const builtin = @import("builtin"); -const StackTrace = std.builtin.StackTrace; +const StackTrace = std.debug.StackTrace; const std = @import("std"); const log = std.log.scoped(.DebugAllocator); @@ -229,7 +229,7 @@ pub fn DebugAllocator(comptime config: Config) type { std.debug.dumpStackTrace(self.getStackTrace(trace_kind)); } - fn getStackTrace(self: *LargeAlloc, trace_kind: TraceKind) std.builtin.StackTrace { + fn getStackTrace(self: *LargeAlloc, trace_kind: TraceKind) std.debug.StackTrace { assert(@intFromEnum(trace_kind) < trace_n); const stack_addresses = &self.stack_addresses[@intFromEnum(trace_kind)]; var len: usize = 0; @@ -237,8 +237,8 @@ pub fn DebugAllocator(comptime config: Config) type { len += 1; } return .{ - .instruction_addresses = stack_addresses, - .index = len, + .return_addresses = stack_addresses[0..len], + .skipped = if (len < stack_addresses.len) .none else .unknown, }; } @@ -339,8 +339,8 @@ pub fn DebugAllocator(comptime config: Config) type { len += 1; } return .{ - .instruction_addresses = stack_addresses, - .index = len, + .return_addresses = stack_addresses[0..len], + .skipped = if (len < stack_addresses.len) .none else .unknown, }; } @@ -508,7 +508,7 @@ pub fn DebugAllocator(comptime config: Config) type { fn collectStackTrace(first_trace_addr: usize, addr_buf: *[stack_n]usize) void { const st = std.debug.captureCurrentStackTrace(.{ .first_address = first_trace_addr }, addr_buf); - @memset(addr_buf[@min(st.index, addr_buf.len)..], 0); + @memset(addr_buf[@min(st.return_addresses.len, addr_buf.len)..], 0); } fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void { diff --git a/lib/std/pdb.zig b/lib/std/pdb.zig index 094537972b..5478c01c41 100644 --- a/lib/std/pdb.zig +++ b/lib/std/pdb.zig @@ -314,11 +314,9 @@ pub const SymbolKind = enum(u16) { pub const TypeIndex = u32; -// TODO According to this header: -// https://github.com/microsoft/microsoft-pdb/blob/082c5290e5aff028ae84e43affa8be717aa7af73/include/cvinfo.h#L3722 -// we should define RecordPrefix as part of the ProcSym structure. -// This might be important when we start generating PDB in self-hosted with our own PE linker. pub const ProcSym = extern struct { + record_len: u16, + record_kind: SymbolKind, parent: u32, end: u32, next: u32, @@ -508,3 +506,144 @@ pub const SuperBlock = extern struct { // implement it so we're kind of safe making this assumption for now. block_map_addr: u32, }; + +pub const IpiStreamVersion = enum(u32) { + v40 = 19950410, + v41 = 19951122, + v50 = 19961031, + v70 = 19990903, + v80 = 20040203, + _, +}; + +pub const IpiStreamHeader = extern struct { + version: IpiStreamVersion, + header_size: u32, + type_index_begin: u32, + type_index_end: u32, + type_record_bytes: u32, + hash_stream_index: u16, + hash_aux_stream_index: u16, + hash_key_size: u32, + num_hash_buckets: u32, + hash_value_buffer_offset: i32, + hash_value_buffer_length: u32, + index_offset_buffer_offset: i32, + index_offset_buffer_length: u32, + hash_adj_buffer_offset: i32, + hash_adj_buffer_length: u32, +}; + +pub const LfRecordPrefix = extern struct { + len: u16, + kind: LfRecordKind, +}; + +pub const LfRecordKind = enum(u16) { + pointer = 0x1002, + modifier = 0x1001, + procedure = 0x1008, + mfunction = 0x1009, + label = 0x000e, + arglist = 0x1201, + fieldlist = 0x1203, + array = 0x1503, + class = 0x1504, + structure = 0x1505, + interface = 0x1519, + @"union" = 0x1506, + @"enum" = 0x1507, + typeserver2 = 0x1515, + vftable = 0x151d, + vtshape = 0x000a, + bitfield = 0x1205, + func_id = 0x1601, + mfunc_id = 0x1602, + buildinfo = 0x1603, + substr_list = 0x1604, + string_id = 0x1605, + udt_src_line = 0x1606, + udt_mod_src_line = 0x1607, + methodlist = 0x1206, + precomp = 0x1509, + endprecomp = 0x0014, + bclass = 0x1400, + binterface = 0x151a, + vbclass = 0x1401, + ivbclass = 0x1402, + vfunctab = 0x1409, + stmember = 0x150e, + method = 0x150f, + member = 0x150d, + nesttype = 0x1510, + onemethod = 0x1511, + enumerate = 0x1502, + index = 0x1404, + pad0 = 0xf0, + _, +}; + +pub const LfFuncId = extern struct { + len: u16, + kind: LfRecordKind, + scope_id: u32, + type: u32, + name: [1]u8, // null-terminated +}; + +pub const LfMFuncId = extern struct { + len: u16, + kind: LfRecordKind, + parent_type: u32, + type: u32, + name: [1]u8, // null-terminated +}; + +pub const InlineSiteSym = extern struct { + record_len: u16, + record_kind: SymbolKind, + parent: u32, + end: u32, + inlinee: u32, +}; + +pub const InlineSiteSym2 = extern struct { + record_len: u16, + record_kind: SymbolKind, + parent: u32, + end: u32, + inlinee: u32, + invocations: u32, +}; + +pub const InlineeSourceLineSignature = enum(u32) { normal = 0, ex = 1, _ }; + +pub const InlineeSourceLine = extern struct { + inlinee: u32, + file_id: u32, + source_line_num: u32, +}; + +pub const InlineeSourceLineEx = extern struct { + inlinee: u32, + file_id: u32, + source_line_num: u32, + count_of_extra_files: u32, +}; + +pub const BinaryAnnotationOpcode = enum(u8) { + invalid = 0, + code_offset = 1, + change_code_offset_base = 2, + change_code_offset = 3, + change_code_length = 4, + change_file = 5, + change_line_offset = 6, + change_line_end_delta = 7, + change_range_kind = 8, + change_column_start = 9, + change_column_end_delta = 10, + change_code_offset_and_line_offset = 11, + change_code_length_and_code_offset = 12, + change_column_end = 13, +}; diff --git a/lib/std/start.zig b/lib/std/start.zig index 29c76fdfad..01b33ba5f1 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -761,7 +761,7 @@ inline fn wrapMain(result: anytype) u8 { std.log.err("{t}", .{err}); switch (native_os) { .freestanding, .other => {}, - else => if (@errorReturnTrace()) |trace| std.debug.dumpStackTrace(trace), + else => if (@errorReturnTrace()) |trace| std.debug.dumpErrorReturnTrace(trace), } return 1; }; diff --git a/lib/std/std.zig b/lib/std/std.zig index 5cb856f9cc..e700c728c1 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -165,6 +165,8 @@ pub const Options = struct { /// * `debug.dumpCurrentStackTrace` /// * `debug.writeStackTrace` /// * `debug.dumpStackTrace` + /// * `debug.writeErrorReturnTrace` + /// * `debug.dumpErrorReturnTrace` /// /// Stack traces can generally be collected and printed when debug info is stripped, but are /// often less useful since they usually cannot be mapped to source locations and/or have bad diff --git a/lib/std/testing/FailingAllocator.zig b/lib/std/testing/FailingAllocator.zig index 6476725a2f..a3852fe09e 100644 --- a/lib/std/testing/FailingAllocator.zig +++ b/lib/std/testing/FailingAllocator.zig @@ -65,7 +65,7 @@ fn alloc( if (self.alloc_index == self.fail_index) { if (!self.has_induced_failure) { const st = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &self.stack_addresses); - @memset(self.stack_addresses[@min(st.index, self.stack_addresses.len)..], 0); + @memset(self.stack_addresses[@min(st.return_addresses.len, self.stack_addresses.len)..], 0); self.has_induced_failure = true; } return null; @@ -131,15 +131,15 @@ fn free( } /// Only valid once `has_induced_failure == true` -pub fn getStackTrace(self: *FailingAllocator) std.builtin.StackTrace { +pub fn getStackTrace(self: *FailingAllocator) std.debug.StackTrace { std.debug.assert(self.has_induced_failure); var len: usize = 0; while (len < self.stack_addresses.len and self.stack_addresses[len] != 0) { len += 1; } return .{ - .instruction_addresses = &self.stack_addresses, - .index = len, + .return_addresses = self.stack_addresses[0..len], + .skipped = if (len == self.stack_addresses.len) .unknown else .none, }; } diff --git a/test/cases/disable_stack_tracing.zig b/test/cases/disable_stack_tracing.zig index 36620130c9..b360b0b26e 100644 --- a/test/cases/disable_stack_tracing.zig +++ b/test/cases/disable_stack_tracing.zig @@ -9,11 +9,11 @@ pub fn main() !void { const captured_st = try foo(&stdout.interface, &st_buf); try std.debug.writeStackTrace(&captured_st, .{ .writer = &stdout.interface, .mode = .no_color }); - try stdout.interface.print("stack trace index: {d}\n", .{captured_st.index}); + try stdout.interface.print("stack trace index: {d}\n", .{captured_st.return_addresses.len}); try stdout.interface.flush(); } -fn foo(w: *std.Io.Writer, st_buf: []usize) !std.builtin.StackTrace { +fn foo(w: *std.Io.Writer, st_buf: []usize) !std.debug.StackTrace { try std.debug.writeCurrentStackTrace(.{}, .{ .writer = w, .mode = .no_color }); return std.debug.captureCurrentStackTrace(.{}, st_buf); } diff --git a/test/error_traces.zig b/test/error_traces.zig index 6c9cdc7166..356a4440c6 100644 --- a/test/error_traces.zig +++ b/test/error_traces.zig @@ -1,4 +1,6 @@ -pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext) void { +const std = @import("std"); + +pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext, os: std.Target.Os.Tag) void { cases.addCase(.{ .name = "return", .source = @@ -464,17 +466,33 @@ pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext) void { \\} , .expect_error = "ThisIsSoSad", - .expect_trace = - \\source.zig:8:5: [address] in bar - \\ return error.ThisIsSoSad; - \\ ^ - \\source.zig:5:5: [address] in foo - \\ try bar(); - \\ ^ - \\source.zig:2:5: [address] in main - \\ try foo(); - \\ ^ - , + .expect_trace = switch (os) { + // LLVM doesn't emit column info in the binary annotations for inlinee callees in PDBs, + // so our expected result is slightly different for Windows than on other operating + // systems. + .windows => + \\source.zig:8:5: [address] in bar + \\ return error.ThisIsSoSad; + \\ ^ + \\source.zig:5: [address] in foo + \\ try bar(); + \\ + \\source.zig:2:5: [address] in main + \\ try foo(); + \\ ^ + , + else => + \\source.zig:8:5: [address] in bar + \\ return error.ThisIsSoSad; + \\ ^ + \\source.zig:5:5: [address] in foo + \\ try bar(); + \\ ^ + \\source.zig:2:5: [address] in main + \\ try foo(); + \\ ^ + , + }, .disable_trace_optimized = &.{ .{ .x86_64, .freebsd }, .{ .x86_64, .netbsd }, @@ -493,10 +511,5 @@ pub fn addCases(cases: *@import("tests.zig").ErrorTracesContext) void { .{ .x86_64, .macos }, .{ .aarch64, .macos }, }, - // TODO: the standard library has a bug in PDB parsing where given an address corresponding - // to an inline call, the frame we see will be for the *caller*, not the *callee*. As a - // result this test gives bogus results on Windows right now. - // This is a part of https://codeberg.org/ziglang/zig/issues/30847. - .disable_trace_pdb = true, }); } diff --git a/test/src/ErrorTrace.zig b/test/src/ErrorTrace.zig index ac93f3a57d..f6f80f8b13 100644 --- a/test/src/ErrorTrace.zig +++ b/test/src/ErrorTrace.zig @@ -17,8 +17,6 @@ pub const Case = struct { /// LLVM ReleaseSmall builds always have the trace disabled regardless of this field, because it /// seems that LLVM is particularly good at optimizing traces away in those. disable_trace_optimized: []const DisableConfig = &.{}, - /// If `true` then we will not test the error trace on Windows due to bugs in PDB handling. - disable_trace_pdb: bool = false, pub const DisableConfig = struct { std.Target.Cpu.Arch, std.Target.Os.Tag }; pub const Backend = enum { llvm, selfhosted }; @@ -62,7 +60,6 @@ fn addCaseConfig( const b = self.b; const error_tracing: bool = tracing: { - if (target.result.os.tag == .windows and case.disable_trace_pdb) break :tracing false; if (optimize == .Debug) break :tracing true; if (backend != .llvm) break :tracing true; if (optimize == .ReleaseSmall) break :tracing false; diff --git a/test/src/convert-stack-trace.zig b/test/src/convert-stack-trace.zig index 272c43e311..5d7356a2d4 100644 --- a/test/src/convert-stack-trace.zig +++ b/test/src/convert-stack-trace.zig @@ -52,24 +52,24 @@ pub fn main(init: std.process.Init) !void { continue; } - const src_col_end = std.mem.indexOf(u8, in_line, ": 0x") orelse { + const src_pos_end = std.mem.indexOf(u8, in_line, ": 0x") orelse { try w.writeAll(in_line); continue; }; - const src_row_end = std.mem.lastIndexOfScalar(u8, in_line[0..src_col_end], ':') orelse { - try w.writeAll(in_line); - continue; - }; - const src_path_end = std.mem.lastIndexOfScalar(u8, in_line[0..src_row_end], ':') orelse { - try w.writeAll(in_line); - continue; + const src_pos_start = b: { + const postfix = ".zig:"; + const postfix_index = std.mem.lastIndexOf(u8, in_line[0..src_pos_end], postfix) orelse { + try w.writeAll(in_line); + continue; + }; + break :b postfix_index + postfix.len; }; - const addr_end = std.mem.indexOfPos(u8, in_line, src_col_end, " in ") orelse { + const addr_end = std.mem.findPos(u8, in_line, src_pos_end, " in ") orelse { try w.writeAll(in_line); continue; }; - const symbol_end = std.mem.indexOfPos(u8, in_line, addr_end, " (") orelse { + const symbol_end = std.mem.findPos(u8, in_line, addr_end, " (") orelse { try w.writeAll(in_line); continue; }; @@ -88,10 +88,10 @@ pub fn main(init: std.process.Init) !void { // // ...with that first '_' being replaced by its basename. - const src_path = in_line[0..src_path_end]; + const src_path = in_line[0..src_pos_start]; const basename_start = if (std.mem.lastIndexOfAny(u8, src_path, "/\\")) |i| i + 1 else 0; const symbol_start = addr_end + " in ".len; - try w.writeAll(in_line[basename_start..src_col_end]); + try w.writeAll(in_line[basename_start..src_pos_end]); try w.writeAll(": [address] in "); try w.writeAll(in_line[symbol_start..symbol_end]); try w.writeByte('\n'); diff --git a/test/stack_traces.zig b/test/stack_traces.zig index d0f1acc08b..037399d36c 100644 --- a/test/stack_traces.zig +++ b/test/stack_traces.zig @@ -1,4 +1,6 @@ -pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { +const std = @import("std"); + +pub fn addCases(cases: *@import("tests.zig").StackTracesContext, os: std.Target.Os.Tag) void { cases.addCase(.{ .name = "simple panic", .source = @@ -118,13 +120,13 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { \\ var stack_trace_buf: [8]usize = undefined; \\ dumpIt(&captureIt(&stack_trace_buf)); \\} - \\fn captureIt(buf: []usize) std.builtin.StackTrace { + \\fn captureIt(buf: []usize) std.debug.StackTrace { \\ return captureItInner(buf); \\} - \\fn dumpIt(st: *const std.builtin.StackTrace) void { + \\fn dumpIt(st: *const std.debug.StackTrace) void { \\ std.debug.dumpStackTrace(st); \\} - \\fn captureItInner(buf: []usize) std.builtin.StackTrace { + \\fn captureItInner(buf: []usize) std.debug.StackTrace { \\ return std.debug.captureCurrentStackTrace(.{}, buf); \\} \\const std = @import("std"); @@ -159,13 +161,13 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { \\ var stack_trace_buf: [8]usize = undefined; \\ dumpIt(&captureIt(&stack_trace_buf)); \\} - \\fn captureIt(buf: []usize) std.builtin.StackTrace { + \\fn captureIt(buf: []usize) std.debug.StackTrace { \\ return captureItInner(buf); \\} - \\fn dumpIt(st: *const std.builtin.StackTrace) void { + \\fn dumpIt(st: *const std.debug.StackTrace) void { \\ std.debug.dumpStackTrace(st); \\} - \\fn captureItInner(buf: []usize) std.builtin.StackTrace { + \\fn captureItInner(buf: []usize) std.debug.StackTrace { \\ return std.debug.captureCurrentStackTrace(.{}, buf); \\} \\const std = @import("std"); @@ -188,13 +190,13 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { \\fn threadMain(stack_trace_buf: []usize) void { \\ dumpIt(&captureIt(stack_trace_buf)); \\} - \\fn captureIt(buf: []usize) std.builtin.StackTrace { + \\fn captureIt(buf: []usize) std.debug.StackTrace { \\ return captureItInner(buf); \\} - \\fn dumpIt(st: *const std.builtin.StackTrace) void { + \\fn dumpIt(st: *const std.debug.StackTrace) void { \\ std.debug.dumpStackTrace(st); \\} - \\fn captureItInner(buf: []usize) std.builtin.StackTrace { + \\fn captureItInner(buf: []usize) std.debug.StackTrace { \\ return std.debug.captureCurrentStackTrace(.{}, buf); \\} \\const std = @import("std"); @@ -221,4 +223,116 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { \\ , }); + + cases.addCase(.{ + .name = "simple inline panic", + .source = + \\pub fn main() void { + \\ foo(); + \\} + \\inline fn foo() void { + \\ @panic("oh no"); + \\} + \\ + , + .unwind = .any, + .expect_panic = true, + .expect = switch (os) { + // LLVM doesn't emit column info in the binary annotations for inlinee callees in PDBs, + // so the first location has only a row. + .windows => + \\panic: oh no + \\source.zig:5: [address] in foo + \\ @panic("oh no"); + \\ + \\source.zig:2:8: [address] in main + \\ foo(); + \\ ^ + \\ + , + // On all other platforms, we resolve the innermost inline callee but we don't yet + // resolve the inline callers. + else => + \\panic: oh no + \\source.zig:5:5: [address] in foo + \\ @panic("oh no"); + \\ ^ + , + }, + .expect_strip = switch (os) { + .windows => + \\panic: oh no + \\???:?:?: [address] in source.foo + \\???:?:?: [address] in source.main + \\ + , + else => + \\panic: oh no + \\???:?:?: [address] in source.foo + \\ + , + }, + }); + + // Make sure all inline calls are resolved and in the right order! + cases.addCase(.{ + .name = "nested inline panic", + .source = + \\pub fn main() void { + \\ foo(); + \\} + \\inline fn foo() void { + \\ bar(); + \\} + \\inline fn bar() void { + \\ baz(); + \\} + \\inline fn baz() void { + \\ @panic("oh no"); + \\} + \\ + , + .unwind = .any, + .expect_panic = true, + // This switch serves a similar purpose as in "inline panic". + .expect = switch (os) { + .windows => + \\panic: oh no + \\source.zig:11: [address] in baz + \\ @panic("oh no"); + \\ + \\source.zig:8: [address] in bar + \\ baz(); + \\ + \\source.zig:5: [address] in foo + \\ bar(); + \\ + \\source.zig:2:8: [address] in main + \\ foo(); + \\ ^ + \\ + , + else => + \\panic: oh no + \\source.zig:11:5: [address] in baz + \\ @panic("oh no"); + \\ ^ + , + }, + .expect_strip = switch (os) { + .windows => + \\panic: oh no + \\???:?:?: [address] in baz + \\???:?:?: [address] in bar + \\???:?:?: [address] in foo + \\???:?:?: [address] in main + \\ + , + else => + \\panic: oh no + \\???:?:?: [address] in baz + \\ + , + }, + }); } diff --git a/test/standalone/coff_dwarf/main.zig b/test/standalone/coff_dwarf/main.zig index 24684d3830..9dbee9ec78 100644 --- a/test/standalone/coff_dwarf/main.zig +++ b/test/standalone/coff_dwarf/main.zig @@ -12,8 +12,26 @@ pub fn main(init: std.process.Init) void { var add_addr: usize = undefined; _ = add(1, 2, &add_addr); - const symbol = di.getSymbol(io, add_addr) catch |err| fatal("failed to get symbol: {t}", .{err}); - defer if (symbol.source_location) |sl| std.debug.getDebugInfoAllocator().free(sl.file_name); + const debug_gpa = std.debug.getDebugInfoAllocator(); + const symbol_allocator = debug_gpa; + + var symbols: std.ArrayList(std.debug.Symbol) = .empty; + defer symbols.deinit(symbol_allocator); + + var text_arena: std.heap.ArenaAllocator = .init(debug_gpa); + defer text_arena.deinit(); + + di.getSymbols( + io, + symbol_allocator, + text_arena.allocator(), + add_addr, + false, + &symbols, + ) catch |err| fatal("failed to get symbol: {t}", .{err}); + + if (symbols.items.len != 1) fatal("expected 1 symbol, found {}", .{symbols.items.len}); + const symbol = symbols.items[0]; if (symbol.name == null) fatal("failed to resolve symbol name", .{}); if (symbol.compile_unit_name == null) fatal("failed to resolve compile unit", .{}); diff --git a/test/tests.zig b/test/tests.zig index 9cb7a879b2..ce9a55f3cf 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -1989,44 +1989,85 @@ const c_abi_targets = blk: { }; }; -/// For stack trace tests, we only test native, because external executors are pretty unreliable at -/// stack tracing. However, if there's a 32-bit equivalent target which the host can trivially run, -/// we may as well at least test that! -fn nativeAndCompatible32bit(b: *std.Build, skip_non_native: bool) []const std.Build.ResolvedTarget { +fn compatible32bitArch(b: *std.Build) ?std.Target.Cpu.Arch { const host = b.graph.host.result; - const only_native = (&b.graph.host)[0..1]; - if (skip_non_native) return only_native; - const arch32: std.Target.Cpu.Arch = switch (host.os.tag) { + return switch (host.os.tag) { .windows => switch (host.cpu.arch) { .x86_64 => .x86, .aarch64 => .thumb, .aarch64_be => .thumbeb, - else => return only_native, + else => null, }, .freebsd => switch (host.cpu.arch) { .aarch64 => .arm, .aarch64_be => .armeb, - else => return only_native, + else => null, }, .linux, .netbsd => switch (host.cpu.arch) { .x86_64 => .x86, .aarch64 => .arm, .aarch64_be => .armeb, - else => return only_native, + else => null, }, - else => return only_native, + else => null, }; +} + +/// For stack trace tests, we only test native by default, because external executors are pretty +/// unreliable at stack tracing. However, if there's a 32-bit equivalent target which the host can +/// trivially run, we may as well at least test that! +fn nativeAndCompatible32bit(b: *std.Build, skip_non_native: bool) []const std.Build.ResolvedTarget { + const host = b.graph.host.result; + const only_native = (&b.graph.host)[0..1]; + if (skip_non_native) return only_native; + const arch32 = compatible32bitArch(b) orelse return only_native; return b.graph.arena.dupe(std.Build.ResolvedTarget, &.{ b.graph.host, b.resolveTargetQuery(.{ .cpu_arch = arch32, .os_tag = host.os.tag }), }) catch @panic("OOM"); } +fn wineAndCompatible32bit(b: *std.Build, skip_non_native: bool) []const std.Build.ResolvedTarget { + var targets: std.ArrayList(std.Build.ResolvedTarget) = .empty; + + const host = b.graph.host.result; + + targets.append(b.graph.arena, b.resolveTargetQuery(.{ + .cpu_arch = host.cpu.arch, + .os_tag = .windows, + })) catch @panic("OOM"); + if (!skip_non_native) { + if (compatible32bitArch(b)) |arch| { + targets.append(b.graph.arena, b.resolveTargetQuery(.{ + .cpu_arch = arch, + .os_tag = .windows, + })) catch @panic("OOM"); + } + } + + return targets.toOwnedSlice(b.graph.arena) catch @panic("OOM"); +} + +fn darlingTargets(b: *std.Build) []const std.Build.ResolvedTarget { + var targets: std.ArrayList(std.Build.ResolvedTarget) = .empty; + + const host = b.graph.host.result; + + targets.append(b.graph.arena, b.resolveTargetQuery(.{ + .cpu_arch = host.cpu.arch, + .os_tag = .macos, + })) catch @panic("OOM"); + + return targets.toOwnedSlice(b.graph.arena) catch @panic("OOM"); +} + pub fn addStackTraceTests( b: *std.Build, test_filters: []const []const u8, skip_non_native: bool, ) *Step { + const step = b.step("test-stack-traces", "Run the stack trace tests"); + const convert_exe = b.addExecutable(.{ .name = "convert-stack-trace", .root_module = b.createModule(.{ @@ -2036,19 +2077,41 @@ pub fn addStackTraceTests( }), }); - const cases = b.allocator.create(StackTracesContext) catch @panic("OOM"); - - cases.* = .{ + const host_cases = b.allocator.create(StackTracesContext) catch @panic("OOM"); + host_cases.* = .{ .b = b, - .step = b.step("test-stack-traces", "Run the stack trace tests"), + .step = step, .test_filters = test_filters, .targets = nativeAndCompatible32bit(b, skip_non_native), .convert_exe = convert_exe, }; + stack_traces.addCases(host_cases, b.graph.host.result.os.tag); - stack_traces.addCases(cases); + if (b.enable_wine) { + const wine_cases = b.allocator.create(StackTracesContext) catch @panic("OOM"); + wine_cases.* = .{ + .b = b, + .step = step, + .test_filters = test_filters, + .targets = wineAndCompatible32bit(b, skip_non_native), + .convert_exe = convert_exe, + }; + stack_traces.addCases(wine_cases, .windows); + } - return cases.step; + if (b.enable_darling) { + const darling_cases = b.allocator.create(StackTracesContext) catch @panic("OOM"); + darling_cases.* = .{ + .b = b, + .step = step, + .test_filters = test_filters, + .targets = darlingTargets(b), + .convert_exe = convert_exe, + }; + stack_traces.addCases(darling_cases, .macos); + } + + return step; } pub fn addErrorTraceTests( @@ -2057,6 +2120,8 @@ pub fn addErrorTraceTests( optimize_modes: []const OptimizeMode, skip_non_native: bool, ) *Step { + const step = b.step("test-error-traces", "Run the error trace tests"); + const convert_exe = b.addExecutable(.{ .name = "convert-stack-trace", .root_module = b.createModule(.{ @@ -2066,19 +2131,44 @@ pub fn addErrorTraceTests( }), }); - const cases = b.allocator.create(ErrorTracesContext) catch @panic("OOM"); - cases.* = .{ + const host_cases = b.allocator.create(ErrorTracesContext) catch @panic("OOM"); + host_cases.* = .{ .b = b, - .step = b.step("test-error-traces", "Run the error trace tests"), + .step = step, .test_filters = test_filters, .targets = nativeAndCompatible32bit(b, skip_non_native), .optimize_modes = optimize_modes, .convert_exe = convert_exe, }; + error_traces.addCases(host_cases, b.graph.host.result.os.tag); - error_traces.addCases(cases); + if (b.enable_wine) { + const wine_cases = b.allocator.create(ErrorTracesContext) catch @panic("OOM"); + wine_cases.* = .{ + .b = b, + .step = step, + .test_filters = test_filters, + .targets = wineAndCompatible32bit(b, skip_non_native), + .optimize_modes = optimize_modes, + .convert_exe = convert_exe, + }; + error_traces.addCases(wine_cases, .windows); + } - return cases.step; + if (b.enable_darling) { + const darling_cases = b.allocator.create(ErrorTracesContext) catch @panic("OOM"); + darling_cases.* = .{ + .b = b, + .step = step, + .test_filters = test_filters, + .targets = darlingTargets(b), + .optimize_modes = optimize_modes, + .convert_exe = convert_exe, + }; + error_traces.addCases(darling_cases, .macos); + } + + return step; } fn compilerHasPackageManager(b: *std.Build) bool {