authorgravatar for kcbanner@gmail.comCasey Banner <kcbanner@gmail.com> 2023-07-02 18:15:36-04:00
committergravatar for kcbanner@gmail.comCasey Banner <kcbanner@gmail.com> 2023-07-20 22:58:14-04:00
log62598c2187a63a7eb2d8c9f3dca0664ec5db270e
tree8439ddffc6f805f1c502cb3ea38fbcc2d0377cb9
parentf04f9705cca5cccdfaff2eab0e57e9bd253e8441

debug: rework how unwind errors are printed, and add module name lookup for linux

This change enhances stack trace output to include a note that debug info was missing, and therefore the stack trace may not be accurate. For example, if the user is using a libc compiled with -fomit-frame-pointer and doesn't have debug symbols installed, any traces that begin in a libc function may not unwind correctly. This allows the user to notice this and potentially install debug symbols to improve the output.

2 files changed, 93 insertions(+), 14 deletions(-)

lib/std/debug.zig+92-13
...@@ -182,6 +182,9 @@ pub fn dumpStackTraceFromBase(context: *const StackTraceContext) void {...@@ -182,6 +182,9 @@ pub fn dumpStackTraceFromBase(context: *const StackTraceContext) void {
182 printSourceAtAddress(debug_info, stderr, it.dwarf_context.pc, tty_config) catch return;182 printSourceAtAddress(debug_info, stderr, it.dwarf_context.pc, tty_config) catch return;
183183
184 while (it.next()) |return_address| {184 while (it.next()) |return_address| {
185 if (it.getLastError()) |unwind_error|
186 printUnwindError(debug_info, stderr, unwind_error.address, unwind_error.err, tty_config) catch {};
187
185 // On arm64 macOS, the address of the last frame is 0x0 rather than 0x1 as on x86_64 macOS,188 // On arm64 macOS, the address of the last frame is 0x0 rather than 0x1 as on x86_64 macOS,
186 // therefore, we do a check for `return_address == 0` before subtracting 1 from it to avoid189 // therefore, we do a check for `return_address == 0` before subtracting 1 from it to avoid
187 // an overflow. We do not need to signal `StackIterator` as it will correctly detect this190 // an overflow. We do not need to signal `StackIterator` as it will correctly detect this
...@@ -225,7 +228,9 @@ pub fn captureStackTrace(first_address: ?usize, stack_trace: *std.builtin.StackT...@@ -225,7 +228,9 @@ pub fn captureStackTrace(first_address: ?usize, stack_trace: *std.builtin.StackT
225 }228 }
226 stack_trace.index = slice.len;229 stack_trace.index = slice.len;
227 } else {230 } else {
228 // TODO: This should use the DWARF unwinder if .eh_frame_hdr is available (so that full debug info parsing isn't required)231 // TODO: This should use the DWARF unwinder if .eh_frame_hdr is available (so that full debug info parsing isn't required).
232 // A new path for loading DebugInfo needs to be created which will only attempt to parse in-memory sections, because
233 // stopping to load other debug info (ie. source line info) from disk here is not required for unwinding.
229 var it = StackIterator.init(first_address, null);234 var it = StackIterator.init(first_address, null);
230 defer it.deinit();235 defer it.deinit();
231 for (stack_trace.instruction_addresses, 0..) |*addr, i| {236 for (stack_trace.instruction_addresses, 0..) |*addr, i| {
...@@ -442,6 +447,11 @@ pub inline fn getContext(context: *StackTraceContext) bool {...@@ -442,6 +447,11 @@ pub inline fn getContext(context: *StackTraceContext) bool {
442 return have_getcontext and os.system.getcontext(context) == 0;447 return have_getcontext and os.system.getcontext(context) == 0;
443}448}
444449
450pub const UnwindError = if (have_ucontext)
451 @typeInfo(@typeInfo(@TypeOf(StackIterator.next_dwarf)).Fn.return_type.?).ErrorUnion.error_set
452else
453 void;
454
445pub const StackIterator = struct {455pub const StackIterator = struct {
446 // Skip every frame before this address is found.456 // Skip every frame before this address is found.
447 first_address: ?usize,457 first_address: ?usize,
...@@ -452,6 +462,8 @@ pub const StackIterator = struct {...@@ -452,6 +462,8 @@ pub const StackIterator = struct {
452 // stacks with frames that don't use a frame pointer (ie. -fomit-frame-pointer).462 // stacks with frames that don't use a frame pointer (ie. -fomit-frame-pointer).
453 debug_info: ?*DebugInfo,463 debug_info: ?*DebugInfo,
454 dwarf_context: if (have_ucontext) DW.UnwindContext else void = undefined,464 dwarf_context: if (have_ucontext) DW.UnwindContext else void = undefined,
465 last_error: if (have_ucontext) ?UnwindError else void = undefined,
466 last_error_address: if (have_ucontext) usize else void = undefined,
455467
456 pub fn init(first_address: ?usize, fp: ?usize) StackIterator {468 pub fn init(first_address: ?usize, fp: ?usize) StackIterator {
457 if (native_arch == .sparc64) {469 if (native_arch == .sparc64) {
...@@ -472,6 +484,7 @@ pub const StackIterator = struct {...@@ -472,6 +484,7 @@ pub const StackIterator = struct {
472 var iterator = init(first_address, null);484 var iterator = init(first_address, null);
473 iterator.debug_info = debug_info;485 iterator.debug_info = debug_info;
474 iterator.dwarf_context = try DW.UnwindContext.init(context, &isValidMemory);486 iterator.dwarf_context = try DW.UnwindContext.init(context, &isValidMemory);
487 iterator.last_error = null;
475 return iterator;488 return iterator;
476 }489 }
477490
...@@ -483,6 +496,23 @@ pub const StackIterator = struct {...@@ -483,6 +496,23 @@ pub const StackIterator = struct {
483 }496 }
484 }497 }
485498
499 pub fn getLastError(self: *StackIterator) ?struct {
500 address: usize,
501 err: UnwindError,
502 } {
503 if (have_ucontext) {
504 if (self.last_error) |err| {
505 self.last_error = null;
506 return .{
507 .address = self.last_error_address,
508 .err = err,
509 };
510 }
511 }
512
513 return null;
514 }
515
486 // Offset of the saved BP wrt the frame pointer.516 // Offset of the saved BP wrt the frame pointer.
487 const fp_offset = if (native_arch.isRISCV())517 const fp_offset = if (native_arch.isRISCV())
488 // On RISC-V the frame pointer points to the top of the saved register518 // On RISC-V the frame pointer points to the top of the saved register
...@@ -579,14 +609,10 @@ pub const StackIterator = struct {...@@ -579,14 +609,10 @@ pub const StackIterator = struct {
579 if (self.next_dwarf()) |return_address| {609 if (self.next_dwarf()) |return_address| {
580 return return_address;610 return return_address;
581 } else |err| {611 } else |err| {
582 if (err != error.MissingFDE) print("DWARF unwind error: {}\n", .{err});612 self.last_error = err;
583613 self.last_error_address = self.dwarf_context.pc;
584 // Fall back to fp unwinding on the first failure,
585 // as the register context won't be updated
586
587 // TODO: Could still attempt dwarf unwinding after this, maybe marking non-updated registers as
588 // invalid, so the unwind only fails if it requires out of date registers?
589614
615 // Fall back to fp unwinding on the first failure, as the register context won't have been updated
590 self.fp = self.dwarf_context.getFp() catch 0;616 self.fp = self.dwarf_context.getFp() catch 0;
591 self.debug_info = null;617 self.debug_info = null;
592 }618 }
...@@ -640,6 +666,9 @@ pub fn writeCurrentStackTrace(...@@ -640,6 +666,9 @@ pub fn writeCurrentStackTrace(
640 defer it.deinit();666 defer it.deinit();
641667
642 while (it.next()) |return_address| {668 while (it.next()) |return_address| {
669 if (it.getLastError()) |unwind_error|
670 try printUnwindError(debug_info, out_stream, unwind_error.address, unwind_error.err, tty_config);
671
643 // On arm64 macOS, the address of the last frame is 0x0 rather than 0x1 as on x86_64 macOS,672 // On arm64 macOS, the address of the last frame is 0x0 rather than 0x1 as on x86_64 macOS,
644 // therefore, we do a check for `return_address == 0` before subtracting 1 from it to avoid673 // therefore, we do a check for `return_address == 0` before subtracting 1 from it to avoid
645 // an overflow. We do not need to signal `StackIterator` as it will correctly detect this674 // an overflow. We do not need to signal `StackIterator` as it will correctly detect this
...@@ -785,6 +814,17 @@ fn printUnknownSource(debug_info: *DebugInfo, out_stream: anytype, address: usiz...@@ -785,6 +814,17 @@ fn printUnknownSource(debug_info: *DebugInfo, out_stream: anytype, address: usiz
785 );814 );
786}815}
787816
817pub fn printUnwindError(debug_info: *DebugInfo, out_stream: anytype, address: usize, err: UnwindError, tty_config: io.tty.Config) !void {
818 const module_name = debug_info.getModuleNameForAddress(address) orelse "???";
819 try tty_config.setColor(out_stream, .dim);
820 if (err != error.MissingDebugInfo) {
821 try out_stream.print("Unwind information for {s} was not available ({}), trace may be incomplete\n\n", .{ module_name, err });
822 } else {
823 try out_stream.print("Unwind information for {s} was not available, trace may be incomplete\n\n", .{module_name});
824 }
825 try tty_config.setColor(out_stream, .reset);
826}
827
788pub fn printSourceAtAddress(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: io.tty.Config) !void {828pub fn printSourceAtAddress(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: io.tty.Config) !void {
789 const module = debug_info.getModuleForAddress(address) catch |err| switch (err) {829 const module = debug_info.getModuleForAddress(address) catch |err| switch (err) {
790 error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),830 error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),
...@@ -1099,16 +1139,14 @@ pub fn readElfDebugInfo(...@@ -1099,16 +1139,14 @@ pub fn readElfDebugInfo(
1099 ) catch break :blk;1139 ) catch break :blk;
11001140
1101 for (global_debug_directories) |global_directory| {1141 for (global_debug_directories) |global_directory| {
1102 // TODO: joinBuf would be ideal (with a fs.MAX_PATH_BYTES buffer)
1103 const path = try fs.path.join(allocator, &.{ global_directory, ".build-id", &id_prefix_buf, filename });1142 const path = try fs.path.join(allocator, &.{ global_directory, ".build-id", &id_prefix_buf, filename });
1104 defer allocator.free(path);1143 defer allocator.free(path);
1105 // TODO: Remove1144
1106 std.debug.print(" Loading external debug info from {s}\n", .{path});
1107 return readElfDebugInfo(allocator, path, null, separate_debug_crc, &sections, mapped_mem) catch continue;1145 return readElfDebugInfo(allocator, path, null, separate_debug_crc, &sections, mapped_mem) catch continue;
1108 }1146 }
1109 }1147 }
11101148
1111 // use the path from .gnu_debuglink, in the search order as gdb1149 // use the path from .gnu_debuglink, in the same search order as gdb
1112 if (separate_debug_filename) |separate_filename| blk: {1150 if (separate_debug_filename) |separate_filename| blk: {
1113 if (elf_filename != null and mem.eql(u8, elf_filename.?, separate_filename)) return error.MissingDebugInfo;1151 if (elf_filename != null and mem.eql(u8, elf_filename.?, separate_filename)) return error.MissingDebugInfo;
11141152
...@@ -1456,6 +1494,9 @@ pub const DebugInfo = struct {...@@ -1456,6 +1494,9 @@ pub const DebugInfo = struct {
1456 }1494 }
1457 }1495 }
14581496
1497 // Returns the module name for a given address.
1498 // This can be called when getModuleForAddress fails, so implementations should provide
1499 // a path that doesn't rely on any side-effects of successful module lookup.
1459 pub fn getModuleNameForAddress(self: *DebugInfo, address: usize) ?[]const u8 {1500 pub fn getModuleNameForAddress(self: *DebugInfo, address: usize) ?[]const u8 {
1460 if (comptime builtin.target.isDarwin()) {1501 if (comptime builtin.target.isDarwin()) {
1461 return null;1502 return null;
...@@ -1466,7 +1507,7 @@ pub const DebugInfo = struct {...@@ -1466,7 +1507,7 @@ pub const DebugInfo = struct {
1466 } else if (comptime builtin.target.isWasm()) {1507 } else if (comptime builtin.target.isWasm()) {
1467 return null;1508 return null;
1468 } else {1509 } else {
1469 return null;1510 return self.lookupModuleNameDl(address);
1470 }1511 }
1471 }1512 }
14721513
...@@ -1624,6 +1665,44 @@ pub const DebugInfo = struct {...@@ -1624,6 +1665,44 @@ pub const DebugInfo = struct {
1624 return null;1665 return null;
1625 }1666 }
16261667
1668 fn lookupModuleNameDl(self: *DebugInfo, address: usize) ?[]const u8 {
1669 _ = self;
1670
1671 var ctx: struct {
1672 // Input
1673 address: usize,
1674 // Output
1675 name: []const u8 = "",
1676 } = .{ .address = address };
1677 const CtxTy = @TypeOf(ctx);
1678
1679 if (os.dl_iterate_phdr(&ctx, error{Found}, struct {
1680 fn callback(info: *os.dl_phdr_info, size: usize, context: *CtxTy) !void {
1681 _ = size;
1682 if (context.address < info.dlpi_addr) return;
1683 const phdrs = info.dlpi_phdr[0..info.dlpi_phnum];
1684 for (phdrs) |*phdr| {
1685 if (phdr.p_type != elf.PT_LOAD) continue;
1686
1687 const seg_start = info.dlpi_addr +% phdr.p_vaddr;
1688 const seg_end = seg_start + phdr.p_memsz;
1689 if (context.address >= seg_start and context.address < seg_end) {
1690 context.name = mem.sliceTo(info.dlpi_name, 0) orelse "";
1691 break;
1692 }
1693 } else return;
1694
1695 return error.Found;
1696 }
1697 }.callback)) {
1698 return null;
1699 } else |err| switch (err) {
1700 error.Found => return fs.path.basename(ctx.name),
1701 }
1702
1703 return null;
1704 }
1705
1627 fn lookupModuleDl(self: *DebugInfo, address: usize) !*ModuleDebugInfo {1706 fn lookupModuleDl(self: *DebugInfo, address: usize) !*ModuleDebugInfo {
1628 var ctx: struct {1707 var ctx: struct {
1629 // Input1708 // Input
lib/std/dwarf.zig+1-1
...@@ -1036,7 +1036,7 @@ pub const DwarfInfo = struct {...@@ -1036,7 +1036,7 @@ pub const DwarfInfo = struct {
1036 }1036 }
10371037
1038 // Returns the next range in the list, or null if the end was reached.1038 // Returns the next range in the list, or null if the end was reached.
1039 pub fn next(self: *@This()) !?struct{ start_addr: u64, end_addr: u64 } {1039 pub fn next(self: *@This()) !?struct { start_addr: u64, end_addr: u64 } {
1040 const in = self.stream.reader();1040 const in = self.stream.reader();
1041 switch (self.section_type) {1041 switch (self.section_type) {
1042 .debug_rnglists => {1042 .debug_rnglists => {