diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 7e8a6226e650..c92dcca2b042 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -20,6 +20,7 @@ pub const Condition = @import("Thread/Condition.zig"); pub const use_pthreads = std.Target.current.os.tag != .windows and builtin.link_libc; const Thread = @This(); +const root = @import("root"); const std = @import("std.zig"); const builtin = std.builtin; const os = std.os; @@ -567,7 +568,12 @@ pub fn getCurrentThreadId() u64 { return @bitCast(u32, c.find_thread(null)); }, else => { - @compileError("getCurrentThreadId not implemented for this platform"); + const get = if (@hasDecl(root, "os") and root.os != std.os and + @hasDecl(root.os, "getCurrentThreadId")) + root.os.getCurrentThreadId + else + @compileError("getCurrentThreadId not implemented for this platform"); + return get(); }, } } diff --git a/lib/std/builtin.zig b/lib/std/builtin.zig index 93de8ae3b9cb..865ae283d0f2 100644 --- a/lib/std/builtin.zig +++ b/lib/std/builtin.zig @@ -58,24 +58,9 @@ pub const StackTrace = struct { index: usize, instruction_addresses: []usize, - pub fn format( - self: StackTrace, - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - const debug_info = std.debug.getSelfDebugInfo() catch |err| { - return writer.print("\nUnable to print stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}); - }; - const tty_config = std.debug.detectTTYConfig(); - try writer.writeAll("\n"); - std.debug.writeStackTrace(self, writer, &arena.allocator, debug_info, tty_config) catch |err| { - try writer.print("Unable to print stack trace: {s}\n", .{@errorName(err)}); - }; - try writer.writeAll("\n"); - } + // TODO(rgreenblatt): maybe this shouldn't exist so the default formatter can be used? + // TODO(rgreenblatt): should this instead default to using no color? + pub const format = @compileError("use 'std.debug.fmtStackTrace' to specify if colors can be used"); }; /// This data structure is used by the Zig language code generation and @@ -659,33 +644,30 @@ pub const PanicFn = fn ([]const u8, ?*StackTrace) noreturn; /// This function is used by the Zig language code generation and /// therefore must be kept in sync with the compiler implementation. -pub const panic: PanicFn = if (@hasDecl(root, "panic")) root.panic else default_panic; +pub const panic: PanicFn = if (@hasDecl(root, "panic")) + root.panic +else if (@hasDecl(root, "os") and @hasDecl(root.os, "panic")) + root.os.panic +else + default_panic; /// This function is used by the Zig language code generation and /// therefore must be kept in sync with the compiler implementation. pub fn default_panic(msg: []const u8, error_return_trace: ?*StackTrace) noreturn { @setCold(true); - if (@hasDecl(root, "os") and @hasDecl(root.os, "panic")) { - root.os.panic(msg, error_return_trace); - unreachable; - } switch (os.tag) { .freestanding => { while (true) { @breakpoint(); } }, - .wasi => { - std.debug.warn("{s}", .{msg}); - std.os.abort(); - }, .uefi => { // TODO look into using the debug info and logging helpful messages std.os.abort(); }, else => { const first_trace_addr = @returnAddress(); - std.debug.panicExtra(error_return_trace, first_trace_addr, "{s}", .{msg}); + std.debug.panicImpl(error_return_trace, first_trace_addr, msg); }, } } diff --git a/lib/std/debug.zig b/lib/std/debug.zig index a7badf7ed145..3815e93826fe 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -3,6 +3,20 @@ // This file is part of [zig](https://ziglang.org/), which is MIT licensed. // The MIT license requires this copyright notice to be included in all copies // and substantial portions of the software. + +//! std.debug contains functions for capturing and printing out stack traces as +//! well as misc. functions such as `print`, `assert`, and `panicExtra`. +//! +//! You can override how addresses are mapped to symbols via +//! `root.debug_config.initSymbolMap` or `root.os.debug.initSymbolMap`. +//! +//! You can override how stack traces are collected via +//! `root.debug_config.captureStackTraceFrom` or +//! `root.os.debug.captureStackTraceFrom` +//! +//! To see what these functions should look like look at the implementations +//! in `debug_config`. + const std = @import("std.zig"); const builtin = std.builtin; const math = std.math; @@ -10,77 +24,119 @@ const mem = std.mem; const io = std.io; const os = std.os; const fs = std.fs; +const lookupDecl = std.meta.lookupDecl; const process = std.process; -const elf = std.elf; -const DW = std.dwarf; -const macho = std.macho; -const coff = std.coff; -const pdb = std.pdb; -const ArrayList = std.ArrayList; const root = @import("root"); -const maxInt = std.math.maxInt; -const File = std.fs.File; -const windows = std.os.windows; +const File = fs.File; +const windows = os.windows; pub const runtime_safety = switch (builtin.mode) { .Debug, .ReleaseSafe => true, .ReleaseFast, .ReleaseSmall => false, }; -const Module = struct { - mod_info: pdb.ModInfo, - module_name: []u8, - obj_file_name: []u8, +// TODO: improve error approach somehow, anyerror shouldn't be used here. - populated: bool, - symbols: []u8, - subsect_info: []u8, - checksum_offset: ?usize, -}; +/// Interface for mapping from addresses to symbols (`SymbolInfo`). +pub const SymbolMap = struct { + pub const LineInfo = struct { + line: u64, + column: u64, + file_name: []const u8, + allocator: ?*mem.Allocator, + + fn deinit(self: @This()) void { + const allocator = self.allocator orelse return; + allocator.free(self.file_name); + } + }; + + pub const SymbolInfo = struct { + symbol_name: []const u8 = "???", + compile_unit_name: []const u8 = "???", + line_info: ?LineInfo = null, + + fn deinit(self: @This()) void { + if (self.line_info) |li| { + li.deinit(); + } + } + }; + + const Self = @This(); + + deinitFn: fn (self: *Self) void, + addressToSymbolFn: fn (self: *Self, address: usize) anyerror!SymbolInfo, + + fn deinit(self: *Self) void { + self.deinitFn(self); + } + + fn addressToSymbol(self: *Self, address: usize) anyerror!SymbolInfo { + return self.addressToSymbolFn(self, address); + } -pub const LineInfo = struct { - line: u64, - column: u64, - file_name: []const u8, - allocator: ?*mem.Allocator, + pub const InitFn = fn (allocator: *mem.Allocator) anyerror!*Self; - fn deinit(self: LineInfo) void { - const allocator = self.allocator orelse return; - allocator.free(self.file_name); + test { + std.testing.refAllDecls(@This()); } }; -var stderr_mutex = std.Thread.Mutex{}; +pub const CaptureStackTraceFn = fn ( + allocator: *std.mem.Allocator, + first_address: ?usize, + base_pointer: ?usize, + stack_trace: *builtin.StackTrace, +) anyerror!void; + +pub const initSymbolMapDarwin = @import("symbol_map_darwin.zig").init; +pub const initSymbolMapUnix = @import("symbol_map_unix.zig").init; +pub const initSymbolMapPDB = @import("symbol_map_pdb.zig").init; +pub const initSymbolMapUnsupported = @import("symbol_map_unsupported.zig").init; + +pub const default_config = struct { + /// A function to initialize a object of the interface type SymbolMap. + pub const initSymbolMap: SymbolMap.InitFn = switch (builtin.os.tag) { + .macos, .ios, .watchos, .tvos => initSymbolMapDarwin, + .linux, .netbsd, .freebsd, .dragonfly, .openbsd => initSymbolMapUnix, + .uefi, .windows => initSymbolMapPDB, + else => initSymbolMapUnsupported, + }; -/// Deprecated. Use `std.log` functions for logging or `std.debug.print` for -/// "printf debugging". -pub const warn = print; + /// Loads a stack trace into the provided argument. + pub const captureStackTraceFrom: CaptureStackTraceFn = defaultCaptureStackTraceFrom; -/// Print to stderr, unbuffered, and silently returning on failure. Intended -/// for use in "printf debugging." Use `std.log` functions for proper logging. -pub fn print(comptime fmt: []const u8, args: anytype) void { - const held = stderr_mutex.acquire(); - defer held.release(); - const stderr = io.getStdErr().writer(); - nosuspend stderr.print(fmt, args) catch return; -} + test { + std.testing.refAllDecls(@This()); + } +}; -pub fn getStderrMutex() *std.Thread.Mutex { - return &stderr_mutex; +const config = lookupDecl(root, &.{"debug_config"}) orelse struct {}; +const os_config = lookupDecl(root, &.{ "os", "debug" }) orelse struct {}; + +fn lookupConfigItem( + comptime name: []const u8, +) @TypeOf(lookupDecl(config, &.{name}) orelse + (lookupDecl(os_config, &.{name}) orelse + @field(default_config, name))) { + return lookupDecl(config, &.{name}) orelse + (lookupDecl(os_config, &.{name}) orelse + @field(default_config, name)); } -/// TODO multithreaded awareness -var self_debug_info: ?DebugInfo = null; +// Slightly different names than in config to avoid redefinition in default. -pub fn getSelfDebugInfo() !*DebugInfo { - if (self_debug_info) |*info| { - return info; - } else { - self_debug_info = try openSelfDebugInfo(getDebugInfoAllocator()); - return &self_debug_info.?; - } +const initSymMap = lookupConfigItem("initSymbolMap"); +const capStackTraceFrom = lookupConfigItem("captureStackTraceFrom"); + +/// Get the writer used for `print`, `panicExtra`, and stack trace dumping. +pub fn getWriter() File.Writer { + return io.getStdErr().writer(); } +/// Detect the `TTY.Config` for the writer returned by `getWriter`. +/// This determines which escape codes can be used. pub fn detectTTYConfig() TTY.Config { var bytes: [128]u8 = undefined; const allocator = &std.heap.FixedBufferAllocator.init(bytes[0..]).allocator; @@ -91,121 +147,312 @@ pub fn detectTTYConfig() TTY.Config { if (stderr_file.supportsAnsiEscapeCodes()) { return .escape_codes; } else if (builtin.os.tag == .windows and stderr_file.isTty()) { - return .windows_api; + return .{ .windows_api = stderr_file }; } else { return .no_color; } } } -/// Tries to print the current stack trace to stderr, unbuffered, and ignores any error returned. +var print_mutex = std.Thread.Mutex{}; + +pub fn getPrintMutex() *std.Thread.Mutex { + return &print_mutex; +} + +pub const getStderrMutex = @compileError("This was renamed to getPrintMutex because " ++ + "the writer used by `debug` can be overriden."); + +/// Deprecated. Use `std.log` functions for logging or `std.debug.print` for +/// "printf debugging". +pub const warn = print; + +/// Print to log writer, unbuffered, and silently returning on failure. Intended +/// for use in "printf debugging." Use `std.log` functions for proper logging. +pub fn print(comptime fmt: []const u8, args: anytype) void { + const held = print_mutex.acquire(); + defer held.release(); + const writer = getWriter(); + nosuspend writer.print(fmt, args) catch {}; +} + /// TODO multithreaded awareness -pub fn dumpCurrentStackTrace(start_addr: ?usize) void { - nosuspend { - const stderr = io.getStdErr().writer(); - if (builtin.strip_debug_info) { - stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; - return; - } - const debug_info = getSelfDebugInfo() catch |err| { - stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; - return; - }; - writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(), start_addr) catch |err| { - stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; - return; - }; +var sym_map: ?*SymbolMap = null; + +pub fn getSymbolMap() !*SymbolMap { + if (sym_map) |info| { + return info; + } else { + sym_map = try initSymMap(getDebugInfoAllocator()); + return sym_map.?; } } -/// Tries to print the stack trace starting from the supplied base pointer to stderr, -/// unbuffered, and ignores any error returned. -/// TODO multithreaded awareness +fn getMappingForDump(writer: anytype) ?*SymbolMap { + return getSymbolMap() catch |err| { + writer.print( + "Unable to dump stack trace: Unable to open debug info: {s}\n", + .{@errorName(err)}, + ) catch {}; + return null; + }; +} + +/// Should be called with the writer locked. +fn getStackTraceDumper(writer: anytype) ?StackTraceDumper(@TypeOf(writer)) { + return if (getMappingForDump(writer)) |mapping| + .{ .writer = writer, .tty_config = detectTTYConfig(), .mapping = mapping } + else + null; +} + +fn dumpHandleError(writer: anytype, err: anytype) void { + writer.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch {}; +} + +/// Tries to print the current stack trace to writer (as determined by +/// getWriter from the config), unbuffered, and ignores any error returned. +pub fn dumpCurrentStackTrace(start_addr: ?usize) void { + const held = print_mutex.acquire(); + defer held.release(); + const writer = getWriter(); + nosuspend if (getStackTraceDumper(writer)) |write_trace| { + write_trace.current(start_addr) catch |err| dumpHandleError(writer, err); + }; +} + +/// Tries to print the stack trace starting from the supplied base pointer +/// to writer (as determined by getWriter from the config), unbuffered, and +/// ignores any error returned. pub fn dumpStackTraceFromBase(bp: usize, ip: usize) void { - nosuspend { - const stderr = io.getStdErr().writer(); - if (builtin.strip_debug_info) { - stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; - return; - } - const debug_info = getSelfDebugInfo() catch |err| { - stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; - return; + const held = print_mutex.acquire(); + defer held.release(); + const writer = getWriter(); + nosuspend if (getStackTraceDumper(writer)) |write_trace| { + write_trace.fromBase(bp, ip) catch |err| dumpHandleError(writer, err); + }; +} + +/// Tries to print a stack trace to writer (as determined by getWriter from +/// the config), unbuffered, and ignores any error returned. +pub fn dumpStackTrace(stack_trace: builtin.StackTrace) void { + const held = print_mutex.acquire(); + defer held.release(); + const writer = getWriter(); + nosuspend if (getStackTraceDumper(writer)) |write_trace| { + write_trace.stackTrace(stack_trace) catch |err| dumpHandleError(writer, err); + }; +} + +const StackTraceWithTTYConfig = struct { + trace: builtin.StackTrace, + tty_config: std.debug.TTY.Config, +}; + +fn formatStackTraceWithTTYConfig( + self: StackTraceWithTTYConfig, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, +) !void { + if (getMappingForDump(writer)) |mapping| { + const dumper = std.debug.StackTraceDumper(@TypeOf(writer)){ + .writer = writer, + .tty_config = self.tty_config, + .mapping = mapping, }; - const tty_config = detectTTYConfig(); - printSourceAtAddress(debug_info, stderr, ip, tty_config) catch return; - var it = StackIterator.init(null, bp); - while (it.next()) |return_address| { - printSourceAtAddress(debug_info, stderr, return_address - 1, tty_config) catch return; - } + dumper.stackTrace(self.trace) catch |err| dumpHandleError(writer, err); } } -/// Returns a slice with the same pointer as addresses, with a potentially smaller len. -/// On Windows, when first_address is not null, we ask for at least 32 stack frames, -/// and then try to find the first address. If addresses.len is more than 32, we -/// capture that many stack frames exactly, and then look for the first address, -/// chopping off the irrelevant frames and shifting so that the returned addresses pointer -/// equals the passed in addresses pointer. -pub fn captureStackTrace(first_address: ?usize, stack_trace: *builtin.StackTrace) void { - if (builtin.os.tag == .windows) { - const addrs = stack_trace.instruction_addresses; - const u32_addrs_len = @intCast(u32, addrs.len); - const first_addr = first_address orelse { - stack_trace.index = windows.ntdll.RtlCaptureStackBackTrace( - 0, - u32_addrs_len, - @ptrCast(**c_void, addrs.ptr), - null, +pub const FmtStackTrace = std.fmt.Formatter(formatStackTraceWithTTYConfig); + +/// Create a "Formatter" struct which will pretty print a stack trace (with +/// color if specified). +pub fn fmtStackTrace( + trace: builtin.StackTrace, + tty_config: std.debug.TTY.Config, +) FmtStackTrace { + return .{ .data = .{ .trace = trace, .tty_config = tty_config } }; +} + +/// Wrapper for nicer internal debug usage. Use captureStackTrace and +/// fmtStackTrace for external stack trace printing. +fn StackTraceDumper(comptime Writer: type) type { + return struct { + const Self = @This(); + + writer: Writer, + tty_config: TTY.Config, + mapping: *SymbolMap, + + fn current( + self: Self, + first_address: ?usize, + ) !void { + return self.currentFrom(first_address, null); + } + + fn currentFrom( + self: Self, + first_address: ?usize, + base_pointer: ?usize, + ) !void { + var addr_buf: [1024]usize = undefined; + var trace = builtin.StackTrace{ + .index = 0, + .instruction_addresses = &addr_buf, + }; + try capStackTraceFrom( + getDebugInfoAllocator(), // TODO: different allocator? + first_address, + base_pointer, + &trace, ); - return; - }; - var addr_buf_stack: [32]usize = undefined; - const addr_buf = if (addr_buf_stack.len > addrs.len) addr_buf_stack[0..] else addrs; - const n = windows.ntdll.RtlCaptureStackBackTrace(0, u32_addrs_len, @ptrCast(**c_void, addr_buf.ptr), null); - const first_index = for (addr_buf[0..n]) |addr, i| { - if (addr == first_addr) { - break i; + return self.stackTrace(trace); + } + + fn fromBase( + self: @This(), + bp: usize, + ip: usize, + ) !void { + try self.sourceAtAddress(ip); + try self.currentFrom(null, bp); + } + + fn stackTrace( + self: @This(), + stack_trace: builtin.StackTrace, + ) !void { + var frame_index: usize = 0; + var frames_left: usize = math.min(stack_trace.index, stack_trace.instruction_addresses.len); + + while (frames_left != 0) : ({ + frames_left -= 1; + frame_index = (frame_index + 1) % stack_trace.instruction_addresses.len; + }) { + const return_address = stack_trace.instruction_addresses[frame_index]; + // should this case be handled here? + if (return_address == 0) break; + try self.sourceAtAddress(return_address - 1); } - } else { - stack_trace.index = 0; - return; - }; - const slice = addr_buf[first_index..n]; - // We use a for loop here because slice and addrs may alias. - for (slice) |addr, i| { - addrs[i] = addr; } - stack_trace.index = slice.len; + + fn sourceAtAddress( + self: @This(), + address: usize, + ) !void { + const symbol_info = try self.mapping.addressToSymbol(address); + defer symbol_info.deinit(); + + const writer = self.writer; + const tty_config = self.tty_config; + + nosuspend { + try formatStackTraceLine( + writer, + tty_config, + address, + symbol_info, + attemptWriteLineFromSourceFile, + ); + } + } + }; +} + +fn formatStackTraceLine( + writer: anytype, + tty_config: TTY.Config, + address: usize, + si: SymbolMap.SymbolInfo, + tryWriteLineFromSourceFile: anytype, +) !void { + tty_config.setColor(writer, .White); + + if (si.line_info) |*li| { + try writer.print("{s}:{d}:{d}", .{ li.file_name, li.line, li.column }); } else { - var it = StackIterator.init(first_address, null); - for (stack_trace.instruction_addresses) |*addr, i| { - addr.* = it.next() orelse { - stack_trace.index = i; - return; - }; + try writer.writeAll("???:?:?"); + } + + tty_config.setColor(writer, .Reset); + try writer.writeAll(": "); + tty_config.setColor(writer, .Dim); + try writer.print("0x{x} in {s} ({s})", .{ address, si.symbol_name, si.compile_unit_name }); + tty_config.setColor(writer, .Reset); + try writer.writeAll("\n"); + + // Show the matching source code line if possible + if (si.line_info) |li| { + if (try tryWriteLineFromSourceFile(writer, li)) { + if (li.column > 0) { + // The caret already takes one char + const space_needed = @intCast(usize, li.column - 1); + + try writer.writeByteNTimes(' ', space_needed); + tty_config.setColor(writer, .Green); + try writer.writeAll("^"); + tty_config.setColor(writer, .Reset); + } + try writer.writeAll("\n"); } - stack_trace.index = stack_trace.instruction_addresses.len; } } -/// Tries to print a stack trace to stderr, unbuffered, and ignores any error returned. -/// TODO multithreaded awareness -pub fn dumpStackTrace(stack_trace: builtin.StackTrace) void { - nosuspend { - const stderr = io.getStdErr().writer(); - if (builtin.strip_debug_info) { - stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; - return; +/// Try to write a line from a source file. If the line couldn't be written but +/// the error is acceptable (end of file, file not found, etc.), returns false. +/// If the line was correctly writen it returns true. +pub fn attemptWriteLineFromSourceFile(writer: anytype, line_info: SymbolMap.LineInfo) !bool { + // TODO: is this the right place to check? + if (comptime builtin.arch.isWasm()) { + return false; + } + + writeLineFromFileAnyOs(writer, line_info) catch |err| { + switch (err) { + error.EndOfFile, error.FileNotFound => {}, + error.BadPathName => {}, + error.AccessDenied => {}, + else => return err, } - const debug_info = getSelfDebugInfo() catch |err| { - stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; - return; - }; - writeStackTrace(stack_trace, stderr, getDebugInfoAllocator(), debug_info, detectTTYConfig()) catch |err| { - stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; - return; - }; + return false; + }; + return true; +} + +fn writeLineFromFileAnyOs(writer: anytype, line_info: SymbolMap.LineInfo) !void { + // Need this to always block even in async I/O mode, because this could potentially + // be called from e.g. the event loop code crashing. + var f = try fs.cwd().openFile(line_info.file_name, .{ .intended_io_mode = .blocking }); + defer f.close(); + // TODO fstat and make sure that the file has the correct size + + var buf: [mem.page_size]u8 = undefined; + var line: usize = 1; + var column: usize = 1; + var abs_index: usize = 0; + while (true) { + const amt_read = try f.read(buf[0..]); + const slice = buf[0..amt_read]; + + for (slice) |byte| { + if (line == line_info.line) { + try writer.writeByte(byte); + if (byte == '\n') { + return; + } + } + if (byte == '\n') { + line += 1; + column = 1; + } else { + column += 1; + } + } + + if (amt_read < buf.len) return error.EndOfFile; } } @@ -225,23 +472,51 @@ pub fn assert(ok: bool) void { pub fn panic(comptime format: []const u8, args: anytype) noreturn { @setCold(true); - // TODO: remove conditional once wasi / LLVM defines __builtin_return_address - const first_trace_addr = if (builtin.os.tag == .wasi) null else @returnAddress(); - panicExtra(null, first_trace_addr, format, args); + + panicExtra(null, format, args); } -/// Non-zero whenever the program triggered a panic. -/// The counter is incremented/decremented atomically. -var panicking: u8 = 0; +/// `panicExtra` is useful when you want to print out an `@errorReturnTrace` +/// and also print out some values. +pub fn panicExtra( + trace: ?*builtin.StackTrace, + comptime format: []const u8, + args: anytype, +) noreturn { + @setCold(true); + + const size = 0x1000; + const trunc_msg = "(msg truncated)"; + var buf: [size + trunc_msg.len]u8 = undefined; + // a minor annoyance with this is that it will result in the NoSpaceLeft + // error being part of the @panic stack trace (but that error should + // only happen rarely) + const msg = std.fmt.bufPrint(buf[0..size], format, args) catch |err| switch (err) { + std.fmt.BufPrintError.NoSpaceLeft => blk: { + std.mem.copy(u8, buf[size..], trunc_msg); + break :blk &buf; + }, + }; + builtin.panic(msg, trace); +} -// Locked to avoid interleaving panic messages from multiple threads. +/// Non-zero whenever the program triggered a panic. +/// Counts the number of threads which are waiting on the `panic_mutex` +/// to print out the stack traces and msg. +var panicking = std.atomic.Int(u32).init(0); + +/// Locked to avoid interleaving panic messages from multiple threads. +/// +/// We can't use the other Mutex ("print_mutex") because a panic may have +/// happended while that mutex is held. Unfortunately, this means that panic +/// messages may interleave with print(...) or similar. var panic_mutex = std.Thread.Mutex{}; /// Counts how many times the panic handler is invoked by this thread. /// This is used to catch and handle panics triggered by the panic handler. threadlocal var panic_stage: usize = 0; -pub fn panicExtra(trace: ?*const builtin.StackTrace, first_trace_addr: ?usize, comptime format: []const u8, args: anytype) noreturn { +pub fn panicImpl(trace: ?*const builtin.StackTrace, first_trace_addr: ?usize, msg: []const u8) noreturn { @setCold(true); if (enable_segfault_handler) { @@ -250,32 +525,32 @@ pub fn panicExtra(trace: ?*const builtin.StackTrace, first_trace_addr: ?usize, c resetSegfaultHandler(); } + // we can't lock the writer (see above comment on panic_mutex) + const writer = getWriter(); + nosuspend switch (panic_stage) { - 0 => { + 0 => blk: { panic_stage = 1; - _ = @atomicRmw(u8, &panicking, .Add, 1, .SeqCst); + _ = panicking.incr(); // Make sure to release the mutex when done { const held = panic_mutex.acquire(); defer held.release(); - const stderr = io.getStdErr().writer(); if (builtin.single_threaded) { - stderr.print("panic: ", .{}) catch os.abort(); + writer.print("panic: ", .{}) catch break :blk; } else { const current_thread_id = std.Thread.getCurrentThreadId(); - stderr.print("thread {d} panic: ", .{current_thread_id}) catch os.abort(); - } - stderr.print(format ++ "\n", args) catch os.abort(); - if (trace) |t| { - dumpStackTrace(t.*); + writer.print("thread {d} panic: ", .{current_thread_id}) catch break :blk; } - dumpCurrentStackTrace(first_trace_addr); + writer.print("{s}\n", .{msg}) catch break :blk; + + writeTracesForPanic(writer, detectTTYConfig(), trace, first_trace_addr); } - if (@atomicRmw(u8, &panicking, .Sub, 1, .SeqCst) != 1) { + if (panicking.decr() != 1) { // Another thread is panicking, wait for the last one to finish // and call abort() @@ -285,14 +560,13 @@ pub fn panicExtra(trace: ?*const builtin.StackTrace, first_trace_addr: ?usize, c unreachable; } }, - 1 => { + 1 => blk: { panic_stage = 2; // A panic happened while trying to print a previous panic message, // we're still holding the mutex but that's fine as we're going to // call abort() - const stderr = io.getStdErr().writer(); - stderr.print("Panicked during a panic. Aborting.\n", .{}) catch os.abort(); + writer.print("Panicked during a panic. Terminating.\n", .{}) catch break :blk; }, else => { // Panicked while printing "Panicked during a panic." @@ -302,6 +576,34 @@ pub fn panicExtra(trace: ?*const builtin.StackTrace, first_trace_addr: ?usize, c os.abort(); } +/// Utility function for dumping out stack traces which swallows errors. +/// This may be useful for writing your own panic implementation. +pub fn writeTracesForPanic( + writer: anytype, + tty_config: TTY.Config, + trace: ?*const builtin.StackTrace, + first_trace_addr: ?usize, +) void { + // we don't use the dump functions because those lock the mutex + const mapping = getMappingForDump(writer) orelse return; + const write_trace = StackTraceDumper(@TypeOf(writer)){ + .writer = writer, + .tty_config = tty_config, + .mapping = mapping, + }; + + if (trace) |t| { + write_trace.stackTrace(t.*) catch |err| { + dumpHandleError(writer, err); + return; + }; + } + write_trace.current(first_trace_addr) catch |err| { + dumpHandleError(writer, err); + return; + }; +} + const RED = "\x1b[31;1m"; const GREEN = "\x1b[32;1m"; const CYAN = "\x1b[36;1m"; @@ -309,41 +611,156 @@ const WHITE = "\x1b[37;1m"; const DIM = "\x1b[2m"; const RESET = "\x1b[0m"; -pub fn writeStackTrace( - stack_trace: builtin.StackTrace, - out_stream: anytype, - allocator: *mem.Allocator, - debug_info: *DebugInfo, - tty_config: TTY.Config, -) !void { - if (builtin.strip_debug_info) return error.MissingDebugInfo; - var frame_index: usize = 0; - var frames_left: usize = std.math.min(stack_trace.index, stack_trace.instruction_addresses.len); - - while (frames_left != 0) : ({ - frames_left -= 1; - frame_index = (frame_index + 1) % stack_trace.instruction_addresses.len; - }) { - const return_address = stack_trace.instruction_addresses[frame_index]; - try printSourceAtAddress(debug_info, out_stream, return_address - 1, tty_config); - } -} +pub const TTY = struct { + pub const Color = enum { + Red, + Green, + Cyan, + White, + Dim, + Bold, + Reset, + }; -pub const StackIterator = struct { - // Skip every frame before this address is found. - first_address: ?usize, - // Last known value of the frame pointer register. - fp: usize, + pub const Config = union(enum) { + no_color: void, + escape_codes: void, + // TODO: should be noreturn instead of void, see + // https://github.com/ziglang/zig/issues/3257 + // making this noreturn right now causes a crash + windows_api: if (builtin.os.tag == .windows) File else void, - pub fn init(first_address: ?usize, fp: ?usize) StackIterator { - return StackIterator{ - .first_address = first_address, - .fp = fp orelse @frameAddress(), - }; - } + pub fn setColor(conf: Config, writer: anytype, color: Color) void { + nosuspend switch (conf) { + .no_color => return, + .escape_codes => switch (color) { + .Red => writer.writeAll(RED) catch return, + .Green => writer.writeAll(GREEN) catch return, + .Cyan => writer.writeAll(CYAN) catch return, + .White, .Bold => writer.writeAll(WHITE) catch return, + .Dim => writer.writeAll(DIM) catch return, + .Reset => writer.writeAll(RESET) catch return, + }, + .windows_api => |file| if (builtin.os.tag == .windows) { + const S = struct { + var attrs: windows.WORD = undefined; + var init_attrs = false; + }; + if (!S.init_attrs) { + S.init_attrs = true; + var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; + // TODO handle error + _ = windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info); + S.attrs = info.wAttributes; + } - // Offset of the saved BP wrt the frame pointer. - const fp_offset = if (builtin.arch.isRISCV()) + // TODO handle errors + switch (color) { + .Red => { + _ = windows.SetConsoleTextAttribute(file.handle, windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY) catch {}; + }, + .Green => { + _ = windows.SetConsoleTextAttribute(file.handle, windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY) catch {}; + }, + .Cyan => { + _ = windows.SetConsoleTextAttribute(file.handle, windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY) catch {}; + }, + .White, .Bold => { + _ = windows.SetConsoleTextAttribute(file.handle, windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY) catch {}; + }, + .Dim => { + _ = windows.SetConsoleTextAttribute(file.handle, windows.FOREGROUND_INTENSITY) catch {}; + }, + .Reset => { + _ = windows.SetConsoleTextAttribute(file.handle, S.attrs) catch {}; + }, + } + } else { + unreachable; + }, + }; + } + }; +}; + +/// Returns a slice with the same pointer as addresses, with a potentially smaller len. +pub fn captureStackTrace( + allocator: *std.mem.Allocator, + first_address: ?usize, + stack_trace: *builtin.StackTrace, +) !void { + // TODO are there any other arguments/registers which captureStackTraceFrom + // should get access to? + try capStackTraceFrom(allocator, first_address, null, stack_trace); +} + +/// On Windows, when first_address is not null, we ask for at least 32 stack frames, +/// and then try to find the first address. If addresses.len is more than 32, we +/// capture that many stack frames exactly, and then look for the first address, +/// chopping off the irrelevant frames and shifting so that the returned addresses pointer +/// equals the passed in addresses pointer. +pub fn defaultCaptureStackTraceFrom( + allocator: *std.mem.Allocator, + first_address: ?usize, + base_pointer: ?usize, + stack_trace: *builtin.StackTrace, +) !void { + if (builtin.os.tag == .windows) { + const addrs = stack_trace.instruction_addresses; + const u32_addrs_len = @intCast(u32, addrs.len); + const first_addr = first_address orelse { + stack_trace.index = windows.ntdll.RtlCaptureStackBackTrace( + 0, + u32_addrs_len, + @ptrCast(**c_void, addrs.ptr), + null, + ); + return; + }; + var addr_buf_stack: [32]usize = undefined; + const addr_buf = if (addr_buf_stack.len > addrs.len) addr_buf_stack[0..] else addrs; + const n = windows.ntdll.RtlCaptureStackBackTrace(0, u32_addrs_len, @ptrCast(**c_void, addr_buf.ptr), null); + const first_index = for (addr_buf[0..n]) |addr, i| { + if (addr == first_addr) { + break i; + } + } else { + stack_trace.index = 0; + return; + }; + const slice = addr_buf[first_index..n]; + // We use a for loop here because slice and addrs may alias. + for (slice) |addr, i| { + addrs[i] = addr; + } + stack_trace.index = slice.len; + } else { + var it = StackIterator.init(first_address, base_pointer); + for (stack_trace.instruction_addresses) |*addr, i| { + addr.* = it.next() orelse { + stack_trace.index = i; + return; + }; + } + stack_trace.index = stack_trace.instruction_addresses.len; + } +} + +pub const StackIterator = struct { + // Skip every frame before this address is found. + first_address: ?usize, + // Last known value of the frame pointer register. + fp: usize, + + pub fn init(first_address: ?usize, fp: ?usize) StackIterator { + return StackIterator{ + .first_address = first_address, + .fp = fp orelse @frameAddress(), + }; + } + + // Offset of the saved BP wrt the frame pointer. + const fp_offset = if (builtin.arch.isRISCV()) // On RISC-V the frame pointer points to the top of the saved register // area, on pretty much every other architecture it points to the stack // slot where the previous frame pointer is saved. @@ -410,1307 +827,11 @@ pub const StackIterator = struct { } }; -pub fn writeCurrentStackTrace( - out_stream: anytype, - debug_info: *DebugInfo, - tty_config: TTY.Config, - start_addr: ?usize, -) !void { - if (builtin.os.tag == .windows) { - return writeCurrentStackTraceWindows(out_stream, debug_info, tty_config, start_addr); - } - var it = StackIterator.init(start_addr, null); - while (it.next()) |return_address| { - try printSourceAtAddress(debug_info, out_stream, return_address - 1, tty_config); - } -} - -pub fn writeCurrentStackTraceWindows( - out_stream: anytype, - debug_info: *DebugInfo, - tty_config: TTY.Config, - start_addr: ?usize, -) !void { - var addr_buf: [1024]usize = undefined; - const n = windows.ntdll.RtlCaptureStackBackTrace(0, addr_buf.len, @ptrCast(**c_void, &addr_buf), null); - const addrs = addr_buf[0..n]; - var start_i: usize = if (start_addr) |saddr| blk: { - for (addrs) |addr, i| { - if (addr == saddr) break :blk i; - } - return; - } else 0; - for (addrs[start_i..]) |addr| { - try printSourceAtAddress(debug_info, out_stream, addr - 1, tty_config); - } -} - -pub const TTY = struct { - pub const Color = enum { - Red, - Green, - Cyan, - White, - Dim, - Bold, - Reset, - }; - - pub const Config = enum { - no_color, - escape_codes, - // TODO give this a payload of file handle - windows_api, - - fn setColor(conf: Config, out_stream: anytype, color: Color) void { - nosuspend switch (conf) { - .no_color => return, - .escape_codes => switch (color) { - .Red => out_stream.writeAll(RED) catch return, - .Green => out_stream.writeAll(GREEN) catch return, - .Cyan => out_stream.writeAll(CYAN) catch return, - .White, .Bold => out_stream.writeAll(WHITE) catch return, - .Dim => out_stream.writeAll(DIM) catch return, - .Reset => out_stream.writeAll(RESET) catch return, - }, - .windows_api => if (builtin.os.tag == .windows) { - const stderr_file = io.getStdErr(); - const S = struct { - var attrs: windows.WORD = undefined; - var init_attrs = false; - }; - if (!S.init_attrs) { - S.init_attrs = true; - var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - // TODO handle error - _ = windows.kernel32.GetConsoleScreenBufferInfo(stderr_file.handle, &info); - S.attrs = info.wAttributes; - } - - // TODO handle errors - switch (color) { - .Red => { - _ = windows.SetConsoleTextAttribute(stderr_file.handle, windows.FOREGROUND_RED | windows.FOREGROUND_INTENSITY) catch {}; - }, - .Green => { - _ = windows.SetConsoleTextAttribute(stderr_file.handle, windows.FOREGROUND_GREEN | windows.FOREGROUND_INTENSITY) catch {}; - }, - .Cyan => { - _ = windows.SetConsoleTextAttribute(stderr_file.handle, windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY) catch {}; - }, - .White, .Bold => { - _ = windows.SetConsoleTextAttribute(stderr_file.handle, windows.FOREGROUND_RED | windows.FOREGROUND_GREEN | windows.FOREGROUND_BLUE | windows.FOREGROUND_INTENSITY) catch {}; - }, - .Dim => { - _ = windows.SetConsoleTextAttribute(stderr_file.handle, windows.FOREGROUND_INTENSITY) catch {}; - }, - .Reset => { - _ = windows.SetConsoleTextAttribute(stderr_file.handle, S.attrs) catch {}; - }, - } - } else { - unreachable; - }, - }; - } - }; -}; - -/// TODO resources https://github.com/ziglang/zig/issues/4353 -fn populateModule(di: *ModuleDebugInfo, mod: *Module) !void { - if (mod.populated) - return; - const allocator = getDebugInfoAllocator(); - - // At most one can be non-zero. - if (mod.mod_info.C11ByteSize != 0 and mod.mod_info.C13ByteSize != 0) - return error.InvalidDebugInfo; - - if (mod.mod_info.C13ByteSize == 0) - return; - - const modi = di.pdb.getStreamById(mod.mod_info.ModuleSymStream) orelse return error.MissingDebugInfo; - - const signature = try modi.reader().readIntLittle(u32); - if (signature != 4) - return error.InvalidDebugInfo; - - mod.symbols = try allocator.alloc(u8, mod.mod_info.SymByteSize - 4); - try modi.reader().readNoEof(mod.symbols); - - mod.subsect_info = try allocator.alloc(u8, mod.mod_info.C13ByteSize); - try modi.reader().readNoEof(mod.subsect_info); - - var sect_offset: usize = 0; - var skip_len: usize = undefined; - while (sect_offset != mod.subsect_info.len) : (sect_offset += skip_len) { - const subsect_hdr = @ptrCast(*pdb.DebugSubsectionHeader, &mod.subsect_info[sect_offset]); - skip_len = subsect_hdr.Length; - sect_offset += @sizeOf(pdb.DebugSubsectionHeader); - - switch (subsect_hdr.Kind) { - .FileChecksums => { - mod.checksum_offset = sect_offset; - break; - }, - else => {}, - } - - if (sect_offset > mod.subsect_info.len) - return error.InvalidDebugInfo; - } - - mod.populated = true; -} - -fn machoSearchSymbols(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol { - var min: usize = 0; - var max: usize = symbols.len - 1; // Exclude sentinel. - while (min < max) { - const mid = min + (max - min) / 2; - const curr = &symbols[mid]; - const next = &symbols[mid + 1]; - if (address >= next.address()) { - min = mid + 1; - } else if (address < curr.address()) { - max = mid; - } else { - return curr; - } - } - return null; -} - -/// TODO resources https://github.com/ziglang/zig/issues/4353 -pub fn printSourceAtAddress(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: TTY.Config) !void { - const module = debug_info.getModuleForAddress(address) catch |err| switch (err) { - error.MissingDebugInfo, error.InvalidDebugInfo => { - return printLineInfo( - out_stream, - null, - address, - "???", - "???", - tty_config, - printLineFromFileAnyOs, - ); - }, - else => return err, - }; - - const symbol_info = try module.getSymbolAtAddress(address); - defer symbol_info.deinit(); - - return printLineInfo( - out_stream, - symbol_info.line_info, - address, - symbol_info.symbol_name, - symbol_info.compile_unit_name, - tty_config, - printLineFromFileAnyOs, - ); -} - -fn printLineInfo( - out_stream: anytype, - line_info: ?LineInfo, - address: usize, - symbol_name: []const u8, - compile_unit_name: []const u8, - tty_config: TTY.Config, - comptime printLineFromFile: anytype, -) !void { - nosuspend { - tty_config.setColor(out_stream, .White); - - if (line_info) |*li| { - try out_stream.print("{s}:{d}:{d}", .{ li.file_name, li.line, li.column }); - } else { - try out_stream.writeAll("???:?:?"); - } - - tty_config.setColor(out_stream, .Reset); - try out_stream.writeAll(": "); - tty_config.setColor(out_stream, .Dim); - try out_stream.print("0x{x} in {s} ({s})", .{ address, symbol_name, compile_unit_name }); - tty_config.setColor(out_stream, .Reset); - try out_stream.writeAll("\n"); - - // Show the matching source code line if possible - if (line_info) |li| { - if (printLineFromFile(out_stream, li)) { - if (li.column > 0) { - // The caret already takes one char - const space_needed = @intCast(usize, li.column - 1); - - try out_stream.writeByteNTimes(' ', space_needed); - tty_config.setColor(out_stream, .Green); - try out_stream.writeAll("^"); - tty_config.setColor(out_stream, .Reset); - } - try out_stream.writeAll("\n"); - } else |err| switch (err) { - error.EndOfFile, error.FileNotFound => {}, - error.BadPathName => {}, - error.AccessDenied => {}, - else => return err, - } - } - } -} - -// TODO use this -pub const OpenSelfDebugInfoError = error{ - MissingDebugInfo, - OutOfMemory, - UnsupportedOperatingSystem, -}; - -/// TODO resources https://github.com/ziglang/zig/issues/4353 -pub fn openSelfDebugInfo(allocator: *mem.Allocator) anyerror!DebugInfo { - nosuspend { - if (builtin.strip_debug_info) - return error.MissingDebugInfo; - if (@hasDecl(root, "os") and @hasDecl(root.os, "debug") and @hasDecl(root.os.debug, "openSelfDebugInfo")) { - return root.os.debug.openSelfDebugInfo(allocator); - } - switch (builtin.os.tag) { - .linux, - .freebsd, - .netbsd, - .dragonfly, - .openbsd, - .macos, - .windows, - => return DebugInfo.init(allocator), - else => return error.UnsupportedDebugInfo, - } - } -} - -/// This takes ownership of coff_file: users of this function should not close -/// it themselves, even on error. -/// TODO resources https://github.com/ziglang/zig/issues/4353 -/// TODO it's weird to take ownership even on error, rework this code. -fn readCoffDebugInfo(allocator: *mem.Allocator, coff_file: File) !ModuleDebugInfo { - nosuspend { - errdefer coff_file.close(); - - const coff_obj = try allocator.create(coff.Coff); - coff_obj.* = coff.Coff.init(allocator, coff_file); - - var di = ModuleDebugInfo{ - .base_address = undefined, - .coff = coff_obj, - .pdb = undefined, - .sect_contribs = undefined, - .modules = undefined, - }; - - try di.coff.loadHeader(); - - var path_buf: [windows.MAX_PATH]u8 = undefined; - const len = try di.coff.getPdbPath(path_buf[0..]); - const raw_path = path_buf[0..len]; - - const path = try fs.path.resolve(allocator, &[_][]const u8{raw_path}); - - try di.pdb.openFile(di.coff, path); - - var pdb_stream = di.pdb.getStream(pdb.StreamType.Pdb) orelse return error.InvalidDebugInfo; - const version = try pdb_stream.reader().readIntLittle(u32); - const signature = try pdb_stream.reader().readIntLittle(u32); - const age = try pdb_stream.reader().readIntLittle(u32); - var guid: [16]u8 = undefined; - try pdb_stream.reader().readNoEof(&guid); - if (version != 20000404) // VC70, only value observed by LLVM team - return error.UnknownPDBVersion; - if (!mem.eql(u8, &di.coff.guid, &guid) or di.coff.age != age) - return error.PDBMismatch; - // We validated the executable and pdb match. - - const string_table_index = str_tab_index: { - const name_bytes_len = try pdb_stream.reader().readIntLittle(u32); - const name_bytes = try allocator.alloc(u8, name_bytes_len); - try pdb_stream.reader().readNoEof(name_bytes); - - const HashTableHeader = packed struct { - Size: u32, - Capacity: u32, - - fn maxLoad(cap: u32) u32 { - return cap * 2 / 3 + 1; - } - }; - const hash_tbl_hdr = try pdb_stream.reader().readStruct(HashTableHeader); - if (hash_tbl_hdr.Capacity == 0) - return error.InvalidDebugInfo; - - if (hash_tbl_hdr.Size > HashTableHeader.maxLoad(hash_tbl_hdr.Capacity)) - return error.InvalidDebugInfo; - - const present = try readSparseBitVector(&pdb_stream.reader(), allocator); - if (present.len != hash_tbl_hdr.Size) - return error.InvalidDebugInfo; - const deleted = try readSparseBitVector(&pdb_stream.reader(), allocator); - - const Bucket = struct { - first: u32, - second: u32, - }; - const bucket_list = try allocator.alloc(Bucket, present.len); - for (present) |_| { - const name_offset = try pdb_stream.reader().readIntLittle(u32); - const name_index = try pdb_stream.reader().readIntLittle(u32); - const name = mem.spanZ(std.meta.assumeSentinel(name_bytes.ptr + name_offset, 0)); - if (mem.eql(u8, name, "/names")) { - break :str_tab_index name_index; - } - } - return error.MissingDebugInfo; - }; - - di.pdb.string_table = di.pdb.getStreamById(string_table_index) orelse return error.MissingDebugInfo; - di.pdb.dbi = di.pdb.getStream(pdb.StreamType.Dbi) orelse return error.MissingDebugInfo; - - const dbi = di.pdb.dbi; - - // Dbi Header - const dbi_stream_header = try dbi.reader().readStruct(pdb.DbiStreamHeader); - if (dbi_stream_header.VersionHeader != 19990903) // V70, only value observed by LLVM team - return error.UnknownPDBVersion; - if (dbi_stream_header.Age != age) - return error.UnmatchingPDB; - - const mod_info_size = dbi_stream_header.ModInfoSize; - const section_contrib_size = dbi_stream_header.SectionContributionSize; - - var modules = ArrayList(Module).init(allocator); - - // Module Info Substream - var mod_info_offset: usize = 0; - while (mod_info_offset != mod_info_size) { - const mod_info = try dbi.reader().readStruct(pdb.ModInfo); - var this_record_len: usize = @sizeOf(pdb.ModInfo); - - const module_name = try dbi.readNullTermString(allocator); - this_record_len += module_name.len + 1; - - const obj_file_name = try dbi.readNullTermString(allocator); - this_record_len += obj_file_name.len + 1; - - if (this_record_len % 4 != 0) { - const round_to_next_4 = (this_record_len | 0x3) + 1; - const march_forward_bytes = round_to_next_4 - this_record_len; - try dbi.seekBy(@intCast(isize, march_forward_bytes)); - this_record_len += march_forward_bytes; - } - - try modules.append(Module{ - .mod_info = mod_info, - .module_name = module_name, - .obj_file_name = obj_file_name, - - .populated = false, - .symbols = undefined, - .subsect_info = undefined, - .checksum_offset = null, - }); - - mod_info_offset += this_record_len; - if (mod_info_offset > mod_info_size) - return error.InvalidDebugInfo; - } - - di.modules = modules.toOwnedSlice(); - - // Section Contribution Substream - var sect_contribs = ArrayList(pdb.SectionContribEntry).init(allocator); - var sect_cont_offset: usize = 0; - if (section_contrib_size != 0) { - const ver = @intToEnum(pdb.SectionContrSubstreamVersion, try dbi.reader().readIntLittle(u32)); - if (ver != pdb.SectionContrSubstreamVersion.Ver60) - return error.InvalidDebugInfo; - sect_cont_offset += @sizeOf(u32); - } - while (sect_cont_offset != section_contrib_size) { - const entry = try sect_contribs.addOne(); - entry.* = try dbi.reader().readStruct(pdb.SectionContribEntry); - sect_cont_offset += @sizeOf(pdb.SectionContribEntry); - - if (sect_cont_offset > section_contrib_size) - return error.InvalidDebugInfo; - } - - di.sect_contribs = sect_contribs.toOwnedSlice(); - - return di; - } -} - -fn readSparseBitVector(stream: anytype, allocator: *mem.Allocator) ![]usize { - const num_words = try stream.readIntLittle(u32); - var word_i: usize = 0; - var list = ArrayList(usize).init(allocator); - while (word_i != num_words) : (word_i += 1) { - const word = try stream.readIntLittle(u32); - var bit_i: u5 = 0; - while (true) : (bit_i += 1) { - if (word & (@as(u32, 1) << bit_i) != 0) { - try list.append(word_i * 32 + bit_i); - } - if (bit_i == maxInt(u5)) break; - } - } - return list.toOwnedSlice(); -} - -fn chopSlice(ptr: []const u8, offset: u64, size: u64) ![]const u8 { - const start = try math.cast(usize, offset); - const end = start + try math.cast(usize, size); - return ptr[start..end]; -} - -/// This takes ownership of elf_file: users of this function should not close -/// it themselves, even on error. -/// TODO resources https://github.com/ziglang/zig/issues/4353 -/// TODO it's weird to take ownership even on error, rework this code. -pub fn readElfDebugInfo(allocator: *mem.Allocator, elf_file: File) !ModuleDebugInfo { - nosuspend { - const mapped_mem = try mapWholeFile(elf_file); - const hdr = @ptrCast(*const elf.Ehdr, &mapped_mem[0]); - if (!mem.eql(u8, hdr.e_ident[0..4], "\x7fELF")) return error.InvalidElfMagic; - if (hdr.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion; - - const endian: builtin.Endian = switch (hdr.e_ident[elf.EI_DATA]) { - elf.ELFDATA2LSB => .Little, - elf.ELFDATA2MSB => .Big, - else => return error.InvalidElfEndian, - }; - assert(endian == std.builtin.endian); // this is our own debug info - - const shoff = hdr.e_shoff; - const str_section_off = shoff + @as(u64, hdr.e_shentsize) * @as(u64, hdr.e_shstrndx); - const str_shdr = @ptrCast( - *const elf.Shdr, - @alignCast(@alignOf(elf.Shdr), &mapped_mem[try math.cast(usize, str_section_off)]), - ); - const header_strings = mapped_mem[str_shdr.sh_offset .. str_shdr.sh_offset + str_shdr.sh_size]; - const shdrs = @ptrCast( - [*]const elf.Shdr, - @alignCast(@alignOf(elf.Shdr), &mapped_mem[shoff]), - )[0..hdr.e_shnum]; - - var opt_debug_info: ?[]const u8 = null; - var opt_debug_abbrev: ?[]const u8 = null; - var opt_debug_str: ?[]const u8 = null; - var opt_debug_line: ?[]const u8 = null; - var opt_debug_ranges: ?[]const u8 = null; - - for (shdrs) |*shdr| { - if (shdr.sh_type == elf.SHT_NULL) continue; - - const name = std.mem.span(std.meta.assumeSentinel(header_strings[shdr.sh_name..].ptr, 0)); - if (mem.eql(u8, name, ".debug_info")) { - opt_debug_info = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); - } else if (mem.eql(u8, name, ".debug_abbrev")) { - opt_debug_abbrev = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); - } else if (mem.eql(u8, name, ".debug_str")) { - opt_debug_str = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); - } else if (mem.eql(u8, name, ".debug_line")) { - opt_debug_line = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); - } else if (mem.eql(u8, name, ".debug_ranges")) { - opt_debug_ranges = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); - } - } - - var di = DW.DwarfInfo{ - .endian = endian, - .debug_info = opt_debug_info orelse return error.MissingDebugInfo, - .debug_abbrev = opt_debug_abbrev orelse return error.MissingDebugInfo, - .debug_str = opt_debug_str orelse return error.MissingDebugInfo, - .debug_line = opt_debug_line orelse return error.MissingDebugInfo, - .debug_ranges = opt_debug_ranges, - }; - - try DW.openDwarfDebugInfo(&di, allocator); - - return ModuleDebugInfo{ - .base_address = undefined, - .dwarf = di, - .mapped_memory = mapped_mem, - }; - } -} - -/// TODO resources https://github.com/ziglang/zig/issues/4353 -/// This takes ownership of coff_file: users of this function should not close -/// it themselves, even on error. -/// TODO it's weird to take ownership even on error, rework this code. -fn readMachODebugInfo(allocator: *mem.Allocator, macho_file: File) !ModuleDebugInfo { - const mapped_mem = try mapWholeFile(macho_file); - - const hdr = @ptrCast( - *const macho.mach_header_64, - @alignCast(@alignOf(macho.mach_header_64), mapped_mem.ptr), - ); - if (hdr.magic != macho.MH_MAGIC_64) - return error.InvalidDebugInfo; - - const hdr_base = @ptrCast([*]const u8, hdr); - var ptr = hdr_base + @sizeOf(macho.mach_header_64); - var ncmd: u32 = hdr.ncmds; - const symtab = while (ncmd != 0) : (ncmd -= 1) { - const lc = @ptrCast(*const std.macho.load_command, ptr); - switch (lc.cmd) { - std.macho.LC_SYMTAB => break @ptrCast(*const std.macho.symtab_command, ptr), - else => {}, - } - ptr = @alignCast(@alignOf(std.macho.load_command), ptr + lc.cmdsize); - } else { - return error.MissingDebugInfo; - }; - const syms = @ptrCast([*]const macho.nlist_64, @alignCast(@alignOf(macho.nlist_64), hdr_base + symtab.symoff))[0..symtab.nsyms]; - const strings = @ptrCast([*]const u8, hdr_base + symtab.stroff)[0 .. symtab.strsize - 1 :0]; - - const symbols_buf = try allocator.alloc(MachoSymbol, syms.len); - - var ofile: ?*const macho.nlist_64 = null; - var reloc: u64 = 0; - var symbol_index: usize = 0; - var last_len: u64 = 0; - for (syms) |*sym| { - if (sym.n_type & std.macho.N_STAB != 0) { - switch (sym.n_type) { - std.macho.N_OSO => { - ofile = sym; - reloc = 0; - }, - std.macho.N_FUN => { - if (sym.n_sect == 0) { - last_len = sym.n_value; - } else { - symbols_buf[symbol_index] = MachoSymbol{ - .nlist = sym, - .ofile = ofile, - .reloc = reloc, - }; - symbol_index += 1; - } - }, - std.macho.N_BNSYM => { - if (reloc == 0) { - reloc = sym.n_value; - } - }, - else => continue, - } - } - } - const sentinel = try allocator.create(macho.nlist_64); - sentinel.* = macho.nlist_64{ - .n_strx = 0, - .n_type = 36, - .n_sect = 0, - .n_desc = 0, - .n_value = symbols_buf[symbol_index - 1].nlist.n_value + last_len, - }; - - const symbols = allocator.shrink(symbols_buf, symbol_index); - - // Even though lld emits symbols in ascending order, this debug code - // should work for programs linked in any valid way. - // This sort is so that we can binary search later. - std.sort.sort(MachoSymbol, symbols, {}, MachoSymbol.addressLessThan); - - return ModuleDebugInfo{ - .base_address = undefined, - .mapped_memory = mapped_mem, - .ofiles = ModuleDebugInfo.OFileTable.init(allocator), - .symbols = symbols, - .strings = strings, - }; -} - -fn printLineFromFileAnyOs(out_stream: anytype, line_info: LineInfo) !void { - // Need this to always block even in async I/O mode, because this could potentially - // be called from e.g. the event loop code crashing. - var f = try fs.cwd().openFile(line_info.file_name, .{ .intended_io_mode = .blocking }); - defer f.close(); - // TODO fstat and make sure that the file has the correct size - - var buf: [mem.page_size]u8 = undefined; - var line: usize = 1; - var column: usize = 1; - var abs_index: usize = 0; - while (true) { - const amt_read = try f.read(buf[0..]); - const slice = buf[0..amt_read]; - - for (slice) |byte| { - if (line == line_info.line) { - try out_stream.writeByte(byte); - if (byte == '\n') { - return; - } - } - if (byte == '\n') { - line += 1; - column = 1; - } else { - column += 1; - } - } - - if (amt_read < buf.len) return error.EndOfFile; - } -} - -const MachoSymbol = struct { - nlist: *const macho.nlist_64, - ofile: ?*const macho.nlist_64, - reloc: u64, - - /// Returns the address from the macho file - fn address(self: MachoSymbol) u64 { - return self.nlist.n_value; - } - - fn addressLessThan(context: void, lhs: MachoSymbol, rhs: MachoSymbol) bool { - return lhs.address() < rhs.address(); - } -}; - -/// `file` is expected to have been opened with .intended_io_mode == .blocking. -/// Takes ownership of file, even on error. -/// TODO it's weird to take ownership even on error, rework this code. -fn mapWholeFile(file: File) ![]align(mem.page_size) const u8 { - nosuspend { - defer file.close(); - - const file_len = try math.cast(usize, try file.getEndPos()); - const mapped_mem = try os.mmap( - null, - file_len, - os.PROT_READ, - os.MAP_SHARED, - file.handle, - 0, - ); - errdefer os.munmap(mapped_mem); - - return mapped_mem; - } -} - -pub const DebugInfo = struct { - allocator: *mem.Allocator, - address_map: std.AutoHashMap(usize, *ModuleDebugInfo), - - pub fn init(allocator: *mem.Allocator) DebugInfo { - return DebugInfo{ - .allocator = allocator, - .address_map = std.AutoHashMap(usize, *ModuleDebugInfo).init(allocator), - }; - } - - pub fn deinit(self: *DebugInfo) void { - // TODO: resources https://github.com/ziglang/zig/issues/4353 - self.address_map.deinit(); - } - - pub fn getModuleForAddress(self: *DebugInfo, address: usize) !*ModuleDebugInfo { - if (comptime std.Target.current.isDarwin()) { - return self.lookupModuleDyld(address); - } else if (builtin.os.tag == .windows) { - return self.lookupModuleWin32(address); - } else if (builtin.os.tag == .haiku) { - return self.lookupModuleHaiku(address); - } else { - return self.lookupModuleDl(address); - } - } - - fn lookupModuleDyld(self: *DebugInfo, address: usize) !*ModuleDebugInfo { - const image_count = std.c._dyld_image_count(); - - var i: u32 = 0; - while (i < image_count) : (i += 1) { - const base_address = std.c._dyld_get_image_vmaddr_slide(i); - - if (address < base_address) continue; - - const header = std.c._dyld_get_image_header(i) orelse continue; - // The array of load commands is right after the header - var cmd_ptr = @intToPtr([*]u8, @ptrToInt(header) + @sizeOf(macho.mach_header_64)); - - var cmds = header.ncmds; - while (cmds != 0) : (cmds -= 1) { - const lc = @ptrCast( - *macho.load_command, - @alignCast(@alignOf(macho.load_command), cmd_ptr), - ); - cmd_ptr += lc.cmdsize; - if (lc.cmd != macho.LC_SEGMENT_64) continue; - - const segment_cmd = @ptrCast( - *const std.macho.segment_command_64, - @alignCast(@alignOf(std.macho.segment_command_64), lc), - ); - - const rebased_address = address - base_address; - const seg_start = segment_cmd.vmaddr; - const seg_end = seg_start + segment_cmd.vmsize; - - if (rebased_address >= seg_start and rebased_address < seg_end) { - if (self.address_map.get(base_address)) |obj_di| { - return obj_di; - } - - const obj_di = try self.allocator.create(ModuleDebugInfo); - errdefer self.allocator.destroy(obj_di); - - const macho_path = mem.spanZ(std.c._dyld_get_image_name(i)); - const macho_file = fs.cwd().openFile(macho_path, .{ .intended_io_mode = .blocking }) catch |err| switch (err) { - error.FileNotFound => return error.MissingDebugInfo, - else => return err, - }; - obj_di.* = try readMachODebugInfo(self.allocator, macho_file); - obj_di.base_address = base_address; - - try self.address_map.putNoClobber(base_address, obj_di); - - return obj_di; - } - } - } - - return error.MissingDebugInfo; - } - - fn lookupModuleWin32(self: *DebugInfo, address: usize) !*ModuleDebugInfo { - const process_handle = windows.kernel32.GetCurrentProcess(); - - // Find how many modules are actually loaded - var dummy: windows.HMODULE = undefined; - var bytes_needed: windows.DWORD = undefined; - if (windows.kernel32.K32EnumProcessModules( - process_handle, - @ptrCast([*]windows.HMODULE, &dummy), - 0, - &bytes_needed, - ) == 0) - return error.MissingDebugInfo; - - const needed_modules = bytes_needed / @sizeOf(windows.HMODULE); - - // Fetch the complete module list - var modules = try self.allocator.alloc(windows.HMODULE, needed_modules); - defer self.allocator.free(modules); - if (windows.kernel32.K32EnumProcessModules( - process_handle, - modules.ptr, - try math.cast(windows.DWORD, modules.len * @sizeOf(windows.HMODULE)), - &bytes_needed, - ) == 0) - return error.MissingDebugInfo; - - // There's an unavoidable TOCTOU problem here, the module list may have - // changed between the two EnumProcessModules call. - // Pick the smallest amount of elements to avoid processing garbage. - const needed_modules_after = bytes_needed / @sizeOf(windows.HMODULE); - const loaded_modules = math.min(needed_modules, needed_modules_after); - - for (modules[0..loaded_modules]) |module| { - var info: windows.MODULEINFO = undefined; - if (windows.kernel32.K32GetModuleInformation( - process_handle, - module, - &info, - @sizeOf(@TypeOf(info)), - ) == 0) - return error.MissingDebugInfo; - - const seg_start = @ptrToInt(info.lpBaseOfDll); - const seg_end = seg_start + info.SizeOfImage; - - if (address >= seg_start and address < seg_end) { - if (self.address_map.get(seg_start)) |obj_di| { - return obj_di; - } - - var name_buffer: [windows.PATH_MAX_WIDE + 4:0]u16 = undefined; - // openFileAbsoluteW requires the prefix to be present - mem.copy(u16, name_buffer[0..4], &[_]u16{ '\\', '?', '?', '\\' }); - const len = windows.kernel32.K32GetModuleFileNameExW( - process_handle, - module, - @ptrCast(windows.LPWSTR, &name_buffer[4]), - windows.PATH_MAX_WIDE, - ); - assert(len > 0); - - const obj_di = try self.allocator.create(ModuleDebugInfo); - errdefer self.allocator.destroy(obj_di); - - const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) { - error.FileNotFound => return error.MissingDebugInfo, - else => return err, - }; - obj_di.* = try readCoffDebugInfo(self.allocator, coff_file); - obj_di.base_address = seg_start; - - try self.address_map.putNoClobber(seg_start, obj_di); - - return obj_di; - } - } - - return error.MissingDebugInfo; - } - - fn lookupModuleDl(self: *DebugInfo, address: usize) !*ModuleDebugInfo { - var ctx: struct { - // Input - address: usize, - // Output - base_address: usize = undefined, - name: []const u8 = undefined, - } = .{ .address = address }; - const CtxTy = @TypeOf(ctx); - - if (os.dl_iterate_phdr(&ctx, anyerror, struct { - fn callback(info: *os.dl_phdr_info, size: usize, context: *CtxTy) !void { - // The base address is too high - if (context.address < info.dlpi_addr) - return; - - const phdrs = info.dlpi_phdr[0..info.dlpi_phnum]; - for (phdrs) |*phdr| { - if (phdr.p_type != elf.PT_LOAD) continue; - - const seg_start = info.dlpi_addr + phdr.p_vaddr; - const seg_end = seg_start + phdr.p_memsz; - - if (context.address >= seg_start and context.address < seg_end) { - // Android libc uses NULL instead of an empty string to mark the - // main program - context.name = mem.spanZ(info.dlpi_name) orelse ""; - context.base_address = info.dlpi_addr; - // Stop the iteration - return error.Found; - } - } - } - }.callback)) { - return error.MissingDebugInfo; - } else |err| switch (err) { - error.Found => {}, - else => return error.MissingDebugInfo, - } - - if (self.address_map.get(ctx.base_address)) |obj_di| { - return obj_di; - } - - const obj_di = try self.allocator.create(ModuleDebugInfo); - errdefer self.allocator.destroy(obj_di); - - // TODO https://github.com/ziglang/zig/issues/5525 - const copy = if (ctx.name.len > 0) - fs.cwd().openFile(ctx.name, .{ .intended_io_mode = .blocking }) - else - fs.openSelfExe(.{ .intended_io_mode = .blocking }); - - const elf_file = copy catch |err| switch (err) { - error.FileNotFound => return error.MissingDebugInfo, - else => return err, - }; - - obj_di.* = try readElfDebugInfo(self.allocator, elf_file); - obj_di.base_address = ctx.base_address; - - try self.address_map.putNoClobber(ctx.base_address, obj_di); - - return obj_di; - } - - fn lookupModuleHaiku(self: *DebugInfo, address: usize) !*ModuleDebugInfo { - @panic("TODO implement lookup module for Haiku"); - } -}; - -const SymbolInfo = struct { - symbol_name: []const u8 = "???", - compile_unit_name: []const u8 = "???", - line_info: ?LineInfo = null, - - fn deinit(self: @This()) void { - if (self.line_info) |li| { - li.deinit(); - } - } -}; - -pub const ModuleDebugInfo = switch (builtin.os.tag) { - .macos, .ios, .watchos, .tvos => struct { - base_address: usize, - mapped_memory: []const u8, - symbols: []const MachoSymbol, - strings: [:0]const u8, - ofiles: OFileTable, - - const OFileTable = std.StringHashMap(DW.DwarfInfo); - - pub fn allocator(self: @This()) *mem.Allocator { - return self.ofiles.allocator; - } - - fn loadOFile(self: *@This(), o_file_path: []const u8) !DW.DwarfInfo { - const o_file = try fs.cwd().openFile(o_file_path, .{ .intended_io_mode = .blocking }); - const mapped_mem = try mapWholeFile(o_file); - - const hdr = @ptrCast( - *const macho.mach_header_64, - @alignCast(@alignOf(macho.mach_header_64), mapped_mem.ptr), - ); - if (hdr.magic != std.macho.MH_MAGIC_64) - return error.InvalidDebugInfo; - - const hdr_base = @ptrCast([*]const u8, hdr); - var ptr = hdr_base + @sizeOf(macho.mach_header_64); - var ncmd: u32 = hdr.ncmds; - const segcmd = while (ncmd != 0) : (ncmd -= 1) { - const lc = @ptrCast(*const std.macho.load_command, ptr); - switch (lc.cmd) { - std.macho.LC_SEGMENT_64 => { - break @ptrCast( - *const std.macho.segment_command_64, - @alignCast(@alignOf(std.macho.segment_command_64), ptr), - ); - }, - else => {}, - } - ptr = @alignCast(@alignOf(std.macho.load_command), ptr + lc.cmdsize); - } else { - return error.MissingDebugInfo; - }; - - var opt_debug_line: ?*const macho.section_64 = null; - var opt_debug_info: ?*const macho.section_64 = null; - var opt_debug_abbrev: ?*const macho.section_64 = null; - var opt_debug_str: ?*const macho.section_64 = null; - var opt_debug_ranges: ?*const macho.section_64 = null; - - const sections = @ptrCast( - [*]const macho.section_64, - @alignCast(@alignOf(macho.section_64), ptr + @sizeOf(std.macho.segment_command_64)), - )[0..segcmd.nsects]; - for (sections) |*sect| { - // The section name may not exceed 16 chars and a trailing null may - // not be present - const name = if (mem.indexOfScalar(u8, sect.sectname[0..], 0)) |last| - sect.sectname[0..last] - else - sect.sectname[0..]; - - if (mem.eql(u8, name, "__debug_line")) { - opt_debug_line = sect; - } else if (mem.eql(u8, name, "__debug_info")) { - opt_debug_info = sect; - } else if (mem.eql(u8, name, "__debug_abbrev")) { - opt_debug_abbrev = sect; - } else if (mem.eql(u8, name, "__debug_str")) { - opt_debug_str = sect; - } else if (mem.eql(u8, name, "__debug_ranges")) { - opt_debug_ranges = sect; - } - } - - const debug_line = opt_debug_line orelse - return error.MissingDebugInfo; - const debug_info = opt_debug_info orelse - return error.MissingDebugInfo; - const debug_str = opt_debug_str orelse - return error.MissingDebugInfo; - const debug_abbrev = opt_debug_abbrev orelse - return error.MissingDebugInfo; - - var di = DW.DwarfInfo{ - .endian = .Little, - .debug_info = try chopSlice(mapped_mem, debug_info.offset, debug_info.size), - .debug_abbrev = try chopSlice(mapped_mem, debug_abbrev.offset, debug_abbrev.size), - .debug_str = try chopSlice(mapped_mem, debug_str.offset, debug_str.size), - .debug_line = try chopSlice(mapped_mem, debug_line.offset, debug_line.size), - .debug_ranges = if (opt_debug_ranges) |debug_ranges| - try chopSlice(mapped_mem, debug_ranges.offset, debug_ranges.size) - else - null, - }; - - try DW.openDwarfDebugInfo(&di, self.allocator()); - - // Add the debug info to the cache - try self.ofiles.putNoClobber(o_file_path, di); - - return di; - } - - pub fn getSymbolAtAddress(self: *@This(), address: usize) !SymbolInfo { - nosuspend { - // Translate the VA into an address into this object - const relocated_address = address - self.base_address; - assert(relocated_address >= 0x100000000); - - // Find the .o file where this symbol is defined - const symbol = machoSearchSymbols(self.symbols, relocated_address) orelse - return SymbolInfo{}; - - // Take the symbol name from the N_FUN STAB entry, we're going to - // use it if we fail to find the DWARF infos - const stab_symbol = mem.spanZ(self.strings[symbol.nlist.n_strx..]); - - if (symbol.ofile == null) - return SymbolInfo{ .symbol_name = stab_symbol }; - - const o_file_path = mem.spanZ(self.strings[symbol.ofile.?.n_strx..]); - - // Check if its debug infos are already in the cache - var o_file_di = self.ofiles.get(o_file_path) orelse - (self.loadOFile(o_file_path) catch |err| switch (err) { - error.FileNotFound, - error.MissingDebugInfo, - error.InvalidDebugInfo, - => { - return SymbolInfo{ .symbol_name = stab_symbol }; - }, - else => return err, - }); - - // Translate again the address, this time into an address inside the - // .o file - const relocated_address_o = relocated_address - symbol.reloc; - - if (o_file_di.findCompileUnit(relocated_address_o)) |compile_unit| { - return SymbolInfo{ - .symbol_name = o_file_di.getSymbolName(relocated_address_o) orelse "???", - .compile_unit_name = compile_unit.die.getAttrString(&o_file_di, DW.AT_name) catch |err| switch (err) { - error.MissingDebugInfo, error.InvalidDebugInfo => "???", - else => return err, - }, - .line_info = o_file_di.getLineNumberInfo(compile_unit.*, relocated_address_o) catch |err| switch (err) { - error.MissingDebugInfo, error.InvalidDebugInfo => null, - else => return err, - }, - }; - } else |err| switch (err) { - error.MissingDebugInfo, error.InvalidDebugInfo => { - return SymbolInfo{ .symbol_name = stab_symbol }; - }, - else => return err, - } - - unreachable; - } - } - }, - .uefi, .windows => struct { - base_address: usize, - pdb: pdb.Pdb, - coff: *coff.Coff, - sect_contribs: []pdb.SectionContribEntry, - modules: []Module, - - pub fn allocator(self: @This()) *mem.Allocator { - return self.coff.allocator; - } - - pub fn getSymbolAtAddress(self: *@This(), address: usize) !SymbolInfo { - // Translate the VA into an address into this object - const relocated_address = address - self.base_address; - - var coff_section: *coff.Section = undefined; - const mod_index = for (self.sect_contribs) |sect_contrib| { - if (sect_contrib.Section > self.coff.sections.items.len) continue; - // Remember that SectionContribEntry.Section is 1-based. - coff_section = &self.coff.sections.items[sect_contrib.Section - 1]; - - const vaddr_start = coff_section.header.virtual_address + sect_contrib.Offset; - const vaddr_end = vaddr_start + sect_contrib.Size; - if (relocated_address >= vaddr_start and relocated_address < vaddr_end) { - break sect_contrib.ModuleIndex; - } - } else { - // we have no information to add to the address - return SymbolInfo{}; - }; - - const mod = &self.modules[mod_index]; - try populateModule(self, mod); - const obj_basename = fs.path.basename(mod.obj_file_name); - - var symbol_i: usize = 0; - const symbol_name = if (!mod.populated) "???" else while (symbol_i != mod.symbols.len) { - const prefix = @ptrCast(*pdb.RecordPrefix, &mod.symbols[symbol_i]); - if (prefix.RecordLen < 2) - return error.InvalidDebugInfo; - switch (prefix.RecordKind) { - .S_LPROC32, .S_GPROC32 => { - const proc_sym = @ptrCast(*pdb.ProcSym, &mod.symbols[symbol_i + @sizeOf(pdb.RecordPrefix)]); - const vaddr_start = coff_section.header.virtual_address + proc_sym.CodeOffset; - const vaddr_end = vaddr_start + proc_sym.CodeSize; - if (relocated_address >= vaddr_start and relocated_address < vaddr_end) { - break mem.spanZ(@ptrCast([*:0]u8, proc_sym) + @sizeOf(pdb.ProcSym)); - } - }, - else => {}, - } - symbol_i += prefix.RecordLen + @sizeOf(u16); - if (symbol_i > mod.symbols.len) - return error.InvalidDebugInfo; - } else "???"; - - const subsect_info = mod.subsect_info; - - var sect_offset: usize = 0; - var skip_len: usize = undefined; - const opt_line_info = subsections: { - const checksum_offset = mod.checksum_offset orelse break :subsections null; - while (sect_offset != subsect_info.len) : (sect_offset += skip_len) { - const subsect_hdr = @ptrCast(*pdb.DebugSubsectionHeader, &subsect_info[sect_offset]); - skip_len = subsect_hdr.Length; - sect_offset += @sizeOf(pdb.DebugSubsectionHeader); - - switch (subsect_hdr.Kind) { - .Lines => { - var line_index = sect_offset; - - const line_hdr = @ptrCast(*pdb.LineFragmentHeader, &subsect_info[line_index]); - if (line_hdr.RelocSegment == 0) - return error.MissingDebugInfo; - line_index += @sizeOf(pdb.LineFragmentHeader); - const frag_vaddr_start = coff_section.header.virtual_address + line_hdr.RelocOffset; - const frag_vaddr_end = frag_vaddr_start + line_hdr.CodeSize; - - if (relocated_address >= frag_vaddr_start and relocated_address < frag_vaddr_end) { - // There is an unknown number of LineBlockFragmentHeaders (and their accompanying line and column records) - // from now on. We will iterate through them, and eventually find a LineInfo that we're interested in, - // breaking out to :subsections. If not, we will make sure to not read anything outside of this subsection. - const subsection_end_index = sect_offset + subsect_hdr.Length; - - while (line_index < subsection_end_index) { - const block_hdr = @ptrCast(*pdb.LineBlockFragmentHeader, &subsect_info[line_index]); - line_index += @sizeOf(pdb.LineBlockFragmentHeader); - const start_line_index = line_index; - - const has_column = line_hdr.Flags.LF_HaveColumns; - - // All line entries are stored inside their line block by ascending start address. - // Heuristic: we want to find the last line entry - // that has a vaddr_start <= relocated_address. - // This is done with a simple linear search. - var line_i: u32 = 0; - while (line_i < block_hdr.NumLines) : (line_i += 1) { - const line_num_entry = @ptrCast(*pdb.LineNumberEntry, &subsect_info[line_index]); - line_index += @sizeOf(pdb.LineNumberEntry); - - const vaddr_start = frag_vaddr_start + line_num_entry.Offset; - if (relocated_address < vaddr_start) { - break; - } - } - - // line_i == 0 would mean that no matching LineNumberEntry was found. - if (line_i > 0) { - const subsect_index = checksum_offset + block_hdr.NameIndex; - const chksum_hdr = @ptrCast(*pdb.FileChecksumEntryHeader, &mod.subsect_info[subsect_index]); - const strtab_offset = @sizeOf(pdb.PDBStringTableHeader) + chksum_hdr.FileNameOffset; - try self.pdb.string_table.seekTo(strtab_offset); - const source_file_name = try self.pdb.string_table.readNullTermString(self.allocator()); - - const line_entry_idx = line_i - 1; - - const column = if (has_column) blk: { - const start_col_index = start_line_index + @sizeOf(pdb.LineNumberEntry) * block_hdr.NumLines; - const col_index = start_col_index + @sizeOf(pdb.ColumnNumberEntry) * line_entry_idx; - const col_num_entry = @ptrCast(*pdb.ColumnNumberEntry, &subsect_info[col_index]); - break :blk col_num_entry.StartColumn; - } else 0; - - const found_line_index = start_line_index + line_entry_idx * @sizeOf(pdb.LineNumberEntry); - const line_num_entry = @ptrCast(*pdb.LineNumberEntry, &subsect_info[found_line_index]); - const flags = @ptrCast(*pdb.LineNumberEntry.Flags, &line_num_entry.Flags); - - break :subsections LineInfo{ - .allocator = self.allocator(), - .file_name = source_file_name, - .line = flags.Start, - .column = column, - }; - } - } - - // Checking that we are not reading garbage after the (possibly) multiple block fragments. - if (line_index != subsection_end_index) { - return error.InvalidDebugInfo; - } - } - }, - else => {}, - } - - if (sect_offset > subsect_info.len) - return error.InvalidDebugInfo; - } else { - break :subsections null; - } - }; - - return SymbolInfo{ - .symbol_name = symbol_name, - .compile_unit_name = obj_basename, - .line_info = opt_line_info, - }; - } - }, - .linux, .netbsd, .freebsd, .dragonfly, .openbsd => struct { - base_address: usize, - dwarf: DW.DwarfInfo, - mapped_memory: []const u8, - - pub fn getSymbolAtAddress(self: *@This(), address: usize) !SymbolInfo { - // Translate the VA into an address into this object - const relocated_address = address - self.base_address; - - if (nosuspend self.dwarf.findCompileUnit(relocated_address)) |compile_unit| { - return SymbolInfo{ - .symbol_name = nosuspend self.dwarf.getSymbolName(relocated_address) orelse "???", - .compile_unit_name = compile_unit.die.getAttrString(&self.dwarf, DW.AT_name) catch |err| switch (err) { - error.MissingDebugInfo, error.InvalidDebugInfo => "???", - else => return err, - }, - .line_info = nosuspend self.dwarf.getLineNumberInfo(compile_unit.*, relocated_address) catch |err| switch (err) { - error.MissingDebugInfo, error.InvalidDebugInfo => null, - else => return err, - }, - }; - } else |err| switch (err) { - error.MissingDebugInfo, error.InvalidDebugInfo => { - return SymbolInfo{}; - }, - else => return err, - } - - unreachable; - } - }, - else => DW.DwarfInfo, -}; - /// TODO multithreaded awareness var debug_info_allocator: ?*mem.Allocator = null; var debug_info_arena_allocator: std.heap.ArenaAllocator = undefined; +/// Note: this is also used from StackTraceDumper, so be careful when deinit +/// is (eventually) implemented fn getDebugInfoAllocator() *mem.Allocator { if (debug_info_allocator) |a| return a; @@ -1791,13 +912,13 @@ fn handleSegfaultLinux(sig: i32, info: *const os.siginfo_t, ctx_ptr: ?*const c_v else => unreachable, }; - // Don't use std.debug.print() as stderr_mutex may still be locked. + // Don't use std.debug.print() as print_mutex may still be locked. nosuspend { - const stderr = io.getStdErr().writer(); + const writer = getWriter(); _ = switch (sig) { - os.SIGSEGV => stderr.print("Segmentation fault at address 0x{x}\n", .{addr}), - os.SIGILL => stderr.print("Illegal instruction at address 0x{x}\n", .{addr}), - os.SIGBUS => stderr.print("Bus error at address 0x{x}\n", .{addr}), + os.SIGSEGV => writer.print("Segmentation fault at address 0x{x}\n", .{addr}), + os.SIGILL => writer.print("Illegal instruction at address 0x{x}\n", .{addr}), + os.SIGBUS => writer.print("Bus error at address 0x{x}\n", .{addr}), else => unreachable, } catch os.abort(); } @@ -1862,13 +983,13 @@ fn handleSegfaultWindowsExtra(info: *windows.EXCEPTION_POINTERS, comptime msg: u const exception_address = @ptrToInt(info.ExceptionRecord.ExceptionAddress); if (@hasDecl(windows, "CONTEXT")) { const regs = info.ContextRecord.getRegs(); - // Don't use std.debug.print() as stderr_mutex may still be locked. + // Don't use std.debug.print() as print_mutex may still be locked. nosuspend { - const stderr = io.getStdErr().writer(); + const writer = getWriter(); _ = switch (msg) { - 0 => stderr.print("{s}\n", .{format.?}), - 1 => stderr.print("Segmentation fault at address 0x{x}\n", .{info.ExceptionRecord.ExceptionInformation[1]}), - 2 => stderr.print("Illegal instruction at address 0x{x}\n", .{regs.ip}), + 0 => writer.print("{s}\n", .{format.?}), + 1 => writer.print("Segmentation fault at address 0x{x}\n", .{info.ExceptionRecord.ExceptionInformation[1]}), + 2 => writer.print("Illegal instruction at address 0x{x}\n", .{regs.ip}), else => unreachable, } catch os.abort(); } @@ -1889,5 +1010,5 @@ pub fn dumpStackPointerAddr(prefix: []const u8) void { const sp = asm ("" : [argc] "={rsp}" (-> usize) ); - std.debug.warn("{} sp = 0x{x}\n", .{ prefix, sp }); + std.debug.print("{} sp = 0x{x}\n", .{ prefix, sp }); } diff --git a/lib/std/debug_info.zig b/lib/std/debug_info.zig new file mode 100644 index 000000000000..dcecd5921d3c --- /dev/null +++ b/lib/std/debug_info.zig @@ -0,0 +1,97 @@ +//! Misc utility functions for creating a `SymbolMap` for `std.debug` +//! TODO: better name for this file? + +const std = @import("std.zig"); +const SymbolMap = std.debug.SymbolMap; +const SymbolInfo = SymbolMap.SymbolInfo; +const mem = std.mem; +const DW = std.dwarf; +const os = std.os; +const math = std.math; +const File = std.fs.File; + +pub fn SymbolMapStateFromModuleInfo(comptime Module: type) type { + return struct { + const Self = @This(); + + pub const AddressMap = std.AutoHashMap(usize, *Module); + + allocator: *mem.Allocator, + address_map: AddressMap, + symbol_map: SymbolMap, + + pub fn init(allocator: *mem.Allocator) !*SymbolMap { + const value = try allocator.create(Self); + value.* = Self{ + .allocator = allocator, + .address_map = std.AutoHashMap(usize, *Module).init(allocator), + .symbol_map = .{ + .deinitFn = deinit, + .addressToSymbolFn = addressToSymbol, + }, + }; + + return &value.symbol_map; + } + + fn deinit(symbol_map: *SymbolMap) void { + const self = @fieldParentPtr(Self, "symbol_map", symbol_map); + self.address_map.deinit(); + self.allocator.destroy(self); + } + + fn addressToSymbol(symbol_map: *SymbolMap, address: usize) !SymbolInfo { + const self = @fieldParentPtr(Self, "symbol_map", symbol_map); + const module = Module.lookup(self.allocator, &self.address_map, address) catch |err| + return if (std.meta.errorInSet(err, BaseError)) SymbolInfo{} else return err; + return module.addressToSymbol(address); + } + }; +} + +pub const BaseError = error{ + MissingDebugInfo, + InvalidDebugInfo, + UnsupportedOperatingSystem, +}; + +pub fn chopSlice(ptr: []const u8, offset: u64, size: u64) ![]const u8 { + const start = try math.cast(usize, offset); + const end = start + try math.cast(usize, size); + return ptr[start..end]; +} + +/// `file` is expected to have been opened with .intended_io_mode == .blocking. +/// Takes ownership of file, even on error. +/// TODO it's weird to take ownership even on error, rework this code. +pub fn mapWholeFile(file: File) ![]align(mem.page_size) const u8 { + nosuspend { + defer file.close(); + + const file_len = try math.cast(usize, try file.getEndPos()); + const mapped_mem = try os.mmap( + null, + file_len, + os.PROT_READ, + os.MAP_SHARED, + file.handle, + 0, + ); + errdefer os.munmap(mapped_mem); + + return mapped_mem; + } +} + +pub fn dwarfAddressToSymbolInfo(dwarf: *DW.DwarfInfo, relocated_address: usize) !SymbolInfo { + const compile_unit = nosuspend dwarf.findCompileUnit(relocated_address) catch |err| + return if (std.meta.errorInSet(err, BaseError)) SymbolInfo{} else err; + + return SymbolInfo{ + .symbol_name = nosuspend dwarf.getSymbolName(relocated_address) orelse "???", + .compile_unit_name = compile_unit.die.getAttrString(dwarf, DW.AT_name) catch |err| + if (std.meta.errorInSet(err, BaseError)) "???" else return err, + .line_info = nosuspend dwarf.getLineNumberInfo(compile_unit.*, relocated_address) catch |err| + if (std.meta.errorInSet(err, BaseError)) null else return err, + }; +} diff --git a/lib/std/dwarf.zig b/lib/std/dwarf.zig index 7df3a1bff6c7..d46fe0490d1e 100644 --- a/lib/std/dwarf.zig +++ b/lib/std/dwarf.zig @@ -5,7 +5,7 @@ // and substantial portions of the software. const std = @import("std.zig"); const builtin = @import("builtin"); -const debug = std.debug; +const SymbolMap = std.debug.SymbolMap; const fs = std.fs; const io = std.io; const mem = std.mem; @@ -16,6 +16,8 @@ const ArrayList = std.ArrayList; pub usingnamespace @import("dwarf_bits.zig"); +const Error = std.debug_info.BaseError; + const PcRange = struct { start: u64, end: u64, @@ -71,7 +73,7 @@ const Constant = struct { signed: bool, fn asUnsignedLe(self: *const Constant) !u64 { - if (self.signed) return error.InvalidDebugInfo; + if (self.signed) return Error.InvalidDebugInfo; return self.payload; } }; @@ -97,7 +99,7 @@ const Die = struct { const form_value = self.getAttr(id) orelse return error.MissingDebugInfo; return switch (form_value.*) { FormValue.Address => |value| value, - else => error.InvalidDebugInfo, + else => Error.InvalidDebugInfo, }; } @@ -106,7 +108,7 @@ const Die = struct { return switch (form_value.*) { FormValue.Const => |value| value.asUnsignedLe(), FormValue.SecOffset => |value| value, - else => error.InvalidDebugInfo, + else => Error.InvalidDebugInfo, }; } @@ -114,7 +116,7 @@ const Die = struct { const form_value = self.getAttr(id) orelse return error.MissingDebugInfo; return switch (form_value.*) { FormValue.Const => |value| value.asUnsignedLe(), - else => error.InvalidDebugInfo, + else => Error.InvalidDebugInfo, }; } @@ -122,7 +124,7 @@ const Die = struct { const form_value = self.getAttr(id) orelse return error.MissingDebugInfo; return switch (form_value.*) { FormValue.Ref => |value| value, - else => error.InvalidDebugInfo, + else => Error.InvalidDebugInfo, }; } @@ -131,7 +133,7 @@ const Die = struct { return switch (form_value.*) { FormValue.String => |value| value, FormValue.StrPtr => |offset| di.getString(offset), - else => error.InvalidDebugInfo, + else => Error.InvalidDebugInfo, }; } }; @@ -207,20 +209,20 @@ const LineNumberProgram = struct { }; } - pub fn checkLineMatch(self: *LineNumberProgram) !?debug.LineInfo { + pub fn checkLineMatch(self: *LineNumberProgram) !?SymbolMap.LineInfo { if (self.target_address >= self.prev_address and self.target_address < self.address) { const file_entry = if (self.prev_file == 0) { return error.MissingDebugInfo; } else if (self.prev_file - 1 >= self.file_entries.items.len) { - return error.InvalidDebugInfo; + return Error.InvalidDebugInfo; } else &self.file_entries.items[self.prev_file - 1]; const dir_name = if (file_entry.dir_index >= self.include_dirs.len) { - return error.InvalidDebugInfo; + return Error.InvalidDebugInfo; } else self.include_dirs[file_entry.dir_index]; const file_name = try fs.path.join(self.file_entries.allocator, &[_][]const u8{ dir_name, file_entry.file_name }); errdefer self.file_entries.allocator.free(file_name); - return debug.LineInfo{ + return SymbolMap.LineInfo{ .line = if (self.prev_line >= 0) @intCast(u64, self.prev_line) else 0, .column = self.prev_column, .file_name = file_name, @@ -245,7 +247,7 @@ fn readUnitLength(in_stream: anytype, endian: builtin.Endian, is_64: *bool) !u64 if (is_64.*) { return in_stream.readInt(u64, endian); } else { - if (first_32_bits >= 0xfffffff0) return error.InvalidDebugInfo; + if (first_32_bits >= 0xfffffff0) return Error.InvalidDebugInfo; // TODO this cast should not be needed return @as(u64, first_32_bits); } @@ -364,7 +366,7 @@ fn parseFormValue(allocator: *mem.Allocator, in_stream: anytype, form_id: u64, e defer allocator.destroy(frame); return await @asyncCall(frame, {}, parseFormValue, .{ allocator, in_stream, child_form_id, endian, is_64 }); }, - else => error.InvalidDebugInfo, + else => Error.InvalidDebugInfo, }; } @@ -419,12 +421,12 @@ pub const DwarfInfo = struct { const next_offset = unit_length + (if (is_64) @as(usize, 12) else @as(usize, 4)); const version = try in.readInt(u16, di.endian); - if (version < 2 or version > 5) return error.InvalidDebugInfo; + if (version < 2 or version > 5) return Error.InvalidDebugInfo; const debug_abbrev_offset = if (is_64) try in.readInt(u64, di.endian) else try in.readInt(u32, di.endian); const address_size = try in.readByte(); - if (address_size != @sizeOf(usize)) return error.InvalidDebugInfo; + if (address_size != @sizeOf(usize)) return Error.InvalidDebugInfo; const compile_unit_pos = try seekable.getPos(); const abbrev_table = try di.getAbbrevTable(debug_abbrev_offset); @@ -452,15 +454,15 @@ pub const DwarfInfo = struct { } else if (this_die_obj.getAttr(AT_abstract_origin)) |ref| { // Follow the DIE it points to and repeat const ref_offset = try this_die_obj.getAttrRef(AT_abstract_origin); - if (ref_offset > next_offset) return error.InvalidDebugInfo; + if (ref_offset > next_offset) return Error.InvalidDebugInfo; try seekable.seekTo(this_unit_offset + ref_offset); - this_die_obj = (try di.parseDie(in, abbrev_table, is_64)) orelse return error.InvalidDebugInfo; + this_die_obj = (try di.parseDie(in, abbrev_table, is_64)) orelse return Error.InvalidDebugInfo; } else if (this_die_obj.getAttr(AT_specification)) |ref| { // Follow the DIE it points to and repeat const ref_offset = try this_die_obj.getAttrRef(AT_specification); - if (ref_offset > next_offset) return error.InvalidDebugInfo; + if (ref_offset > next_offset) return Error.InvalidDebugInfo; try seekable.seekTo(this_unit_offset + ref_offset); - this_die_obj = (try di.parseDie(in, abbrev_table, is_64)) orelse return error.InvalidDebugInfo; + this_die_obj = (try di.parseDie(in, abbrev_table, is_64)) orelse return Error.InvalidDebugInfo; } else { break :x null; } @@ -478,7 +480,7 @@ pub const DwarfInfo = struct { const offset = try value.asUnsignedLe(); break :b (low_pc + offset); }, - else => return error.InvalidDebugInfo, + else => return Error.InvalidDebugInfo, }; break :x PcRange{ .start = low_pc, @@ -523,12 +525,12 @@ pub const DwarfInfo = struct { const next_offset = unit_length + (if (is_64) @as(usize, 12) else @as(usize, 4)); const version = try in.readInt(u16, di.endian); - if (version < 2 or version > 5) return error.InvalidDebugInfo; + if (version < 2 or version > 5) return Error.InvalidDebugInfo; const debug_abbrev_offset = if (is_64) try in.readInt(u64, di.endian) else try in.readInt(u32, di.endian); const address_size = try in.readByte(); - if (address_size != @sizeOf(usize)) return error.InvalidDebugInfo; + if (address_size != @sizeOf(usize)) return Error.InvalidDebugInfo; const compile_unit_pos = try seekable.getPos(); const abbrev_table = try di.getAbbrevTable(debug_abbrev_offset); @@ -536,9 +538,9 @@ pub const DwarfInfo = struct { try seekable.seekTo(compile_unit_pos); const compile_unit_die = try di.allocator().create(Die); - compile_unit_die.* = (try di.parseDie(in, abbrev_table, is_64)) orelse return error.InvalidDebugInfo; + compile_unit_die.* = (try di.parseDie(in, abbrev_table, is_64)) orelse return Error.InvalidDebugInfo; - if (compile_unit_die.tag_id != TAG_compile_unit) return error.InvalidDebugInfo; + if (compile_unit_die.tag_id != TAG_compile_unit) return Error.InvalidDebugInfo; const pc_range = x: { if (compile_unit_die.getAttrAddr(AT_low_pc)) |low_pc| { @@ -549,7 +551,7 @@ pub const DwarfInfo = struct { const offset = try value.asUnsignedLe(); break :b (low_pc + offset); }, - else => return error.InvalidDebugInfo, + else => return Error.InvalidDebugInfo, }; break :x PcRange{ .start = low_pc, @@ -670,7 +672,7 @@ pub const DwarfInfo = struct { fn parseDie(di: *DwarfInfo, in_stream: anytype, abbrev_table: *const AbbrevTable, is_64: bool) !?Die { const abbrev_code = try leb.readULEB128(u64, in_stream); if (abbrev_code == 0) return null; - const table_entry = getAbbrevTableEntry(abbrev_table, abbrev_code) orelse return error.InvalidDebugInfo; + const table_entry = getAbbrevTableEntry(abbrev_table, abbrev_code) orelse return Error.InvalidDebugInfo; var result = Die{ .tag_id = table_entry.tag_id, @@ -687,7 +689,7 @@ pub const DwarfInfo = struct { return result; } - pub fn getLineNumberInfo(di: *DwarfInfo, compile_unit: CompileUnit, target_address: usize) !debug.LineInfo { + pub fn getLineNumberInfo(di: *DwarfInfo, compile_unit: CompileUnit, target_address: usize) !SymbolMap.LineInfo { var stream = io.fixedBufferStream(di.debug_line); const in = &stream.reader(); const seekable = &stream.seekableStream(); @@ -705,13 +707,13 @@ pub const DwarfInfo = struct { const next_offset = unit_length + (if (is_64) @as(usize, 12) else @as(usize, 4)); const version = try in.readInt(u16, di.endian); - if (version < 2 or version > 4) return error.InvalidDebugInfo; + if (version < 2 or version > 4) return Error.InvalidDebugInfo; const prologue_length = if (is_64) try in.readInt(u64, di.endian) else try in.readInt(u32, di.endian); const prog_start_offset = (try seekable.getPos()) + prologue_length; const minimum_instruction_length = try in.readByte(); - if (minimum_instruction_length == 0) return error.InvalidDebugInfo; + if (minimum_instruction_length == 0) return Error.InvalidDebugInfo; if (version >= 4) { // maximum_operations_per_instruction @@ -722,7 +724,7 @@ pub const DwarfInfo = struct { const line_base = try in.readByteSigned(); const line_range = try in.readByte(); - if (line_range == 0) return error.InvalidDebugInfo; + if (line_range == 0) return Error.InvalidDebugInfo; const opcode_base = try in.readByte(); @@ -770,7 +772,7 @@ pub const DwarfInfo = struct { if (opcode == LNS_extended_op) { const op_size = try leb.readULEB128(u64, in); - if (op_size < 1) return error.InvalidDebugInfo; + if (op_size < 1) return Error.InvalidDebugInfo; var sub_op = try in.readByte(); switch (sub_op) { LNE_end_sequence => { @@ -795,7 +797,7 @@ pub const DwarfInfo = struct { }); }, else => { - const fwd_amt = math.cast(isize, op_size - 1) catch return error.InvalidDebugInfo; + const fwd_amt = math.cast(isize, op_size - 1) catch return Error.InvalidDebugInfo; try seekable.seekBy(fwd_amt); }, } @@ -846,7 +848,7 @@ pub const DwarfInfo = struct { }, LNS_set_prologue_end => {}, else => { - if (opcode - 1 >= standard_opcode_lengths.len) return error.InvalidDebugInfo; + if (opcode - 1 >= standard_opcode_lengths.len) return Error.InvalidDebugInfo; const len_bytes = standard_opcode_lengths[opcode - 1]; try seekable.seekBy(len_bytes); }, @@ -859,16 +861,16 @@ pub const DwarfInfo = struct { fn getString(di: *DwarfInfo, offset: u64) ![]const u8 { if (offset > di.debug_str.len) - return error.InvalidDebugInfo; + return Error.InvalidDebugInfo; const casted_offset = math.cast(usize, offset) catch - return error.InvalidDebugInfo; + return Error.InvalidDebugInfo; // Valid strings always have a terminating zero byte if (mem.indexOfScalarPos(u8, di.debug_str, casted_offset, 0)) |last| { return di.debug_str[casted_offset..last]; } - return error.InvalidDebugInfo; + return Error.InvalidDebugInfo; } }; diff --git a/lib/std/heap/general_purpose_allocator.zig b/lib/std/heap/general_purpose_allocator.zig index c731f22d66e2..c5fa8937beb9 100644 --- a/lib/std/heap/general_purpose_allocator.zig +++ b/lib/std/heap/general_purpose_allocator.zig @@ -99,6 +99,7 @@ const std = @import("std"); const log = std.log.scoped(.gpa); +const logDetectTTYConfig = std.log.detectTTYConfig; const math = std.math; const assert = std.debug.assert; const mem = std.mem; @@ -203,23 +204,27 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { const small_bucket_count = math.log2(page_size); const largest_bucket_object_size = 1 << (small_bucket_count - 1); + const FmtStackTrace = std.debug.FmtStackTrace; + + fn fmtStackTrace(trace: StackTrace) FmtStackTrace { + // TODO: If there is a way to determine if the log can be colorized, + // use that. + return std.debug.fmtStackTrace(trace, .no_color); + } + const LargeAlloc = struct { bytes: []u8, stack_addresses: [stack_n]usize, - fn dumpStackTrace(self: *LargeAlloc) void { - std.debug.dumpStackTrace(self.getStackTrace()); - } - - fn getStackTrace(self: *LargeAlloc) std.builtin.StackTrace { + fn getStackTrace(self: *LargeAlloc) FmtStackTrace { var len: usize = 0; while (len < stack_n and self.stack_addresses[len] != 0) { len += 1; } - return .{ + return fmtStackTrace(.{ .instruction_addresses = &self.stack_addresses, .index = len, - }; + }); } }; const LargeAllocTable = std.AutoHashMapUnmanaged(usize, LargeAlloc); @@ -254,6 +259,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { fn captureStackTrace( bucket: *BucketHeader, + self: *Self, ret_addr: usize, size_class: usize, slot_index: SlotIndex, @@ -262,7 +268,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { // Initialize them to 0. When determining the count we must look // for non zero addresses. const stack_addresses = bucket.stackTracePtr(size_class, slot_index, trace_kind); - collectStackTrace(ret_addr, stack_addresses); + self.collectStackTrace(ret_addr, stack_addresses); } }; @@ -271,16 +277,16 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { size_class: usize, slot_index: SlotIndex, trace_kind: TraceKind, - ) StackTrace { + ) FmtStackTrace { const stack_addresses = bucket.stackTracePtr(size_class, slot_index, trace_kind); var len: usize = 0; while (len < stack_n and stack_addresses[len] != 0) { len += 1; } - return StackTrace{ + return fmtStackTrace(.{ .instruction_addresses = stack_addresses, .index = len, - }; + }); } fn bucketStackFramesStart(size_class: usize) usize { @@ -317,7 +323,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { if (is_used) { const slot_index = @intCast(SlotIndex, used_bits_byte * 8 + bit_index); const stack_trace = bucketStackTrace(bucket, size_class, slot_index, .alloc); - log.err("Memory leak detected: {s}", .{stack_trace}); + log.err("Memory leak detected:\n{}", .{stack_trace}); leaks = true; } if (bit_index == math.maxInt(u3)) @@ -345,7 +351,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { } var it = self.large_allocations.iterator(); while (it.next()) |large_alloc| { - log.err("Memory leak detected: {s}", .{large_alloc.value.getStackTrace()}); + log.err("Memory leak detected:\n{}", .{large_alloc.value.getStackTrace()}); leaks = true; } return leaks; @@ -358,14 +364,39 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { return leaks; } - fn collectStackTrace(first_trace_addr: usize, addresses: *[stack_n]usize) void { + /// capture a stack trace: we wrap this function to pass in an allocator + fn captureStackTraceWrapper( + self: *Self, + first_trace_addr: ?usize, + stack_trace: *StackTrace, + ) void { + // Any allocator would work, but we can't pass in ourselves because + // that might cause unbounded recursion! + var allocator_state = std.heap.ArenaAllocator.init(self.backing_allocator); + defer allocator_state.deinit(); + std.debug.captureStackTrace( + &allocator_state.allocator, + first_trace_addr, + stack_trace, + ) catch |err| { + log.err("failed to capture stack trace: {s}", .{@errorName(err)}); + stack_trace.instruction_addresses = &[0]usize{}; + stack_trace.index = 0; + }; + } + + fn collectStackTrace( + self: *Self, + first_trace_addr: usize, + addresses: *[stack_n]usize, + ) void { if (stack_n == 0) return; mem.set(usize, addresses, 0); var stack_trace = StackTrace{ .instruction_addresses = addresses, .index = 0, }; - std.debug.captureStackTrace(first_trace_addr, &stack_trace); + self.captureStackTraceWrapper(first_trace_addr, &stack_trace); } fn allocSlot(self: *Self, size_class: usize, trace_addr: usize) Error![*]u8 { @@ -398,7 +429,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { const used_bit_index: u3 = @intCast(u3, slot_index % 8); // TODO cast should be unnecessary used_bits_byte.* |= (@as(u8, 1) << used_bit_index); bucket.used_count += 1; - bucket.captureStackTrace(trace_addr, size_class, slot_index, .alloc); + bucket.captureStackTrace(self, trace_addr, size_class, slot_index, .alloc); return bucket.page + slot_index * size_class; } @@ -445,12 +476,12 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { .instruction_addresses = &addresses, .index = 0, }; - std.debug.captureStackTrace(ret_addr, &free_stack_trace); - log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {s} Free: {s}", .{ + self.captureStackTraceWrapper(ret_addr, &free_stack_trace); + log.err("Allocation size {d} bytes does not match free size {d}. Allocation:\n{}\nFree:\n{}", .{ entry.value.bytes.len, old_mem.len, entry.value.getStackTrace(), - free_stack_trace, + fmtStackTrace(free_stack_trace), }); } @@ -471,7 +502,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { }); } entry.value.bytes = old_mem.ptr[0..result_len]; - collectStackTrace(ret_addr, &entry.value.stack_addresses); + self.collectStackTrace(ret_addr, &entry.value.stack_addresses); return result_len; } @@ -537,11 +568,11 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { .instruction_addresses = &addresses, .index = 0, }; - std.debug.captureStackTrace(ret_addr, &second_free_stack_trace); - log.err("Double free detected. Allocation: {s} First free: {s} Second free: {s}", .{ + self.captureStackTraceWrapper(ret_addr, &second_free_stack_trace); + log.err("Double free detected. Allocation:\n{}\nFirst free:\n{}\nSecond free:\n{}", .{ alloc_stack_trace, free_stack_trace, - second_free_stack_trace, + fmtStackTrace(second_free_stack_trace), }); if (new_size == 0) { // Recoverable. Restore self.total_requested_bytes if needed, as we @@ -558,7 +589,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { } if (new_size == 0) { // Capture stack trace to be the "first free", in case a double free happens. - bucket.captureStackTrace(ret_addr, size_class, slot_index, .free); + bucket.captureStackTrace(self, ret_addr, size_class, slot_index, .free); used_byte.* &= ~(@as(u8, 1) << used_bit_index); bucket.used_count -= 1; @@ -641,7 +672,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type { const gop = self.large_allocations.getOrPutAssumeCapacity(@ptrToInt(slice.ptr)); assert(!gop.found_existing); // This would mean the kernel double-mapped pages. gop.entry.value.bytes = slice; - collectStackTrace(ret_addr, &gop.entry.value.stack_addresses); + self.collectStackTrace(ret_addr, &gop.entry.value.stack_addresses); if (config.verbose_log) { log.info("large alloc {d} bytes at {*}", .{ slice.len, slice.ptr }); diff --git a/lib/std/log.zig b/lib/std/log.zig index 215e611bc1ea..937cab79b8a4 100644 --- a/lib/std/log.zig +++ b/lib/std/log.zig @@ -143,7 +143,7 @@ fn log( }; const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; const stderr = std.io.getStdErr().writer(); - const held = std.debug.getStderrMutex().acquire(); + const held = std.debug.getPrintMutex().acquire(); defer held.release(); nosuspend stderr.print(level_txt ++ prefix2 ++ format ++ "\n", args) catch return; } diff --git a/lib/std/meta.zig b/lib/std/meta.zig index cdc93e5d333c..0ba976340d7c 100644 --- a/lib/std/meta.zig +++ b/lib/std/meta.zig @@ -103,6 +103,115 @@ test "std.meta.stringToEnum" { testing.expect(null == stringToEnum(E1, "C")); } +pub fn errorInSet(err: anytype, comptime Set: type) bool { + const set_errs = comptime blk: { + const err_infos = @typeInfo(Set).ErrorSet orelse + return true; // global error set! + var errs: [err_infos.len]Set = undefined; + inline for (err_infos) |err_info, i| { + errs[i] = @field(Set, err_info.name); + } + + break :blk errs; + }; + + for (set_errs) |set_err| { + if (err == @errSetCast(anyerror, set_err)) { + return true; + } + } + + return false; +} + +test "std.meta.errorInSet" { + testing.expect(!errorInSet(error.A, error{})); + testing.expect(!errorInSet(error.B, error{A})); + testing.expect(errorInSet(error.A, error{A})); + testing.expect(!errorInSet(error.D, error{ A, B, C })); + testing.expect(errorInSet(error.B, error{ A, B, C })); + testing.expect(errorInSet(error.B, anyerror)); + testing.expect(errorInSet(error.ErrorName, anyerror)); + testing.expect(errorInSet(error.OtherError, anyerror)); +} + +/// lookupDecl but switching out the return statements. +/// Probably the cleanest way to implement this without infered return types. +fn lookupDeclType(comptime T: type, comptime names: []const []const u8) type { + comptime var Running = T; + inline for (names) |name, i| { + if (!@hasDecl(Running, name)) { + return type; + } + + const next = @field(Running, name); + if (i == names.len - 1) { + return @TypeOf(next); + } + if (@TypeOf(next) != type) { + @compileError("Cannot lookup on decl which " ++ + "isn't a type, perhaps this was supposed to be a " ++ + "type (struct)?"); + } + + Running = next; + } + comptime std.debug.assert(names.len == 0); + return type; +} + +/// Get a decl on T by indexing into each field. Returns null if one of the +/// sub-decls doesn't exist, `compileError` if a sub-decl other than the last one +/// is a value instead of a type. +pub fn lookupDecl(comptime T: type, comptime names: []const []const u8) ?lookupDeclType(T, names) { + // any edits to this function should be copied to the above function :( + comptime var Running = T; + inline for (names) |name, i| { + if (!@hasDecl(Running, name)) { + return null; + } + + const next = @field(Running, name); + if (i == names.len - 1) { + return next; + } + Running = next; + } + comptime std.debug.assert(names.len == 0); + return T; +} + +test "std.meta.lookupDecl" { + const empty = struct {}; + + // we flip the order of "expectEqual" because we need casting to go + // the opposite way + + testing.expectEqual(lookupDecl(empty, &.{}), empty); + testing.expectEqual(lookupDecl(empty, &.{"a"}), null); + testing.expectEqual(lookupDecl(empty, &.{ "a", "b", "c" }), null); + + const has_decls = struct { + const a = struct { + const b = struct { + pub fn c() void {} + }; + }; + }; + + testing.expectEqual(lookupDecl(has_decls, &.{}), has_decls); + testing.expectEqual(lookupDecl(has_decls, &.{"a"}), has_decls.a); + testing.expectEqual(lookupDecl(has_decls, &.{ "a", "b" }), has_decls.a.b); + testing.expectEqual(lookupDecl(has_decls, &.{ "a", "b", "c" }), has_decls.a.b.c); + testing.expectEqual( + @TypeOf(lookupDecl(has_decls, &.{ "a", "b", "c" })), + ?@TypeOf(has_decls.a.b.c), + ); + testing.expectEqual(lookupDecl(has_decls, &.{"b"}), null); + testing.expectEqual(lookupDecl(has_decls, &.{ "a", "b", "d" }), null); + testing.expectEqual(lookupDecl(has_decls, &.{ "a", "b", "d" }) orelse 8, 8); +} + pub fn bitCount(comptime T: type) comptime_int { return switch (@typeInfo(T)) { .Bool => 1, @@ -975,7 +1084,7 @@ fn castPtr(comptime DestType: type, target: anytype) DestType { } fn ptrInfo(comptime PtrType: type) TypeInfo.Pointer { - return switch(@typeInfo(PtrType)){ + return switch (@typeInfo(PtrType)) { .Optional => |opt_info| @typeInfo(opt_info.child).Pointer, .Pointer => |ptr_info| ptr_info, else => unreachable, diff --git a/lib/std/pdb.zig b/lib/std/pdb.zig index 6a47cd6e8b03..a35b4d7d3613 100644 --- a/lib/std/pdb.zig +++ b/lib/std/pdb.zig @@ -16,6 +16,8 @@ const File = std.fs.File; const ArrayList = std.ArrayList; +const BaseError = std.debug_info.BaseError; + // Note: most of this is based on information gathered from LLVM source code, // documentation and/or contributors. @@ -506,15 +508,15 @@ const Msf = struct { // Sanity checks if (!mem.eql(u8, &superblock.FileMagic, SuperBlock.file_magic)) - return error.InvalidDebugInfo; + return BaseError.InvalidDebugInfo; if (superblock.FreeBlockMapBlock != 1 and superblock.FreeBlockMapBlock != 2) - return error.InvalidDebugInfo; + return BaseError.InvalidDebugInfo; if (superblock.NumBlocks * superblock.BlockSize != try file.getEndPos()) - return error.InvalidDebugInfo; + return BaseError.InvalidDebugInfo; switch (superblock.BlockSize) { // llvm only supports 4096 but we can handle any of these values 512, 1024, 2048, 4096 => {}, - else => return error.InvalidDebugInfo, + else => return BaseError.InvalidDebugInfo, } const dir_block_count = blockCountFromSize(superblock.NumDirectoryBytes, superblock.BlockSize); diff --git a/lib/std/std.zig b/lib/std/std.zig index 82249af1572d..5e895a98459d 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -55,6 +55,7 @@ pub const compress = @import("compress.zig"); pub const crypto = @import("crypto.zig"); pub const cstr = @import("cstr.zig"); pub const debug = @import("debug.zig"); +pub const debug_info = @import("debug_info.zig"); pub const dwarf = @import("dwarf.zig"); pub const elf = @import("elf.zig"); pub const enums = @import("enums.zig"); diff --git a/lib/std/symbol_map_darwin.zig b/lib/std/symbol_map_darwin.zig new file mode 100644 index 000000000000..c4b6d845a537 --- /dev/null +++ b/lib/std/symbol_map_darwin.zig @@ -0,0 +1,330 @@ +const std = @import("std.zig"); +const SymbolInfo = std.debug.SymbolMap.SymbolInfo; +const assert = std.debug.assert; +const debug_info = std.debug_info; +const BaseError = debug_info.BaseError; +const chopSlice = debug_info.chopSlice; +const mem = std.mem; +const macho = std.macho; +const fs = std.fs; +const File = std.fs.File; +const DW = std.dwarf; + +const SymbolMapState = debug_info.SymbolMapStateFromModuleInfo(Module); +pub const init = SymbolMapState.init; + +const Module = struct { + const Self = @This(); + + base_address: usize, + mapped_memory: []const u8, + symbols: []const MachoSymbol, + strings: [:0]const u8, + ofiles: OFileTable, + + const MachoSymbol = struct { + nlist: *const macho.nlist_64, + ofile: ?*const macho.nlist_64, + reloc: u64, + + /// Returns the address from the macho file + fn address(self: MachoSymbol) u64 { + return self.nlist.n_value; + } + + fn addressLessThan(context: void, lhs: MachoSymbol, rhs: MachoSymbol) bool { + return lhs.address() < rhs.address(); + } + }; + + const OFileTable = std.StringHashMap(DW.DwarfInfo); + + pub fn lookup(allocator: *mem.Allocator, address_map: *SymbolMapState.AddressMap, address: usize) !*Self { + const image_count = std.c._dyld_image_count(); + + var i: u32 = 0; + while (i < image_count) : (i += 1) { + const base_address = std.c._dyld_get_image_vmaddr_slide(i); + + if (address < base_address) continue; + + const header = std.c._dyld_get_image_header(i) orelse continue; + // The array of load commands is right after the header + var cmd_ptr = @intToPtr([*]u8, @ptrToInt(header) + @sizeOf(macho.mach_header_64)); + + var cmds = header.ncmds; + while (cmds != 0) : (cmds -= 1) { + const lc = @ptrCast( + *macho.load_command, + @alignCast(@alignOf(macho.load_command), cmd_ptr), + ); + cmd_ptr += lc.cmdsize; + if (lc.cmd != macho.LC_SEGMENT_64) continue; + + const segment_cmd = @ptrCast( + *const std.macho.segment_command_64, + @alignCast(@alignOf(std.macho.segment_command_64), lc), + ); + + const rebased_address = address - base_address; + const seg_start = segment_cmd.vmaddr; + const seg_end = seg_start + segment_cmd.vmsize; + + if (rebased_address >= seg_start and rebased_address < seg_end) { + if (address_map.get(base_address)) |obj_di| { + return obj_di; + } + + const obj_di = try allocator.create(Self); + errdefer allocator.destroy(obj_di); + + const macho_path = mem.spanZ(std.c._dyld_get_image_name(i)); + const macho_file = fs.cwd().openFile(macho_path, .{ .intended_io_mode = .blocking }) catch |err| switch (err) { + error.FileNotFound => return BaseError.MissingDebugInfo, + else => return err, + }; + obj_di.* = try readMachODebugInfo(allocator, macho_file); + obj_di.base_address = base_address; + + try address_map.putNoClobber(base_address, obj_di); + + return obj_di; + } + } + } + + return BaseError.MissingDebugInfo; + } + + /// TODO resources https://github.com/ziglang/zig/issues/4353 + /// This takes ownership of coff_file: users of this function should not close + /// it themselves, even on error. + /// TODO it's weird to take ownership even on error, rework this code. + fn readMachODebugInfo(allocator: *mem.Allocator, macho_file: File) !Self { + const mapped_mem = try debug_info.mapWholeFile(macho_file); + + const hdr = @ptrCast( + *const macho.mach_header_64, + @alignCast(@alignOf(macho.mach_header_64), mapped_mem.ptr), + ); + if (hdr.magic != macho.MH_MAGIC_64) + return BaseError.InvalidDebugInfo; + + const hdr_base = @ptrCast([*]const u8, hdr); + var ptr = hdr_base + @sizeOf(macho.mach_header_64); + var ncmd: u32 = hdr.ncmds; + const symtab = while (ncmd != 0) : (ncmd -= 1) { + const lc = @ptrCast(*const std.macho.load_command, ptr); + switch (lc.cmd) { + std.macho.LC_SYMTAB => break @ptrCast(*const std.macho.symtab_command, ptr), + else => {}, + } + ptr = @alignCast(@alignOf(std.macho.load_command), ptr + lc.cmdsize); + } else { + return BaseError.MissingDebugInfo; + }; + const syms = @ptrCast([*]const macho.nlist_64, @alignCast(@alignOf(macho.nlist_64), hdr_base + symtab.symoff))[0..symtab.nsyms]; + const strings = @ptrCast([*]const u8, hdr_base + symtab.stroff)[0 .. symtab.strsize - 1 :0]; + + const symbols_buf = try allocator.alloc(MachoSymbol, syms.len); + + var ofile: ?*const macho.nlist_64 = null; + var reloc: u64 = 0; + var symbol_index: usize = 0; + var last_len: u64 = 0; + for (syms) |*sym| { + if (sym.n_type & std.macho.N_STAB != 0) { + switch (sym.n_type) { + std.macho.N_OSO => { + ofile = sym; + reloc = 0; + }, + std.macho.N_FUN => { + if (sym.n_sect == 0) { + last_len = sym.n_value; + } else { + symbols_buf[symbol_index] = MachoSymbol{ + .nlist = sym, + .ofile = ofile, + .reloc = reloc, + }; + symbol_index += 1; + } + }, + std.macho.N_BNSYM => { + if (reloc == 0) { + reloc = sym.n_value; + } + }, + else => continue, + } + } + } + const sentinel = try allocator.create(macho.nlist_64); + sentinel.* = macho.nlist_64{ + .n_strx = 0, + .n_type = 36, + .n_sect = 0, + .n_desc = 0, + .n_value = symbols_buf[symbol_index - 1].nlist.n_value + last_len, + }; + + const symbols = allocator.shrink(symbols_buf, symbol_index); + + // Even though lld emits symbols in ascending order, this debug code + // should work for programs linked in any valid way. + // This sort is so that we can binary search later. + std.sort.sort(MachoSymbol, symbols, {}, MachoSymbol.addressLessThan); + + return Self{ + .base_address = undefined, + .mapped_memory = mapped_mem, + .ofiles = OFileTable.init(allocator), + .symbols = symbols, + .strings = strings, + }; + } + + pub fn addressToSymbol(self: *Self, address: usize) !SymbolInfo { + nosuspend { + // Translate the VA into an address into this object + const relocated_address = address - self.base_address; + assert(relocated_address >= 0x100000000); + + // Find the .o file where this symbol is defined + const symbol = machoSearchSymbols(self.symbols, relocated_address) orelse + return SymbolInfo{}; + + // Take the symbol name from the N_FUN STAB entry, we're going to + // use it if we fail to find the DWARF infos + const stab_symbol = mem.spanZ(self.strings[symbol.nlist.n_strx..]); + + if (symbol.ofile == null) + return SymbolInfo{ .symbol_name = stab_symbol }; + + const o_file_path = mem.spanZ(self.strings[symbol.ofile.?.n_strx..]); + + // Check if its debug infos are already in the cache + var o_file_di = self.ofiles.get(o_file_path) orelse + (self.loadOFile(o_file_path) catch |err| + return if (std.meta.errorInSet(err, debug_info.BaseError || error{FileNotFound})) + SymbolInfo{ .symbol_name = stab_symbol } + else + err); + + // Translate again the address, this time into an address inside the + // .o file + const relocated_address_o = relocated_address - symbol.reloc; + + return debug_info.dwarfAddressToSymbolInfo(&o_file_di, relocated_address); + } + } + + fn machoSearchSymbols(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol { + var min: usize = 0; + var max: usize = symbols.len - 1; // Exclude sentinel. + while (min < max) { + const mid = min + (max - min) / 2; + const curr = &symbols[mid]; + const next = &symbols[mid + 1]; + if (address >= next.address()) { + min = mid + 1; + } else if (address < curr.address()) { + max = mid; + } else { + return curr; + } + } + return null; + } + + fn loadOFile(self: *Self, o_file_path: []const u8) !DW.DwarfInfo { + const o_file = try fs.cwd().openFile(o_file_path, .{ .intended_io_mode = .blocking }); + const mapped_mem = try debug_info.mapWholeFile(o_file); + + const hdr = @ptrCast( + *const macho.mach_header_64, + @alignCast(@alignOf(macho.mach_header_64), mapped_mem.ptr), + ); + if (hdr.magic != std.macho.MH_MAGIC_64) + return BaseError.InvalidDebugInfo; + + const hdr_base = @ptrCast([*]const u8, hdr); + var ptr = hdr_base + @sizeOf(macho.mach_header_64); + var ncmd: u32 = hdr.ncmds; + const segcmd = while (ncmd != 0) : (ncmd -= 1) { + const lc = @ptrCast(*const std.macho.load_command, ptr); + switch (lc.cmd) { + std.macho.LC_SEGMENT_64 => { + break @ptrCast( + *const std.macho.segment_command_64, + @alignCast(@alignOf(std.macho.segment_command_64), ptr), + ); + }, + else => {}, + } + ptr = @alignCast(@alignOf(std.macho.load_command), ptr + lc.cmdsize); + } else { + return BaseError.MissingDebugInfo; + }; + + var opt_debug_line: ?*const macho.section_64 = null; + var opt_debug_info: ?*const macho.section_64 = null; + var opt_debug_abbrev: ?*const macho.section_64 = null; + var opt_debug_str: ?*const macho.section_64 = null; + var opt_debug_ranges: ?*const macho.section_64 = null; + + const sections = @ptrCast( + [*]const macho.section_64, + @alignCast(@alignOf(macho.section_64), ptr + @sizeOf(std.macho.segment_command_64)), + )[0..segcmd.nsects]; + for (sections) |*sect| { + // The section name may not exceed 16 chars and a trailing null may + // not be present + const name = if (mem.indexOfScalar(u8, sect.sectname[0..], 0)) |last| + sect.sectname[0..last] + else + sect.sectname[0..]; + + if (mem.eql(u8, name, "__debug_line")) { + opt_debug_line = sect; + } else if (mem.eql(u8, name, "__debug_info")) { + opt_debug_info = sect; + } else if (mem.eql(u8, name, "__debug_abbrev")) { + opt_debug_abbrev = sect; + } else if (mem.eql(u8, name, "__debug_str")) { + opt_debug_str = sect; + } else if (mem.eql(u8, name, "__debug_ranges")) { + opt_debug_ranges = sect; + } + } + + const debug_line = opt_debug_line orelse + return BaseError.MissingDebugInfo; + const debug_info_v = opt_debug_info orelse + return BaseError.MissingDebugInfo; + const debug_str = opt_debug_str orelse + return BaseError.MissingDebugInfo; + const debug_abbrev = opt_debug_abbrev orelse + return BaseError.MissingDebugInfo; + + var di = DW.DwarfInfo{ + .endian = .Little, + .debug_info = try chopSlice(mapped_mem, debug_info_v.offset, debug_info_v.size), + .debug_abbrev = try chopSlice(mapped_mem, debug_abbrev.offset, debug_abbrev.size), + .debug_str = try chopSlice(mapped_mem, debug_str.offset, debug_str.size), + .debug_line = try chopSlice(mapped_mem, debug_line.offset, debug_line.size), + .debug_ranges = if (opt_debug_ranges) |debug_ranges| + try chopSlice(mapped_mem, debug_ranges.offset, debug_ranges.size) + else + null, + }; + + try DW.openDwarfDebugInfo(&di, self.ofiles.allocator); + + // Add the debug info to the cache + try self.ofiles.putNoClobber(o_file_path, di); + + return di; + } +}; diff --git a/lib/std/symbol_map_pdb.zig b/lib/std/symbol_map_pdb.zig new file mode 100644 index 000000000000..540c216eb9c4 --- /dev/null +++ b/lib/std/symbol_map_pdb.zig @@ -0,0 +1,501 @@ +const std = @import("std.zig"); +const builtin = std.builtin; +const assert = std.debug.assert; +const SymbolInfo = std.debug.SymbolMap.SymbolInfo; +const LineInfo = std.debug.SymbolMap.LineInfo; +const debug_info = std.debug_info; +const BaseError = debug_info.BaseError; +const mem = std.mem; +const math = std.math; +const fs = std.fs; +const File = std.fs.File; +const pdb = std.pdb; +const coff = std.coff; +const windows = std.os.windows; +const ArrayList = std.ArrayList; + +const SymbolMapState = debug_info.SymbolMapStateFromModuleInfo(Module); +pub const init = SymbolMapState.init; + +const Module = struct { + const Self = @This(); + + base_address: usize, + pdb: pdb.Pdb, + coff: *coff.Coff, + sect_contribs: []pdb.SectionContribEntry, + modules: []PDBModule, + + const PDBModule = struct { + mod_info: pdb.ModInfo, + module_name: []u8, + obj_file_name: []u8, + + populated: bool, + symbols: []u8, + subsect_info: []u8, + checksum_offset: ?usize, + }; + + pub fn lookup(allocator: *mem.Allocator, address_map: *SymbolMapState.AddressMap, address: usize) !*Self { + if (builtin.os.tag != .windows) { + // TODO: implement uefi case + return BaseError.UnsupportedOperatingSystem; + } + + return lookupModuleWin32(allocator, address_map, address); + } + + fn lookupModuleWin32(allocator: *mem.Allocator, address_map: *SymbolMapState.AddressMap, address: usize) !*Self { + const process_handle = windows.kernel32.GetCurrentProcess(); + + // Find how many modules are actually loaded + var dummy: windows.HMODULE = undefined; + var bytes_needed: windows.DWORD = undefined; + if (windows.kernel32.K32EnumProcessModules( + process_handle, + @ptrCast([*]windows.HMODULE, &dummy), + 0, + &bytes_needed, + ) == 0) + return BaseError.MissingDebugInfo; + + const needed_modules = bytes_needed / @sizeOf(windows.HMODULE); + + // Fetch the complete module list + var modules = try allocator.alloc(windows.HMODULE, needed_modules); + defer allocator.free(modules); + if (windows.kernel32.K32EnumProcessModules( + process_handle, + modules.ptr, + try math.cast(windows.DWORD, modules.len * @sizeOf(windows.HMODULE)), + &bytes_needed, + ) == 0) + return BaseError.MissingDebugInfo; + + // There's an unavoidable TOCTOU problem here, the module list may have + // changed between the two EnumProcessModules call. + // Pick the smallest amount of elements to avoid processing garbage. + const needed_modules_after = bytes_needed / @sizeOf(windows.HMODULE); + const loaded_modules = math.min(needed_modules, needed_modules_after); + + for (modules[0..loaded_modules]) |module| { + var info: windows.MODULEINFO = undefined; + if (windows.kernel32.K32GetModuleInformation( + process_handle, + module, + &info, + @sizeOf(@TypeOf(info)), + ) == 0) + return BaseError.MissingDebugInfo; + + const seg_start = @ptrToInt(info.lpBaseOfDll); + const seg_end = seg_start + info.SizeOfImage; + + if (address >= seg_start and address < seg_end) { + if (address_map.get(seg_start)) |obj_di| { + return obj_di; + } + + var name_buffer: [windows.PATH_MAX_WIDE + 4:0]u16 = undefined; + // openFileAbsoluteW requires the prefix to be present + mem.copy(u16, name_buffer[0..4], &[_]u16{ '\\', '?', '?', '\\' }); + const len = windows.kernel32.K32GetModuleFileNameExW( + process_handle, + module, + @ptrCast(windows.LPWSTR, &name_buffer[4]), + windows.PATH_MAX_WIDE, + ); + assert(len > 0); + + const obj_di = try allocator.create(Self); + errdefer allocator.destroy(obj_di); + + const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) { + error.FileNotFound => return BaseError.MissingDebugInfo, + else => return err, + }; + obj_di.* = try readCoffDebugInfo(allocator, coff_file); + obj_di.base_address = seg_start; + + try address_map.putNoClobber(seg_start, obj_di); + + return obj_di; + } + } + + return BaseError.MissingDebugInfo; + } + + /// This takes ownership of coff_file: users of this function should not close + /// it themselves, even on error. + /// TODO resources https://github.com/ziglang/zig/issues/4353 + /// TODO it's weird to take ownership even on error, rework this code. + fn readCoffDebugInfo(allocator: *mem.Allocator, coff_file: File) !Self { + nosuspend { + errdefer coff_file.close(); + + const coff_obj = try allocator.create(coff.Coff); + coff_obj.* = coff.Coff.init(allocator, coff_file); + + var di = Self{ + .base_address = undefined, + .coff = coff_obj, + .pdb = undefined, + .sect_contribs = undefined, + .modules = undefined, + }; + + try di.coff.loadHeader(); + + var path_buf: [windows.MAX_PATH]u8 = undefined; + const len = try di.coff.getPdbPath(path_buf[0..]); + const raw_path = path_buf[0..len]; + + const path = try fs.path.resolve(allocator, &[_][]const u8{raw_path}); + + try di.pdb.openFile(di.coff, path); + + var pdb_stream = di.pdb.getStream(pdb.StreamType.Pdb) orelse return BaseError.InvalidDebugInfo; + const version = try pdb_stream.reader().readIntLittle(u32); + const signature = try pdb_stream.reader().readIntLittle(u32); + const age = try pdb_stream.reader().readIntLittle(u32); + var guid: [16]u8 = undefined; + try pdb_stream.reader().readNoEof(&guid); + if (version != 20000404) // VC70, only value observed by LLVM team + return error.UnknownPDBVersion; + if (!mem.eql(u8, &di.coff.guid, &guid) or di.coff.age != age) + return error.PDBMismatch; + // We validated the executable and pdb match. + + const string_table_index = str_tab_index: { + const name_bytes_len = try pdb_stream.reader().readIntLittle(u32); + const name_bytes = try allocator.alloc(u8, name_bytes_len); + try pdb_stream.reader().readNoEof(name_bytes); + + const HashTableHeader = packed struct { + Size: u32, + Capacity: u32, + + fn maxLoad(cap: u32) u32 { + return cap * 2 / 3 + 1; + } + }; + const hash_tbl_hdr = try pdb_stream.reader().readStruct(HashTableHeader); + if (hash_tbl_hdr.Capacity == 0) + return BaseError.InvalidDebugInfo; + + if (hash_tbl_hdr.Size > HashTableHeader.maxLoad(hash_tbl_hdr.Capacity)) + return BaseError.InvalidDebugInfo; + + const present = try readSparseBitVector(&pdb_stream.reader(), allocator); + if (present.len != hash_tbl_hdr.Size) + return BaseError.InvalidDebugInfo; + const deleted = try readSparseBitVector(&pdb_stream.reader(), allocator); + + const Bucket = struct { + first: u32, + second: u32, + }; + const bucket_list = try allocator.alloc(Bucket, present.len); + for (present) |_| { + const name_offset = try pdb_stream.reader().readIntLittle(u32); + const name_index = try pdb_stream.reader().readIntLittle(u32); + const name = mem.spanZ(std.meta.assumeSentinel(name_bytes.ptr + name_offset, 0)); + if (mem.eql(u8, name, "/names")) { + break :str_tab_index name_index; + } + } + return BaseError.MissingDebugInfo; + }; + + di.pdb.string_table = di.pdb.getStreamById(string_table_index) orelse return BaseError.MissingDebugInfo; + di.pdb.dbi = di.pdb.getStream(pdb.StreamType.Dbi) orelse return BaseError.MissingDebugInfo; + + const dbi = di.pdb.dbi; + + // Dbi Header + const dbi_stream_header = try dbi.reader().readStruct(pdb.DbiStreamHeader); + if (dbi_stream_header.VersionHeader != 19990903) // V70, only value observed by LLVM team + return error.UnknownPDBVersion; + if (dbi_stream_header.Age != age) + return error.UnmatchingPDB; + + const mod_info_size = dbi_stream_header.ModInfoSize; + const section_contrib_size = dbi_stream_header.SectionContributionSize; + + var modules = ArrayList(PDBModule).init(allocator); + + // Module Info Substream + var mod_info_offset: usize = 0; + while (mod_info_offset != mod_info_size) { + const mod_info = try dbi.reader().readStruct(pdb.ModInfo); + var this_record_len: usize = @sizeOf(pdb.ModInfo); + + const module_name = try dbi.readNullTermString(allocator); + this_record_len += module_name.len + 1; + + const obj_file_name = try dbi.readNullTermString(allocator); + this_record_len += obj_file_name.len + 1; + + if (this_record_len % 4 != 0) { + const round_to_next_4 = (this_record_len | 0x3) + 1; + const march_forward_bytes = round_to_next_4 - this_record_len; + try dbi.seekBy(@intCast(isize, march_forward_bytes)); + this_record_len += march_forward_bytes; + } + + try modules.append(.{ + .mod_info = mod_info, + .module_name = module_name, + .obj_file_name = obj_file_name, + + .populated = false, + .symbols = undefined, + .subsect_info = undefined, + .checksum_offset = null, + }); + + mod_info_offset += this_record_len; + if (mod_info_offset > mod_info_size) + return BaseError.InvalidDebugInfo; + } + + di.modules = modules.toOwnedSlice(); + + // Section Contribution Substream + var sect_contribs = ArrayList(pdb.SectionContribEntry).init(allocator); + var sect_cont_offset: usize = 0; + if (section_contrib_size != 0) { + const ver = @intToEnum(pdb.SectionContrSubstreamVersion, try dbi.reader().readIntLittle(u32)); + if (ver != pdb.SectionContrSubstreamVersion.Ver60) + return BaseError.InvalidDebugInfo; + sect_cont_offset += @sizeOf(u32); + } + while (sect_cont_offset != section_contrib_size) { + const entry = try sect_contribs.addOne(); + entry.* = try dbi.reader().readStruct(pdb.SectionContribEntry); + sect_cont_offset += @sizeOf(pdb.SectionContribEntry); + + if (sect_cont_offset > section_contrib_size) + return BaseError.InvalidDebugInfo; + } + + di.sect_contribs = sect_contribs.toOwnedSlice(); + + return di; + } + } + + fn readSparseBitVector(reader: anytype, allocator: *mem.Allocator) ![]usize { + const num_words = try reader.readIntLittle(u32); + var word_i: usize = 0; + var list = ArrayList(usize).init(allocator); + while (word_i != num_words) : (word_i += 1) { + const word = try reader.readIntLittle(u32); + var bit_i: u5 = 0; + while (true) : (bit_i += 1) { + if (word & (@as(u32, 1) << bit_i) != 0) { + try list.append(word_i * 32 + bit_i); + } + if (bit_i == math.maxInt(u5)) break; + } + } + return list.toOwnedSlice(); + } + + pub fn addressToSymbol(self: *Self, address: usize) !SymbolInfo { + // Translate the VA into an address into this object + const relocated_address = address - self.base_address; + + var coff_section: *coff.Section = undefined; + const mod_index = for (self.sect_contribs) |sect_contrib| { + if (sect_contrib.Section > self.coff.sections.items.len) continue; + // Remember that SectionContribEntry.Section is 1-based. + coff_section = &self.coff.sections.items[sect_contrib.Section - 1]; + + const vaddr_start = coff_section.header.virtual_address + sect_contrib.Offset; + const vaddr_end = vaddr_start + sect_contrib.Size; + if (relocated_address >= vaddr_start and relocated_address < vaddr_end) { + break sect_contrib.ModuleIndex; + } + } else { + // we have no information to add to the address + return SymbolInfo{}; + }; + + const allocator = self.coff.allocator; + + const mod = &self.modules[mod_index]; + try populateModule(self, allocator, mod); + const obj_basename = fs.path.basename(mod.obj_file_name); + + var symbol_i: usize = 0; + const symbol_name = if (!mod.populated) "???" else while (symbol_i != mod.symbols.len) { + const prefix = @ptrCast(*pdb.RecordPrefix, &mod.symbols[symbol_i]); + if (prefix.RecordLen < 2) + return BaseError.InvalidDebugInfo; + switch (prefix.RecordKind) { + .S_LPROC32, .S_GPROC32 => { + const proc_sym = @ptrCast(*pdb.ProcSym, &mod.symbols[symbol_i + @sizeOf(pdb.RecordPrefix)]); + const vaddr_start = coff_section.header.virtual_address + proc_sym.CodeOffset; + const vaddr_end = vaddr_start + proc_sym.CodeSize; + if (relocated_address >= vaddr_start and relocated_address < vaddr_end) { + break mem.spanZ(@ptrCast([*:0]u8, proc_sym) + @sizeOf(pdb.ProcSym)); + } + }, + else => {}, + } + symbol_i += prefix.RecordLen + @sizeOf(u16); + if (symbol_i > mod.symbols.len) + return BaseError.InvalidDebugInfo; + } else "???"; + + const subsect_info = mod.subsect_info; + + var sect_offset: usize = 0; + var skip_len: usize = undefined; + const opt_line_info = subsections: { + const checksum_offset = mod.checksum_offset orelse break :subsections null; + while (sect_offset != subsect_info.len) : (sect_offset += skip_len) { + const subsect_hdr = @ptrCast(*pdb.DebugSubsectionHeader, &subsect_info[sect_offset]); + skip_len = subsect_hdr.Length; + sect_offset += @sizeOf(pdb.DebugSubsectionHeader); + + switch (subsect_hdr.Kind) { + .Lines => { + var line_index = sect_offset; + + const line_hdr = @ptrCast(*pdb.LineFragmentHeader, &subsect_info[line_index]); + if (line_hdr.RelocSegment == 0) + return BaseError.MissingDebugInfo; + line_index += @sizeOf(pdb.LineFragmentHeader); + const frag_vaddr_start = coff_section.header.virtual_address + line_hdr.RelocOffset; + const frag_vaddr_end = frag_vaddr_start + line_hdr.CodeSize; + + if (relocated_address >= frag_vaddr_start and relocated_address < frag_vaddr_end) { + // There is an unknown number of LineBlockFragmentHeaders (and their accompanying line and column records) + // from now on. We will iterate through them, and eventually find a LineInfo that we're interested in, + // breaking out to :subsections. If not, we will make sure to not read anything outside of this subsection. + const subsection_end_index = sect_offset + subsect_hdr.Length; + + while (line_index < subsection_end_index) { + const block_hdr = @ptrCast(*pdb.LineBlockFragmentHeader, &subsect_info[line_index]); + line_index += @sizeOf(pdb.LineBlockFragmentHeader); + const start_line_index = line_index; + + const has_column = line_hdr.Flags.LF_HaveColumns; + + // All line entries are stored inside their line block by ascending start address. + // Heuristic: we want to find the last line entry + // that has a vaddr_start <= relocated_address. + // This is done with a simple linear search. + var line_i: u32 = 0; + while (line_i < block_hdr.NumLines) : (line_i += 1) { + const line_num_entry = @ptrCast(*pdb.LineNumberEntry, &subsect_info[line_index]); + line_index += @sizeOf(pdb.LineNumberEntry); + + const vaddr_start = frag_vaddr_start + line_num_entry.Offset; + if (relocated_address < vaddr_start) { + break; + } + } + + // line_i == 0 would mean that no matching LineNumberEntry was found. + if (line_i > 0) { + const subsect_index = checksum_offset + block_hdr.NameIndex; + const chksum_hdr = @ptrCast(*pdb.FileChecksumEntryHeader, &mod.subsect_info[subsect_index]); + const strtab_offset = @sizeOf(pdb.PDBStringTableHeader) + chksum_hdr.FileNameOffset; + try self.pdb.string_table.seekTo(strtab_offset); + const source_file_name = try self.pdb.string_table.readNullTermString(allocator); + + const line_entry_idx = line_i - 1; + + const column = if (has_column) blk: { + const start_col_index = start_line_index + @sizeOf(pdb.LineNumberEntry) * block_hdr.NumLines; + const col_index = start_col_index + @sizeOf(pdb.ColumnNumberEntry) * line_entry_idx; + const col_num_entry = @ptrCast(*pdb.ColumnNumberEntry, &subsect_info[col_index]); + break :blk col_num_entry.StartColumn; + } else 0; + + const found_line_index = start_line_index + line_entry_idx * @sizeOf(pdb.LineNumberEntry); + const line_num_entry = @ptrCast(*pdb.LineNumberEntry, &subsect_info[found_line_index]); + const flags = @ptrCast(*pdb.LineNumberEntry.Flags, &line_num_entry.Flags); + + break :subsections LineInfo{ + .allocator = allocator, + .file_name = source_file_name, + .line = flags.Start, + .column = column, + }; + } + } + + // Checking that we are not reading garbage after the (possibly) multiple block fragments. + if (line_index != subsection_end_index) { + return BaseError.InvalidDebugInfo; + } + } + }, + else => {}, + } + + if (sect_offset > subsect_info.len) + return BaseError.InvalidDebugInfo; + } else { + break :subsections null; + } + }; + + return SymbolInfo{ + .symbol_name = symbol_name, + .compile_unit_name = obj_basename, + .line_info = opt_line_info, + }; + } + + /// TODO resources https://github.com/ziglang/zig/issues/4353 + fn populateModule(di: *Self, allocator: *mem.Allocator, mod: *PDBModule) !void { + if (mod.populated) + return; + // At most one can be non-zero. + if (mod.mod_info.C11ByteSize != 0 and mod.mod_info.C13ByteSize != 0) + return BaseError.InvalidDebugInfo; + + if (mod.mod_info.C13ByteSize == 0) + return; + + const modi = di.pdb.getStreamById(mod.mod_info.ModuleSymStream) orelse return BaseError.MissingDebugInfo; + + const signature = try modi.reader().readIntLittle(u32); + if (signature != 4) + return BaseError.InvalidDebugInfo; + + mod.symbols = try allocator.alloc(u8, mod.mod_info.SymByteSize - 4); + try modi.reader().readNoEof(mod.symbols); + + mod.subsect_info = try allocator.alloc(u8, mod.mod_info.C13ByteSize); + try modi.reader().readNoEof(mod.subsect_info); + + var sect_offset: usize = 0; + var skip_len: usize = undefined; + while (sect_offset != mod.subsect_info.len) : (sect_offset += skip_len) { + const subsect_hdr = @ptrCast(*pdb.DebugSubsectionHeader, &mod.subsect_info[sect_offset]); + skip_len = subsect_hdr.Length; + sect_offset += @sizeOf(pdb.DebugSubsectionHeader); + + switch (subsect_hdr.Kind) { + .FileChecksums => { + mod.checksum_offset = sect_offset; + break; + }, + else => {}, + } + + if (sect_offset > mod.subsect_info.len) + return BaseError.InvalidDebugInfo; + } + + mod.populated = true; + } +}; diff --git a/lib/std/symbol_map_unix.zig b/lib/std/symbol_map_unix.zig new file mode 100644 index 000000000000..0e4d43b05498 --- /dev/null +++ b/lib/std/symbol_map_unix.zig @@ -0,0 +1,173 @@ +//! TODO: the name for this file is a bit misleading, this is actually for +//! *nix - darwin, but unix_other_than_darwin is too long. + +const std = @import("std.zig"); +const builtin = std.builtin; +const assert = std.debug.assert; +const SymbolInfo = std.debug.SymbolMap.SymbolInfo; +const debug_info = std.debug_info; +const BaseError = debug_info.BaseError; +const chopSlice = debug_info.chopSlice; +const mem = std.mem; +const DW = std.dwarf; +const elf = std.elf; +const os = std.os; +const math = std.math; +const fs = std.fs; +const File = fs.File; + +const SymbolMapState = debug_info.SymbolMapStateFromModuleInfo(Module); +pub const init = SymbolMapState.init; + +const Module = struct { + const Self = @This(); + + base_address: usize, + dwarf: DW.DwarfInfo, + mapped_memory: []const u8, + + pub fn lookup(allocator: *mem.Allocator, address_map: *SymbolMapState.AddressMap, address: usize) !*Self { + var ctx: struct { + // Input + address: usize, + // Output + base_address: usize = undefined, + name: []const u8 = undefined, + } = .{ .address = address }; + const CtxTy = @TypeOf(ctx); + + if (os.dl_iterate_phdr(&ctx, anyerror, struct { + fn callback(info: *os.dl_phdr_info, size: usize, context: *CtxTy) !void { + // The base address is too high + if (context.address < info.dlpi_addr) + return; + + const phdrs = info.dlpi_phdr[0..info.dlpi_phnum]; + for (phdrs) |*phdr| { + if (phdr.p_type != elf.PT_LOAD) continue; + + const seg_start = info.dlpi_addr + phdr.p_vaddr; + const seg_end = seg_start + phdr.p_memsz; + + if (context.address >= seg_start and context.address < seg_end) { + // Android libc uses NULL instead of an empty string to mark the + // main program + context.name = mem.spanZ(info.dlpi_name) orelse ""; + context.base_address = info.dlpi_addr; + // Stop the iteration + return error.Found; + } + } + } + }.callback)) { + return BaseError.MissingDebugInfo; + } else |err| switch (err) { + error.Found => {}, + else => return BaseError.MissingDebugInfo, + } + + if (address_map.get(ctx.base_address)) |obj_di| { + return obj_di; + } + + const obj_di = try allocator.create(Self); + errdefer allocator.destroy(obj_di); + + // TODO https://github.com/ziglang/zig/issues/5525 + const copy = if (ctx.name.len > 0) + fs.cwd().openFile(ctx.name, .{ .intended_io_mode = .blocking }) + else + fs.openSelfExe(.{ .intended_io_mode = .blocking }); + + const elf_file = copy catch |err| switch (err) { + error.FileNotFound => return BaseError.MissingDebugInfo, + else => return err, + }; + + obj_di.* = try readElfDebugInfo(allocator, elf_file); + obj_di.base_address = ctx.base_address; + + try address_map.putNoClobber(ctx.base_address, obj_di); + + return obj_di; + } + + /// This takes ownership of elf_file: users of this function should not close + /// it themselves, even on error. + /// TODO resources https://github.com/ziglang/zig/issues/4353 + /// TODO it's weird to take ownership even on error, rework this code. + fn readElfDebugInfo(allocator: *mem.Allocator, elf_file: File) !Self { + nosuspend { + const mapped_mem = try debug_info.mapWholeFile(elf_file); + const hdr = @ptrCast(*const elf.Ehdr, &mapped_mem[0]); + if (!mem.eql(u8, hdr.e_ident[0..4], "\x7fELF")) return error.InvalidElfMagic; + if (hdr.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion; + + const endian: builtin.Endian = switch (hdr.e_ident[elf.EI_DATA]) { + elf.ELFDATA2LSB => .Little, + elf.ELFDATA2MSB => .Big, + else => return error.InvalidElfEndian, + }; + assert(endian == std.builtin.endian); // this is our own debug info + + const shoff = hdr.e_shoff; + const str_section_off = shoff + @as(u64, hdr.e_shentsize) * @as(u64, hdr.e_shstrndx); + const str_shdr = @ptrCast( + *const elf.Shdr, + @alignCast(@alignOf(elf.Shdr), &mapped_mem[try math.cast(usize, str_section_off)]), + ); + const header_strings = mapped_mem[str_shdr.sh_offset .. str_shdr.sh_offset + str_shdr.sh_size]; + const shdrs = @ptrCast( + [*]const elf.Shdr, + @alignCast(@alignOf(elf.Shdr), &mapped_mem[shoff]), + )[0..hdr.e_shnum]; + + var opt_debug_info: ?[]const u8 = null; + var opt_debug_abbrev: ?[]const u8 = null; + var opt_debug_str: ?[]const u8 = null; + var opt_debug_line: ?[]const u8 = null; + var opt_debug_ranges: ?[]const u8 = null; + + for (shdrs) |*shdr| { + if (shdr.sh_type == elf.SHT_NULL) continue; + + const name = std.mem.span(std.meta.assumeSentinel(header_strings[shdr.sh_name..].ptr, 0)); + if (mem.eql(u8, name, ".debug_info")) { + opt_debug_info = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); + } else if (mem.eql(u8, name, ".debug_abbrev")) { + opt_debug_abbrev = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); + } else if (mem.eql(u8, name, ".debug_str")) { + opt_debug_str = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); + } else if (mem.eql(u8, name, ".debug_line")) { + opt_debug_line = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); + } else if (mem.eql(u8, name, ".debug_ranges")) { + opt_debug_ranges = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); + } + } + + var di = DW.DwarfInfo{ + .endian = endian, + .debug_info = opt_debug_info orelse return BaseError.MissingDebugInfo, + .debug_abbrev = opt_debug_abbrev orelse return BaseError.MissingDebugInfo, + .debug_str = opt_debug_str orelse return BaseError.MissingDebugInfo, + .debug_line = opt_debug_line orelse return BaseError.MissingDebugInfo, + .debug_ranges = opt_debug_ranges, + }; + + try DW.openDwarfDebugInfo(&di, allocator); + + return Self{ + .base_address = undefined, + .dwarf = di, + .mapped_memory = mapped_mem, + }; + } + } + + pub fn addressToSymbol(self: *Self, address: usize) !SymbolInfo { + // Translate the VA into an address into this object + const relocated_address = address - self.base_address; + + return debug_info.dwarfAddressToSymbolInfo(&self.dwarf, relocated_address); + } +}; diff --git a/lib/std/symbol_map_unsupported.zig b/lib/std/symbol_map_unsupported.zig new file mode 100644 index 000000000000..79d7dab4f542 --- /dev/null +++ b/lib/std/symbol_map_unsupported.zig @@ -0,0 +1,39 @@ +//! This is so printing a stack trace on an unsupported platform just prints +//! with empty symbols instead of failing to build. This is important for +//! GeneralPurposeAllocator and similar. +//! +//! To implement actual debug symbols, use `root.debug_config.initSymbolMap` +//! or `root.os.debug.SymbolMap`. + +const std = @import("std.zig"); +const SymbolMap = std.debug.SymbolMap; +const SymbolInfo = SymbolMap.SymbolInfo; +const mem = std.mem; + +const Self = @This(); + +allocator: *mem.Allocator, +symbol_map: SymbolMap, + +pub fn init(allocator: *mem.Allocator) !*SymbolMap { + const value = try allocator.create(Self); + value.* = Self{ + .allocator = allocator, + .symbol_map = .{ + .deinitFn = deinit, + .addressToSymbolFn = addressToSymbol, + }, + }; + + return &value.symbol_map; +} + +fn deinit(symbol_map: *SymbolMap) void {} + +fn addressToSymbol(symbol_map: *SymbolMap, address: usize) !SymbolInfo { + return SymbolInfo{}; +} + +test { + std.testing.refAllDecls(Self); +}