diff --git a/lib/std/c.zig b/lib/std/c.zig index 149f3ab7e199..860fdab92900 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -413,6 +413,13 @@ pub extern "c" fn timer_delete(timerid: c.timer_t) c_int; pub extern "c" fn timer_settime(timerid: c.timer_t, flags: c_int, new_value: *const c.itimerspec, old_value: *c.itimerspec) c_int; pub extern "c" fn timer_gettime(timerid: c.timer_t, flags: c_int, curr_value: *c.itimerspec) c_int; +pub usingnamespace if (builtin.os.tag == .linux and builtin.target.isMusl()) struct { + // musl does not implement getcontext + pub const getcontext = std.os.linux.getcontext; +} else struct { + pub extern "c" fn getcontext(ucp: *std.os.ucontext_t) c_int; +}; + pub const max_align_t = if (builtin.abi == .msvc) f64 else if (builtin.target.isDarwin()) diff --git a/lib/std/c/darwin.zig b/lib/std/c/darwin.zig index 0f60c2f841d4..1901271b83c2 100644 --- a/lib/std/c/darwin.zig +++ b/lib/std/c/darwin.zig @@ -148,12 +148,10 @@ pub const ucontext_t = extern struct { link: ?*ucontext_t, mcsize: u64, mcontext: *mcontext_t, + __mcontext_data: mcontext_t, }; -pub const mcontext_t = extern struct { - es: arch_bits.exception_state, - ss: arch_bits.thread_state, -}; +pub const mcontext_t = arch_bits.mcontext_t; extern "c" fn __error() *c_int; pub extern "c" fn NSVersionOfRunTimeLibrary(library_name: [*:0]const u8) u32; diff --git a/lib/std/c/darwin/aarch64.zig b/lib/std/c/darwin/aarch64.zig index 48b03363a165..d00b92af8383 100644 --- a/lib/std/c/darwin/aarch64.zig +++ b/lib/std/c/darwin/aarch64.zig @@ -1,5 +1,12 @@ // See C headers in // lib/libc/include/aarch64-macos.12-gnu/mach/arm/_structs.h +// lib/libc/include/aarch64-macos.13-none/arm/_mcontext.h + +pub const mcontext_t = extern struct { + es: exception_state, + ss: thread_state, + ns: neon_state, +}; pub const exception_state = extern struct { far: u64, // Virtual Fault Address @@ -17,6 +24,12 @@ pub const thread_state = extern struct { __pad: u32, }; +pub const neon_state = extern struct { + q: [32]u128, + fpsr: u32, + fpcr: u32, +}; + pub const EXC_TYPES_COUNT = 14; pub const EXC_MASK_MACHINE = 0; diff --git a/lib/std/c/darwin/x86_64.zig b/lib/std/c/darwin/x86_64.zig index c7671bc23a14..7b66fb2e9798 100644 --- a/lib/std/c/darwin/x86_64.zig +++ b/lib/std/c/darwin/x86_64.zig @@ -1,5 +1,11 @@ const c = @import("../darwin.zig"); +pub const mcontext_t = extern struct { + es: exception_state, + ss: thread_state, + fs: float_state, +}; + pub const exception_state = extern struct { trapno: u16, cpu: u16, @@ -31,6 +37,29 @@ pub const thread_state = extern struct { gs: u64, }; +const stmm_reg = [16]u8; +const xmm_reg = [16]u8; +pub const float_state = extern struct { + reserved: [2]c_int, + fcw: u16, + fsw: u16, + ftw: u8, + rsrv1: u8, + fop: u16, + ip: u32, + cs: u16, + rsrv2: u16, + dp: u32, + ds: u16, + rsrv3: u16, + mxcsr: u32, + mxcsrmask: u32, + stmm: [8]stmm_reg, + xmm: [16]xmm_reg, + rsrv4: [96]u8, + reserved1: c_int, +}; + pub const THREAD_STATE = 4; pub const THREAD_STATE_COUNT: c.mach_msg_type_number_t = @sizeOf(thread_state) / @sizeOf(c_int); diff --git a/lib/std/coff.zig b/lib/std/coff.zig index a08c2c514d18..55fe5aa2546f 100644 --- a/lib/std/coff.zig +++ b/lib/std/coff.zig @@ -1214,6 +1214,11 @@ pub const Coff = struct { return Strtab{ .buffer = self.data[offset..][0..size] }; } + pub fn strtabRequired(self: *const Coff) bool { + for (self.getSectionHeaders()) |*sect_hdr| if (sect_hdr.getName() == null) return true; + return false; + } + pub fn getSectionHeaders(self: *const Coff) []align(1) const SectionHeader { const coff_header = self.getCoffHeader(); const offset = self.coff_header_offset + @sizeOf(CoffHeader) + coff_header.size_of_optional_header; @@ -1248,14 +1253,12 @@ pub const Coff = struct { return null; } - pub fn getSectionData(self: *const Coff, comptime name: []const u8) ![]const u8 { - const sec = self.getSectionByName(name) orelse return error.MissingCoffSection; + pub fn getSectionData(self: *const Coff, sec: *align(1) const SectionHeader) []const u8 { return self.data[sec.pointer_to_raw_data..][0..sec.virtual_size]; } - // Return an owned slice full of the section data - pub fn getSectionDataAlloc(self: *const Coff, comptime name: []const u8, allocator: mem.Allocator) ![]u8 { - const section_data = try self.getSectionData(name); + pub fn getSectionDataAlloc(self: *const Coff, sec: *align(1) const SectionHeader, allocator: mem.Allocator) ![]u8 { + const section_data = self.getSectionData(sec); return allocator.dupe(u8, section_data); } }; diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 44f6ce136759..948a76c2df30 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -133,10 +133,80 @@ pub fn dumpCurrentStackTrace(start_addr: ?usize) void { } } +pub const have_ucontext = @hasDecl(os.system, "ucontext_t") and + (builtin.os.tag != .linux or switch (builtin.cpu.arch) { + .mips, .mipsel, .mips64, .mips64el, .riscv64 => false, + else => true, +}); + +/// Platform-specific thread state. This contains register state, and on some platforms +/// information about the stack. This is not safe to trivially copy, because some platforms +/// use internal pointers within this structure. To make a copy, use `copyContext`. +pub const ThreadContext = blk: { + if (native_os == .windows) { + break :blk std.os.windows.CONTEXT; + } else if (have_ucontext) { + break :blk os.ucontext_t; + } else { + break :blk void; + } +}; + +/// Copies one context to another, updating any internal pointers +pub fn copyContext(source: *const ThreadContext, dest: *ThreadContext) void { + if (!have_ucontext) return {}; + dest.* = source.*; + relocateContext(dest); +} + +/// Updates any internal pointers in the context to reflect its current location +pub fn relocateContext(context: *ThreadContext) void { + return switch (native_os) { + .macos => { + context.mcontext = &context.__mcontext_data; + }, + else => {}, + }; +} + +pub const have_getcontext = @hasDecl(os.system, "getcontext") and + (builtin.os.tag != .linux or switch (builtin.cpu.arch) { + .x86, + .x86_64, + => true, + else => builtin.link_libc and !builtin.target.isMusl(), +}); + +/// Capture the current context. The register values in the context will reflect the +/// state after the platform `getcontext` function returns. +/// +/// It is valid to call this if the platform doesn't have context capturing support, +/// in that case false will be returned. +pub inline fn getContext(context: *ThreadContext) bool { + if (native_os == .windows) { + context.* = std.mem.zeroes(windows.CONTEXT); + windows.ntdll.RtlCaptureContext(context); + return true; + } + + const result = have_getcontext and os.system.getcontext(context) == 0; + if (native_os == .macos) { + assert(context.mcsize == @sizeOf(std.c.mcontext_t)); + + // On aarch64-macos, the system getcontext doesn't write anything into the pc + // register slot, it only writes lr. This makes the context consistent with + // other aarch64 getcontext implementations which write the current lr + // (where getcontext will return to) into both the lr and pc slot of the context. + if (native_arch == .aarch64) context.mcontext.ss.pc = context.mcontext.ss.lr; + } + + return result; +} + /// Tries to print the stack trace starting from the supplied base pointer to stderr, /// unbuffered, and ignores any error returned. /// TODO multithreaded awareness -pub fn dumpStackTraceFromBase(bp: usize, ip: usize) void { +pub fn dumpStackTraceFromBase(context: *const ThreadContext) void { nosuspend { if (comptime builtin.target.isWasm()) { if (native_os == .wasi) { @@ -156,13 +226,25 @@ pub fn dumpStackTraceFromBase(bp: usize, ip: usize) void { }; const tty_config = io.tty.detectConfig(io.getStdErr()); if (native_os == .windows) { - writeCurrentStackTraceWindows(stderr, debug_info, tty_config, ip) catch return; + // On x86_64 and aarch64, the stack will be unwound using RtlVirtualUnwind using the context + // provided by the exception handler. On x86, RtlVirtualUnwind doesn't exist. Instead, a new backtrace + // will be captured and frames prior to the exception will be filtered. + // The caveat is that RtlCaptureStackBackTrace does not include the KiUserExceptionDispatcher frame, + // which is where the IP in `context` points to, so it can't be used as start_addr. + // Instead, start_addr is recovered from the stack. + const start_addr = if (builtin.cpu.arch == .x86) @as(*const usize, @ptrFromInt(context.getRegs().bp + 4)).* else null; + writeStackTraceWindows(stderr, debug_info, tty_config, context, start_addr) catch return; return; } - printSourceAtAddress(debug_info, stderr, ip, tty_config) catch return; - var it = StackIterator.init(null, bp); + var it = StackIterator.initWithContext(null, debug_info, context) catch return; + defer it.deinit(); + printSourceAtAddress(debug_info, stderr, it.unwind_state.?.dwarf_context.pc, tty_config) catch return; + while (it.next()) |return_address| { + if (it.getLastError()) |unwind_error| + printUnwindError(debug_info, stderr, unwind_error.address, unwind_error.err, tty_config) catch {}; + // On arm64 macOS, the address of the last frame is 0x0 rather than 0x1 as on x86_64 macOS, // therefore, we do a check for `return_address == 0` before subtracting 1 from it to avoid // an overflow. We do not need to signal `StackIterator` as it will correctly detect this @@ -184,12 +266,12 @@ pub fn captureStackTrace(first_address: ?usize, stack_trace: *std.builtin.StackT if (native_os == .windows) { const addrs = stack_trace.instruction_addresses; const first_addr = first_address orelse { - stack_trace.index = walkStackWindows(addrs[0..]); + stack_trace.index = walkStackWindows(addrs[0..], 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 = walkStackWindows(addr_buf[0..]); + const n = walkStackWindows(addr_buf[0..], null); const first_index = for (addr_buf[0..n], 0..) |addr, i| { if (addr == first_addr) { break i; @@ -206,7 +288,11 @@ pub fn captureStackTrace(first_address: ?usize, stack_trace: *std.builtin.StackT } stack_trace.index = slice.len; } else { + // TODO: This should use the DWARF unwinder if .eh_frame_hdr is available (so that full debug info parsing isn't required). + // A new path for loading DebugInfo needs to be created which will only attempt to parse in-memory sections, because + // stopping to load other debug info (ie. source line info) from disk here is not required for unwinding. var it = StackIterator.init(first_address, null); + defer it.deinit(); for (stack_trace.instruction_addresses, 0..) |*addr, i| { addr.* = it.next() orelse { stack_trace.index = i; @@ -399,12 +485,27 @@ pub fn writeStackTrace( } } +pub const UnwindError = if (have_ucontext) + @typeInfo(@typeInfo(@TypeOf(StackIterator.next_unwind)).Fn.return_type.?).ErrorUnion.error_set +else + void; + 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, + // When DebugInfo and a register context is available, this iterator can unwind + // stacks with frames that don't use a frame pointer (ie. -fomit-frame-pointer), + // using DWARF and MachO unwind info. + unwind_state: if (have_ucontext) ?struct { + debug_info: *DebugInfo, + dwarf_context: DW.UnwindContext, + last_error: ?UnwindError = null, + failed: bool = false, + } else void = if (have_ucontext) null else {}, + pub fn init(first_address: ?usize, fp: ?usize) StackIterator { if (native_arch == .sparc64) { // Flush all the register windows on stack. @@ -419,6 +520,44 @@ pub const StackIterator = struct { }; } + pub fn initWithContext(first_address: ?usize, debug_info: *DebugInfo, context: *const os.ucontext_t) !StackIterator { + // The implementation of DWARF unwinding on aarch64-macos is not complete. However, Apple mandates that + // the frame pointer register is always used, so on this platform we can safely use the FP-based unwinder. + if (comptime builtin.target.isDarwin() and native_arch == .aarch64) { + return init(first_address, context.mcontext.ss.fp); + } else { + var iterator = init(first_address, null); + iterator.unwind_state = .{ + .debug_info = debug_info, + .dwarf_context = try DW.UnwindContext.init(debug_info.allocator, context, &isValidMemory), + }; + + return iterator; + } + } + + pub fn deinit(self: *StackIterator) void { + if (have_ucontext and self.unwind_state != null) self.unwind_state.?.dwarf_context.deinit(); + } + + pub fn getLastError(self: *StackIterator) ?struct { + err: UnwindError, + address: usize, + } { + if (!have_ucontext) return null; + if (self.unwind_state) |*unwind_state| { + if (unwind_state.last_error) |err| { + unwind_state.last_error = null; + return .{ + .err = err, + .address = unwind_state.dwarf_context.pc, + }; + } + } + + return null; + } + // Offset of the saved BP wrt the frame pointer. const fp_offset = if (native_arch.isRISCV()) // On RISC-V the frame pointer points to the top of the saved register @@ -461,6 +600,7 @@ pub const StackIterator = struct { if (native_os == .freestanding) return true; const aligned_address = address & ~@as(usize, @intCast((mem.page_size - 1))); + if (aligned_address == 0) return false; const aligned_memory = @as([*]align(mem.page_size) u8, @ptrFromInt(aligned_address))[0..mem.page_size]; if (native_os != .windows) { @@ -500,7 +640,49 @@ pub const StackIterator = struct { } } + fn next_unwind(self: *StackIterator) !usize { + const unwind_state = &self.unwind_state.?; + const module = try unwind_state.debug_info.getModuleForAddress(unwind_state.dwarf_context.pc); + switch (native_os) { + .macos, .ios, .watchos, .tvos => { + // __unwind_info is a requirement for unwinding on Darwin. It may fall back to DWARF, but unwinding + // via DWARF before attempting to use the compact unwind info will produce incorrect results. + if (module.unwind_info) |unwind_info| { + if (DW.unwindFrameMachO(&unwind_state.dwarf_context, unwind_info, module.eh_frame, module.base_address)) |return_address| { + return return_address; + } else |err| { + if (err != error.RequiresDWARFUnwind) return err; + } + } else return error.MissingUnwindInfo; + }, + else => {}, + } + + if (try module.getDwarfInfoForAddress(unwind_state.debug_info.allocator, unwind_state.dwarf_context.pc)) |di| { + return di.unwindFrame(&unwind_state.dwarf_context, null); + } else return error.MissingDebugInfo; + } + fn next_internal(self: *StackIterator) ?usize { + if (have_ucontext) { + if (self.unwind_state) |*unwind_state| { + if (!unwind_state.failed) { + if (unwind_state.dwarf_context.pc == 0) return null; + if (self.next_unwind()) |return_address| { + self.fp = unwind_state.dwarf_context.getFp() catch 0; + return return_address; + } else |err| { + unwind_state.last_error = err; + unwind_state.failed = true; + + // Fall back to fp-based unwinding on the first failure. + // We can't attempt it again for other modules higher in the + // stack because the full register state won't have been unwound. + } + } + } + } + const fp = if (comptime native_arch.isSPARC()) // On SPARC the offset is positive. (!) math.add(usize, self.fp, fp_offset) catch return null @@ -537,11 +719,21 @@ pub fn writeCurrentStackTrace( tty_config: io.tty.Config, start_addr: ?usize, ) !void { + var context: ThreadContext = undefined; + const has_context = getContext(&context); if (native_os == .windows) { - return writeCurrentStackTraceWindows(out_stream, debug_info, tty_config, start_addr); + return writeStackTraceWindows(out_stream, debug_info, tty_config, &context, start_addr); } - var it = StackIterator.init(start_addr, null); + + var it = (if (has_context) blk: { + break :blk StackIterator.initWithContext(start_addr, debug_info, &context) catch null; + } else null) orelse StackIterator.init(start_addr, null); + defer it.deinit(); + while (it.next()) |return_address| { + if (it.getLastError()) |unwind_error| + try printUnwindError(debug_info, out_stream, unwind_error.address, unwind_error.err, tty_config); + // On arm64 macOS, the address of the last frame is 0x0 rather than 0x1 as on x86_64 macOS, // therefore, we do a check for `return_address == 0` before subtracting 1 from it to avoid // an overflow. We do not need to signal `StackIterator` as it will correctly detect this @@ -552,7 +744,7 @@ pub fn writeCurrentStackTrace( } } -pub noinline fn walkStackWindows(addresses: []usize) usize { +pub noinline fn walkStackWindows(addresses: []usize, existing_context: ?*const windows.CONTEXT) usize { if (builtin.cpu.arch == .x86) { // RtlVirtualUnwind doesn't exist on x86 return windows.ntdll.RtlCaptureStackBackTrace(0, addresses.len, @as(**anyopaque, @ptrCast(addresses.ptr)), null); @@ -560,8 +752,13 @@ pub noinline fn walkStackWindows(addresses: []usize) usize { const tib = @as(*const windows.NT_TIB, @ptrCast(&windows.teb().Reserved1)); - var context: windows.CONTEXT = std.mem.zeroes(windows.CONTEXT); - windows.ntdll.RtlCaptureContext(&context); + var context: windows.CONTEXT = undefined; + if (existing_context) |context_ptr| { + context = context_ptr.*; + } else { + context = std.mem.zeroes(windows.CONTEXT); + windows.ntdll.RtlCaptureContext(&context); + } var i: usize = 0; var image_base: usize = undefined; @@ -603,14 +800,15 @@ pub noinline fn walkStackWindows(addresses: []usize) usize { return i; } -pub fn writeCurrentStackTraceWindows( +pub fn writeStackTraceWindows( out_stream: anytype, debug_info: *DebugInfo, tty_config: io.tty.Config, + context: *const windows.CONTEXT, start_addr: ?usize, ) !void { var addr_buf: [1024]usize = undefined; - const n = walkStackWindows(addr_buf[0..]); + const n = walkStackWindows(addr_buf[0..], context); const addrs = addr_buf[0..n]; var start_i: usize = if (start_addr) |saddr| blk: { for (addrs, 0..) |addr, i| { @@ -681,6 +879,13 @@ fn printUnknownSource(debug_info: *DebugInfo, out_stream: anytype, address: usiz ); } +pub fn printUnwindError(debug_info: *DebugInfo, out_stream: anytype, address: usize, err: UnwindError, tty_config: io.tty.Config) !void { + const module_name = debug_info.getModuleNameForAddress(address) orelse "???"; + try tty_config.setColor(out_stream, .dim); + try out_stream.print("Unwind information for `{s}:0x{x}` was not available ({}), trace may be incomplete\n\n", .{ module_name, address, err }); + try tty_config.setColor(out_stream, .reset); +} + pub fn printSourceAtAddress(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: io.tty.Config) !void { const module = debug_info.getModuleForAddress(address) catch |err| switch (err) { error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config), @@ -779,12 +984,8 @@ pub fn openSelfDebugInfo(allocator: mem.Allocator) OpenSelfDebugInfoError!DebugI } } -fn readCoffDebugInfo(allocator: mem.Allocator, coff_bytes: []const u8) !ModuleDebugInfo { +fn readCoffDebugInfo(allocator: mem.Allocator, coff_obj: *coff.Coff) !ModuleDebugInfo { nosuspend { - const coff_obj = try allocator.create(coff.Coff); - defer allocator.destroy(coff_obj); - coff_obj.* = try coff.Coff.init(coff_bytes); - var di = ModuleDebugInfo{ .base_address = undefined, .coff_image_base = coff_obj.getImageBase(), @@ -792,62 +993,35 @@ fn readCoffDebugInfo(allocator: mem.Allocator, coff_bytes: []const u8) !ModuleDe .debug_data = undefined, }; - if (coff_obj.getSectionByName(".debug_info")) |sec| { + if (coff_obj.getSectionByName(".debug_info")) |_| { // This coff file has embedded DWARF debug info - _ = sec; - - const debug_info = coff_obj.getSectionDataAlloc(".debug_info", allocator) catch return error.MissingDebugInfo; - errdefer allocator.free(debug_info); - const debug_abbrev = coff_obj.getSectionDataAlloc(".debug_abbrev", allocator) catch return error.MissingDebugInfo; - errdefer allocator.free(debug_abbrev); - const debug_str = coff_obj.getSectionDataAlloc(".debug_str", allocator) catch return error.MissingDebugInfo; - errdefer allocator.free(debug_str); - const debug_line = coff_obj.getSectionDataAlloc(".debug_line", allocator) catch return error.MissingDebugInfo; - errdefer allocator.free(debug_line); - - const debug_str_offsets = coff_obj.getSectionDataAlloc(".debug_str_offsets", allocator) catch null; - const debug_line_str = coff_obj.getSectionDataAlloc(".debug_line_str", allocator) catch null; - const debug_ranges = coff_obj.getSectionDataAlloc(".debug_ranges", allocator) catch null; - const debug_loclists = coff_obj.getSectionDataAlloc(".debug_loclists", allocator) catch null; - const debug_rnglists = coff_obj.getSectionDataAlloc(".debug_rnglists", allocator) catch null; - const debug_addr = coff_obj.getSectionDataAlloc(".debug_addr", allocator) catch null; - const debug_names = coff_obj.getSectionDataAlloc(".debug_names", allocator) catch null; - const debug_frame = coff_obj.getSectionDataAlloc(".debug_frame", allocator) catch null; + var sections: DW.DwarfInfo.SectionArray = DW.DwarfInfo.null_section_array; + errdefer for (sections) |section| if (section) |s| if (s.owned) allocator.free(s.data); + + inline for (@typeInfo(DW.DwarfSection).Enum.fields, 0..) |section, i| { + sections[i] = if (coff_obj.getSectionByName("." ++ section.name)) |section_header| blk: { + break :blk .{ + .data = try coff_obj.getSectionDataAlloc(section_header, allocator), + .virtual_address = section_header.virtual_address, + .owned = true, + }; + } else null; + } var dwarf = DW.DwarfInfo{ .endian = native_endian, - .debug_info = debug_info, - .debug_abbrev = debug_abbrev, - .debug_str = debug_str, - .debug_str_offsets = debug_str_offsets, - .debug_line = debug_line, - .debug_line_str = debug_line_str, - .debug_ranges = debug_ranges, - .debug_loclists = debug_loclists, - .debug_rnglists = debug_rnglists, - .debug_addr = debug_addr, - .debug_names = debug_names, - .debug_frame = debug_frame, - }; - - DW.openDwarfDebugInfo(&dwarf, allocator) catch |err| { - if (debug_str_offsets) |d| allocator.free(d); - if (debug_line_str) |d| allocator.free(d); - if (debug_ranges) |d| allocator.free(d); - if (debug_loclists) |d| allocator.free(d); - if (debug_rnglists) |d| allocator.free(d); - if (debug_addr) |d| allocator.free(d); - if (debug_names) |d| allocator.free(d); - if (debug_frame) |d| allocator.free(d); - return err; + .sections = sections, + .is_macho = false, }; + try DW.openDwarfDebugInfo(&dwarf, allocator); di.debug_data = PdbOrDwarf{ .dwarf = dwarf }; return di; } // Only used by pdb path di.coff_section_headers = try coff_obj.getSectionHeadersAlloc(allocator); + errdefer allocator.free(di.coff_section_headers); var path_buf: [windows.MAX_PATH]u8 = undefined; const len = try coff_obj.getPdbPath(path_buf[0..]); @@ -877,13 +1051,35 @@ fn chopSlice(ptr: []const u8, offset: u64, size: u64) error{Overflow}![]const u8 return ptr[start..end]; } -/// This takes ownership of elf_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. -pub fn readElfDebugInfo(allocator: mem.Allocator, elf_file: File) !ModuleDebugInfo { +/// Reads debug info from an ELF file, or the current binary if none in specified. +/// If the required sections aren't present but a reference to external debug info is, +/// then this this function will recurse to attempt to load the debug sections from +/// an external file. +pub fn readElfDebugInfo( + allocator: mem.Allocator, + elf_filename: ?[]const u8, + build_id: ?[]const u8, + expected_crc: ?u32, + parent_sections: *DW.DwarfInfo.SectionArray, + parent_mapped_mem: ?[]align(mem.page_size) const u8, +) !ModuleDebugInfo { nosuspend { + + // TODO https://github.com/ziglang/zig/issues/5525 + const elf_file = (if (elf_filename) |filename| blk: { + break :blk if (fs.path.isAbsolute(filename)) + fs.openFileAbsolute(filename, .{ .intended_io_mode = .blocking }) + else + fs.cwd().openFile(filename, .{ .intended_io_mode = .blocking }); + } else fs.openSelfExe(.{ .intended_io_mode = .blocking })) catch |err| switch (err) { + error.FileNotFound => return error.MissingDebugInfo, + else => return err, + }; + const mapped_mem = try mapWholeFile(elf_file); - const hdr = @as(*const elf.Ehdr, @ptrCast(&mapped_mem[0])); + if (expected_crc) |crc| if (crc != std.hash.crc.Crc32SmallWithPoly(.IEEE).hash(mapped_mem)) return error.InvalidDebugInfo; + + const hdr: *const elf.Ehdr = @ptrCast(&mapped_mem[0]); if (!mem.eql(u8, hdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic; if (hdr.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion; @@ -896,73 +1092,152 @@ pub fn readElfDebugInfo(allocator: mem.Allocator, elf_file: File) !ModuleDebugIn const shoff = hdr.e_shoff; const str_section_off = shoff + @as(u64, hdr.e_shentsize) * @as(u64, hdr.e_shstrndx); - const str_shdr: *const elf.Shdr = @ptrCast(@alignCast( - &mapped_mem[math.cast(usize, str_section_off) orelse return error.Overflow], - )); - const header_strings = mapped_mem[str_shdr.sh_offset .. str_shdr.sh_offset + str_shdr.sh_size]; + const str_shdr: *const elf.Shdr = @ptrCast(@alignCast(&mapped_mem[math.cast(usize, str_section_off) orelse return error.Overflow])); + const header_strings = mapped_mem[str_shdr.sh_offset..][0..str_shdr.sh_size]; const shdrs = @as( [*]const elf.Shdr, @ptrCast(@alignCast(&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_str_offsets: ?[]const u8 = null; - var opt_debug_line: ?[]const u8 = null; - var opt_debug_line_str: ?[]const u8 = null; - var opt_debug_ranges: ?[]const u8 = null; - var opt_debug_loclists: ?[]const u8 = null; - var opt_debug_rnglists: ?[]const u8 = null; - var opt_debug_addr: ?[]const u8 = null; - var opt_debug_names: ?[]const u8 = null; - var opt_debug_frame: ?[]const u8 = null; + var sections: DW.DwarfInfo.SectionArray = DW.DwarfInfo.null_section_array; - for (shdrs) |*shdr| { - if (shdr.sh_type == elf.SHT_NULL) continue; + // Combine section list. This takes ownership over any owned sections from the parent scope. + for (parent_sections, §ions) |*parent, *section| { + if (parent.*) |*p| { + section.* = p.*; + p.owned = false; + } + } + errdefer for (sections) |section| if (section) |s| if (s.owned) allocator.free(s.data); + + var separate_debug_filename: ?[]const u8 = null; + var separate_debug_crc: ?u32 = null; + for (shdrs) |*shdr| { + if (shdr.sh_type == elf.SHT_NULL or shdr.sh_type == elf.SHT_NOBITS) continue; const name = mem.sliceTo(header_strings[shdr.sh_name..], 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_str_offsets")) { - opt_debug_str_offsets = 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_line_str")) { - opt_debug_line_str = 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); - } else if (mem.eql(u8, name, ".debug_loclists")) { - opt_debug_loclists = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); - } else if (mem.eql(u8, name, ".debug_rnglists")) { - opt_debug_rnglists = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); - } else if (mem.eql(u8, name, ".debug_addr")) { - opt_debug_addr = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); - } else if (mem.eql(u8, name, ".debug_names")) { - opt_debug_names = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); - } else if (mem.eql(u8, name, ".debug_frame")) { - opt_debug_frame = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); + + if (mem.eql(u8, name, ".gnu_debuglink")) { + const gnu_debuglink = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); + const debug_filename = mem.sliceTo(@as([*:0]const u8, @ptrCast(gnu_debuglink.ptr)), 0); + const crc_offset = mem.alignForward(usize, @intFromPtr(&debug_filename[debug_filename.len]) + 1, 4) - @intFromPtr(gnu_debuglink.ptr); + const crc_bytes = gnu_debuglink[crc_offset .. crc_offset + 4]; + separate_debug_crc = mem.readIntSliceNative(u32, crc_bytes); + separate_debug_filename = debug_filename; + continue; + } + + var section_index: ?usize = null; + inline for (@typeInfo(DW.DwarfSection).Enum.fields, 0..) |section, i| { + if (mem.eql(u8, "." ++ section.name, name)) section_index = i; + } + if (section_index == null) continue; + if (sections[section_index.?] != null) continue; + + const section_bytes = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size); + sections[section_index.?] = if ((shdr.sh_flags & elf.SHF_COMPRESSED) > 0) blk: { + var section_stream = io.fixedBufferStream(section_bytes); + var section_reader = section_stream.reader(); + const chdr = section_reader.readStruct(elf.Chdr) catch continue; + if (chdr.ch_type != .ZLIB) continue; + + var zlib_stream = std.compress.zlib.decompressStream(allocator, section_stream.reader()) catch continue; + defer zlib_stream.deinit(); + + var decompressed_section = try allocator.alloc(u8, chdr.ch_size); + errdefer allocator.free(decompressed_section); + + const read = zlib_stream.reader().readAll(decompressed_section) catch continue; + assert(read == decompressed_section.len); + + break :blk .{ + .data = decompressed_section, + .virtual_address = shdr.sh_addr, + .owned = true, + }; + } else .{ + .data = section_bytes, + .virtual_address = shdr.sh_addr, + .owned = false, + }; + } + + const missing_debug_info = + sections[@intFromEnum(DW.DwarfSection.debug_info)] == null or + sections[@intFromEnum(DW.DwarfSection.debug_abbrev)] == null or + sections[@intFromEnum(DW.DwarfSection.debug_str)] == null or + sections[@intFromEnum(DW.DwarfSection.debug_line)] == null; + + // Attempt to load debug info from an external file + // See: https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html + if (missing_debug_info) { + + // Only allow one level of debug info nesting + if (parent_mapped_mem) |_| { + return error.MissingDebugInfo; + } + + const global_debug_directories = [_][]const u8{ + "/usr/lib/debug", + }; + + // /.build-id/<2-character id prefix>/.debug + if (build_id) |id| blk: { + if (id.len < 3) break :blk; + + // Either md5 (16 bytes) or sha1 (20 bytes) are used here in practice + const extension = ".debug"; + var id_prefix_buf: [2]u8 = undefined; + var filename_buf: [38 + extension.len]u8 = undefined; + + _ = std.fmt.bufPrint(&id_prefix_buf, "{s}", .{std.fmt.fmtSliceHexLower(id[0..1])}) catch unreachable; + const filename = std.fmt.bufPrint( + &filename_buf, + "{s}" ++ extension, + .{std.fmt.fmtSliceHexLower(id[1..])}, + ) catch break :blk; + + for (global_debug_directories) |global_directory| { + const path = try fs.path.join(allocator, &.{ global_directory, ".build-id", &id_prefix_buf, filename }); + defer allocator.free(path); + + return readElfDebugInfo(allocator, path, null, separate_debug_crc, §ions, mapped_mem) catch continue; + } + } + + // use the path from .gnu_debuglink, in the same search order as gdb + if (separate_debug_filename) |separate_filename| blk: { + if (elf_filename != null and mem.eql(u8, elf_filename.?, separate_filename)) return error.MissingDebugInfo; + + // / + if (readElfDebugInfo(allocator, separate_filename, null, separate_debug_crc, §ions, mapped_mem)) |debug_info| return debug_info else |_| {} + + // /.debug/ + { + const path = try fs.path.join(allocator, &.{ ".debug", separate_filename }); + defer allocator.free(path); + + if (readElfDebugInfo(allocator, path, null, separate_debug_crc, §ions, mapped_mem)) |debug_info| return debug_info else |_| {} + } + + var cwd_buf: [fs.MAX_PATH_BYTES]u8 = undefined; + const cwd_path = fs.cwd().realpath("", &cwd_buf) catch break :blk; + + // // + for (global_debug_directories) |global_directory| { + const path = try fs.path.join(allocator, &.{ global_directory, cwd_path, separate_filename }); + defer allocator.free(path); + if (readElfDebugInfo(allocator, path, null, separate_debug_crc, §ions, mapped_mem)) |debug_info| return debug_info else |_| {} + } } + + return error.MissingDebugInfo; } 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_str_offsets = opt_debug_str_offsets, - .debug_line = opt_debug_line orelse return error.MissingDebugInfo, - .debug_line_str = opt_debug_line_str, - .debug_ranges = opt_debug_ranges, - .debug_loclists = opt_debug_loclists, - .debug_rnglists = opt_debug_rnglists, - .debug_addr = opt_debug_addr, - .debug_names = opt_debug_names, - .debug_frame = opt_debug_frame, + .sections = sections, + .is_macho = false, }; try DW.openDwarfDebugInfo(&di, allocator); @@ -970,7 +1245,8 @@ pub fn readElfDebugInfo(allocator: mem.Allocator, elf_file: File) !ModuleDebugIn return ModuleDebugInfo{ .base_address = undefined, .dwarf = di, - .mapped_memory = mapped_mem, + .mapped_memory = parent_mapped_mem orelse mapped_mem, + .external_mapped_memory = if (parent_mapped_mem != null) mapped_mem else null, }; } } @@ -1094,6 +1370,7 @@ fn readMachODebugInfo(allocator: mem.Allocator, macho_file: File) !ModuleDebugIn return ModuleDebugInfo{ .base_address = undefined, + .vmaddr_slide = undefined, .mapped_memory = mapped_mem, .ofiles = ModuleDebugInfo.OFileTable.init(allocator), .symbols = symbols, @@ -1180,6 +1457,21 @@ pub const WindowsModuleInfo = struct { base_address: usize, size: u32, name: []const u8, + handle: windows.HMODULE, + + // Set when the image file needed to be mapped from disk + mapped_file: ?struct { + file: File, + section_handle: windows.HANDLE, + section_view: []const u8, + + pub fn deinit(self: @This()) void { + const process_handle = windows.kernel32.GetCurrentProcess(); + assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @constCast(@ptrCast(self.section_view.ptr))) == .SUCCESS); + windows.CloseHandle(self.section_handle); + self.file.close(); + } + } = null, }; pub const DebugInfo = struct { @@ -1195,6 +1487,8 @@ pub const DebugInfo = struct { }; if (native_os == .windows) { + errdefer debug_info.modules.deinit(allocator); + const handle = windows.kernel32.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE | windows.TH32CS_SNAPMODULE32, 0); if (handle == windows.INVALID_HANDLE_VALUE) { switch (windows.kernel32.GetLastError()) { @@ -1212,9 +1506,16 @@ pub const DebugInfo = struct { var module_valid = true; while (module_valid) { const module_info = try debug_info.modules.addOne(allocator); - module_info.base_address = @intFromPtr(module_entry.modBaseAddr); - module_info.size = module_entry.modBaseSize; - module_info.name = allocator.dupe(u8, mem.sliceTo(&module_entry.szModule, 0)) catch &.{}; + const name = allocator.dupe(u8, mem.sliceTo(&module_entry.szModule, 0)) catch &.{}; + errdefer allocator.free(name); + + module_info.* = .{ + .base_address = @intFromPtr(module_entry.modBaseAddr), + .size = module_entry.modBaseSize, + .name = name, + .handle = module_entry.hModule, + }; + module_valid = windows.kernel32.Module32Next(handle, &module_entry) == 1; } } @@ -1233,6 +1534,7 @@ pub const DebugInfo = struct { if (native_os == .windows) { for (self.modules.items) |module| { self.allocator.free(module.name); + if (module.mapped_file) |mapped_file| mapped_file.deinit(); } self.modules.deinit(self.allocator); } @@ -1252,9 +1554,12 @@ pub const DebugInfo = struct { } } + // Returns the module name for a given address. + // This can be called when getModuleForAddress fails, so implementations should provide + // a path that doesn't rely on any side-effects of a prior successful module lookup. pub fn getModuleNameForAddress(self: *DebugInfo, address: usize) ?[]const u8 { if (comptime builtin.target.isDarwin()) { - return null; + return self.lookupModuleNameDyld(address); } else if (native_os == .windows) { return self.lookupModuleNameWin32(address); } else if (native_os == .haiku) { @@ -1262,7 +1567,7 @@ pub const DebugInfo = struct { } else if (comptime builtin.target.isWasm()) { return null; } else { - return null; + return self.lookupModuleNameDl(address); } } @@ -1271,11 +1576,10 @@ pub const DebugInfo = struct { 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; + const base_address = @intFromPtr(header); + if (address < base_address) continue; + const vmaddr_slide = std.c._dyld_get_image_vmaddr_slide(i); var it = macho.LoadCommandIterator{ .ncmds = header.ncmds, @@ -1284,18 +1588,29 @@ pub const DebugInfo = struct { @ptrFromInt(@intFromPtr(header) + @sizeOf(macho.mach_header_64)), )[0..header.sizeofcmds]), }; + + var unwind_info: ?[]const u8 = null; + var eh_frame: ?[]const u8 = null; while (it.next()) |cmd| switch (cmd.cmd()) { .SEGMENT_64 => { const segment_cmd = cmd.cast(macho.segment_command_64).?; - const rebased_address = address - base_address; - const seg_start = segment_cmd.vmaddr; - const seg_end = seg_start + segment_cmd.vmsize; + if (!mem.eql(u8, "__TEXT", segment_cmd.segName())) continue; - if (rebased_address >= seg_start and rebased_address < seg_end) { + const seg_start = segment_cmd.vmaddr + vmaddr_slide; + const seg_end = seg_start + segment_cmd.vmsize; + if (address >= seg_start and address < seg_end) { if (self.address_map.get(base_address)) |obj_di| { return obj_di; } + for (cmd.getSections()) |sect| { + if (mem.eql(u8, "__unwind_info", sect.sectName())) { + unwind_info = @as([*]const u8, @ptrFromInt(sect.addr + vmaddr_slide))[0..sect.size]; + } else if (mem.eql(u8, "__eh_frame", sect.sectName())) { + eh_frame = @as([*]const u8, @ptrFromInt(sect.addr + vmaddr_slide))[0..sect.size]; + } + } + const obj_di = try self.allocator.create(ModuleDebugInfo); errdefer self.allocator.destroy(obj_di); @@ -1308,6 +1623,9 @@ pub const DebugInfo = struct { }; obj_di.* = try readMachODebugInfo(self.allocator, macho_file); obj_di.base_address = base_address; + obj_di.vmaddr_slide = vmaddr_slide; + obj_di.unwind_info = unwind_info; + obj_di.eh_frame = eh_frame; try self.address_map.putNoClobber(base_address, obj_di); @@ -1321,18 +1639,124 @@ pub const DebugInfo = struct { return error.MissingDebugInfo; } + fn lookupModuleNameDyld(self: *DebugInfo, address: usize) ?[]const u8 { + _ = self; + const image_count = std.c._dyld_image_count(); + + var i: u32 = 0; + while (i < image_count) : (i += 1) { + const header = std.c._dyld_get_image_header(i) orelse continue; + const base_address = @intFromPtr(header); + if (address < base_address) continue; + const vmaddr_slide = std.c._dyld_get_image_vmaddr_slide(i); + + var it = macho.LoadCommandIterator{ + .ncmds = header.ncmds, + .buffer = @alignCast(@as( + [*]u8, + @ptrFromInt(@intFromPtr(header) + @sizeOf(macho.mach_header_64)), + )[0..header.sizeofcmds]), + }; + + while (it.next()) |cmd| switch (cmd.cmd()) { + .SEGMENT_64 => { + const segment_cmd = cmd.cast(macho.segment_command_64).?; + if (!mem.eql(u8, "__TEXT", segment_cmd.segName())) continue; + + const original_address = address - vmaddr_slide; + const seg_start = segment_cmd.vmaddr; + const seg_end = seg_start + segment_cmd.vmsize; + if (original_address >= seg_start and original_address < seg_end) { + return fs.path.basename(mem.sliceTo(std.c._dyld_get_image_name(i), 0)); + } + }, + else => {}, + }; + } + + return null; + } + fn lookupModuleWin32(self: *DebugInfo, address: usize) !*ModuleDebugInfo { - for (self.modules.items) |module| { + for (self.modules.items) |*module| { if (address >= module.base_address and address < module.base_address + module.size) { if (self.address_map.get(module.base_address)) |obj_di| { return obj_di; } - const mapped_module = @as([*]const u8, @ptrFromInt(module.base_address))[0..module.size]; const obj_di = try self.allocator.create(ModuleDebugInfo); errdefer self.allocator.destroy(obj_di); - obj_di.* = try readCoffDebugInfo(self.allocator, mapped_module); + const mapped_module = @as([*]const u8, @ptrFromInt(module.base_address))[0..module.size]; + var coff_obj = try coff.Coff.init(mapped_module); + + // The string table is not mapped into memory by the loader, so if a section name is in the + // string table then we have to map the full image file from disk. This can happen when + // a binary is produced with -gdwarf, since the section names are longer than 8 bytes. + if (coff_obj.strtabRequired()) { + 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 process_handle = windows.kernel32.GetCurrentProcess(); + const len = windows.kernel32.K32GetModuleFileNameExW( + process_handle, + module.handle, + @ptrCast(&name_buffer[4]), + windows.PATH_MAX_WIDE, + ); + + if (len == 0) return error.MissingDebugInfo; + const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) { + error.FileNotFound => return error.MissingDebugInfo, + else => return err, + }; + errdefer coff_file.close(); + + var section_handle: windows.HANDLE = undefined; + const create_section_rc = windows.ntdll.NtCreateSection( + §ion_handle, + windows.STANDARD_RIGHTS_REQUIRED | windows.SECTION_QUERY | windows.SECTION_MAP_READ, + null, + null, + windows.PAGE_READONLY, + // The documentation states that if no AllocationAttribute is specified, then SEC_COMMIT is the default. + // In practice, this isn't the case and specifying 0 will result in INVALID_PARAMETER_6. + windows.SEC_COMMIT, + coff_file.handle, + ); + if (create_section_rc != .SUCCESS) return error.MissingDebugInfo; + errdefer windows.CloseHandle(section_handle); + + var coff_len: usize = 0; + var base_ptr: usize = 0; + const map_section_rc = windows.ntdll.NtMapViewOfSection( + section_handle, + process_handle, + @ptrCast(&base_ptr), + null, + 0, + null, + &coff_len, + .ViewUnmap, + 0, + windows.PAGE_READONLY, + ); + if (map_section_rc != .SUCCESS) return error.MissingDebugInfo; + errdefer assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @ptrFromInt(base_ptr)) == .SUCCESS); + + const section_view = @as([*]const u8, @ptrFromInt(base_ptr))[0..coff_len]; + coff_obj = try coff.Coff.init(section_view); + + module.mapped_file = .{ + .file = coff_file, + .section_handle = section_handle, + .section_view = section_view, + }; + } + errdefer if (module.mapped_file) |mapped_file| mapped_file.deinit(); + + obj_di.* = try readCoffDebugInfo(self.allocator, &coff_obj); obj_di.base_address = module.base_address; try self.address_map.putNoClobber(module.base_address, obj_di); @@ -1352,6 +1776,44 @@ pub const DebugInfo = struct { return null; } + fn lookupModuleNameDl(self: *DebugInfo, address: usize) ?[]const u8 { + _ = self; + + var ctx: struct { + // Input + address: usize, + // Output + name: []const u8 = "", + } = .{ .address = address }; + const CtxTy = @TypeOf(ctx); + + if (os.dl_iterate_phdr(&ctx, error{Found}, struct { + fn callback(info: *os.dl_phdr_info, size: usize, context: *CtxTy) !void { + _ = size; + 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) { + context.name = mem.sliceTo(info.dlpi_name, 0) orelse ""; + break; + } + } else return; + + return error.Found; + } + }.callback)) { + return null; + } else |err| switch (err) { + error.Found => return fs.path.basename(ctx.name), + } + + return null; + } + fn lookupModuleDl(self: *DebugInfo, address: usize) !*ModuleDebugInfo { var ctx: struct { // Input @@ -1359,6 +1821,8 @@ pub const DebugInfo = struct { // Output base_address: usize = undefined, name: []const u8 = undefined, + build_id: ?[]const u8 = null, + gnu_eh_frame: ?[]const u8 = null, } = .{ .address = address }; const CtxTy = @TypeOf(ctx); @@ -1373,18 +1837,40 @@ pub const DebugInfo = struct { for (phdrs) |*phdr| { if (phdr.p_type != elf.PT_LOAD) continue; - const seg_start = info.dlpi_addr + phdr.p_vaddr; + // Overflowing addition is used to handle the case of VSDOs having a p_vaddr = 0xffffffffff700000 + 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.sliceTo(info.dlpi_name, 0) orelse ""; context.base_address = info.dlpi_addr; - // Stop the iteration - return error.Found; + break; + } + } else return; + + for (info.dlpi_phdr[0..info.dlpi_phnum]) |phdr| { + switch (phdr.p_type) { + elf.PT_NOTE => { + // Look for .note.gnu.build-id + const note_bytes = @as([*]const u8, @ptrFromInt(info.dlpi_addr + phdr.p_vaddr))[0..phdr.p_memsz]; + const name_size = mem.readIntSliceNative(u32, note_bytes[0..4]); + if (name_size != 4) continue; + const desc_size = mem.readIntSliceNative(u32, note_bytes[4..8]); + const note_type = mem.readIntSliceNative(u32, note_bytes[8..12]); + if (note_type != elf.NT_GNU_BUILD_ID) continue; + if (!mem.eql(u8, "GNU\x00", note_bytes[12..16])) continue; + context.build_id = note_bytes[16..][0..desc_size]; + }, + elf.PT_GNU_EH_FRAME => { + context.gnu_eh_frame = @as([*]const u8, @ptrFromInt(info.dlpi_addr + phdr.p_vaddr))[0..phdr.p_memsz]; + }, + else => {}, } } + + // Stop the iteration + return error.Found; } }.callback)) { return error.MissingDebugInfo; @@ -1399,20 +1885,24 @@ pub const DebugInfo = struct { 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, - }; + var sections: DW.DwarfInfo.SectionArray = DW.DwarfInfo.null_section_array; + if (ctx.gnu_eh_frame) |eh_frame_hdr| { + // This is a special case - pointer offsets inside .eh_frame_hdr + // are encoded relative to its base address, so we must use the + // version that is already memory mapped, and not the one that + // will be mapped separately from the ELF file. + sections[@intFromEnum(DW.DwarfSection.eh_frame_hdr)] = .{ + .data = eh_frame_hdr, + .owned = false, + }; + } - obj_di.* = try readElfDebugInfo(self.allocator, elf_file); + obj_di.* = try readElfDebugInfo(self.allocator, if (ctx.name.len > 0) ctx.name else null, ctx.build_id, null, §ions, null); obj_di.base_address = ctx.base_address; + // Missing unwind info isn't treated as a failure, as the unwinder will fall back to FP-based unwinding + obj_di.dwarf.scanAllUnwindInfo(self.allocator, ctx.base_address) catch {}; + try self.address_map.putNoClobber(ctx.base_address, obj_di); return obj_di; @@ -1434,11 +1924,16 @@ pub const DebugInfo = struct { pub const ModuleDebugInfo = switch (native_os) { .macos, .ios, .watchos, .tvos => struct { base_address: usize, + vmaddr_slide: usize, mapped_memory: []align(mem.page_size) const u8, symbols: []const MachoSymbol, strings: [:0]const u8, ofiles: OFileTable, + // Backed by the in-memory sections mapped by the loader + unwind_info: ?[]const u8 = null, + eh_frame: ?[]const u8 = null, + const OFileTable = std.StringHashMap(OFileInfo); const OFileInfo = struct { di: DW.DwarfInfo, @@ -1457,7 +1952,7 @@ pub const ModuleDebugInfo = switch (native_os) { os.munmap(self.mapped_memory); } - fn loadOFile(self: *@This(), allocator: mem.Allocator, o_file_path: []const u8) !OFileInfo { + fn loadOFile(self: *@This(), allocator: mem.Allocator, o_file_path: []const u8) !*OFileInfo { const o_file = try fs.cwd().openFile(o_file_path, .{ .intended_io_mode = .blocking }); const mapped_mem = try mapWholeFile(o_file); @@ -1500,95 +1995,40 @@ pub const ModuleDebugInfo = switch (native_os) { addr_table.putAssumeCapacityNoClobber(sym_name, sym.n_value); } - var opt_debug_line: ?macho.section_64 = null; - var opt_debug_info: ?macho.section_64 = null; - var opt_debug_abbrev: ?macho.section_64 = null; - var opt_debug_str: ?macho.section_64 = null; - var opt_debug_str_offsets: ?macho.section_64 = null; - var opt_debug_line_str: ?macho.section_64 = null; - var opt_debug_ranges: ?macho.section_64 = null; - var opt_debug_loclists: ?macho.section_64 = null; - var opt_debug_rnglists: ?macho.section_64 = null; - var opt_debug_addr: ?macho.section_64 = null; - var opt_debug_names: ?macho.section_64 = null; - var opt_debug_frame: ?macho.section_64 = null; + var sections: DW.DwarfInfo.SectionArray = DW.DwarfInfo.null_section_array; + if (self.eh_frame) |eh_frame| sections[@intFromEnum(DW.DwarfSection.eh_frame)] = .{ + .data = eh_frame, + .owned = false, + }; for (segcmd.?.getSections()) |sect| { - const name = sect.sectName(); - 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_str_offsets")) { - opt_debug_str_offsets = sect; - } else if (mem.eql(u8, name, "__debug_line_str")) { - opt_debug_line_str = sect; - } else if (mem.eql(u8, name, "__debug_ranges")) { - opt_debug_ranges = sect; - } else if (mem.eql(u8, name, "__debug_loclists")) { - opt_debug_loclists = sect; - } else if (mem.eql(u8, name, "__debug_rnglists")) { - opt_debug_rnglists = sect; - } else if (mem.eql(u8, name, "__debug_addr")) { - opt_debug_addr = sect; - } else if (mem.eql(u8, name, "__debug_names")) { - opt_debug_names = sect; - } else if (mem.eql(u8, name, "__debug_frame")) { - opt_debug_frame = sect; + if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue; + + var section_index: ?usize = null; + inline for (@typeInfo(DW.DwarfSection).Enum.fields, 0..) |section, i| { + if (mem.eql(u8, "__" ++ section.name, sect.sectName())) section_index = i; } + if (section_index == null) continue; + + const section_bytes = try chopSlice(mapped_mem, sect.offset, sect.size); + sections[section_index.?] = .{ + .data = section_bytes, + .virtual_address = sect.addr, + .owned = false, + }; } - 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; + const missing_debug_info = + sections[@intFromEnum(DW.DwarfSection.debug_info)] == null or + sections[@intFromEnum(DW.DwarfSection.debug_abbrev)] == null or + sections[@intFromEnum(DW.DwarfSection.debug_str)] == null or + sections[@intFromEnum(DW.DwarfSection.debug_line)] == null; + if (missing_debug_info) 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_str_offsets = if (opt_debug_str_offsets) |debug_str_offsets| - try chopSlice(mapped_mem, debug_str_offsets.offset, debug_str_offsets.size) - else - null, - .debug_line = try chopSlice(mapped_mem, debug_line.offset, debug_line.size), - .debug_line_str = if (opt_debug_line_str) |debug_line_str| - try chopSlice(mapped_mem, debug_line_str.offset, debug_line_str.size) - else - null, - .debug_ranges = if (opt_debug_ranges) |debug_ranges| - try chopSlice(mapped_mem, debug_ranges.offset, debug_ranges.size) - else - null, - .debug_loclists = if (opt_debug_loclists) |debug_loclists| - try chopSlice(mapped_mem, debug_loclists.offset, debug_loclists.size) - else - null, - .debug_rnglists = if (opt_debug_rnglists) |debug_rnglists| - try chopSlice(mapped_mem, debug_rnglists.offset, debug_rnglists.size) - else - null, - .debug_addr = if (opt_debug_addr) |debug_addr| - try chopSlice(mapped_mem, debug_addr.offset, debug_addr.size) - else - null, - .debug_names = if (opt_debug_names) |debug_names| - try chopSlice(mapped_mem, debug_names.offset, debug_names.size) - else - null, - .debug_frame = if (opt_debug_frame) |debug_frame| - try chopSlice(mapped_mem, debug_frame.offset, debug_frame.size) - else - null, + .sections = sections, + .is_macho = true, }; try DW.openDwarfDebugInfo(&di, allocator); @@ -1598,52 +2038,38 @@ pub const ModuleDebugInfo = switch (native_os) { }; // Add the debug info to the cache - try self.ofiles.putNoClobber(o_file_path, info); + const result = try self.ofiles.getOrPut(o_file_path); + assert(!result.found_existing); + result.value_ptr.* = info; - return info; + return result.value_ptr; } pub fn getSymbolAtAddress(self: *@This(), allocator: mem.Allocator, address: usize) !SymbolInfo { nosuspend { - // Translate the VA into an address into this object - const relocated_address = address - self.base_address; - - // Find the .o file where this symbol is defined - const symbol = machoSearchSymbols(self.symbols, relocated_address) orelse - return SymbolInfo{}; - const addr_off = relocated_address - symbol.addr; + const result = try self.getOFileInfoForAddress(allocator, address); + if (result.symbol == null) return .{}; // 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.sliceTo(self.strings[symbol.strx..], 0); - const o_file_path = mem.sliceTo(self.strings[symbol.ofile..], 0); - - // Check if its debug infos are already in the cache - var o_file_info = self.ofiles.get(o_file_path) orelse - (self.loadOFile(allocator, o_file_path) catch |err| switch (err) { - error.FileNotFound, - error.MissingDebugInfo, - error.InvalidDebugInfo, - => { - return SymbolInfo{ .symbol_name = stab_symbol }; - }, - else => return err, - }); - const o_file_di = &o_file_info.di; + const stab_symbol = mem.sliceTo(self.strings[result.symbol.?.strx..], 0); + if (result.o_file_info == null) return .{ .symbol_name = stab_symbol }; // Translate again the address, this time into an address inside the // .o file - const relocated_address_o = o_file_info.addr_table.get(stab_symbol) orelse return SymbolInfo{ + const relocated_address_o = result.o_file_info.?.addr_table.get(stab_symbol) orelse return .{ .symbol_name = "???", }; + const addr_off = result.relocated_address - result.symbol.?.addr; + const o_file_di = &result.o_file_info.?.di; 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, - o_file_di.debug_str, + o_file_di.section(.debug_str), compile_unit.*, ) catch |err| switch (err) { error.MissingDebugInfo, error.InvalidDebugInfo => "???", @@ -1663,39 +2089,61 @@ pub const ModuleDebugInfo = switch (native_os) { }, else => return err, } + } + } + + pub fn getOFileInfoForAddress(self: *@This(), allocator: mem.Allocator, address: usize) !struct { + relocated_address: usize, + symbol: ?*const MachoSymbol = null, + o_file_info: ?*OFileInfo = null, + } { + nosuspend { + // Translate the VA into an address into this object + const relocated_address = address - self.vmaddr_slide; + + // Find the .o file where this symbol is defined + const symbol = machoSearchSymbols(self.symbols, relocated_address) orelse return .{ + .relocated_address = relocated_address, + }; + + // Check if its debug infos are already in the cache + const o_file_path = mem.sliceTo(self.strings[symbol.ofile..], 0); + var o_file_info = self.ofiles.getPtr(o_file_path) orelse + (self.loadOFile(allocator, o_file_path) catch |err| switch (err) { + error.FileNotFound, + error.MissingDebugInfo, + error.InvalidDebugInfo, + => return .{ + .relocated_address = relocated_address, + .symbol = symbol, + }, + else => return err, + }); - unreachable; + return .{ + .relocated_address = relocated_address, + .symbol = symbol, + .o_file_info = o_file_info, + }; } } + + pub fn getDwarfInfoForAddress(self: *@This(), allocator: mem.Allocator, address: usize) !?*const DW.DwarfInfo { + return if ((try self.getOFileInfoForAddress(allocator, address)).o_file_info) |o_file_info| &o_file_info.di else null; + } }, .uefi, .windows => struct { base_address: usize, debug_data: PdbOrDwarf, coff_image_base: u64, + /// Only used if debug_data is .pdb coff_section_headers: []coff.SectionHeader, fn deinit(self: *@This(), allocator: mem.Allocator) void { - switch (self.debug_data) { - .dwarf => |*dwarf| { - allocator.free(dwarf.debug_info); - allocator.free(dwarf.debug_abbrev); - allocator.free(dwarf.debug_str); - allocator.free(dwarf.debug_line); - if (dwarf.debug_str_offsets) |d| allocator.free(d); - if (dwarf.debug_line_str) |d| allocator.free(d); - if (dwarf.debug_ranges) |d| allocator.free(d); - if (dwarf.debug_loclists) |d| allocator.free(d); - if (dwarf.debug_rnglists) |d| allocator.free(d); - if (dwarf.debug_addr) |d| allocator.free(d); - if (dwarf.debug_names) |d| allocator.free(d); - if (dwarf.debug_frame) |d| allocator.free(d); - }, - .pdb => { - allocator.free(self.coff_section_headers); - }, - } - self.debug_data.deinit(allocator); + if (self.debug_data == .pdb) { + allocator.free(self.coff_section_headers); + } } pub fn getSymbolAtAddress(self: *@This(), allocator: mem.Allocator, address: usize) !SymbolInfo { @@ -1747,15 +2195,27 @@ pub const ModuleDebugInfo = switch (native_os) { .line_info = opt_line_info, }; } + + pub fn getDwarfInfoForAddress(self: *@This(), allocator: mem.Allocator, address: usize) !?*const DW.DwarfInfo { + _ = allocator; + _ = address; + + return switch (self.debug_data) { + .dwarf => |*dwarf| dwarf, + else => null, + }; + } }, .linux, .netbsd, .freebsd, .dragonfly, .openbsd, .haiku, .solaris => struct { base_address: usize, dwarf: DW.DwarfInfo, mapped_memory: []align(mem.page_size) const u8, + external_mapped_memory: ?[]align(mem.page_size) const u8, fn deinit(self: *@This(), allocator: mem.Allocator) void { self.dwarf.deinit(allocator); os.munmap(self.mapped_memory); + if (self.external_mapped_memory) |m| os.munmap(m); } pub fn getSymbolAtAddress(self: *@This(), allocator: mem.Allocator, address: usize) !SymbolInfo { @@ -1763,6 +2223,12 @@ pub const ModuleDebugInfo = switch (native_os) { const relocated_address = address - self.base_address; return getSymbolFromDwarf(allocator, relocated_address, &self.dwarf); } + + pub fn getDwarfInfoForAddress(self: *@This(), allocator: mem.Allocator, address: usize) !?*const DW.DwarfInfo { + _ = allocator; + _ = address; + return &self.dwarf; + } }, .wasi => struct { fn deinit(self: *@This(), allocator: mem.Allocator) void { @@ -1776,6 +2242,13 @@ pub const ModuleDebugInfo = switch (native_os) { _ = address; return SymbolInfo{}; } + + pub fn getDwarfInfoForAddress(self: *@This(), allocator: mem.Allocator, address: usize) !?*const DW.DwarfInfo { + _ = self; + _ = allocator; + _ = address; + return null; + } }, else => DW.DwarfInfo, }; @@ -1784,7 +2257,7 @@ fn getSymbolFromDwarf(allocator: mem.Allocator, address: u64, di: *DW.DwarfInfo) if (nosuspend di.findCompileUnit(address)) |compile_unit| { return SymbolInfo{ .symbol_name = nosuspend di.getSymbolName(address) orelse "???", - .compile_unit_name = compile_unit.die.getAttrString(di, DW.AT.name, di.debug_str, compile_unit.*) catch |err| switch (err) { + .compile_unit_name = compile_unit.die.getAttrString(di, DW.AT.name, di.section(.debug_str), compile_unit.*) catch |err| switch (err) { error.MissingDebugInfo, error.InvalidDebugInfo => "???", }, .line_info = nosuspend di.getLineNumberInfo(allocator, compile_unit.*, address) catch |err| switch (err) { @@ -1932,52 +2405,13 @@ fn dumpSegfaultInfoPosix(sig: i32, addr: usize, ctx_ptr: ?*const anyopaque) void } catch os.abort(); switch (native_arch) { - .x86 => { - const ctx: *const os.ucontext_t = @ptrCast(@alignCast(ctx_ptr)); - const ip = @as(usize, @intCast(ctx.mcontext.gregs[os.REG.EIP])); - const bp = @as(usize, @intCast(ctx.mcontext.gregs[os.REG.EBP])); - dumpStackTraceFromBase(bp, ip); - }, - .x86_64 => { - const ctx: *const os.ucontext_t = @ptrCast(@alignCast(ctx_ptr)); - const ip = switch (native_os) { - .linux, .netbsd, .solaris => @as(usize, @intCast(ctx.mcontext.gregs[os.REG.RIP])), - .freebsd => @as(usize, @intCast(ctx.mcontext.rip)), - .openbsd => @as(usize, @intCast(ctx.sc_rip)), - .macos => @as(usize, @intCast(ctx.mcontext.ss.rip)), - else => unreachable, - }; - const bp = switch (native_os) { - .linux, .netbsd, .solaris => @as(usize, @intCast(ctx.mcontext.gregs[os.REG.RBP])), - .openbsd => @as(usize, @intCast(ctx.sc_rbp)), - .freebsd => @as(usize, @intCast(ctx.mcontext.rbp)), - .macos => @as(usize, @intCast(ctx.mcontext.ss.rbp)), - else => unreachable, - }; - dumpStackTraceFromBase(bp, ip); - }, - .arm => { + .x86, + .x86_64, + .arm, + .aarch64, + => { const ctx: *const os.ucontext_t = @ptrCast(@alignCast(ctx_ptr)); - const ip = @as(usize, @intCast(ctx.mcontext.arm_pc)); - const bp = @as(usize, @intCast(ctx.mcontext.arm_fp)); - dumpStackTraceFromBase(bp, ip); - }, - .aarch64 => { - const ctx: *const os.ucontext_t = @ptrCast(@alignCast(ctx_ptr)); - const ip = switch (native_os) { - .macos => @as(usize, @intCast(ctx.mcontext.ss.pc)), - .netbsd => @as(usize, @intCast(ctx.mcontext.gregs[os.REG.PC])), - .freebsd => @as(usize, @intCast(ctx.mcontext.gpregs.elr)), - else => @as(usize, @intCast(ctx.mcontext.pc)), - }; - // x29 is the ABI-designated frame pointer - const bp = switch (native_os) { - .macos => @as(usize, @intCast(ctx.mcontext.ss.fp)), - .netbsd => @as(usize, @intCast(ctx.mcontext.gregs[os.REG.FP])), - .freebsd => @as(usize, @intCast(ctx.mcontext.gpregs.x[os.REG.FP])), - else => @as(usize, @intCast(ctx.mcontext.regs[29])), - }; - dumpStackTraceFromBase(bp, ip); + dumpStackTraceFromBase(ctx); }, else => {}, } @@ -2036,16 +2470,15 @@ fn handleSegfaultWindowsExtra( } fn dumpSegfaultInfoWindows(info: *windows.EXCEPTION_POINTERS, msg: u8, label: ?[]const u8) void { - const regs = info.ContextRecord.getRegs(); const stderr = io.getStdErr().writer(); _ = switch (msg) { 0 => stderr.print("{s}\n", .{label.?}), 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}), + 2 => stderr.print("Illegal instruction at address 0x{x}\n", .{info.ContextRecord.getRegs().ip}), else => unreachable, } catch os.abort(); - dumpStackTraceFromBase(regs.bp, regs.ip); + dumpStackTraceFromBase(info.ContextRecord); } pub fn dumpStackPointerAddr(prefix: []const u8) void { diff --git a/lib/std/dwarf.zig b/lib/std/dwarf.zig index aa1ac6959fd4..639772cf6ed1 100644 --- a/lib/std/dwarf.zig +++ b/lib/std/dwarf.zig @@ -3,9 +3,11 @@ const std = @import("std.zig"); const debug = std.debug; const fs = std.fs; const io = std.io; +const os = std.os; const mem = std.mem; const math = std.math; const leb = @import("leb128.zig"); +const assert = std.debug.assert; pub const TAG = @import("dwarf/TAG.zig"); pub const AT = @import("dwarf/AT.zig"); @@ -13,6 +15,10 @@ pub const OP = @import("dwarf/OP.zig"); pub const LANG = @import("dwarf/LANG.zig"); pub const FORM = @import("dwarf/FORM.zig"); pub const ATE = @import("dwarf/ATE.zig"); +pub const EH = @import("dwarf/EH.zig"); +pub const abi = @import("dwarf/abi.zig"); +pub const call_frame = @import("dwarf/call_frame.zig"); +pub const expressions = @import("dwarf/expressions.zig"); pub const LLE = struct { pub const end_of_list = 0x00; @@ -140,11 +146,11 @@ pub const CC = enum(u8) { pass_by_reference = 0x4, pass_by_value = 0x5, - lo_user = 0x40, - hi_user = 0xff, - GNU_renesas_sh = 0x40, GNU_borland_fastcall_i386 = 0x41, + + pub const lo_user = 0x40; + pub const hi_user = 0xff; }; pub const Format = enum { @"32", @"64" }; @@ -157,15 +163,9 @@ const PcRange = struct { const Func = struct { pc_range: ?PcRange, name: ?[]const u8, - - fn deinit(func: *Func, allocator: mem.Allocator) void { - if (func.name) |name| { - allocator.free(name); - } - } }; -const CompileUnit = struct { +pub const CompileUnit = struct { version: u16, is_64: bool, die: *Die, @@ -175,6 +175,7 @@ const CompileUnit = struct { addr_base: usize, rnglists_base: usize, loclists_base: usize, + frame_base: ?*const FormValue, }; const AbbrevTable = std.ArrayList(AbbrevTableEntry); @@ -210,7 +211,7 @@ const AbbrevAttr = struct { payload: i64, }; -const FormValue = union(enum) { +pub const FormValue = union(enum) { Address: u64, AddrOffset: usize, Block: []u8, @@ -292,7 +293,7 @@ const Die = struct { fn getAttrAddr( self: *const Die, - di: *DwarfInfo, + di: *const DwarfInfo, id: u64, compile_unit: CompileUnit, ) error{ InvalidDebugInfo, MissingDebugInfo }!u64 { @@ -337,7 +338,7 @@ const Die = struct { FormValue.String => |value| return value, FormValue.StrPtr => |offset| return di.getString(offset), FormValue.StrOffset => |index| { - const debug_str_offsets = di.debug_str_offsets orelse return badDwarf(); + const debug_str_offsets = di.section(.debug_str_offsets) orelse return badDwarf(); if (compile_unit.str_offsets_base == 0) return badDwarf(); if (compile_unit.is_64) { const byte_offset = compile_unit.str_offsets_base + 8 * index; @@ -642,27 +643,75 @@ fn getAbbrevTableEntry(abbrev_table: *const AbbrevTable, abbrev_code: u64) ?*con return null; } +pub const DwarfSection = enum { + debug_info, + debug_abbrev, + debug_str, + debug_str_offsets, + debug_line, + debug_line_str, + debug_ranges, + debug_loclists, + debug_rnglists, + debug_addr, + debug_names, + debug_frame, + eh_frame, + eh_frame_hdr, +}; + pub const DwarfInfo = struct { + pub const Section = struct { + data: []const u8, + // Module-relative virtual address. + // Only set if the section data was loaded from disk. + virtual_address: ?usize = null, + // If `data` is owned by this DwarfInfo. + owned: bool, + + // For sections that are not memory mapped by the loader, this is an offset + // from `data.ptr` to where the section would have been mapped. Otherwise, + // `data` is directly backed by the section and the offset is zero. + pub fn virtualOffset(self: Section, base_address: usize) i64 { + return if (self.virtual_address) |va| + @as(i64, @intCast(base_address + va)) - + @as(i64, @intCast(@intFromPtr(self.data.ptr))) + else + 0; + } + }; + + const num_sections = std.enums.directEnumArrayLen(DwarfSection, 0); + pub const SectionArray = [num_sections]?Section; + pub const null_section_array = [_]?Section{null} ** num_sections; + endian: std.builtin.Endian, - // No memory is owned by the DwarfInfo - debug_info: []const u8, - debug_abbrev: []const u8, - debug_str: []const u8, - debug_str_offsets: ?[]const u8, - debug_line: []const u8, - debug_line_str: ?[]const u8, - debug_ranges: ?[]const u8, - debug_loclists: ?[]const u8, - debug_rnglists: ?[]const u8, - debug_addr: ?[]const u8, - debug_names: ?[]const u8, - debug_frame: ?[]const u8, + sections: SectionArray = null_section_array, + is_macho: bool, + // Filled later by the initializer abbrev_table_list: std.ArrayListUnmanaged(AbbrevTableHeader) = .{}, compile_unit_list: std.ArrayListUnmanaged(CompileUnit) = .{}, func_list: std.ArrayListUnmanaged(Func) = .{}, + eh_frame_hdr: ?ExceptionFrameHeader = null, + // These lookup tables are only used if `eh_frame_hdr` is null + cie_map: std.AutoArrayHashMapUnmanaged(u64, CommonInformationEntry) = .{}, + // Sorted by start_pc + fde_list: std.ArrayListUnmanaged(FrameDescriptionEntry) = .{}, + + pub fn section(di: DwarfInfo, dwarf_section: DwarfSection) ?[]const u8 { + return if (di.sections[@intFromEnum(dwarf_section)]) |s| s.data else null; + } + + pub fn sectionVirtualOffset(di: DwarfInfo, dwarf_section: DwarfSection, base_address: usize) ?i64 { + return if (di.sections[@intFromEnum(dwarf_section)]) |s| s.virtualOffset(base_address) else null; + } + pub fn deinit(di: *DwarfInfo, allocator: mem.Allocator) void { + for (di.sections) |opt_section| { + if (opt_section) |s| if (s.owned) allocator.free(s.data); + } for (di.abbrev_table_list.items) |*abbrev| { abbrev.deinit(); } @@ -672,10 +721,9 @@ pub const DwarfInfo = struct { allocator.destroy(cu.die); } di.compile_unit_list.deinit(allocator); - for (di.func_list.items) |*func| { - func.deinit(allocator); - } di.func_list.deinit(allocator); + di.cie_map.deinit(allocator); + di.fde_list.deinit(allocator); } pub fn getSymbolName(di: *DwarfInfo, address: u64) ?[]const u8 { @@ -691,7 +739,7 @@ pub const DwarfInfo = struct { } fn scanAllFunctions(di: *DwarfInfo, allocator: mem.Allocator) !void { - var stream = io.fixedBufferStream(di.debug_info); + var stream = io.fixedBufferStream(di.section(.debug_info).?); const in = stream.reader(); const seekable = &stream.seekableStream(); var this_unit_offset: u64 = 0; @@ -755,6 +803,7 @@ pub const DwarfInfo = struct { .addr_base = if (die_obj.getAttr(AT.addr_base)) |fv| try fv.getUInt(usize) else 0, .rnglists_base = if (die_obj.getAttr(AT.rnglists_base)) |fv| try fv.getUInt(usize) else 0, .loclists_base = if (die_obj.getAttr(AT.loclists_base)) |fv| try fv.getUInt(usize) else 0, + .frame_base = die_obj.getAttr(AT.frame_base), }; }, TAG.subprogram, TAG.inlined_subroutine, TAG.subroutine, TAG.entry_point => { @@ -764,8 +813,7 @@ pub const DwarfInfo = struct { // Prevent endless loops while (depth > 0) : (depth -= 1) { if (this_die_obj.getAttr(AT.name)) |_| { - const name = try this_die_obj.getAttrString(di, AT.name, di.debug_str, compile_unit); - break :x try allocator.dupe(u8, name); + break :x try this_die_obj.getAttrString(di, AT.name, di.section(.debug_str), compile_unit); } else if (this_die_obj.getAttr(AT.abstract_origin)) |_| { // Follow the DIE it points to and repeat const ref_offset = try this_die_obj.getAttrRef(AT.abstract_origin); @@ -796,34 +844,58 @@ pub const DwarfInfo = struct { break :x null; }; - const pc_range = x: { - if (die_obj.getAttrAddr(di, AT.low_pc, compile_unit)) |low_pc| { - if (die_obj.getAttr(AT.high_pc)) |high_pc_value| { - const pc_end = switch (high_pc_value.*) { - FormValue.Address => |value| value, - FormValue.Const => |value| b: { - const offset = try value.asUnsignedLe(); - break :b (low_pc + offset); - }, - else => return badDwarf(), - }; - break :x PcRange{ + var range_added = if (die_obj.getAttrAddr(di, AT.low_pc, compile_unit)) |low_pc| blk: { + if (die_obj.getAttr(AT.high_pc)) |high_pc_value| { + const pc_end = switch (high_pc_value.*) { + FormValue.Address => |value| value, + FormValue.Const => |value| b: { + const offset = try value.asUnsignedLe(); + break :b (low_pc + offset); + }, + else => return badDwarf(), + }; + + try di.func_list.append(allocator, Func{ + .name = fn_name, + .pc_range = .{ .start = low_pc, .end = pc_end, - }; - } else { - break :x null; - } - } else |err| { - if (err != error.MissingDebugInfo) return err; - break :x null; + }, + }); + + break :blk true; } + + break :blk false; + } else |err| blk: { + if (err != error.MissingDebugInfo) return err; + break :blk false; }; - try di.func_list.append(allocator, Func{ - .name = fn_name, - .pc_range = pc_range, - }); + if (die_obj.getAttr(AT.ranges)) |ranges_value| blk: { + var iter = DebugRangeIterator.init(ranges_value, di, &compile_unit) catch |err| { + if (err != error.MissingDebugInfo) return err; + break :blk; + }; + + while (try iter.next()) |range| { + range_added = true; + try di.func_list.append(allocator, Func{ + .name = fn_name, + .pc_range = .{ + .start = range.start_addr, + .end = range.end_addr, + }, + }); + } + } + + if (fn_name != null and !range_added) { + try di.func_list.append(allocator, Func{ + .name = fn_name, + .pc_range = null, + }); + } }, else => {}, } @@ -836,7 +908,7 @@ pub const DwarfInfo = struct { } fn scanAllCompileUnits(di: *DwarfInfo, allocator: mem.Allocator) !void { - var stream = io.fixedBufferStream(di.debug_info); + var stream = io.fixedBufferStream(di.section(.debug_info).?); const in = &stream.reader(); const seekable = &stream.seekableStream(); var this_unit_offset: u64 = 0; @@ -892,6 +964,7 @@ pub const DwarfInfo = struct { .addr_base = if (compile_unit_die.getAttr(AT.addr_base)) |fv| try fv.getUInt(usize) else 0, .rnglists_base = if (compile_unit_die.getAttr(AT.rnglists_base)) |fv| try fv.getUInt(usize) else 0, .loclists_base = if (compile_unit_die.getAttr(AT.loclists_base)) |fv| try fv.getUInt(usize) else 0, + .frame_base = compile_unit_die.getAttr(AT.frame_base), }; compile_unit.pc_range = x: { @@ -924,17 +997,18 @@ pub const DwarfInfo = struct { } } - pub fn findCompileUnit(di: *DwarfInfo, target_address: u64) !*const CompileUnit { - for (di.compile_unit_list.items) |*compile_unit| { - if (compile_unit.pc_range) |range| { - if (target_address >= range.start and target_address < range.end) return compile_unit; - } + const DebugRangeIterator = struct { + base_address: u64, + section_type: DwarfSection, + di: *const DwarfInfo, + compile_unit: *const CompileUnit, + stream: io.FixedBufferStream([]const u8), - const opt_debug_ranges = if (compile_unit.version >= 5) di.debug_rnglists else di.debug_ranges; - const debug_ranges = opt_debug_ranges orelse continue; + pub fn init(ranges_value: *const FormValue, di: *const DwarfInfo, compile_unit: *const CompileUnit) !@This() { + const section_type = if (compile_unit.version >= 5) DwarfSection.debug_rnglists else DwarfSection.debug_ranges; + const debug_ranges = di.section(section_type) orelse return error.MissingDebugInfo; - const ranges_val = compile_unit.die.getAttr(AT.ranges) orelse continue; - const ranges_offset = switch (ranges_val.*) { + const ranges_offset = switch (ranges_value.*) { .SecOffset => |off| off, .Const => |c| try c.asUnsignedLe(), .RangeListOffset => |idx| off: { @@ -954,8 +1028,7 @@ pub const DwarfInfo = struct { }; var stream = io.fixedBufferStream(debug_ranges); - const in = &stream.reader(); - const seekable = &stream.seekableStream(); + try stream.seekTo(ranges_offset); // All the addresses in the list are relative to the value // specified by DW_AT.low_pc or to some other value encoded @@ -966,86 +1039,122 @@ pub const DwarfInfo = struct { else => return err, }; - try seekable.seekTo(ranges_offset); + return .{ + .base_address = base_address, + .section_type = section_type, + .di = di, + .compile_unit = compile_unit, + .stream = stream, + }; + } - if (compile_unit.version >= 5) { - while (true) { + // Returns the next range in the list, or null if the end was reached. + pub fn next(self: *@This()) !?struct { start_addr: u64, end_addr: u64 } { + const in = self.stream.reader(); + switch (self.section_type) { + .debug_rnglists => { const kind = try in.readByte(); switch (kind) { - RLE.end_of_list => break, + RLE.end_of_list => return null, RLE.base_addressx => { const index = try leb.readULEB128(usize, in); - base_address = try di.readDebugAddr(compile_unit.*, index); + self.base_address = try self.di.readDebugAddr(self.compile_unit.*, index); + return try self.next(); }, RLE.startx_endx => { const start_index = try leb.readULEB128(usize, in); - const start_addr = try di.readDebugAddr(compile_unit.*, start_index); + const start_addr = try self.di.readDebugAddr(self.compile_unit.*, start_index); const end_index = try leb.readULEB128(usize, in); - const end_addr = try di.readDebugAddr(compile_unit.*, end_index); + const end_addr = try self.di.readDebugAddr(self.compile_unit.*, end_index); - if (target_address >= start_addr and target_address < end_addr) { - return compile_unit; - } + return .{ + .start_addr = start_addr, + .end_addr = end_addr, + }; }, RLE.startx_length => { const start_index = try leb.readULEB128(usize, in); - const start_addr = try di.readDebugAddr(compile_unit.*, start_index); + const start_addr = try self.di.readDebugAddr(self.compile_unit.*, start_index); const len = try leb.readULEB128(usize, in); const end_addr = start_addr + len; - if (target_address >= start_addr and target_address < end_addr) { - return compile_unit; - } + return .{ + .start_addr = start_addr, + .end_addr = end_addr, + }; }, RLE.offset_pair => { const start_addr = try leb.readULEB128(usize, in); const end_addr = try leb.readULEB128(usize, in); + // This is the only kind that uses the base address - if (target_address >= base_address + start_addr and target_address < base_address + end_addr) { - return compile_unit; - } + return .{ + .start_addr = self.base_address + start_addr, + .end_addr = self.base_address + end_addr, + }; }, RLE.base_address => { - base_address = try in.readInt(usize, di.endian); + self.base_address = try in.readInt(usize, self.di.endian); + return try self.next(); }, RLE.start_end => { - const start_addr = try in.readInt(usize, di.endian); - const end_addr = try in.readInt(usize, di.endian); - if (target_address >= start_addr and target_address < end_addr) { - return compile_unit; - } + const start_addr = try in.readInt(usize, self.di.endian); + const end_addr = try in.readInt(usize, self.di.endian); + + return .{ + .start_addr = start_addr, + .end_addr = end_addr, + }; }, RLE.start_length => { - const start_addr = try in.readInt(usize, di.endian); + const start_addr = try in.readInt(usize, self.di.endian); const len = try leb.readULEB128(usize, in); const end_addr = start_addr + len; - if (target_address >= start_addr and target_address < end_addr) { - return compile_unit; - } + + return .{ + .start_addr = start_addr, + .end_addr = end_addr, + }; }, else => return badDwarf(), } - } - } else { - while (true) { - const begin_addr = try in.readInt(usize, di.endian); - const end_addr = try in.readInt(usize, di.endian); - if (begin_addr == 0 and end_addr == 0) { - break; - } + }, + .debug_ranges => { + const start_addr = try in.readInt(usize, self.di.endian); + const end_addr = try in.readInt(usize, self.di.endian); + if (start_addr == 0 and end_addr == 0) return null; + // This entry selects a new value for the base address - if (begin_addr == math.maxInt(usize)) { - base_address = end_addr; - continue; + if (start_addr == math.maxInt(usize)) { + self.base_address = end_addr; + return try self.next(); } - if (target_address >= base_address + begin_addr and target_address < base_address + end_addr) { - return compile_unit; - } - } + + return .{ + .start_addr = self.base_address + start_addr, + .end_addr = self.base_address + end_addr, + }; + }, + else => unreachable, } } + }; + + pub fn findCompileUnit(di: *const DwarfInfo, target_address: u64) !*const CompileUnit { + for (di.compile_unit_list.items) |*compile_unit| { + if (compile_unit.pc_range) |range| { + if (target_address >= range.start and target_address < range.end) return compile_unit; + } + + const ranges_value = compile_unit.die.getAttr(AT.ranges) orelse continue; + var iter = DebugRangeIterator.init(ranges_value, di, compile_unit) catch continue; + while (try iter.next()) |range| { + if (target_address >= range.start_addr and target_address < range.end_addr) return compile_unit; + } + } + return missingDwarf(); } @@ -1065,7 +1174,7 @@ pub const DwarfInfo = struct { } fn parseAbbrevTable(di: *DwarfInfo, allocator: mem.Allocator, offset: u64) !AbbrevTable { - var stream = io.fixedBufferStream(di.debug_abbrev); + var stream = io.fixedBufferStream(di.section(.debug_abbrev).?); const in = &stream.reader(); const seekable = &stream.seekableStream(); @@ -1146,11 +1255,11 @@ pub const DwarfInfo = struct { compile_unit: CompileUnit, target_address: u64, ) !debug.LineInfo { - var stream = io.fixedBufferStream(di.debug_line); + var stream = io.fixedBufferStream(di.section(.debug_line).?); const in = &stream.reader(); const seekable = &stream.seekableStream(); - const compile_unit_cwd = try compile_unit.die.getAttrString(di, AT.comp_dir, di.debug_line_str, compile_unit); + const compile_unit_cwd = try compile_unit.die.getAttrString(di, AT.comp_dir, di.section(.debug_line_str), compile_unit); const line_info_offset = try compile_unit.die.getAttrSecOffset(AT.stmt_list); try seekable.seekTo(line_info_offset); @@ -1416,15 +1525,15 @@ pub const DwarfInfo = struct { } fn getString(di: DwarfInfo, offset: u64) ![]const u8 { - return getStringGeneric(di.debug_str, offset); + return getStringGeneric(di.section(.debug_str), offset); } fn getLineString(di: DwarfInfo, offset: u64) ![]const u8 { - return getStringGeneric(di.debug_line_str, offset); + return getStringGeneric(di.section(.debug_line_str), offset); } fn readDebugAddr(di: DwarfInfo, compile_unit: CompileUnit, index: u64) !u64 { - const debug_addr = di.debug_addr orelse return badDwarf(); + const debug_addr = di.section(.debug_addr) orelse return badDwarf(); // addr_base points to the first item after the header, however we // need to read the header to know the size of each item. Empirically, @@ -1448,10 +1557,689 @@ pub const DwarfInfo = struct { else => badDwarf(), }; } + + /// If .eh_frame_hdr is present, then only the header needs to be parsed. + /// + /// Otherwise, .eh_frame and .debug_frame are scanned and a sorted list + /// of FDEs is built for binary searching during unwinding. + pub fn scanAllUnwindInfo(di: *DwarfInfo, allocator: mem.Allocator, base_address: usize) !void { + if (di.section(.eh_frame_hdr)) |eh_frame_hdr| blk: { + var stream = io.fixedBufferStream(eh_frame_hdr); + const reader = stream.reader(); + + const version = try reader.readByte(); + if (version != 1) break :blk; + + const eh_frame_ptr_enc = try reader.readByte(); + if (eh_frame_ptr_enc == EH.PE.omit) break :blk; + const fde_count_enc = try reader.readByte(); + if (fde_count_enc == EH.PE.omit) break :blk; + const table_enc = try reader.readByte(); + if (table_enc == EH.PE.omit) break :blk; + + const eh_frame_ptr = std.math.cast(usize, try readEhPointer(reader, eh_frame_ptr_enc, @sizeOf(usize), .{ + .pc_rel_base = @intFromPtr(&eh_frame_hdr[stream.pos]), + .follow_indirect = true, + }, builtin.cpu.arch.endian()) orelse return badDwarf()) orelse return badDwarf(); + + const fde_count = std.math.cast(usize, try readEhPointer(reader, fde_count_enc, @sizeOf(usize), .{ + .pc_rel_base = @intFromPtr(&eh_frame_hdr[stream.pos]), + .follow_indirect = true, + }, builtin.cpu.arch.endian()) orelse return badDwarf()) orelse return badDwarf(); + + const entry_size = try ExceptionFrameHeader.entrySize(table_enc); + const entries_len = fde_count * entry_size; + if (entries_len > eh_frame_hdr.len - stream.pos) return badDwarf(); + + di.eh_frame_hdr = .{ + .eh_frame_ptr = eh_frame_ptr, + .table_enc = table_enc, + .fde_count = fde_count, + .entries = eh_frame_hdr[stream.pos..][0..entries_len], + }; + + // No need to scan .eh_frame, we have a binary search table already + return; + } + + const frame_sections = [2]DwarfSection{ .eh_frame, .debug_frame }; + for (frame_sections) |frame_section| { + if (di.section(frame_section)) |section_data| { + var stream = io.fixedBufferStream(section_data); + while (stream.pos < stream.buffer.len) { + const entry_header = try EntryHeader.read(&stream, frame_section, di.endian); + switch (entry_header.type) { + .cie => { + const cie = try CommonInformationEntry.parse( + entry_header.entry_bytes, + di.sectionVirtualOffset(frame_section, base_address).?, + true, + entry_header.is_64, + frame_section, + entry_header.length_offset, + @sizeOf(usize), + di.endian, + ); + try di.cie_map.put(allocator, entry_header.length_offset, cie); + }, + .fde => |cie_offset| { + const cie = di.cie_map.get(cie_offset) orelse return badDwarf(); + const fde = try FrameDescriptionEntry.parse( + entry_header.entry_bytes, + di.sectionVirtualOffset(frame_section, base_address).?, + true, + cie, + @sizeOf(usize), + di.endian, + ); + try di.fde_list.append(allocator, fde); + }, + .terminator => break, + } + } + + std.mem.sort(FrameDescriptionEntry, di.fde_list.items, {}, struct { + fn lessThan(ctx: void, a: FrameDescriptionEntry, b: FrameDescriptionEntry) bool { + _ = ctx; + return a.pc_begin < b.pc_begin; + } + }.lessThan); + } + } + } + + /// Unwind a stack frame using DWARF unwinding info, updating the register context. + /// + /// If `.eh_frame_hdr` is available, it will be used to binary search for the FDE. + /// Otherwise, a linear scan of `.eh_frame` and `.debug_frame` is done to find the FDE. + /// + /// `explicit_fde_offset` is for cases where the FDE offset is known, such as when __unwind_info + /// defers unwinding to DWARF. This is an offset into the `.eh_frame` section. + pub fn unwindFrame(di: *const DwarfInfo, context: *UnwindContext, explicit_fde_offset: ?usize) !usize { + if (!comptime abi.isSupportedArch(builtin.target.cpu.arch)) return error.UnsupportedCpuArchitecture; + if (context.pc == 0) return 0; + + // Find the FDE and CIE + var cie: CommonInformationEntry = undefined; + var fde: FrameDescriptionEntry = undefined; + + if (explicit_fde_offset) |fde_offset| { + const dwarf_section: DwarfSection = .eh_frame; + const frame_section = di.section(dwarf_section) orelse return error.MissingFDE; + if (fde_offset >= frame_section.len) return error.MissingFDE; + + var stream = io.fixedBufferStream(frame_section); + try stream.seekTo(fde_offset); + + const fde_entry_header = try EntryHeader.read(&stream, dwarf_section, di.endian); + if (fde_entry_header.type != .fde) return error.MissingFDE; + + const cie_offset = fde_entry_header.type.fde; + try stream.seekTo(cie_offset); + + const cie_entry_header = try EntryHeader.read(&stream, dwarf_section, builtin.cpu.arch.endian()); + if (cie_entry_header.type != .cie) return badDwarf(); + + cie = try CommonInformationEntry.parse( + cie_entry_header.entry_bytes, + 0, + true, + cie_entry_header.is_64, + dwarf_section, + cie_entry_header.length_offset, + @sizeOf(usize), + builtin.cpu.arch.endian(), + ); + + fde = try FrameDescriptionEntry.parse( + fde_entry_header.entry_bytes, + 0, + true, + cie, + @sizeOf(usize), + builtin.cpu.arch.endian(), + ); + } else if (di.eh_frame_hdr) |header| { + const eh_frame_len = if (di.section(.eh_frame)) |eh_frame| eh_frame.len else null; + try header.findEntry( + context.isValidMemory, + eh_frame_len, + @intFromPtr(di.section(.eh_frame_hdr).?.ptr), + context.pc, + &cie, + &fde, + ); + } else { + const index = std.sort.binarySearch(FrameDescriptionEntry, context.pc, di.fde_list.items, {}, struct { + pub fn compareFn(_: void, pc: usize, mid_item: FrameDescriptionEntry) std.math.Order { + if (pc < mid_item.pc_begin) return .lt; + + const range_end = mid_item.pc_begin + mid_item.pc_range; + if (pc < range_end) return .eq; + + return .gt; + } + }.compareFn); + + fde = if (index) |i| di.fde_list.items[i] else return error.MissingFDE; + cie = di.cie_map.get(fde.cie_length_offset) orelse return error.MissingCIE; + } + + var expression_context = .{ + .is_64 = cie.is_64, + .isValidMemory = context.isValidMemory, + .compile_unit = di.findCompileUnit(fde.pc_begin) catch null, + .thread_context = context.thread_context, + .reg_context = context.reg_context, + .cfa = context.cfa, + }; + + context.vm.reset(); + context.reg_context.eh_frame = cie.version != 4; + context.reg_context.is_macho = di.is_macho; + + const row = try context.vm.runToNative(context.allocator, context.pc, cie, fde); + context.cfa = switch (row.cfa.rule) { + .val_offset => |offset| blk: { + const register = row.cfa.register orelse return error.InvalidCFARule; + const value = mem.readIntSliceNative(usize, try abi.regBytes(context.thread_context, register, context.reg_context)); + break :blk try call_frame.applyOffset(value, offset); + }, + .expression => |expression| blk: { + context.stack_machine.reset(); + const value = try context.stack_machine.run( + expression, + context.allocator, + expression_context, + context.cfa, + ); + + if (value) |v| { + if (v != .generic) return error.InvalidExpressionValue; + break :blk v.generic; + } else return error.NoExpressionValue; + }, + else => return error.InvalidCFARule, + }; + + if (!context.isValidMemory(context.cfa.?)) return error.InvalidCFA; + expression_context.cfa = context.cfa; + + // Buffering the modifications is done because copying the thread context is not portable, + // some implementations (ie. darwin) use internal pointers to the mcontext. + var arena = std.heap.ArenaAllocator.init(context.allocator); + defer arena.deinit(); + const update_allocator = arena.allocator(); + + const RegisterUpdate = struct { + // Backed by thread_context + dest: []u8, + // Backed by arena + src: []const u8, + prev: ?*@This(), + }; + + var update_tail: ?*RegisterUpdate = null; + var has_return_address = true; + for (context.vm.rowColumns(row)) |column| { + if (column.register) |register| { + if (register == cie.return_address_register) { + has_return_address = column.rule != .undefined; + } + + const dest = try abi.regBytes(context.thread_context, register, context.reg_context); + const src = try update_allocator.alloc(u8, dest.len); + + const prev = update_tail; + update_tail = try update_allocator.create(RegisterUpdate); + update_tail.?.* = .{ + .dest = dest, + .src = src, + .prev = prev, + }; + + try column.resolveValue( + context, + expression_context, + src, + ); + } + } + + // On all implemented architectures, the CFA is defined as being the previous frame's SP + (try abi.regValueNative(usize, context.thread_context, abi.spRegNum(context.reg_context), context.reg_context)).* = context.cfa.?; + + while (update_tail) |tail| { + @memcpy(tail.dest, tail.src); + update_tail = tail.prev; + } + + if (has_return_address) { + context.pc = abi.stripInstructionPtrAuthCode(mem.readIntSliceNative(usize, try abi.regBytes( + context.thread_context, + cie.return_address_register, + context.reg_context, + ))); + } else { + context.pc = 0; + } + + (try abi.regValueNative(usize, context.thread_context, abi.ipRegNum(), context.reg_context)).* = context.pc; + + // The call instruction will have pushed the address of the instruction that follows the call as the return address. + // This next instruction may be past the end of the function if the caller was `noreturn` (ie. the last instruction in + // the function was the call). If we were to look up an FDE entry using the return address directly, it could end up + // either not finding an FDE at all, or using the next FDE in the program, producing incorrect results. To prevent this, + // we subtract one so that the next lookup is guaranteed to land inside the + // + // The exception to this rule is signal frames, where we return execution would be returned to the instruction + // that triggered the handler. + const return_address = context.pc; + if (context.pc > 0 and !cie.isSignalFrame()) context.pc -= 1; + + return return_address; + } +}; + +/// Returns the DWARF register number for an x86_64 register number found in compact unwind info +fn compactUnwindToDwarfRegNumber(unwind_reg_number: u3) !u8 { + return switch (unwind_reg_number) { + 1 => 3, // RBX + 2 => 12, // R12 + 3 => 13, // R13 + 4 => 14, // R14 + 5 => 15, // R15 + 6 => 6, // RBP + else => error.InvalidUnwindRegisterNumber, + }; +} + +const macho = std.macho; + +/// Unwind a frame using MachO compact unwind info (from __unwind_info). +/// If the compact encoding can't encode a way to unwind a frame, it will +/// defer unwinding to DWARF, in which case `.eh_frame` will be used if available. +pub fn unwindFrameMachO(context: *UnwindContext, unwind_info: []const u8, eh_frame: ?[]const u8, module_base_address: usize) !usize { + const header = mem.bytesAsValue( + macho.unwind_info_section_header, + unwind_info[0..@sizeOf(macho.unwind_info_section_header)], + ); + const indices = mem.bytesAsSlice( + macho.unwind_info_section_header_index_entry, + unwind_info[header.indexSectionOffset..][0 .. header.indexCount * @sizeOf(macho.unwind_info_section_header_index_entry)], + ); + if (indices.len == 0) return error.MissingUnwindInfo; + + const mapped_pc = context.pc - module_base_address; + const second_level_index = blk: { + var left: usize = 0; + var len: usize = indices.len; + + while (len > 1) { + const mid = left + len / 2; + const offset = indices[mid].functionOffset; + if (mapped_pc < offset) { + len /= 2; + } else { + left = mid; + if (mapped_pc == offset) break; + len -= len / 2; + } + } + + // Last index is a sentinel containing the highest address as its functionOffset + if (indices[left].secondLevelPagesSectionOffset == 0) return error.MissingUnwindInfo; + break :blk &indices[left]; + }; + + const common_encodings = mem.bytesAsSlice( + macho.compact_unwind_encoding_t, + unwind_info[header.commonEncodingsArraySectionOffset..][0 .. header.commonEncodingsArrayCount * @sizeOf(macho.compact_unwind_encoding_t)], + ); + + const start_offset = second_level_index.secondLevelPagesSectionOffset; + const kind = mem.bytesAsValue( + macho.UNWIND_SECOND_LEVEL, + unwind_info[start_offset..][0..@sizeOf(macho.UNWIND_SECOND_LEVEL)], + ); + + const entry: struct { + function_offset: usize, + raw_encoding: u32, + } = switch (kind.*) { + .REGULAR => blk: { + const page_header = mem.bytesAsValue( + macho.unwind_info_regular_second_level_page_header, + unwind_info[start_offset..][0..@sizeOf(macho.unwind_info_regular_second_level_page_header)], + ); + + const entries = mem.bytesAsSlice( + macho.unwind_info_regular_second_level_entry, + unwind_info[start_offset + page_header.entryPageOffset ..][0 .. page_header.entryCount * @sizeOf(macho.unwind_info_regular_second_level_entry)], + ); + if (entries.len == 0) return error.InvalidUnwindInfo; + + var left: usize = 0; + var len: usize = entries.len; + while (len > 1) { + const mid = left + len / 2; + const offset = entries[mid].functionOffset; + if (mapped_pc < offset) { + len /= 2; + } else { + left = mid; + if (mapped_pc == offset) break; + len -= len / 2; + } + } + + break :blk .{ + .function_offset = entries[left].functionOffset, + .raw_encoding = entries[left].encoding, + }; + }, + .COMPRESSED => blk: { + const page_header = mem.bytesAsValue( + macho.unwind_info_compressed_second_level_page_header, + unwind_info[start_offset..][0..@sizeOf(macho.unwind_info_compressed_second_level_page_header)], + ); + + const entries = mem.bytesAsSlice( + macho.UnwindInfoCompressedEntry, + unwind_info[start_offset + page_header.entryPageOffset ..][0 .. page_header.entryCount * @sizeOf(macho.UnwindInfoCompressedEntry)], + ); + if (entries.len == 0) return error.InvalidUnwindInfo; + + var left: usize = 0; + var len: usize = entries.len; + while (len > 1) { + const mid = left + len / 2; + const offset = second_level_index.functionOffset + entries[mid].funcOffset; + if (mapped_pc < offset) { + len /= 2; + } else { + left = mid; + if (mapped_pc == offset) break; + len -= len / 2; + } + } + + const entry = entries[left]; + const function_offset = second_level_index.functionOffset + entry.funcOffset; + if (entry.encodingIndex < header.commonEncodingsArrayCount) { + if (entry.encodingIndex >= common_encodings.len) return error.InvalidUnwindInfo; + break :blk .{ + .function_offset = function_offset, + .raw_encoding = common_encodings[entry.encodingIndex], + }; + } else { + const local_index = try std.math.sub( + u8, + entry.encodingIndex, + std.math.cast(u8, header.commonEncodingsArrayCount) orelse return error.InvalidUnwindInfo, + ); + const local_encodings = mem.bytesAsSlice( + macho.compact_unwind_encoding_t, + unwind_info[start_offset + page_header.encodingsPageOffset ..][0 .. page_header.encodingsCount * @sizeOf(macho.compact_unwind_encoding_t)], + ); + if (local_index >= local_encodings.len) return error.InvalidUnwindInfo; + break :blk .{ + .function_offset = function_offset, + .raw_encoding = local_encodings[local_index], + }; + } + }, + else => return error.InvalidUnwindInfo, + }; + + if (entry.raw_encoding == 0) return error.NoUnwindInfo; + const reg_context = abi.RegisterContext{ + .eh_frame = false, + .is_macho = true, + }; + + const encoding: macho.CompactUnwindEncoding = @bitCast(entry.raw_encoding); + const new_ip = switch (builtin.cpu.arch) { + .x86_64 => switch (encoding.mode.x86_64) { + .OLD => return error.UnimplementedUnwindEncoding, + .RBP_FRAME => blk: { + const regs: [5]u3 = .{ + encoding.value.x86_64.frame.reg0, + encoding.value.x86_64.frame.reg1, + encoding.value.x86_64.frame.reg2, + encoding.value.x86_64.frame.reg3, + encoding.value.x86_64.frame.reg4, + }; + + const frame_offset = encoding.value.x86_64.frame.frame_offset * @sizeOf(usize); + var max_reg: usize = 0; + inline for (regs, 0..) |reg, i| { + if (reg > 0) max_reg = i; + } + + const fp = (try abi.regValueNative(usize, context.thread_context, abi.fpRegNum(reg_context), reg_context)).*; + const new_sp = fp + 2 * @sizeOf(usize); + + // Verify the stack range we're about to read register values from + if (!context.isValidMemory(new_sp) or !context.isValidMemory(fp - frame_offset + max_reg * @sizeOf(usize))) return error.InvalidUnwindInfo; + + const ip_ptr = fp + @sizeOf(usize); + const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*; + const new_fp = @as(*const usize, @ptrFromInt(fp)).*; + + (try abi.regValueNative(usize, context.thread_context, abi.fpRegNum(reg_context), reg_context)).* = new_fp; + (try abi.regValueNative(usize, context.thread_context, abi.spRegNum(reg_context), reg_context)).* = new_sp; + (try abi.regValueNative(usize, context.thread_context, abi.ipRegNum(), reg_context)).* = new_ip; + + for (regs, 0..) |reg, i| { + if (reg == 0) continue; + const addr = fp - frame_offset + i * @sizeOf(usize); + const reg_number = try compactUnwindToDwarfRegNumber(reg); + (try abi.regValueNative(usize, context.thread_context, reg_number, reg_context)).* = @as(*const usize, @ptrFromInt(addr)).*; + } + + break :blk new_ip; + }, + .STACK_IMMD, + .STACK_IND, + => blk: { + const sp = (try abi.regValueNative(usize, context.thread_context, abi.spRegNum(reg_context), reg_context)).*; + const stack_size = if (encoding.mode.x86_64 == .STACK_IMMD) + @as(usize, encoding.value.x86_64.frameless.stack.direct.stack_size) * @sizeOf(usize) + else stack_size: { + // In .STACK_IND, the stack size is inferred from the subq instruction at the beginning of the function. + const sub_offset_addr = + module_base_address + + entry.function_offset + + encoding.value.x86_64.frameless.stack.indirect.sub_offset; + if (!context.isValidMemory(sub_offset_addr)) return error.InvalidUnwindInfo; + + // `sub_offset_addr` points to the offset of the literal within the instruction + const sub_operand = @as(*align(1) const u32, @ptrFromInt(sub_offset_addr)).*; + break :stack_size sub_operand + @sizeOf(usize) * @as(usize, encoding.value.x86_64.frameless.stack.indirect.stack_adjust); + }; + + // Decode the Lehmer-coded sequence of registers. + // For a description of the encoding see lib/libc/include/any-macos.13-any/mach-o/compact_unwind_encoding.h + + // Decode the variable-based permutation number into its digits. Each digit represents + // an index into the list of register numbers that weren't yet used in the sequence at + // the time the digit was added. + const reg_count = encoding.value.x86_64.frameless.stack_reg_count; + const ip_ptr = if (reg_count > 0) reg_blk: { + var digits: [6]u3 = undefined; + var accumulator: usize = encoding.value.x86_64.frameless.stack_reg_permutation; + var base: usize = 2; + for (0..reg_count) |i| { + const div = accumulator / base; + digits[digits.len - 1 - i] = @intCast(accumulator - base * div); + accumulator = div; + base += 1; + } + + const reg_numbers = [_]u3{ 1, 2, 3, 4, 5, 6 }; + var registers: [reg_numbers.len]u3 = undefined; + var used_indices = [_]bool{false} ** reg_numbers.len; + for (digits[digits.len - reg_count ..], 0..) |target_unused_index, i| { + var unused_count: u8 = 0; + const unused_index = for (used_indices, 0..) |used, index| { + if (!used) { + if (target_unused_index == unused_count) break index; + unused_count += 1; + } + } else unreachable; + + registers[i] = reg_numbers[unused_index]; + used_indices[unused_index] = true; + } + + var reg_addr = sp + stack_size - @sizeOf(usize) * @as(usize, reg_count + 1); + if (!context.isValidMemory(reg_addr)) return error.InvalidUnwindInfo; + for (0..reg_count) |i| { + const reg_number = try compactUnwindToDwarfRegNumber(registers[i]); + (try abi.regValueNative(usize, context.thread_context, reg_number, reg_context)).* = @as(*const usize, @ptrFromInt(reg_addr)).*; + reg_addr += @sizeOf(usize); + } + + break :reg_blk reg_addr; + } else sp + stack_size - @sizeOf(usize); + + const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*; + const new_sp = ip_ptr + @sizeOf(usize); + if (!context.isValidMemory(new_sp)) return error.InvalidUnwindInfo; + + (try abi.regValueNative(usize, context.thread_context, abi.spRegNum(reg_context), reg_context)).* = new_sp; + (try abi.regValueNative(usize, context.thread_context, abi.ipRegNum(), reg_context)).* = new_ip; + + break :blk new_ip; + }, + .DWARF => { + return unwindFrameMachODwarf(context, eh_frame orelse return error.MissingEhFrame, @intCast(encoding.value.x86_64.dwarf)); + }, + }, + .aarch64 => switch (encoding.mode.arm64) { + .OLD => return error.UnimplementedUnwindEncoding, + .FRAMELESS => blk: { + const sp = (try abi.regValueNative(usize, context.thread_context, abi.spRegNum(reg_context), reg_context)).*; + const new_sp = sp + encoding.value.arm64.frameless.stack_size * 16; + const new_ip = (try abi.regValueNative(usize, context.thread_context, 30, reg_context)).*; + if (!context.isValidMemory(new_sp)) return error.InvalidUnwindInfo; + (try abi.regValueNative(usize, context.thread_context, abi.spRegNum(reg_context), reg_context)).* = new_sp; + break :blk new_ip; + }, + .DWARF => { + return unwindFrameMachODwarf(context, eh_frame orelse return error.MissingEhFrame, @intCast(encoding.value.arm64.dwarf)); + }, + .FRAME => blk: { + const fp = (try abi.regValueNative(usize, context.thread_context, abi.fpRegNum(reg_context), reg_context)).*; + const new_sp = fp + 16; + const ip_ptr = fp + @sizeOf(usize); + + const num_restored_pairs: usize = + @popCount(@as(u5, @bitCast(encoding.value.arm64.frame.x_reg_pairs))) + + @popCount(@as(u4, @bitCast(encoding.value.arm64.frame.d_reg_pairs))); + const min_reg_addr = fp - num_restored_pairs * 2 * @sizeOf(usize); + + if (!context.isValidMemory(new_sp) or !context.isValidMemory(min_reg_addr)) return error.InvalidUnwindInfo; + + var reg_addr = fp - @sizeOf(usize); + inline for (@typeInfo(@TypeOf(encoding.value.arm64.frame.x_reg_pairs)).Struct.fields, 0..) |field, i| { + if (@field(encoding.value.arm64.frame.x_reg_pairs, field.name) != 0) { + (try abi.regValueNative(usize, context.thread_context, 19 + i, reg_context)).* = @as(*const usize, @ptrFromInt(reg_addr)).*; + reg_addr += @sizeOf(usize); + (try abi.regValueNative(usize, context.thread_context, 20 + i, reg_context)).* = @as(*const usize, @ptrFromInt(reg_addr)).*; + reg_addr += @sizeOf(usize); + } + } + + inline for (@typeInfo(@TypeOf(encoding.value.arm64.frame.d_reg_pairs)).Struct.fields, 0..) |field, i| { + if (@field(encoding.value.arm64.frame.d_reg_pairs, field.name) != 0) { + // Only the lower half of the 128-bit V registers are restored during unwinding + @memcpy( + try abi.regBytes(context.thread_context, 64 + 8 + i, context.reg_context), + mem.asBytes(@as(*const usize, @ptrFromInt(reg_addr))), + ); + reg_addr += @sizeOf(usize); + @memcpy( + try abi.regBytes(context.thread_context, 64 + 9 + i, context.reg_context), + mem.asBytes(@as(*const usize, @ptrFromInt(reg_addr))), + ); + reg_addr += @sizeOf(usize); + } + } + + const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*; + const new_fp = @as(*const usize, @ptrFromInt(fp)).*; + + (try abi.regValueNative(usize, context.thread_context, abi.fpRegNum(reg_context), reg_context)).* = new_fp; + (try abi.regValueNative(usize, context.thread_context, abi.ipRegNum(), reg_context)).* = new_ip; + + break :blk new_ip; + }, + }, + else => return error.UnimplementedArch, + }; + + context.pc = abi.stripInstructionPtrAuthCode(new_ip); + if (context.pc > 0) context.pc -= 1; + return new_ip; +} + +fn unwindFrameMachODwarf(context: *UnwindContext, eh_frame: []const u8, fde_offset: usize) !usize { + var di = DwarfInfo{ + .endian = builtin.cpu.arch.endian(), + .is_macho = true, + }; + defer di.deinit(context.allocator); + + di.sections[@intFromEnum(DwarfSection.eh_frame)] = .{ + .data = eh_frame, + .owned = false, + }; + + return di.unwindFrame(context, fde_offset); +} + +pub const UnwindContext = struct { + allocator: mem.Allocator, + cfa: ?usize, + pc: usize, + thread_context: *debug.ThreadContext, + reg_context: abi.RegisterContext, + isValidMemory: *const fn (address: usize) bool, + vm: call_frame.VirtualMachine = .{}, + stack_machine: expressions.StackMachine(.{ .call_frame_context = true }) = .{}, + + pub fn init(allocator: mem.Allocator, thread_context: *const debug.ThreadContext, isValidMemory: *const fn (address: usize) bool) !UnwindContext { + const pc = abi.stripInstructionPtrAuthCode((try abi.regValueNative(usize, thread_context, abi.ipRegNum(), null)).*); + + const context_copy = try allocator.create(debug.ThreadContext); + debug.copyContext(thread_context, context_copy); + + return .{ + .allocator = allocator, + .cfa = null, + .pc = pc, + .thread_context = context_copy, + .reg_context = undefined, + .isValidMemory = isValidMemory, + }; + } + + pub fn deinit(self: *UnwindContext) void { + self.vm.deinit(self.allocator); + self.stack_machine.deinit(self.allocator); + self.allocator.destroy(self.thread_context); + } + + pub fn getFp(self: *const UnwindContext) !usize { + return (try abi.regValueNative(usize, self.thread_context, abi.fpRegNum(self.reg_context), self.reg_context)).*; + } }; /// Initialize DWARF info. The caller has the responsibility to initialize most -/// the DwarfInfo fields before calling. +/// the DwarfInfo fields before calling. `binary_mem` is the raw bytes of the +/// main binary file (not the secondary debug info file). pub fn openDwarfDebugInfo(di: *DwarfInfo, allocator: mem.Allocator) !void { try di.scanAllFunctions(allocator); try di.scanAllCompileUnits(allocator); @@ -1477,3 +2265,561 @@ fn getStringGeneric(opt_str: ?[]const u8, offset: u64) ![:0]const u8 { const last = mem.indexOfScalarPos(u8, str, casted_offset, 0) orelse return badDwarf(); return str[casted_offset..last :0]; } + +const EhPointerContext = struct { + // The address of the pointer field itself + pc_rel_base: u64, + + // Whether or not to follow indirect pointers. This should only be + // used when decoding pointers at runtime using the current process's + // debug info + follow_indirect: bool, + + // These relative addressing modes are only used in specific cases, and + // might not be available / required in all parsing contexts + data_rel_base: ?u64 = null, + text_rel_base: ?u64 = null, + function_rel_base: ?u64 = null, +}; + +fn readEhPointer(reader: anytype, enc: u8, addr_size_bytes: u8, ctx: EhPointerContext, endian: std.builtin.Endian) !?u64 { + if (enc == EH.PE.omit) return null; + + const value: union(enum) { + signed: i64, + unsigned: u64, + } = switch (enc & EH.PE.type_mask) { + EH.PE.absptr => .{ + .unsigned = switch (addr_size_bytes) { + 2 => try reader.readInt(u16, endian), + 4 => try reader.readInt(u32, endian), + 8 => try reader.readInt(u64, endian), + else => return error.InvalidAddrSize, + }, + }, + EH.PE.uleb128 => .{ .unsigned = try leb.readULEB128(u64, reader) }, + EH.PE.udata2 => .{ .unsigned = try reader.readInt(u16, endian) }, + EH.PE.udata4 => .{ .unsigned = try reader.readInt(u32, endian) }, + EH.PE.udata8 => .{ .unsigned = try reader.readInt(u64, endian) }, + EH.PE.sleb128 => .{ .signed = try leb.readILEB128(i64, reader) }, + EH.PE.sdata2 => .{ .signed = try reader.readInt(i16, endian) }, + EH.PE.sdata4 => .{ .signed = try reader.readInt(i32, endian) }, + EH.PE.sdata8 => .{ .signed = try reader.readInt(i64, endian) }, + else => return badDwarf(), + }; + + var base = switch (enc & EH.PE.rel_mask) { + EH.PE.pcrel => ctx.pc_rel_base, + EH.PE.textrel => ctx.text_rel_base orelse return error.PointerBaseNotSpecified, + EH.PE.datarel => ctx.data_rel_base orelse return error.PointerBaseNotSpecified, + EH.PE.funcrel => ctx.function_rel_base orelse return error.PointerBaseNotSpecified, + else => null, + }; + + const ptr: u64 = if (base) |b| switch (value) { + .signed => |s| @intCast(try math.add(i64, s, @as(i64, @intCast(b)))), + // absptr can actually contain signed values in some cases (aarch64 MachO) + .unsigned => |u| u +% b, + } else switch (value) { + .signed => |s| @as(u64, @intCast(s)), + .unsigned => |u| u, + }; + + if ((enc & EH.PE.indirect) > 0 and ctx.follow_indirect) { + if (@sizeOf(usize) != addr_size_bytes) { + // See the documentation for `follow_indirect` + return error.NonNativeIndirection; + } + + const native_ptr = math.cast(usize, ptr) orelse return error.PointerOverflow; + return switch (addr_size_bytes) { + 2, 4, 8 => return @as(*const usize, @ptrFromInt(native_ptr)).*, + else => return error.UnsupportedAddrSize, + }; + } else { + return ptr; + } +} + +/// This represents the decoded .eh_frame_hdr header +pub const ExceptionFrameHeader = struct { + eh_frame_ptr: usize, + table_enc: u8, + fde_count: usize, + entries: []const u8, + + pub fn entrySize(table_enc: u8) !u8 { + return switch (table_enc & EH.PE.type_mask) { + EH.PE.udata2, + EH.PE.sdata2, + => 4, + EH.PE.udata4, + EH.PE.sdata4, + => 8, + EH.PE.udata8, + EH.PE.sdata8, + => 16, + // This is a binary search table, so all entries must be the same length + else => return badDwarf(), + }; + } + + fn isValidPtr( + self: ExceptionFrameHeader, + ptr: usize, + isValidMemory: *const fn (address: usize) bool, + eh_frame_len: ?usize, + ) bool { + if (eh_frame_len) |len| { + return ptr >= self.eh_frame_ptr and ptr < self.eh_frame_ptr + len; + } else { + return isValidMemory(ptr); + } + } + + /// Find an entry by binary searching the eh_frame_hdr section. + /// + /// Since the length of the eh_frame section (`eh_frame_len`) may not be known by the caller, + /// `isValidMemory` will be called before accessing any memory referenced by + /// the header entries. If `eh_frame_len` is provided, then these checks can be skipped. + pub fn findEntry( + self: ExceptionFrameHeader, + isValidMemory: *const fn (address: usize) bool, + eh_frame_len: ?usize, + eh_frame_hdr_ptr: usize, + pc: usize, + cie: *CommonInformationEntry, + fde: *FrameDescriptionEntry, + ) !void { + const entry_size = try entrySize(self.table_enc); + + var left: usize = 0; + var len: usize = self.fde_count; + + var stream = io.fixedBufferStream(self.entries); + const reader = stream.reader(); + + while (len > 1) { + const mid = left + len / 2; + + try stream.seekTo(mid * entry_size); + const pc_begin = try readEhPointer(reader, self.table_enc, @sizeOf(usize), .{ + .pc_rel_base = @intFromPtr(&self.entries[stream.pos]), + .follow_indirect = true, + .data_rel_base = eh_frame_hdr_ptr, + }, builtin.cpu.arch.endian()) orelse return badDwarf(); + + if (pc < pc_begin) { + len /= 2; + } else { + left = mid; + if (pc == pc_begin) break; + len -= len / 2; + } + } + + if (len == 0) return badDwarf(); + try stream.seekTo(left * entry_size); + + // Read past the pc_begin field of the entry + _ = try readEhPointer(reader, self.table_enc, @sizeOf(usize), .{ + .pc_rel_base = @intFromPtr(&self.entries[stream.pos]), + .follow_indirect = true, + .data_rel_base = eh_frame_hdr_ptr, + }, builtin.cpu.arch.endian()) orelse return badDwarf(); + + const fde_ptr = math.cast(usize, try readEhPointer(reader, self.table_enc, @sizeOf(usize), .{ + .pc_rel_base = @intFromPtr(&self.entries[stream.pos]), + .follow_indirect = true, + .data_rel_base = eh_frame_hdr_ptr, + }, builtin.cpu.arch.endian()) orelse return badDwarf()) orelse return badDwarf(); + + // Verify the length fields of the FDE header are readable + if (!self.isValidPtr(fde_ptr, isValidMemory, eh_frame_len) or fde_ptr < self.eh_frame_ptr) return badDwarf(); + + var fde_entry_header_len: usize = 4; + if (!self.isValidPtr(fde_ptr + 3, isValidMemory, eh_frame_len)) return badDwarf(); + if (self.isValidPtr(fde_ptr + 11, isValidMemory, eh_frame_len)) fde_entry_header_len = 12; + + // Even if eh_frame_len is not specified, all ranges accssed are checked by isValidPtr + const eh_frame = @as([*]const u8, @ptrFromInt(self.eh_frame_ptr))[0 .. eh_frame_len orelse math.maxInt(u32)]; + + const fde_offset = fde_ptr - self.eh_frame_ptr; + var eh_frame_stream = io.fixedBufferStream(eh_frame); + try eh_frame_stream.seekTo(fde_offset); + + const fde_entry_header = try EntryHeader.read(&eh_frame_stream, .eh_frame, builtin.cpu.arch.endian()); + if (!self.isValidPtr(@intFromPtr(&fde_entry_header.entry_bytes[fde_entry_header.entry_bytes.len - 1]), isValidMemory, eh_frame_len)) return badDwarf(); + if (fde_entry_header.type != .fde) return badDwarf(); + + // CIEs always come before FDEs (the offset is a subtraction), so we can assume this memory is readable + const cie_offset = fde_entry_header.type.fde; + try eh_frame_stream.seekTo(cie_offset); + const cie_entry_header = try EntryHeader.read(&eh_frame_stream, .eh_frame, builtin.cpu.arch.endian()); + if (!self.isValidPtr(@intFromPtr(&cie_entry_header.entry_bytes[cie_entry_header.entry_bytes.len - 1]), isValidMemory, eh_frame_len)) return badDwarf(); + if (cie_entry_header.type != .cie) return badDwarf(); + + cie.* = try CommonInformationEntry.parse( + cie_entry_header.entry_bytes, + 0, + true, + cie_entry_header.is_64, + .eh_frame, + cie_entry_header.length_offset, + @sizeOf(usize), + builtin.cpu.arch.endian(), + ); + + fde.* = try FrameDescriptionEntry.parse( + fde_entry_header.entry_bytes, + 0, + true, + cie.*, + @sizeOf(usize), + builtin.cpu.arch.endian(), + ); + } +}; + +pub const EntryHeader = struct { + /// Offset of the length field in the backing buffer + length_offset: usize, + is_64: bool, + type: union(enum) { + cie, + /// Value is the offset of the corresponding CIE + fde: u64, + terminator: void, + }, + /// The entry's contents, not including the ID field + entry_bytes: []const u8, + + /// Reads a header for either an FDE or a CIE, then advances the stream to the position after the trailing structure. + /// `stream` must be a stream backed by either the .eh_frame or .debug_frame sections. + pub fn read(stream: *std.io.FixedBufferStream([]const u8), dwarf_section: DwarfSection, endian: std.builtin.Endian) !EntryHeader { + assert(dwarf_section == .eh_frame or dwarf_section == .debug_frame); + + const reader = stream.reader(); + const length_offset = stream.pos; + + var is_64: bool = undefined; + const length = math.cast(usize, try readUnitLength(reader, endian, &is_64)) orelse return badDwarf(); + if (length == 0) return .{ + .length_offset = length_offset, + .is_64 = is_64, + .type = .{ .terminator = {} }, + .entry_bytes = &.{}, + }; + + const id_len = @as(u8, if (is_64) 8 else 4); + const id = if (is_64) try reader.readInt(u64, endian) else try reader.readInt(u32, endian); + const entry_bytes = stream.buffer[stream.pos..][0 .. length - id_len]; + const cie_id: u64 = switch (dwarf_section) { + .eh_frame => CommonInformationEntry.eh_id, + .debug_frame => if (is_64) CommonInformationEntry.dwarf64_id else CommonInformationEntry.dwarf32_id, + else => unreachable, + }; + + const result = EntryHeader{ + .length_offset = length_offset, + .is_64 = is_64, + .type = if (id == cie_id) .{ .cie = {} } else .{ + .fde = switch (dwarf_section) { + .eh_frame => try std.math.sub(u64, stream.pos - id_len, id), + .debug_frame => id, + else => unreachable, + }, + }, + .entry_bytes = entry_bytes, + }; + + stream.pos += entry_bytes.len; + return result; + } + + /// The length of the entry including the ID field, but not the length field itself + pub fn entryLength(self: EntryHeader) usize { + return self.entry_bytes.len + @as(u8, if (self.is_64) 8 else 4); + } +}; + +pub const CommonInformationEntry = struct { + // Used in .eh_frame + pub const eh_id = 0; + + // Used in .debug_frame (DWARF32) + pub const dwarf32_id = math.maxInt(u32); + + // Used in .debug_frame (DWARF64) + pub const dwarf64_id = math.maxInt(u64); + + // Offset of the length field of this entry in the eh_frame section. + // This is the key that FDEs use to reference CIEs. + length_offset: u64, + version: u8, + address_size: u8, + is_64: bool, + + // Only present in version 4 + segment_selector_size: ?u8, + + code_alignment_factor: u32, + data_alignment_factor: i32, + return_address_register: u8, + + aug_str: []const u8, + aug_data: []const u8, + lsda_pointer_enc: u8, + personality_enc: ?u8, + personality_routine_pointer: ?u64, + fde_pointer_enc: u8, + initial_instructions: []const u8, + + pub fn isSignalFrame(self: CommonInformationEntry) bool { + for (self.aug_str) |c| if (c == 'S') return true; + return false; + } + + pub fn addressesSignedWithBKey(self: CommonInformationEntry) bool { + for (self.aug_str) |c| if (c == 'B') return true; + return false; + } + + pub fn mteTaggedFrame(self: CommonInformationEntry) bool { + for (self.aug_str) |c| if (c == 'G') return true; + return false; + } + + /// This function expects to read the CIE starting with the version field. + /// The returned struct references memory backed by cie_bytes. + /// + /// See the FrameDescriptionEntry.parse documentation for the description + /// of `pc_rel_offset` and `is_runtime`. + /// + /// `length_offset` specifies the offset of this CIE's length field in the + /// .eh_frame / .debug_frame section. + pub fn parse( + cie_bytes: []const u8, + pc_rel_offset: i64, + is_runtime: bool, + is_64: bool, + dwarf_section: DwarfSection, + length_offset: u64, + addr_size_bytes: u8, + endian: std.builtin.Endian, + ) !CommonInformationEntry { + if (addr_size_bytes > 8) return error.UnsupportedAddrSize; + + var stream = io.fixedBufferStream(cie_bytes); + const reader = stream.reader(); + + const version = try reader.readByte(); + switch (dwarf_section) { + .eh_frame => if (version != 1 and version != 3) return error.UnsupportedDwarfVersion, + .debug_frame => if (version != 4) return error.UnsupportedDwarfVersion, + else => return error.UnsupportedDwarfSection, + } + + var has_eh_data = false; + var has_aug_data = false; + + var aug_str_len: usize = 0; + var aug_str_start = stream.pos; + var aug_byte = try reader.readByte(); + while (aug_byte != 0) : (aug_byte = try reader.readByte()) { + switch (aug_byte) { + 'z' => { + if (aug_str_len != 0) return badDwarf(); + has_aug_data = true; + }, + 'e' => { + if (has_aug_data or aug_str_len != 0) return badDwarf(); + if (try reader.readByte() != 'h') return badDwarf(); + has_eh_data = true; + }, + else => if (has_eh_data) return badDwarf(), + } + + aug_str_len += 1; + } + + if (has_eh_data) { + // legacy data created by older versions of gcc - unsupported here + for (0..addr_size_bytes) |_| _ = try reader.readByte(); + } + + const address_size = if (version == 4) try reader.readByte() else addr_size_bytes; + const segment_selector_size = if (version == 4) try reader.readByte() else null; + + const code_alignment_factor = try leb.readULEB128(u32, reader); + const data_alignment_factor = try leb.readILEB128(i32, reader); + const return_address_register = if (version == 1) try reader.readByte() else try leb.readULEB128(u8, reader); + + var lsda_pointer_enc: u8 = EH.PE.omit; + var personality_enc: ?u8 = null; + var personality_routine_pointer: ?u64 = null; + var fde_pointer_enc: u8 = EH.PE.absptr; + + var aug_data: []const u8 = &[_]u8{}; + const aug_str = if (has_aug_data) blk: { + const aug_data_len = try leb.readULEB128(usize, reader); + const aug_data_start = stream.pos; + aug_data = cie_bytes[aug_data_start..][0..aug_data_len]; + + const aug_str = cie_bytes[aug_str_start..][0..aug_str_len]; + for (aug_str[1..]) |byte| { + switch (byte) { + 'L' => { + lsda_pointer_enc = try reader.readByte(); + }, + 'P' => { + personality_enc = try reader.readByte(); + personality_routine_pointer = try readEhPointer( + reader, + personality_enc.?, + addr_size_bytes, + .{ + .pc_rel_base = try pcRelBase(@intFromPtr(&cie_bytes[stream.pos]), pc_rel_offset), + .follow_indirect = is_runtime, + }, + endian, + ); + }, + 'R' => { + fde_pointer_enc = try reader.readByte(); + }, + 'S', 'B', 'G' => {}, + else => return badDwarf(), + } + } + + // aug_data_len can include padding so the CIE ends on an address boundary + try stream.seekTo(aug_data_start + aug_data_len); + break :blk aug_str; + } else &[_]u8{}; + + const initial_instructions = cie_bytes[stream.pos..]; + return .{ + .length_offset = length_offset, + .version = version, + .address_size = address_size, + .is_64 = is_64, + .segment_selector_size = segment_selector_size, + .code_alignment_factor = code_alignment_factor, + .data_alignment_factor = data_alignment_factor, + .return_address_register = return_address_register, + .aug_str = aug_str, + .aug_data = aug_data, + .lsda_pointer_enc = lsda_pointer_enc, + .personality_enc = personality_enc, + .personality_routine_pointer = personality_routine_pointer, + .fde_pointer_enc = fde_pointer_enc, + .initial_instructions = initial_instructions, + }; + } +}; + +pub const FrameDescriptionEntry = struct { + // Offset into eh_frame where the CIE for this FDE is stored + cie_length_offset: u64, + + pc_begin: u64, + pc_range: u64, + lsda_pointer: ?u64, + aug_data: []const u8, + instructions: []const u8, + + /// This function expects to read the FDE starting at the PC Begin field. + /// The returned struct references memory backed by `fde_bytes`. + /// + /// `pc_rel_offset` specifies an offset to be applied to pc_rel_base values + /// used when decoding pointers. This should be set to zero if fde_bytes is + /// backed by the memory of a .eh_frame / .debug_frame section in the running executable. + /// Otherwise, it should be the relative offset to translate addresses from + /// where the section is currently stored in memory, to where it *would* be + /// stored at runtime: section base addr - backing data base ptr. + /// + /// Similarly, `is_runtime` specifies this function is being called on a runtime + /// section, and so indirect pointers can be followed. + pub fn parse( + fde_bytes: []const u8, + pc_rel_offset: i64, + is_runtime: bool, + cie: CommonInformationEntry, + addr_size_bytes: u8, + endian: std.builtin.Endian, + ) !FrameDescriptionEntry { + if (addr_size_bytes > 8) return error.InvalidAddrSize; + + var stream = io.fixedBufferStream(fde_bytes); + const reader = stream.reader(); + + const pc_begin = try readEhPointer( + reader, + cie.fde_pointer_enc, + addr_size_bytes, + .{ + .pc_rel_base = try pcRelBase(@intFromPtr(&fde_bytes[stream.pos]), pc_rel_offset), + .follow_indirect = is_runtime, + }, + endian, + ) orelse return badDwarf(); + + const pc_range = try readEhPointer( + reader, + cie.fde_pointer_enc, + addr_size_bytes, + .{ + .pc_rel_base = 0, + .follow_indirect = false, + }, + endian, + ) orelse return badDwarf(); + + var aug_data: []const u8 = &[_]u8{}; + const lsda_pointer = if (cie.aug_str.len > 0) blk: { + const aug_data_len = try leb.readULEB128(usize, reader); + const aug_data_start = stream.pos; + aug_data = fde_bytes[aug_data_start..][0..aug_data_len]; + + const lsda_pointer = if (cie.lsda_pointer_enc != EH.PE.omit) + try readEhPointer( + reader, + cie.lsda_pointer_enc, + addr_size_bytes, + .{ + .pc_rel_base = try pcRelBase(@intFromPtr(&fde_bytes[stream.pos]), pc_rel_offset), + .follow_indirect = is_runtime, + }, + endian, + ) + else + null; + + try stream.seekTo(aug_data_start + aug_data_len); + break :blk lsda_pointer; + } else null; + + const instructions = fde_bytes[stream.pos..]; + return .{ + .cie_length_offset = cie.length_offset, + .pc_begin = pc_begin, + .pc_range = pc_range, + .lsda_pointer = lsda_pointer, + .aug_data = aug_data, + .instructions = instructions, + }; + } +}; + +fn pcRelBase(field_ptr: usize, pc_rel_offset: i64) !usize { + if (pc_rel_offset < 0) { + return math.sub(usize, field_ptr, @as(usize, @intCast(-pc_rel_offset))); + } else { + return math.add(usize, field_ptr, @as(usize, @intCast(pc_rel_offset))); + } +} + +test { + std.testing.refAllDecls(@This()); +} diff --git a/lib/std/dwarf/EH.zig b/lib/std/dwarf/EH.zig new file mode 100644 index 000000000000..3ee7e0be0f07 --- /dev/null +++ b/lib/std/dwarf/EH.zig @@ -0,0 +1,27 @@ +pub const PE = struct { + pub const absptr = 0x00; + + pub const size_mask = 0x7; + pub const sign_mask = 0x8; + pub const type_mask = size_mask | sign_mask; + + pub const uleb128 = 0x01; + pub const udata2 = 0x02; + pub const udata4 = 0x03; + pub const udata8 = 0x04; + pub const sleb128 = 0x09; + pub const sdata2 = 0x0A; + pub const sdata4 = 0x0B; + pub const sdata8 = 0x0C; + + pub const rel_mask = 0x70; + pub const pcrel = 0x10; + pub const textrel = 0x20; + pub const datarel = 0x30; + pub const funcrel = 0x40; + pub const aligned = 0x50; + + pub const indirect = 0x80; + + pub const omit = 0xff; +}; diff --git a/lib/std/dwarf/abi.zig b/lib/std/dwarf/abi.zig new file mode 100644 index 000000000000..7f349d97ad33 --- /dev/null +++ b/lib/std/dwarf/abi.zig @@ -0,0 +1,387 @@ +const builtin = @import("builtin"); +const std = @import("../std.zig"); +const os = std.os; +const mem = std.mem; + +pub fn isSupportedArch(arch: std.Target.Cpu.Arch) bool { + return switch (arch) { + .x86, + .x86_64, + .arm, + .aarch64, + => true, + else => false, + }; +} + +pub fn ipRegNum() u8 { + return switch (builtin.cpu.arch) { + .x86 => 8, + .x86_64 => 16, + .arm => 15, + .aarch64 => 32, + else => unreachable, + }; +} + +pub fn fpRegNum(reg_context: RegisterContext) u8 { + return switch (builtin.cpu.arch) { + // GCC on OS X historicaly did the opposite of ELF for these registers (only in .eh_frame), and that is now the convention for MachO + .x86 => if (reg_context.eh_frame and reg_context.is_macho) 4 else 5, + .x86_64 => 6, + .arm => 11, + .aarch64 => 29, + else => unreachable, + }; +} + +pub fn spRegNum(reg_context: RegisterContext) u8 { + return switch (builtin.cpu.arch) { + .x86 => if (reg_context.eh_frame and reg_context.is_macho) 5 else 4, + .x86_64 => 7, + .arm => 13, + .aarch64 => 31, + else => unreachable, + }; +} + +/// Some platforms use pointer authentication - the upper bits of instruction pointers contain a signature. +/// This function clears these signature bits to make the pointer usable. +pub inline fn stripInstructionPtrAuthCode(ptr: usize) usize { + if (builtin.cpu.arch == .aarch64) { + // `hint 0x07` maps to `xpaclri` (or `nop` if the hardware doesn't support it) + // The save / restore is because `xpaclri` operates on x30 (LR) + return asm ( + \\mov x16, x30 + \\mov x30, x15 + \\hint 0x07 + \\mov x15, x30 + \\mov x30, x16 + : [ret] "={x15}" (-> usize), + : [ptr] "{x15}" (ptr), + : "x16" + ); + } + + return ptr; +} + +pub const RegisterContext = struct { + eh_frame: bool, + is_macho: bool, +}; + +pub const AbiError = error{ + InvalidRegister, + UnimplementedArch, + UnimplementedOs, + RegisterContextRequired, + ThreadContextNotSupported, +}; + +fn RegValueReturnType(comptime ContextPtrType: type, comptime T: type) type { + const reg_bytes_type = comptime RegBytesReturnType(ContextPtrType); + const info = @typeInfo(reg_bytes_type).Pointer; + return @Type(.{ + .Pointer = .{ + .size = .One, + .is_const = info.is_const, + .is_volatile = info.is_volatile, + .is_allowzero = info.is_allowzero, + .alignment = info.alignment, + .address_space = info.address_space, + .child = T, + .sentinel = null, + }, + }); +} + +/// Returns a pointer to a register stored in a ThreadContext, preserving the pointer attributes of the context. +pub fn regValueNative( + comptime T: type, + thread_context_ptr: anytype, + reg_number: u8, + reg_context: ?RegisterContext, +) !RegValueReturnType(@TypeOf(thread_context_ptr), T) { + const reg_bytes = try regBytes(thread_context_ptr, reg_number, reg_context); + if (@sizeOf(T) != reg_bytes.len) return error.IncompatibleRegisterSize; + return mem.bytesAsValue(T, reg_bytes[0..@sizeOf(T)]); +} + +fn RegBytesReturnType(comptime ContextPtrType: type) type { + const info = @typeInfo(ContextPtrType); + if (info != .Pointer or info.Pointer.child != std.debug.ThreadContext) { + @compileError("Expected a pointer to std.debug.ThreadContext, got " ++ @typeName(@TypeOf(ContextPtrType))); + } + + return if (info.Pointer.is_const) return []const u8 else []u8; +} + +/// Returns a slice containing the backing storage for `reg_number`. +/// +/// `reg_context` describes in what context the register number is used, as it can have different +/// meanings depending on the DWARF container. It is only required when getting the stack or +/// frame pointer register on some architectures. +pub fn regBytes( + thread_context_ptr: anytype, + reg_number: u8, + reg_context: ?RegisterContext, +) AbiError!RegBytesReturnType(@TypeOf(thread_context_ptr)) { + if (builtin.os.tag == .windows) { + return switch (builtin.cpu.arch) { + .x86 => switch (reg_number) { + 0 => mem.asBytes(&thread_context_ptr.Eax), + 1 => mem.asBytes(&thread_context_ptr.Ecx), + 2 => mem.asBytes(&thread_context_ptr.Edx), + 3 => mem.asBytes(&thread_context_ptr.Ebx), + 4 => mem.asBytes(&thread_context_ptr.Esp), + 5 => mem.asBytes(&thread_context_ptr.Ebp), + 6 => mem.asBytes(&thread_context_ptr.Esi), + 7 => mem.asBytes(&thread_context_ptr.Edi), + 8 => mem.asBytes(&thread_context_ptr.Eip), + 9 => mem.asBytes(&thread_context_ptr.EFlags), + 10 => mem.asBytes(&thread_context_ptr.SegCs), + 11 => mem.asBytes(&thread_context_ptr.SegSs), + 12 => mem.asBytes(&thread_context_ptr.SegDs), + 13 => mem.asBytes(&thread_context_ptr.SegEs), + 14 => mem.asBytes(&thread_context_ptr.SegFs), + 15 => mem.asBytes(&thread_context_ptr.SegGs), + else => error.InvalidRegister, + }, + .x86_64 => switch (reg_number) { + 0 => mem.asBytes(&thread_context_ptr.Rax), + 1 => mem.asBytes(&thread_context_ptr.Rdx), + 2 => mem.asBytes(&thread_context_ptr.Rcx), + 3 => mem.asBytes(&thread_context_ptr.Rbx), + 4 => mem.asBytes(&thread_context_ptr.Rsi), + 5 => mem.asBytes(&thread_context_ptr.Rdi), + 6 => mem.asBytes(&thread_context_ptr.Rbp), + 7 => mem.asBytes(&thread_context_ptr.Rsp), + 8 => mem.asBytes(&thread_context_ptr.R8), + 9 => mem.asBytes(&thread_context_ptr.R9), + 10 => mem.asBytes(&thread_context_ptr.R10), + 11 => mem.asBytes(&thread_context_ptr.R11), + 12 => mem.asBytes(&thread_context_ptr.R12), + 13 => mem.asBytes(&thread_context_ptr.R13), + 14 => mem.asBytes(&thread_context_ptr.R14), + 15 => mem.asBytes(&thread_context_ptr.R15), + 16 => mem.asBytes(&thread_context_ptr.Rip), + else => error.InvalidRegister, + }, + .aarch64 => switch (reg_number) { + 0...30 => mem.asBytes(&thread_context_ptr.DUMMYUNIONNAME.X[reg_number]), + 31 => mem.asBytes(&thread_context_ptr.Sp), + 32 => mem.asBytes(&thread_context_ptr.Pc), + else => error.InvalidRegister, + }, + else => error.UnimplementedArch, + }; + } + + if (!std.debug.have_ucontext) return error.ThreadContextNotSupported; + + const ucontext_ptr = thread_context_ptr; + return switch (builtin.cpu.arch) { + .x86 => switch (builtin.os.tag) { + .linux, .netbsd, .solaris => switch (reg_number) { + 0 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.EAX]), + 1 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.ECX]), + 2 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.EDX]), + 3 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.EBX]), + 4...5 => if (reg_context) |r| bytes: { + if (reg_number == 4) { + break :bytes if (r.eh_frame and r.is_macho) + mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.EBP]) + else + mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.ESP]); + } else { + break :bytes if (r.eh_frame and r.is_macho) + mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.ESP]) + else + mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.EBP]); + } + } else error.RegisterContextRequired, + 6 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.ESI]), + 7 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.EDI]), + 8 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.EIP]), + 9 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.EFL]), + 10 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.CS]), + 11 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.SS]), + 12 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.DS]), + 13 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.ES]), + 14 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.FS]), + 15 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.GS]), + 16...23 => error.InvalidRegister, // TODO: Support loading ST0-ST7 from mcontext.fpregs + 32...39 => error.InvalidRegister, // TODO: Support loading XMM0-XMM7 from mcontext.fpregs + else => error.InvalidRegister, + }, + else => error.UnimplementedOs, + }, + .x86_64 => switch (builtin.os.tag) { + .linux, .netbsd, .solaris => switch (reg_number) { + 0 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.RAX]), + 1 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.RDX]), + 2 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.RCX]), + 3 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.RBX]), + 4 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.RSI]), + 5 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.RDI]), + 6 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.RBP]), + 7 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.RSP]), + 8 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.R8]), + 9 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.R9]), + 10 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.R10]), + 11 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.R11]), + 12 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.R12]), + 13 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.R13]), + 14 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.R14]), + 15 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.R15]), + 16 => mem.asBytes(&ucontext_ptr.mcontext.gregs[os.REG.RIP]), + 17...32 => |i| mem.asBytes(&ucontext_ptr.mcontext.fpregs.xmm[i - 17]), + else => error.InvalidRegister, + }, + .freebsd => switch (reg_number) { + 0 => mem.asBytes(&ucontext_ptr.mcontext.rax), + 1 => mem.asBytes(&ucontext_ptr.mcontext.rdx), + 2 => mem.asBytes(&ucontext_ptr.mcontext.rcx), + 3 => mem.asBytes(&ucontext_ptr.mcontext.rbx), + 4 => mem.asBytes(&ucontext_ptr.mcontext.rsi), + 5 => mem.asBytes(&ucontext_ptr.mcontext.rdi), + 6 => mem.asBytes(&ucontext_ptr.mcontext.rbp), + 7 => mem.asBytes(&ucontext_ptr.mcontext.rsp), + 8 => mem.asBytes(&ucontext_ptr.mcontext.r8), + 9 => mem.asBytes(&ucontext_ptr.mcontext.r9), + 10 => mem.asBytes(&ucontext_ptr.mcontext.r10), + 11 => mem.asBytes(&ucontext_ptr.mcontext.r11), + 12 => mem.asBytes(&ucontext_ptr.mcontext.r12), + 13 => mem.asBytes(&ucontext_ptr.mcontext.r13), + 14 => mem.asBytes(&ucontext_ptr.mcontext.r14), + 15 => mem.asBytes(&ucontext_ptr.mcontext.r15), + 16 => mem.asBytes(&ucontext_ptr.mcontext.rip), + // TODO: Extract xmm state from mcontext.fpstate? + else => error.InvalidRegister, + }, + .openbsd => switch (reg_number) { + 0 => mem.asBytes(&ucontext_ptr.sc_rax), + 1 => mem.asBytes(&ucontext_ptr.sc_rdx), + 2 => mem.asBytes(&ucontext_ptr.sc_rcx), + 3 => mem.asBytes(&ucontext_ptr.sc_rbx), + 4 => mem.asBytes(&ucontext_ptr.sc_rsi), + 5 => mem.asBytes(&ucontext_ptr.sc_rdi), + 6 => mem.asBytes(&ucontext_ptr.sc_rbp), + 7 => mem.asBytes(&ucontext_ptr.sc_rsp), + 8 => mem.asBytes(&ucontext_ptr.sc_r8), + 9 => mem.asBytes(&ucontext_ptr.sc_r9), + 10 => mem.asBytes(&ucontext_ptr.sc_r10), + 11 => mem.asBytes(&ucontext_ptr.sc_r11), + 12 => mem.asBytes(&ucontext_ptr.sc_r12), + 13 => mem.asBytes(&ucontext_ptr.sc_r13), + 14 => mem.asBytes(&ucontext_ptr.sc_r14), + 15 => mem.asBytes(&ucontext_ptr.sc_r15), + 16 => mem.asBytes(&ucontext_ptr.sc_rip), + // TODO: Extract xmm state from sc_fpstate? + else => error.InvalidRegister, + }, + .macos => switch (reg_number) { + 0 => mem.asBytes(&ucontext_ptr.mcontext.ss.rax), + 1 => mem.asBytes(&ucontext_ptr.mcontext.ss.rdx), + 2 => mem.asBytes(&ucontext_ptr.mcontext.ss.rcx), + 3 => mem.asBytes(&ucontext_ptr.mcontext.ss.rbx), + 4 => mem.asBytes(&ucontext_ptr.mcontext.ss.rsi), + 5 => mem.asBytes(&ucontext_ptr.mcontext.ss.rdi), + 6 => mem.asBytes(&ucontext_ptr.mcontext.ss.rbp), + 7 => mem.asBytes(&ucontext_ptr.mcontext.ss.rsp), + 8 => mem.asBytes(&ucontext_ptr.mcontext.ss.r8), + 9 => mem.asBytes(&ucontext_ptr.mcontext.ss.r9), + 10 => mem.asBytes(&ucontext_ptr.mcontext.ss.r10), + 11 => mem.asBytes(&ucontext_ptr.mcontext.ss.r11), + 12 => mem.asBytes(&ucontext_ptr.mcontext.ss.r12), + 13 => mem.asBytes(&ucontext_ptr.mcontext.ss.r13), + 14 => mem.asBytes(&ucontext_ptr.mcontext.ss.r14), + 15 => mem.asBytes(&ucontext_ptr.mcontext.ss.r15), + 16 => mem.asBytes(&ucontext_ptr.mcontext.ss.rip), + else => error.InvalidRegister, + }, + else => error.UnimplementedOs, + }, + .arm => switch (builtin.os.tag) { + .linux => switch (reg_number) { + 0 => mem.asBytes(&ucontext_ptr.mcontext.arm_r0), + 1 => mem.asBytes(&ucontext_ptr.mcontext.arm_r1), + 2 => mem.asBytes(&ucontext_ptr.mcontext.arm_r2), + 3 => mem.asBytes(&ucontext_ptr.mcontext.arm_r3), + 4 => mem.asBytes(&ucontext_ptr.mcontext.arm_r4), + 5 => mem.asBytes(&ucontext_ptr.mcontext.arm_r5), + 6 => mem.asBytes(&ucontext_ptr.mcontext.arm_r6), + 7 => mem.asBytes(&ucontext_ptr.mcontext.arm_r7), + 8 => mem.asBytes(&ucontext_ptr.mcontext.arm_r8), + 9 => mem.asBytes(&ucontext_ptr.mcontext.arm_r9), + 10 => mem.asBytes(&ucontext_ptr.mcontext.arm_r10), + 11 => mem.asBytes(&ucontext_ptr.mcontext.arm_fp), + 12 => mem.asBytes(&ucontext_ptr.mcontext.arm_ip), + 13 => mem.asBytes(&ucontext_ptr.mcontext.arm_sp), + 14 => mem.asBytes(&ucontext_ptr.mcontext.arm_lr), + 15 => mem.asBytes(&ucontext_ptr.mcontext.arm_pc), + // CPSR is not allocated a register number (See: https://github.com/ARM-software/abi-aa/blob/main/aadwarf32/aadwarf32.rst, Section 4.1) + else => error.InvalidRegister, + }, + else => error.UnimplementedOs, + }, + .aarch64 => switch (builtin.os.tag) { + .macos => switch (reg_number) { + 0...28 => mem.asBytes(&ucontext_ptr.mcontext.ss.regs[reg_number]), + 29 => mem.asBytes(&ucontext_ptr.mcontext.ss.fp), + 30 => mem.asBytes(&ucontext_ptr.mcontext.ss.lr), + 31 => mem.asBytes(&ucontext_ptr.mcontext.ss.sp), + 32 => mem.asBytes(&ucontext_ptr.mcontext.ss.pc), + + // TODO: Find storage for this state + //34 => mem.asBytes(&ucontext_ptr.ra_sign_state), + + // V0-V31 + 64...95 => mem.asBytes(&ucontext_ptr.mcontext.ns.q[reg_number - 64]), + else => error.InvalidRegister, + }, + .netbsd => switch (reg_number) { + 0...34 => mem.asBytes(&ucontext_ptr.mcontext.gregs[reg_number]), + else => error.InvalidRegister, + }, + .freebsd => switch (reg_number) { + 0...29 => mem.asBytes(&ucontext_ptr.mcontext.gpregs.x[reg_number]), + 30 => mem.asBytes(&ucontext_ptr.mcontext.gpregs.lr), + 31 => mem.asBytes(&ucontext_ptr.mcontext.gpregs.sp), + + // TODO: This seems wrong, but it was in the previous debug.zig code for mapping PC, check this + 32 => mem.asBytes(&ucontext_ptr.mcontext.gpregs.elr), + + else => error.InvalidRegister, + }, + else => switch (reg_number) { + 0...30 => mem.asBytes(&ucontext_ptr.mcontext.regs[reg_number]), + 31 => mem.asBytes(&ucontext_ptr.mcontext.sp), + 32 => mem.asBytes(&ucontext_ptr.mcontext.pc), + else => error.InvalidRegister, + }, + }, + else => error.UnimplementedArch, + }; +} + +/// Returns the ABI-defined default value this register has in the unwinding table +/// before running any of the CIE instructions. The DWARF spec defines these as having +/// the .undefined rule by default, but allows ABI authors to override that. +pub fn getRegDefaultValue(reg_number: u8, context: *std.dwarf.UnwindContext, out: []u8) !void { + switch (builtin.cpu.arch) { + .aarch64 => { + // Callee-saved registers are initialized as if they had the .same_value rule + if (reg_number >= 19 and reg_number <= 28) { + const src = try regBytes(context.thread_context, reg_number, context.reg_context); + if (src.len != out.len) return error.RegisterSizeMismatch; + @memcpy(out, src); + return; + } + }, + else => {}, + } + + @memset(out, undefined); +} diff --git a/lib/std/dwarf/call_frame.zig b/lib/std/dwarf/call_frame.zig new file mode 100644 index 000000000000..c83cbad81578 --- /dev/null +++ b/lib/std/dwarf/call_frame.zig @@ -0,0 +1,610 @@ +const builtin = @import("builtin"); +const std = @import("../std.zig"); +const mem = std.mem; +const debug = std.debug; +const leb = std.leb; +const dwarf = std.dwarf; +const abi = dwarf.abi; +const expressions = dwarf.expressions; +const assert = std.debug.assert; + +const Opcode = enum(u8) { + advance_loc = 0x1 << 6, + offset = 0x2 << 6, + restore = 0x3 << 6, + + nop = 0x00, + set_loc = 0x01, + advance_loc1 = 0x02, + advance_loc2 = 0x03, + advance_loc4 = 0x04, + offset_extended = 0x05, + restore_extended = 0x06, + undefined = 0x07, + same_value = 0x08, + register = 0x09, + remember_state = 0x0a, + restore_state = 0x0b, + def_cfa = 0x0c, + def_cfa_register = 0x0d, + def_cfa_offset = 0x0e, + def_cfa_expression = 0x0f, + expression = 0x10, + offset_extended_sf = 0x11, + def_cfa_sf = 0x12, + def_cfa_offset_sf = 0x13, + val_offset = 0x14, + val_offset_sf = 0x15, + val_expression = 0x16, + + // These opcodes encode an operand in the lower 6 bits of the opcode itself + pub const lo_inline = @intFromEnum(Opcode.advance_loc); + pub const hi_inline = @intFromEnum(Opcode.restore) | 0b111111; + + // These opcodes are trailed by zero or more operands + pub const lo_reserved = @intFromEnum(Opcode.nop); + pub const hi_reserved = @intFromEnum(Opcode.val_expression); + + // Vendor-specific opcodes + pub const lo_user = 0x1c; + pub const hi_user = 0x3f; +}; + +const Operand = enum { + opcode_delta, + opcode_register, + uleb128_register, + uleb128_offset, + sleb128_offset, + address, + u8_delta, + u16_delta, + u32_delta, + block, + + fn Storage(comptime self: Operand) type { + return switch (self) { + .opcode_delta, .opcode_register => u8, + .uleb128_register => u8, + .uleb128_offset => u64, + .sleb128_offset => i64, + .address => u64, + .u8_delta => u8, + .u16_delta => u16, + .u32_delta => u32, + .block => []const u8, + }; + } + + fn read( + comptime self: Operand, + stream: *std.io.FixedBufferStream([]const u8), + opcode_value: ?u6, + addr_size_bytes: u8, + endian: std.builtin.Endian, + ) !Storage(self) { + const reader = stream.reader(); + return switch (self) { + .opcode_delta, .opcode_register => opcode_value orelse return error.InvalidOperand, + .uleb128_register => try leb.readULEB128(u8, reader), + .uleb128_offset => try leb.readULEB128(u64, reader), + .sleb128_offset => try leb.readILEB128(i64, reader), + .address => switch (addr_size_bytes) { + 2 => try reader.readInt(u16, endian), + 4 => try reader.readInt(u32, endian), + 8 => try reader.readInt(u64, endian), + else => return error.InvalidAddrSize, + }, + .u8_delta => try reader.readByte(), + .u16_delta => try reader.readInt(u16, endian), + .u32_delta => try reader.readInt(u32, endian), + .block => { + const block_len = try leb.readULEB128(usize, reader); + if (stream.pos + block_len > stream.buffer.len) return error.InvalidOperand; + + const block = stream.buffer[stream.pos..][0..block_len]; + reader.context.pos += block_len; + + return block; + }, + }; + } +}; + +fn InstructionType(comptime definition: anytype) type { + const definition_type = @typeInfo(@TypeOf(definition)); + assert(definition_type == .Struct); + + const definition_len = definition_type.Struct.fields.len; + comptime var fields: [definition_len]std.builtin.Type.StructField = undefined; + inline for (definition_type.Struct.fields, &fields) |definition_field, *operands_field| { + const opcode = std.enums.nameCast(Operand, @field(definition, definition_field.name)); + const storage_type = opcode.Storage(); + operands_field.* = .{ + .name = definition_field.name, + .type = storage_type, + .default_value = null, + .is_comptime = false, + .alignment = @alignOf(storage_type), + }; + } + + const InstructionOperands = @Type(.{ + .Struct = .{ + .layout = .Auto, + .fields = &fields, + .decls = &.{}, + .is_tuple = false, + }, + }); + + return struct { + const Self = @This(); + operands: InstructionOperands, + + pub fn read( + stream: *std.io.FixedBufferStream([]const u8), + opcode_value: ?u6, + addr_size_bytes: u8, + endian: std.builtin.Endian, + ) !Self { + var operands: InstructionOperands = undefined; + inline for (definition_type.Struct.fields) |definition_field| { + const operand = comptime std.enums.nameCast(Operand, @field(definition, definition_field.name)); + @field(operands, definition_field.name) = try operand.read(stream, opcode_value, addr_size_bytes, endian); + } + + return .{ .operands = operands }; + } + }; +} + +pub const Instruction = union(Opcode) { + advance_loc: InstructionType(.{ .delta = .opcode_delta }), + offset: InstructionType(.{ .register = .opcode_register, .offset = .uleb128_offset }), + offset_extended: InstructionType(.{ .register = .uleb128_register, .offset = .uleb128_offset }), + restore: InstructionType(.{ .register = .opcode_register }), + restore_extended: InstructionType(.{ .register = .uleb128_register }), + nop: InstructionType(.{}), + set_loc: InstructionType(.{ .address = .address }), + advance_loc1: InstructionType(.{ .delta = .u8_delta }), + advance_loc2: InstructionType(.{ .delta = .u16_delta }), + advance_loc4: InstructionType(.{ .delta = .u32_delta }), + undefined: InstructionType(.{ .register = .uleb128_register }), + same_value: InstructionType(.{ .register = .uleb128_register }), + register: InstructionType(.{ .register = .uleb128_register, .target_register = .uleb128_register }), + remember_state: InstructionType(.{}), + restore_state: InstructionType(.{}), + def_cfa: InstructionType(.{ .register = .uleb128_register, .offset = .uleb128_offset }), + def_cfa_register: InstructionType(.{ .register = .uleb128_register }), + def_cfa_offset: InstructionType(.{ .offset = .uleb128_offset }), + def_cfa_expression: InstructionType(.{ .block = .block }), + expression: InstructionType(.{ .register = .uleb128_register, .block = .block }), + offset_extended_sf: InstructionType(.{ .register = .uleb128_register, .offset = .sleb128_offset }), + def_cfa_sf: InstructionType(.{ .register = .uleb128_register, .offset = .sleb128_offset }), + def_cfa_offset_sf: InstructionType(.{ .offset = .sleb128_offset }), + val_offset: InstructionType(.{ .register = .uleb128_register, .offset = .uleb128_offset }), + val_offset_sf: InstructionType(.{ .register = .uleb128_register, .offset = .sleb128_offset }), + val_expression: InstructionType(.{ .register = .uleb128_register, .block = .block }), + + fn readOperands( + self: *Instruction, + stream: *std.io.FixedBufferStream([]const u8), + opcode_value: ?u6, + addr_size_bytes: u8, + endian: std.builtin.Endian, + ) !void { + switch (self.*) { + inline else => |*inst| inst.* = try @TypeOf(inst.*).read(stream, opcode_value, addr_size_bytes, endian), + } + } + + pub fn read( + stream: *std.io.FixedBufferStream([]const u8), + addr_size_bytes: u8, + endian: std.builtin.Endian, + ) !Instruction { + return switch (try stream.reader().readByte()) { + inline Opcode.lo_inline...Opcode.hi_inline => |opcode| blk: { + const e: Opcode = @enumFromInt(opcode & 0b11000000); + var result = @unionInit(Instruction, @tagName(e), undefined); + try result.readOperands(stream, @as(u6, @intCast(opcode & 0b111111)), addr_size_bytes, endian); + break :blk result; + }, + inline Opcode.lo_reserved...Opcode.hi_reserved => |opcode| blk: { + const e: Opcode = @enumFromInt(opcode); + var result = @unionInit(Instruction, @tagName(e), undefined); + try result.readOperands(stream, null, addr_size_bytes, endian); + break :blk result; + }, + Opcode.lo_user...Opcode.hi_user => error.UnimplementedUserOpcode, + else => error.InvalidOpcode, + }; + } +}; + +/// Since register rules are applied (usually) during a panic, +/// checked addition / subtraction is used so that we can return +/// an error and fall back to FP-based unwinding. +pub fn applyOffset(base: usize, offset: i64) !usize { + return if (offset >= 0) + try std.math.add(usize, base, @as(usize, @intCast(offset))) + else + try std.math.sub(usize, base, @as(usize, @intCast(-offset))); +} + +/// This is a virtual machine that runs DWARF call frame instructions. +pub const VirtualMachine = struct { + /// See section 6.4.1 of the DWARF5 specification for details on each + const RegisterRule = union(enum) { + // The spec says that the default rule for each column is the undefined rule. + // However, it also allows ABI / compiler authors to specify alternate defaults, so + // there is a distinction made here. + default: void, + + undefined: void, + same_value: void, + + // offset(N) + offset: i64, + + // val_offset(N) + val_offset: i64, + + // register(R) + register: u8, + + // expression(E) + expression: []const u8, + + // val_expression(E) + val_expression: []const u8, + + // Augmenter-defined rule + architectural: void, + }; + + /// Each row contains unwinding rules for a set of registers. + pub const Row = struct { + /// Offset from `FrameDescriptionEntry.pc_begin` + offset: u64 = 0, + + /// Special-case column that defines the CFA (Canonical Frame Address) rule. + /// The register field of this column defines the register that CFA is derived from. + cfa: Column = .{}, + + /// The register fields in these columns define the register the rule applies to. + columns: ColumnRange = .{}, + + /// Indicates that the next write to any column in this row needs to copy + /// the backing column storage first, as it may be referenced by previous rows. + copy_on_write: bool = false, + }; + + pub const Column = struct { + register: ?u8 = null, + rule: RegisterRule = .{ .default = {} }, + + /// Resolves the register rule and places the result into `out` (see dwarf.abi.regBytes) + pub fn resolveValue( + self: Column, + context: *dwarf.UnwindContext, + expression_context: dwarf.expressions.ExpressionContext, + out: []u8, + ) !void { + switch (self.rule) { + .default => { + const register = self.register orelse return error.InvalidRegister; + try abi.getRegDefaultValue(register, context, out); + }, + .undefined => { + @memset(out, undefined); + }, + .same_value => { + // TODO: This copy could be eliminated if callers always copy the state then call this function to update it + const register = self.register orelse return error.InvalidRegister; + const src = try abi.regBytes(context.thread_context, register, context.reg_context); + if (src.len != out.len) return error.RegisterSizeMismatch; + @memcpy(out, src); + }, + .offset => |offset| { + if (context.cfa) |cfa| { + const addr = try applyOffset(cfa, offset); + if (expression_context.isValidMemory) |isValidMemory| if (!isValidMemory(addr)) return error.InvalidAddress; + const ptr: *const usize = @ptrFromInt(addr); + mem.writeIntSliceNative(usize, out, ptr.*); + } else return error.InvalidCFA; + }, + .val_offset => |offset| { + if (context.cfa) |cfa| { + mem.writeIntSliceNative(usize, out, try applyOffset(cfa, offset)); + } else return error.InvalidCFA; + }, + .register => |register| { + const src = try abi.regBytes(context.thread_context, register, context.reg_context); + if (src.len != out.len) return error.RegisterSizeMismatch; + @memcpy(out, try abi.regBytes(context.thread_context, register, context.reg_context)); + }, + .expression => |expression| { + context.stack_machine.reset(); + const value = try context.stack_machine.run(expression, context.allocator, expression_context, context.cfa.?); + const addr = if (value) |v| blk: { + if (v != .generic) return error.InvalidExpressionValue; + break :blk v.generic; + } else return error.NoExpressionValue; + + if (!context.isValidMemory(addr)) return error.InvalidExpressionAddress; + const ptr: *usize = @ptrFromInt(addr); + mem.writeIntSliceNative(usize, out, ptr.*); + }, + .val_expression => |expression| { + context.stack_machine.reset(); + const value = try context.stack_machine.run(expression, context.allocator, expression_context, context.cfa.?); + if (value) |v| { + if (v != .generic) return error.InvalidExpressionValue; + mem.writeIntSliceNative(usize, out, v.generic); + } else return error.NoExpressionValue; + }, + .architectural => return error.UnimplementedRegisterRule, + } + } + }; + + const ColumnRange = struct { + /// Index into `columns` of the first column in this row. + start: usize = undefined, + len: u8 = 0, + }; + + columns: std.ArrayListUnmanaged(Column) = .{}, + stack: std.ArrayListUnmanaged(ColumnRange) = .{}, + current_row: Row = .{}, + + /// The result of executing the CIE's initial_instructions + cie_row: ?Row = null, + + pub fn deinit(self: *VirtualMachine, allocator: std.mem.Allocator) void { + self.stack.deinit(allocator); + self.columns.deinit(allocator); + self.* = undefined; + } + + pub fn reset(self: *VirtualMachine) void { + self.stack.clearRetainingCapacity(); + self.columns.clearRetainingCapacity(); + self.current_row = .{}; + self.cie_row = null; + } + + /// Return a slice backed by the row's non-CFA columns + pub fn rowColumns(self: VirtualMachine, row: Row) []Column { + return self.columns.items[row.columns.start..][0..row.columns.len]; + } + + /// Either retrieves or adds a column for `register` (non-CFA) in the current row. + fn getOrAddColumn(self: *VirtualMachine, allocator: std.mem.Allocator, register: u8) !*Column { + for (self.rowColumns(self.current_row)) |*c| { + if (c.register == register) return c; + } + + if (self.current_row.columns.len == 0) { + self.current_row.columns.start = self.columns.items.len; + } + self.current_row.columns.len += 1; + + const column = try self.columns.addOne(allocator); + column.* = .{ + .register = register, + }; + + return column; + } + + /// Runs the CIE instructions, then the FDE instructions. Execution halts + /// once the row that corresponds to `pc` is known, and the row is returned. + pub fn runTo( + self: *VirtualMachine, + allocator: std.mem.Allocator, + pc: u64, + cie: dwarf.CommonInformationEntry, + fde: dwarf.FrameDescriptionEntry, + addr_size_bytes: u8, + endian: std.builtin.Endian, + ) !Row { + assert(self.cie_row == null); + if (pc < fde.pc_begin or pc >= fde.pc_begin + fde.pc_range) return error.AddressOutOfRange; + + var prev_row: Row = self.current_row; + + var cie_stream = std.io.fixedBufferStream(cie.initial_instructions); + var fde_stream = std.io.fixedBufferStream(fde.instructions); + var streams = [_]*std.io.FixedBufferStream([]const u8){ + &cie_stream, + &fde_stream, + }; + + for (&streams, 0..) |stream, i| { + while (stream.pos < stream.buffer.len) { + const instruction = try dwarf.call_frame.Instruction.read(stream, addr_size_bytes, endian); + prev_row = try self.step(allocator, cie, i == 0, instruction); + if (pc < fde.pc_begin + self.current_row.offset) return prev_row; + } + } + + return self.current_row; + } + + pub fn runToNative( + self: *VirtualMachine, + allocator: std.mem.Allocator, + pc: u64, + cie: dwarf.CommonInformationEntry, + fde: dwarf.FrameDescriptionEntry, + ) !Row { + return self.runTo(allocator, pc, cie, fde, @sizeOf(usize), builtin.target.cpu.arch.endian()); + } + + fn resolveCopyOnWrite(self: *VirtualMachine, allocator: std.mem.Allocator) !void { + if (!self.current_row.copy_on_write) return; + + const new_start = self.columns.items.len; + if (self.current_row.columns.len > 0) { + try self.columns.ensureUnusedCapacity(allocator, self.current_row.columns.len); + self.columns.appendSliceAssumeCapacity(self.rowColumns(self.current_row)); + self.current_row.columns.start = new_start; + } + } + + /// Executes a single instruction. + /// If this instruction is from the CIE, `is_initial` should be set. + /// Returns the value of `current_row` before executing this instruction. + pub fn step( + self: *VirtualMachine, + allocator: std.mem.Allocator, + cie: dwarf.CommonInformationEntry, + is_initial: bool, + instruction: Instruction, + ) !Row { + // CIE instructions must be run before FDE instructions + assert(!is_initial or self.cie_row == null); + if (!is_initial and self.cie_row == null) { + self.cie_row = self.current_row; + self.current_row.copy_on_write = true; + } + + const prev_row = self.current_row; + switch (instruction) { + .set_loc => |i| { + if (i.operands.address <= self.current_row.offset) return error.InvalidOperation; + // TODO: Check cie.segment_selector_size != 0 for DWARFV4 + self.current_row.offset = i.operands.address; + }, + inline .advance_loc, + .advance_loc1, + .advance_loc2, + .advance_loc4, + => |i| { + self.current_row.offset += i.operands.delta * cie.code_alignment_factor; + self.current_row.copy_on_write = true; + }, + inline .offset, + .offset_extended, + .offset_extended_sf, + => |i| { + try self.resolveCopyOnWrite(allocator); + const column = try self.getOrAddColumn(allocator, i.operands.register); + column.rule = .{ .offset = @as(i64, @intCast(i.operands.offset)) * cie.data_alignment_factor }; + }, + inline .restore, + .restore_extended, + => |i| { + try self.resolveCopyOnWrite(allocator); + if (self.cie_row) |cie_row| { + const column = try self.getOrAddColumn(allocator, i.operands.register); + column.rule = for (self.rowColumns(cie_row)) |cie_column| { + if (cie_column.register == i.operands.register) break cie_column.rule; + } else .{ .default = {} }; + } else return error.InvalidOperation; + }, + .nop => {}, + .undefined => |i| { + try self.resolveCopyOnWrite(allocator); + const column = try self.getOrAddColumn(allocator, i.operands.register); + column.rule = .{ .undefined = {} }; + }, + .same_value => |i| { + try self.resolveCopyOnWrite(allocator); + const column = try self.getOrAddColumn(allocator, i.operands.register); + column.rule = .{ .same_value = {} }; + }, + .register => |i| { + try self.resolveCopyOnWrite(allocator); + const column = try self.getOrAddColumn(allocator, i.operands.register); + column.rule = .{ .register = i.operands.target_register }; + }, + .remember_state => { + try self.stack.append(allocator, self.current_row.columns); + self.current_row.copy_on_write = true; + }, + .restore_state => { + const restored_columns = self.stack.popOrNull() orelse return error.InvalidOperation; + self.columns.shrinkRetainingCapacity(self.columns.items.len - self.current_row.columns.len); + try self.columns.ensureUnusedCapacity(allocator, restored_columns.len); + + self.current_row.columns.start = self.columns.items.len; + self.current_row.columns.len = restored_columns.len; + self.columns.appendSliceAssumeCapacity(self.columns.items[restored_columns.start..][0..restored_columns.len]); + }, + .def_cfa => |i| { + try self.resolveCopyOnWrite(allocator); + self.current_row.cfa = .{ + .register = i.operands.register, + .rule = .{ .val_offset = @intCast(i.operands.offset) }, + }; + }, + .def_cfa_sf => |i| { + try self.resolveCopyOnWrite(allocator); + self.current_row.cfa = .{ + .register = i.operands.register, + .rule = .{ .val_offset = i.operands.offset * cie.data_alignment_factor }, + }; + }, + .def_cfa_register => |i| { + try self.resolveCopyOnWrite(allocator); + if (self.current_row.cfa.register == null or self.current_row.cfa.rule != .val_offset) return error.InvalidOperation; + self.current_row.cfa.register = i.operands.register; + }, + .def_cfa_offset => |i| { + try self.resolveCopyOnWrite(allocator); + if (self.current_row.cfa.register == null or self.current_row.cfa.rule != .val_offset) return error.InvalidOperation; + self.current_row.cfa.rule = .{ + .val_offset = @intCast(i.operands.offset), + }; + }, + .def_cfa_offset_sf => |i| { + try self.resolveCopyOnWrite(allocator); + if (self.current_row.cfa.register == null or self.current_row.cfa.rule != .val_offset) return error.InvalidOperation; + self.current_row.cfa.rule = .{ + .val_offset = i.operands.offset * cie.data_alignment_factor, + }; + }, + .def_cfa_expression => |i| { + try self.resolveCopyOnWrite(allocator); + self.current_row.cfa.register = undefined; + self.current_row.cfa.rule = .{ + .expression = i.operands.block, + }; + }, + .expression => |i| { + try self.resolveCopyOnWrite(allocator); + const column = try self.getOrAddColumn(allocator, i.operands.register); + column.rule = .{ + .expression = i.operands.block, + }; + }, + .val_offset => |i| { + try self.resolveCopyOnWrite(allocator); + const column = try self.getOrAddColumn(allocator, i.operands.register); + column.rule = .{ + .val_offset = @as(i64, @intCast(i.operands.offset)) * cie.data_alignment_factor, + }; + }, + .val_offset_sf => |i| { + try self.resolveCopyOnWrite(allocator); + const column = try self.getOrAddColumn(allocator, i.operands.register); + column.rule = .{ + .val_offset = i.operands.offset * cie.data_alignment_factor, + }; + }, + .val_expression => |i| { + try self.resolveCopyOnWrite(allocator); + const column = try self.getOrAddColumn(allocator, i.operands.register); + column.rule = .{ + .val_expression = i.operands.block, + }; + }, + } + + return prev_row; + } +}; diff --git a/lib/std/dwarf/expressions.zig b/lib/std/dwarf/expressions.zig new file mode 100644 index 000000000000..88291eab0b8a --- /dev/null +++ b/lib/std/dwarf/expressions.zig @@ -0,0 +1,1639 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const OP = @import("OP.zig"); +const leb = std.leb; +const dwarf = std.dwarf; +const abi = dwarf.abi; +const mem = std.mem; +const assert = std.debug.assert; + +/// Expressions can be evaluated in different contexts, each requiring its own set of inputs. +/// Callers should specify all the fields relevant to their context. If a field is required +/// by the expression and it isn't in the context, error.IncompleteExpressionContext is returned. +pub const ExpressionContext = struct { + /// This expression is from a DWARF64 section + is_64: bool = false, + + /// If specified, any addresses will pass through this function before being acccessed + isValidMemory: ?*const fn (address: usize) bool = null, + + /// The compilation unit this expression relates to, if any + compile_unit: ?*const dwarf.CompileUnit = null, + + /// When evaluating a user-presented expression, this is the address of the object being evaluated + object_address: ?*const anyopaque = null, + + /// .debug_addr section + debug_addr: ?[]const u8 = null, + + /// Thread context + thread_context: ?*std.debug.ThreadContext = null, + reg_context: ?abi.RegisterContext = null, + + /// Call frame address, if in a CFI context + cfa: ?usize = null, + + /// This expression is a sub-expression from an OP.entry_value instruction + entry_value_context: bool = false, +}; + +pub const ExpressionOptions = struct { + /// The address size of the target architecture + addr_size: u8 = @sizeOf(usize), + + /// Endianess of the target architecture + endian: std.builtin.Endian = builtin.target.cpu.arch.endian(), + + /// Restrict the stack machine to a subset of opcodes used in call frame instructions + call_frame_context: bool = false, +}; + +// Explcitly defined to support executing sub-expressions +pub const ExpressionError = error{ + UnimplementedExpressionCall, + UnimplementedOpcode, + UnimplementedUserOpcode, + UnimplementedTypedComparison, + UnimplementedTypeConversion, + + UnknownExpressionOpcode, + + IncompleteExpressionContext, + + InvalidCFAOpcode, + InvalidExpression, + InvalidFrameBase, + InvalidIntegralTypeSize, + InvalidRegister, + InvalidSubExpression, + InvalidTypeLength, + + TruncatedIntegralType, +} || abi.AbiError || error{ EndOfStream, Overflow, OutOfMemory, DivisionByZero }; + +/// A stack machine that can decode and run DWARF expressions. +/// Expressions can be decoded for non-native address size and endianness, +/// but can only be executed if the current target matches the configuration. +pub fn StackMachine(comptime options: ExpressionOptions) type { + const addr_type = switch (options.addr_size) { + 2 => u16, + 4 => u32, + 8 => u64, + else => @compileError("Unsupported address size of " ++ options.addr_size), + }; + + const addr_type_signed = switch (options.addr_size) { + 2 => i16, + 4 => i32, + 8 => i64, + else => @compileError("Unsupported address size of " ++ options.addr_size), + }; + + return struct { + const Self = @This(); + + const Operand = union(enum) { + generic: addr_type, + register: u8, + type_size: u8, + branch_offset: i16, + base_register: struct { + base_register: u8, + offset: i64, + }, + composite_location: struct { + size: u64, + offset: i64, + }, + block: []const u8, + register_type: struct { + register: u8, + type_offset: addr_type, + }, + const_type: struct { + type_offset: addr_type, + value_bytes: []const u8, + }, + deref_type: struct { + size: u8, + type_offset: addr_type, + }, + }; + + const Value = union(enum) { + generic: addr_type, + + // Typed value with a maximum size of a register + regval_type: struct { + // Offset of DW_TAG_base_type DIE + type_offset: addr_type, + type_size: u8, + value: addr_type, + }, + + // Typed value specified directly in the instruction stream + const_type: struct { + // Offset of DW_TAG_base_type DIE + type_offset: addr_type, + // Backed by the instruction stream + value_bytes: []const u8, + }, + + pub fn asIntegral(self: Value) !addr_type { + return switch (self) { + .generic => |v| v, + + // TODO: For these two prongs, look up the type and assert it's integral? + .regval_type => |regval_type| regval_type.value, + .const_type => |const_type| { + const value: u64 = switch (const_type.value_bytes.len) { + 1 => mem.readIntSliceNative(u8, const_type.value_bytes), + 2 => mem.readIntSliceNative(u16, const_type.value_bytes), + 4 => mem.readIntSliceNative(u32, const_type.value_bytes), + 8 => mem.readIntSliceNative(u64, const_type.value_bytes), + else => return error.InvalidIntegralTypeSize, + }; + + return std.math.cast(addr_type, value) orelse error.TruncatedIntegralType; + }, + }; + } + }; + + stack: std.ArrayListUnmanaged(Value) = .{}, + + pub fn reset(self: *Self) void { + self.stack.clearRetainingCapacity(); + } + + pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { + self.stack.deinit(allocator); + } + + fn generic(value: anytype) Operand { + const int_info = @typeInfo(@TypeOf(value)).Int; + if (@sizeOf(@TypeOf(value)) > options.addr_size) { + return .{ .generic = switch (int_info.signedness) { + .signed => @bitCast(@as(addr_type_signed, @truncate(value))), + .unsigned => @truncate(value), + } }; + } else { + return .{ .generic = switch (int_info.signedness) { + .signed => @bitCast(@as(addr_type_signed, @intCast(value))), + .unsigned => @intCast(value), + } }; + } + } + + pub fn readOperand(stream: *std.io.FixedBufferStream([]const u8), opcode: u8, context: ExpressionContext) !?Operand { + const reader = stream.reader(); + return switch (opcode) { + OP.addr => generic(try reader.readInt(addr_type, options.endian)), + OP.call_ref => if (context.is_64) + generic(try reader.readInt(u64, options.endian)) + else + generic(try reader.readInt(u32, options.endian)), + OP.const1u, + OP.pick, + => generic(try reader.readByte()), + OP.deref_size, + OP.xderef_size, + => .{ .type_size = try reader.readByte() }, + OP.const1s => generic(try reader.readByteSigned()), + OP.const2u, + OP.call2, + => generic(try reader.readInt(u16, options.endian)), + OP.call4 => generic(try reader.readInt(u32, options.endian)), + OP.const2s => generic(try reader.readInt(i16, options.endian)), + OP.bra, + OP.skip, + => .{ .branch_offset = try reader.readInt(i16, options.endian) }, + OP.const4u => generic(try reader.readInt(u32, options.endian)), + OP.const4s => generic(try reader.readInt(i32, options.endian)), + OP.const8u => generic(try reader.readInt(u64, options.endian)), + OP.const8s => generic(try reader.readInt(i64, options.endian)), + OP.constu, + OP.plus_uconst, + OP.addrx, + OP.constx, + OP.convert, + OP.reinterpret, + => generic(try leb.readULEB128(u64, reader)), + OP.consts, + OP.fbreg, + => generic(try leb.readILEB128(i64, reader)), + OP.lit0...OP.lit31 => |n| generic(n - OP.lit0), + OP.reg0...OP.reg31 => |n| .{ .register = n - OP.reg0 }, + OP.breg0...OP.breg31 => |n| .{ .base_register = .{ + .base_register = n - OP.breg0, + .offset = try leb.readILEB128(i64, reader), + } }, + OP.regx => .{ .register = try leb.readULEB128(u8, reader) }, + OP.bregx => blk: { + const base_register = try leb.readULEB128(u8, reader); + const offset = try leb.readILEB128(i64, reader); + break :blk .{ .base_register = .{ + .base_register = base_register, + .offset = offset, + } }; + }, + OP.regval_type => blk: { + const register = try leb.readULEB128(u8, reader); + const type_offset = try leb.readULEB128(addr_type, reader); + break :blk .{ .register_type = .{ + .register = register, + .type_offset = type_offset, + } }; + }, + OP.piece => .{ + .composite_location = .{ + .size = try leb.readULEB128(u8, reader), + .offset = 0, + }, + }, + OP.bit_piece => blk: { + const size = try leb.readULEB128(u8, reader); + const offset = try leb.readILEB128(i64, reader); + break :blk .{ .composite_location = .{ + .size = size, + .offset = offset, + } }; + }, + OP.implicit_value, OP.entry_value => blk: { + const size = try leb.readULEB128(u8, reader); + if (stream.pos + size > stream.buffer.len) return error.InvalidExpression; + const block = stream.buffer[stream.pos..][0..size]; + stream.pos += size; + break :blk .{ + .block = block, + }; + }, + OP.const_type => blk: { + const type_offset = try leb.readULEB128(addr_type, reader); + const size = try reader.readByte(); + if (stream.pos + size > stream.buffer.len) return error.InvalidExpression; + const value_bytes = stream.buffer[stream.pos..][0..size]; + stream.pos += size; + break :blk .{ .const_type = .{ + .type_offset = type_offset, + .value_bytes = value_bytes, + } }; + }, + OP.deref_type, + OP.xderef_type, + => .{ + .deref_type = .{ + .size = try reader.readByte(), + .type_offset = try leb.readULEB128(addr_type, reader), + }, + }, + OP.lo_user...OP.hi_user => return error.UnimplementedUserOpcode, + else => null, + }; + } + + pub fn run( + self: *Self, + expression: []const u8, + allocator: std.mem.Allocator, + context: ExpressionContext, + initial_value: ?usize, + ) ExpressionError!?Value { + if (initial_value) |i| try self.stack.append(allocator, .{ .generic = i }); + var stream = std.io.fixedBufferStream(expression); + while (try self.step(&stream, allocator, context)) {} + if (self.stack.items.len == 0) return null; + return self.stack.items[self.stack.items.len - 1]; + } + + /// Reads an opcode and its operands from `stream`, then executes it + pub fn step( + self: *Self, + stream: *std.io.FixedBufferStream([]const u8), + allocator: std.mem.Allocator, + context: ExpressionContext, + ) ExpressionError!bool { + if (@sizeOf(usize) != @sizeOf(addr_type) or options.endian != comptime builtin.target.cpu.arch.endian()) + @compileError("Execution of non-native address sizes / endianness is not supported"); + + const opcode = try stream.reader().readByte(); + if (options.call_frame_context and !isOpcodeValidInCFA(opcode)) return error.InvalidCFAOpcode; + switch (opcode) { + + // 2.5.1.1: Literal Encodings + OP.lit0...OP.lit31, + OP.addr, + OP.const1u, + OP.const2u, + OP.const4u, + OP.const8u, + OP.const1s, + OP.const2s, + OP.const4s, + OP.const8s, + OP.constu, + OP.consts, + => try self.stack.append(allocator, .{ .generic = (try readOperand(stream, opcode, context)).?.generic }), + + OP.const_type => { + const const_type = (try readOperand(stream, opcode, context)).?.const_type; + try self.stack.append(allocator, .{ .const_type = .{ + .type_offset = const_type.type_offset, + .value_bytes = const_type.value_bytes, + } }); + }, + + OP.addrx, + OP.constx, + => { + if (context.compile_unit == null) return error.IncompleteExpressionContext; + if (context.debug_addr == null) return error.IncompleteExpressionContext; + const debug_addr_index = (try readOperand(stream, opcode, context)).?.generic; + const offset = context.compile_unit.?.addr_base + debug_addr_index; + if (offset >= context.debug_addr.?.len) return error.InvalidExpression; + const value = mem.readIntSliceNative(usize, context.debug_addr.?[offset..][0..@sizeOf(usize)]); + try self.stack.append(allocator, .{ .generic = value }); + }, + + // 2.5.1.2: Register Values + OP.fbreg => { + if (context.compile_unit == null) return error.IncompleteExpressionContext; + if (context.compile_unit.?.frame_base == null) return error.IncompleteExpressionContext; + + const offset: i64 = @intCast((try readOperand(stream, opcode, context)).?.generic); + _ = offset; + + switch (context.compile_unit.?.frame_base.?.*) { + .ExprLoc => { + // TODO: Run this expression in a nested stack machine + return error.UnimplementedOpcode; + }, + .LocListOffset => { + // TODO: Read value from .debug_loclists + return error.UnimplementedOpcode; + }, + .SecOffset => { + // TODO: Read value from .debug_loclists + return error.UnimplementedOpcode; + }, + else => return error.InvalidFrameBase, + } + }, + OP.breg0...OP.breg31, + OP.bregx, + => { + if (context.thread_context == null) return error.IncompleteExpressionContext; + + const base_register = (try readOperand(stream, opcode, context)).?.base_register; + var value: i64 = @intCast(mem.readIntSliceNative(usize, try abi.regBytes( + context.thread_context.?, + base_register.base_register, + context.reg_context, + ))); + value += base_register.offset; + try self.stack.append(allocator, .{ .generic = @intCast(value) }); + }, + OP.regval_type => { + const register_type = (try readOperand(stream, opcode, context)).?.register_type; + const value = mem.readIntSliceNative(usize, try abi.regBytes( + context.thread_context.?, + register_type.register, + context.reg_context, + )); + try self.stack.append(allocator, .{ + .regval_type = .{ + .type_offset = register_type.type_offset, + .type_size = @sizeOf(addr_type), + .value = value, + }, + }); + }, + + // 2.5.1.3: Stack Operations + OP.dup => { + if (self.stack.items.len == 0) return error.InvalidExpression; + try self.stack.append(allocator, self.stack.items[self.stack.items.len - 1]); + }, + OP.drop => { + _ = self.stack.pop(); + }, + OP.pick, OP.over => { + const stack_index = if (opcode == OP.over) 1 else (try readOperand(stream, opcode, context)).?.generic; + if (stack_index >= self.stack.items.len) return error.InvalidExpression; + try self.stack.append(allocator, self.stack.items[self.stack.items.len - 1 - stack_index]); + }, + OP.swap => { + if (self.stack.items.len < 2) return error.InvalidExpression; + mem.swap(Value, &self.stack.items[self.stack.items.len - 1], &self.stack.items[self.stack.items.len - 2]); + }, + OP.rot => { + if (self.stack.items.len < 3) return error.InvalidExpression; + const first = self.stack.items[self.stack.items.len - 1]; + self.stack.items[self.stack.items.len - 1] = self.stack.items[self.stack.items.len - 2]; + self.stack.items[self.stack.items.len - 2] = self.stack.items[self.stack.items.len - 3]; + self.stack.items[self.stack.items.len - 3] = first; + }, + OP.deref, + OP.xderef, + OP.deref_size, + OP.xderef_size, + OP.deref_type, + OP.xderef_type, + => { + if (self.stack.items.len == 0) return error.InvalidExpression; + var addr = try self.stack.items[self.stack.items.len - 1].asIntegral(); + const addr_space_identifier: ?usize = switch (opcode) { + OP.xderef, + OP.xderef_size, + OP.xderef_type, + => blk: { + _ = self.stack.pop(); + if (self.stack.items.len == 0) return error.InvalidExpression; + break :blk try self.stack.items[self.stack.items.len - 1].asIntegral(); + }, + else => null, + }; + + // Usage of addr_space_identifier in the address calculation is implementation defined. + // This code will need to be updated to handle any architectures that utilize this. + _ = addr_space_identifier; + + if (context.isValidMemory) |isValidMemory| if (!isValidMemory(addr)) return error.InvalidExpression; + + const operand = try readOperand(stream, opcode, context); + const size = switch (opcode) { + OP.deref, + OP.xderef, + => @sizeOf(addr_type), + OP.deref_size, + OP.xderef_size, + => operand.?.type_size, + OP.deref_type, + OP.xderef_type, + => operand.?.deref_type.size, + else => unreachable, + }; + + const value: addr_type = std.math.cast(addr_type, @as(u64, switch (size) { + 1 => @as(*const u8, @ptrFromInt(addr)).*, + 2 => @as(*const u16, @ptrFromInt(addr)).*, + 4 => @as(*const u32, @ptrFromInt(addr)).*, + 8 => @as(*const u64, @ptrFromInt(addr)).*, + else => return error.InvalidExpression, + })) orelse return error.InvalidExpression; + + switch (opcode) { + OP.deref_type, + OP.xderef_type, + => { + self.stack.items[self.stack.items.len - 1] = .{ + .regval_type = .{ + .type_offset = operand.?.deref_type.type_offset, + .type_size = operand.?.deref_type.size, + .value = value, + }, + }; + }, + else => { + self.stack.items[self.stack.items.len - 1] = .{ .generic = value }; + }, + } + }, + OP.push_object_address => { + // In sub-expressions, `push_object_address` is not meaningful (as per the + // spec), so treat it like a nop + if (!context.entry_value_context) { + if (context.object_address == null) return error.IncompleteExpressionContext; + try self.stack.append(allocator, .{ .generic = @intFromPtr(context.object_address.?) }); + } + }, + OP.form_tls_address => { + return error.UnimplementedOpcode; + }, + OP.call_frame_cfa => { + if (context.cfa) |cfa| { + try self.stack.append(allocator, .{ .generic = cfa }); + } else return error.IncompleteExpressionContext; + }, + + // 2.5.1.4: Arithmetic and Logical Operations + OP.abs => { + if (self.stack.items.len == 0) return error.InvalidExpression; + const value: isize = @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral()); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = std.math.absCast(value), + }; + }, + OP.@"and" => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const a = try self.stack.pop().asIntegral(); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = a & try self.stack.items[self.stack.items.len - 1].asIntegral(), + }; + }, + OP.div => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const a: isize = @bitCast(try self.stack.pop().asIntegral()); + const b: isize = @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral()); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = @bitCast(try std.math.divTrunc(isize, b, a)), + }; + }, + OP.minus => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const b = try self.stack.pop().asIntegral(); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = try std.math.sub(addr_type, try self.stack.items[self.stack.items.len - 1].asIntegral(), b), + }; + }, + OP.mod => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const a: isize = @bitCast(try self.stack.pop().asIntegral()); + const b: isize = @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral()); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = @bitCast(@mod(b, a)), + }; + }, + OP.mul => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const a: isize = @bitCast(try self.stack.pop().asIntegral()); + const b: isize = @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral()); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = @bitCast(@mulWithOverflow(a, b)[0]), + }; + }, + OP.neg => { + if (self.stack.items.len == 0) return error.InvalidExpression; + self.stack.items[self.stack.items.len - 1] = .{ + .generic = @bitCast( + try std.math.negate( + @as(isize, @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral())), + ), + ), + }; + }, + OP.not => { + if (self.stack.items.len == 0) return error.InvalidExpression; + self.stack.items[self.stack.items.len - 1] = .{ + .generic = ~try self.stack.items[self.stack.items.len - 1].asIntegral(), + }; + }, + OP.@"or" => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const a = try self.stack.pop().asIntegral(); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = a | try self.stack.items[self.stack.items.len - 1].asIntegral(), + }; + }, + OP.plus => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const b = try self.stack.pop().asIntegral(); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = try std.math.add(addr_type, try self.stack.items[self.stack.items.len - 1].asIntegral(), b), + }; + }, + OP.plus_uconst => { + if (self.stack.items.len == 0) return error.InvalidExpression; + const constant = (try readOperand(stream, opcode, context)).?.generic; + self.stack.items[self.stack.items.len - 1] = .{ + .generic = try std.math.add(addr_type, try self.stack.items[self.stack.items.len - 1].asIntegral(), constant), + }; + }, + OP.shl => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const a = try self.stack.pop().asIntegral(); + const b = try self.stack.items[self.stack.items.len - 1].asIntegral(); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = std.math.shl(usize, b, a), + }; + }, + OP.shr => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const a = try self.stack.pop().asIntegral(); + const b = try self.stack.items[self.stack.items.len - 1].asIntegral(); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = std.math.shr(usize, b, a), + }; + }, + OP.shra => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const a = try self.stack.pop().asIntegral(); + const b: isize = @bitCast(try self.stack.items[self.stack.items.len - 1].asIntegral()); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = @bitCast(std.math.shr(isize, b, a)), + }; + }, + OP.xor => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const a = try self.stack.pop().asIntegral(); + self.stack.items[self.stack.items.len - 1] = .{ + .generic = a ^ try self.stack.items[self.stack.items.len - 1].asIntegral(), + }; + }, + + // 2.5.1.5: Control Flow Operations + OP.le, + OP.ge, + OP.eq, + OP.lt, + OP.gt, + OP.ne, + => { + if (self.stack.items.len < 2) return error.InvalidExpression; + const a = self.stack.pop(); + const b = self.stack.items[self.stack.items.len - 1]; + + if (a == .generic and b == .generic) { + const a_int: isize = @bitCast(a.asIntegral() catch unreachable); + const b_int: isize = @bitCast(b.asIntegral() catch unreachable); + const result = @intFromBool(switch (opcode) { + OP.le => b_int <= a_int, + OP.ge => b_int >= a_int, + OP.eq => b_int == a_int, + OP.lt => b_int < a_int, + OP.gt => b_int > a_int, + OP.ne => b_int != a_int, + else => unreachable, + }); + + self.stack.items[self.stack.items.len - 1] = .{ .generic = result }; + } else { + // TODO: Load the types referenced by these values, find their comparison operator, and run it + return error.UnimplementedTypedComparison; + } + }, + OP.skip, OP.bra => { + const branch_offset = (try readOperand(stream, opcode, context)).?.branch_offset; + const condition = if (opcode == OP.bra) blk: { + if (self.stack.items.len == 0) return error.InvalidExpression; + break :blk try self.stack.pop().asIntegral() != 0; + } else true; + + if (condition) { + const new_pos = std.math.cast( + usize, + try std.math.add(isize, @as(isize, @intCast(stream.pos)), branch_offset), + ) orelse return error.InvalidExpression; + + if (new_pos < 0 or new_pos > stream.buffer.len) return error.InvalidExpression; + stream.pos = new_pos; + } + }, + OP.call2, + OP.call4, + OP.call_ref, + => { + const debug_info_offset = (try readOperand(stream, opcode, context)).?.generic; + _ = debug_info_offset; + + // TODO: Load a DIE entry at debug_info_offset in a .debug_info section (the spec says that it + // can be in a separate exe / shared object from the one containing this expression). + // Transfer control to the DW_AT_location attribute, with the current stack as input. + + return error.UnimplementedExpressionCall; + }, + + // 2.5.1.6: Type Conversions + OP.convert => { + if (self.stack.items.len == 0) return error.InvalidExpression; + const type_offset = (try readOperand(stream, opcode, context)).?.generic; + + // TODO: Load the DW_TAG_base_type entries in context.compile_unit and verify both types are the same size + const value = self.stack.items[self.stack.items.len - 1]; + if (type_offset == 0) { + self.stack.items[self.stack.items.len - 1] = .{ .generic = try value.asIntegral() }; + } else { + // TODO: Load the DW_TAG_base_type entry in context.compile_unit, find a conversion operator + // from the old type to the new type, run it. + return error.UnimplementedTypeConversion; + } + }, + OP.reinterpret => { + if (self.stack.items.len == 0) return error.InvalidExpression; + const type_offset = (try readOperand(stream, opcode, context)).?.generic; + + // TODO: Load the DW_TAG_base_type entries in context.compile_unit and verify both types are the same size + const value = self.stack.items[self.stack.items.len - 1]; + if (type_offset == 0) { + self.stack.items[self.stack.items.len - 1] = .{ .generic = try value.asIntegral() }; + } else { + self.stack.items[self.stack.items.len - 1] = switch (value) { + .generic => |v| .{ + .regval_type = .{ + .type_offset = type_offset, + .type_size = @sizeOf(addr_type), + .value = v, + }, + }, + .regval_type => |r| .{ + .regval_type = .{ + .type_offset = type_offset, + .type_size = r.type_size, + .value = r.value, + }, + }, + .const_type => |c| .{ + .const_type = .{ + .type_offset = type_offset, + .value_bytes = c.value_bytes, + }, + }, + }; + } + }, + + // 2.5.1.7: Special Operations + OP.nop => {}, + OP.entry_value => { + const block = (try readOperand(stream, opcode, context)).?.block; + if (block.len == 0) return error.InvalidSubExpression; + + // TODO: The spec states that this sub-expression needs to observe the state (ie. registers) + // as it was upon entering the current subprogram. If this isn't being called at the + // end of a frame unwind operation, an additional ThreadContext with this state will be needed. + + if (isOpcodeRegisterLocation(block[0])) { + if (context.thread_context == null) return error.IncompleteExpressionContext; + + var block_stream = std.io.fixedBufferStream(block); + const register = (try readOperand(&block_stream, block[0], context)).?.register; + const value = mem.readIntSliceNative(usize, try abi.regBytes(context.thread_context.?, register, context.reg_context)); + try self.stack.append(allocator, .{ .generic = value }); + } else { + var stack_machine: Self = .{}; + defer stack_machine.deinit(allocator); + + var sub_context = context; + sub_context.entry_value_context = true; + const result = try stack_machine.run(block, allocator, sub_context, null); + try self.stack.append(allocator, result orelse return error.InvalidSubExpression); + } + }, + + // These have already been handled by readOperand + OP.lo_user...OP.hi_user => unreachable, + else => { + //std.debug.print("Unknown DWARF expression opcode: {x}\n", .{opcode}); + return error.UnknownExpressionOpcode; + }, + } + + return stream.pos < stream.buffer.len; + } + }; +} + +pub fn Builder(comptime options: ExpressionOptions) type { + const addr_type = switch (options.addr_size) { + 2 => u16, + 4 => u32, + 8 => u64, + else => @compileError("Unsupported address size of " ++ options.addr_size), + }; + + return struct { + /// Zero-operand instructions + pub fn writeOpcode(writer: anytype, comptime opcode: u8) !void { + if (options.call_frame_context and !comptime isOpcodeValidInCFA(opcode)) return error.InvalidCFAOpcode; + switch (opcode) { + OP.dup, + OP.drop, + OP.over, + OP.swap, + OP.rot, + OP.deref, + OP.xderef, + OP.push_object_address, + OP.form_tls_address, + OP.call_frame_cfa, + OP.abs, + OP.@"and", + OP.div, + OP.minus, + OP.mod, + OP.mul, + OP.neg, + OP.not, + OP.@"or", + OP.plus, + OP.shl, + OP.shr, + OP.shra, + OP.xor, + OP.le, + OP.ge, + OP.eq, + OP.lt, + OP.gt, + OP.ne, + OP.nop, + OP.stack_value, + => try writer.writeByte(opcode), + else => @compileError("This opcode requires operands, use `write()` instead"), + } + } + + // 2.5.1.1: Literal Encodings + pub fn writeLiteral(writer: anytype, literal: u8) !void { + switch (literal) { + 0...31 => |n| try writer.writeByte(n + OP.lit0), + else => return error.InvalidLiteral, + } + } + + pub fn writeConst(writer: anytype, comptime T: type, value: T) !void { + if (@typeInfo(T) != .Int) @compileError("Constants must be integers"); + + switch (T) { + u8, i8, u16, i16, u32, i32, u64, i64 => { + try writer.writeByte(switch (T) { + u8 => OP.const1u, + i8 => OP.const1s, + u16 => OP.const2u, + i16 => OP.const2s, + u32 => OP.const4u, + i32 => OP.const4s, + u64 => OP.const8u, + i64 => OP.const8s, + else => unreachable, + }); + + try writer.writeInt(T, value, options.endian); + }, + else => switch (@typeInfo(T).Int.signedness) { + .unsigned => { + try writer.writeByte(OP.constu); + try leb.writeULEB128(writer, value); + }, + .signed => { + try writer.writeByte(OP.consts); + try leb.writeILEB128(writer, value); + }, + }, + } + } + + pub fn writeConstx(writer: anytype, debug_addr_offset: anytype) !void { + try writer.writeByte(OP.constx); + try leb.writeULEB128(writer, debug_addr_offset); + } + + pub fn writeConstType(writer: anytype, die_offset: anytype, value_bytes: []const u8) !void { + if (options.call_frame_context) return error.InvalidCFAOpcode; + if (value_bytes.len > 0xff) return error.InvalidTypeLength; + try writer.writeByte(OP.const_type); + try leb.writeULEB128(writer, die_offset); + try writer.writeByte(@intCast(value_bytes.len)); + try writer.writeAll(value_bytes); + } + + pub fn writeAddr(writer: anytype, value: addr_type) !void { + try writer.writeByte(OP.addr); + try writer.writeInt(addr_type, value, options.endian); + } + + pub fn writeAddrx(writer: anytype, debug_addr_offset: anytype) !void { + if (options.call_frame_context) return error.InvalidCFAOpcode; + try writer.writeByte(OP.addrx); + try leb.writeULEB128(writer, debug_addr_offset); + } + + // 2.5.1.2: Register Values + pub fn writeFbreg(writer: anytype, offset: anytype) !void { + try writer.writeByte(OP.fbreg); + try leb.writeILEB128(writer, offset); + } + + pub fn writeBreg(writer: anytype, register: u8, offset: anytype) !void { + if (register > 31) return error.InvalidRegister; + try writer.writeByte(OP.breg0 + register); + try leb.writeILEB128(writer, offset); + } + + pub fn writeBregx(writer: anytype, register: anytype, offset: anytype) !void { + try writer.writeByte(OP.bregx); + try leb.writeULEB128(writer, register); + try leb.writeILEB128(writer, offset); + } + + pub fn writeRegvalType(writer: anytype, register: anytype, offset: anytype) !void { + if (options.call_frame_context) return error.InvalidCFAOpcode; + try writer.writeByte(OP.regval_type); + try leb.writeULEB128(writer, register); + try leb.writeULEB128(writer, offset); + } + + // 2.5.1.3: Stack Operations + pub fn writePick(writer: anytype, index: u8) !void { + try writer.writeByte(OP.pick); + try writer.writeByte(index); + } + + pub fn writeDerefSize(writer: anytype, size: u8) !void { + try writer.writeByte(OP.deref_size); + try writer.writeByte(size); + } + + pub fn writeXDerefSize(writer: anytype, size: u8) !void { + try writer.writeByte(OP.xderef_size); + try writer.writeByte(size); + } + + pub fn writeDerefType(writer: anytype, size: u8, die_offset: anytype) !void { + if (options.call_frame_context) return error.InvalidCFAOpcode; + try writer.writeByte(OP.deref_type); + try writer.writeByte(size); + try leb.writeULEB128(writer, die_offset); + } + + pub fn writeXDerefType(writer: anytype, size: u8, die_offset: anytype) !void { + try writer.writeByte(OP.xderef_type); + try writer.writeByte(size); + try leb.writeULEB128(writer, die_offset); + } + + // 2.5.1.4: Arithmetic and Logical Operations + + pub fn writePlusUconst(writer: anytype, uint_value: anytype) !void { + try writer.writeByte(OP.plus_uconst); + try leb.writeULEB128(writer, uint_value); + } + + // 2.5.1.5: Control Flow Operations + + pub fn writeSkip(writer: anytype, offset: i16) !void { + try writer.writeByte(OP.skip); + try writer.writeInt(i16, offset, options.endian); + } + + pub fn writeBra(writer: anytype, offset: i16) !void { + try writer.writeByte(OP.bra); + try writer.writeInt(i16, offset, options.endian); + } + + pub fn writeCall(writer: anytype, comptime T: type, offset: T) !void { + if (options.call_frame_context) return error.InvalidCFAOpcode; + switch (T) { + u16 => try writer.writeByte(OP.call2), + u32 => try writer.writeByte(OP.call4), + else => @compileError("Call operand must be a 2 or 4 byte offset"), + } + + try writer.writeInt(T, offset, options.endian); + } + + pub fn writeCallRef(writer: anytype, comptime is_64: bool, value: if (is_64) u64 else u32) !void { + if (options.call_frame_context) return error.InvalidCFAOpcode; + try writer.writeByte(OP.call_ref); + try writer.writeInt(if (is_64) u64 else u32, value, options.endian); + } + + pub fn writeConvert(writer: anytype, die_offset: anytype) !void { + if (options.call_frame_context) return error.InvalidCFAOpcode; + try writer.writeByte(OP.convert); + try leb.writeULEB128(writer, die_offset); + } + + pub fn writeReinterpret(writer: anytype, die_offset: anytype) !void { + if (options.call_frame_context) return error.InvalidCFAOpcode; + try writer.writeByte(OP.reinterpret); + try leb.writeULEB128(writer, die_offset); + } + + // 2.5.1.7: Special Operations + + pub fn writeEntryValue(writer: anytype, expression: []const u8) !void { + try writer.writeByte(OP.entry_value); + try leb.writeULEB128(writer, expression.len); + try writer.writeAll(expression); + } + + // 2.6: Location Descriptions + pub fn writeReg(writer: anytype, register: u8) !void { + try writer.writeByte(OP.reg0 + register); + } + + pub fn writeRegx(writer: anytype, register: anytype) !void { + try writer.writeByte(OP.regx); + try leb.writeULEB128(writer, register); + } + + pub fn writeImplicitValue(writer: anytype, value_bytes: []const u8) !void { + try writer.writeByte(OP.implicit_value); + try leb.writeULEB128(writer, value_bytes.len); + try writer.writeAll(value_bytes); + } + }; +} + +// Certain opcodes are not allowed in a CFA context, see 6.4.2 +fn isOpcodeValidInCFA(opcode: u8) bool { + return switch (opcode) { + OP.addrx, + OP.call2, + OP.call4, + OP.call_ref, + OP.const_type, + OP.constx, + OP.convert, + OP.deref_type, + OP.regval_type, + OP.reinterpret, + OP.push_object_address, + OP.call_frame_cfa, + => false, + else => true, + }; +} + +fn isOpcodeRegisterLocation(opcode: u8) bool { + return switch (opcode) { + OP.reg0...OP.reg31, OP.regx => true, + else => false, + }; +} + +const testing = std.testing; +test "DWARF expressions" { + const allocator = std.testing.allocator; + + const options = ExpressionOptions{}; + var stack_machine = StackMachine(options){}; + defer stack_machine.deinit(allocator); + + const b = Builder(options); + + var program = std.ArrayList(u8).init(allocator); + defer program.deinit(); + + const writer = program.writer(); + + // Literals + { + const context = ExpressionContext{}; + for (0..32) |i| { + try b.writeLiteral(writer, @intCast(i)); + } + + _ = try stack_machine.run(program.items, allocator, context, 0); + + for (0..32) |i| { + const expected = 31 - i; + try testing.expectEqual(expected, stack_machine.stack.popOrNull().?.generic); + } + } + + // Constants + { + stack_machine.reset(); + program.clearRetainingCapacity(); + + const input = [_]comptime_int{ + 1, + -1, + @as(usize, @truncate(0x0fff)), + @as(isize, @truncate(-0x0fff)), + @as(usize, @truncate(0x0fffffff)), + @as(isize, @truncate(-0x0fffffff)), + @as(usize, @truncate(0x0fffffffffffffff)), + @as(isize, @truncate(-0x0fffffffffffffff)), + @as(usize, @truncate(0x8000000)), + @as(isize, @truncate(-0x8000000)), + @as(usize, @truncate(0x12345678_12345678)), + @as(usize, @truncate(0xffffffff_ffffffff)), + @as(usize, @truncate(0xeeeeeeee_eeeeeeee)), + }; + + try b.writeConst(writer, u8, input[0]); + try b.writeConst(writer, i8, input[1]); + try b.writeConst(writer, u16, input[2]); + try b.writeConst(writer, i16, input[3]); + try b.writeConst(writer, u32, input[4]); + try b.writeConst(writer, i32, input[5]); + try b.writeConst(writer, u64, input[6]); + try b.writeConst(writer, i64, input[7]); + try b.writeConst(writer, u28, input[8]); + try b.writeConst(writer, i28, input[9]); + try b.writeAddr(writer, input[10]); + + var mock_compile_unit: dwarf.CompileUnit = undefined; + mock_compile_unit.addr_base = 1; + + var mock_debug_addr = std.ArrayList(u8).init(allocator); + defer mock_debug_addr.deinit(); + + try mock_debug_addr.writer().writeIntNative(u16, 0); + try mock_debug_addr.writer().writeIntNative(usize, input[11]); + try mock_debug_addr.writer().writeIntNative(usize, input[12]); + + const context = ExpressionContext{ + .compile_unit = &mock_compile_unit, + .debug_addr = mock_debug_addr.items, + }; + + try b.writeConstx(writer, @as(usize, 1)); + try b.writeAddrx(writer, @as(usize, 1 + @sizeOf(usize))); + + const die_offset: usize = @truncate(0xaabbccdd); + const type_bytes: []const u8 = &.{ 1, 2, 3, 4 }; + try b.writeConstType(writer, die_offset, type_bytes); + + _ = try stack_machine.run(program.items, allocator, context, 0); + + const const_type = stack_machine.stack.popOrNull().?.const_type; + try testing.expectEqual(die_offset, const_type.type_offset); + try testing.expectEqualSlices(u8, type_bytes, const_type.value_bytes); + + const expected = .{ + .{ usize, input[12], usize }, + .{ usize, input[11], usize }, + .{ usize, input[10], usize }, + .{ isize, input[9], isize }, + .{ usize, input[8], usize }, + .{ isize, input[7], isize }, + .{ usize, input[6], usize }, + .{ isize, input[5], isize }, + .{ usize, input[4], usize }, + .{ isize, input[3], isize }, + .{ usize, input[2], usize }, + .{ isize, input[1], isize }, + .{ usize, input[0], usize }, + }; + + inline for (expected) |e| { + try testing.expectEqual(@as(e[0], e[1]), @as(e[2], @bitCast(stack_machine.stack.popOrNull().?.generic))); + } + } + + // Register values + if (@sizeOf(std.debug.ThreadContext) != 0) { + stack_machine.reset(); + program.clearRetainingCapacity(); + + const reg_context = abi.RegisterContext{ + .eh_frame = true, + .is_macho = builtin.os.tag == .macos, + }; + var thread_context: std.debug.ThreadContext = undefined; + std.debug.relocateContext(&thread_context); + const context = ExpressionContext{ + .thread_context = &thread_context, + .reg_context = reg_context, + }; + + // Only test register operations on arch / os that have them implemented + if (abi.regBytes(&thread_context, 0, reg_context)) |reg_bytes| { + + // TODO: Test fbreg (once implemented): mock a DIE and point compile_unit.frame_base at it + + mem.writeIntSliceNative(usize, reg_bytes, 0xee); + (try abi.regValueNative(usize, &thread_context, abi.fpRegNum(reg_context), reg_context)).* = 1; + (try abi.regValueNative(usize, &thread_context, abi.spRegNum(reg_context), reg_context)).* = 2; + (try abi.regValueNative(usize, &thread_context, abi.ipRegNum(), reg_context)).* = 3; + + try b.writeBreg(writer, abi.fpRegNum(reg_context), @as(usize, 100)); + try b.writeBreg(writer, abi.spRegNum(reg_context), @as(usize, 200)); + try b.writeBregx(writer, abi.ipRegNum(), @as(usize, 300)); + try b.writeRegvalType(writer, @as(u8, 0), @as(usize, 400)); + + _ = try stack_machine.run(program.items, allocator, context, 0); + + const regval_type = stack_machine.stack.popOrNull().?.regval_type; + try testing.expectEqual(@as(usize, 400), regval_type.type_offset); + try testing.expectEqual(@as(u8, @sizeOf(usize)), regval_type.type_size); + try testing.expectEqual(@as(usize, 0xee), regval_type.value); + + try testing.expectEqual(@as(usize, 303), stack_machine.stack.popOrNull().?.generic); + try testing.expectEqual(@as(usize, 202), stack_machine.stack.popOrNull().?.generic); + try testing.expectEqual(@as(usize, 101), stack_machine.stack.popOrNull().?.generic); + } else |err| { + switch (err) { + error.UnimplementedArch, + error.UnimplementedOs, + error.ThreadContextNotSupported, + => {}, + else => return err, + } + } + } + + // Stack operations + { + var context = ExpressionContext{}; + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u8, 1); + try b.writeOpcode(writer, OP.dup); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 1), stack_machine.stack.popOrNull().?.generic); + try testing.expectEqual(@as(usize, 1), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u8, 1); + try b.writeOpcode(writer, OP.drop); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expect(stack_machine.stack.popOrNull() == null); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u8, 4); + try b.writeConst(writer, u8, 5); + try b.writeConst(writer, u8, 6); + try b.writePick(writer, 2); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 4), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u8, 4); + try b.writeConst(writer, u8, 5); + try b.writeConst(writer, u8, 6); + try b.writeOpcode(writer, OP.over); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 5), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u8, 5); + try b.writeConst(writer, u8, 6); + try b.writeOpcode(writer, OP.swap); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 5), stack_machine.stack.popOrNull().?.generic); + try testing.expectEqual(@as(usize, 6), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u8, 4); + try b.writeConst(writer, u8, 5); + try b.writeConst(writer, u8, 6); + try b.writeOpcode(writer, OP.rot); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 5), stack_machine.stack.popOrNull().?.generic); + try testing.expectEqual(@as(usize, 4), stack_machine.stack.popOrNull().?.generic); + try testing.expectEqual(@as(usize, 6), stack_machine.stack.popOrNull().?.generic); + + const deref_target: usize = @truncate(0xffeeffee_ffeeffee); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeAddr(writer, @intFromPtr(&deref_target)); + try b.writeOpcode(writer, OP.deref); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(deref_target, stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeLiteral(writer, 0); + try b.writeAddr(writer, @intFromPtr(&deref_target)); + try b.writeOpcode(writer, OP.xderef); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(deref_target, stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeAddr(writer, @intFromPtr(&deref_target)); + try b.writeDerefSize(writer, 1); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeLiteral(writer, 0); + try b.writeAddr(writer, @intFromPtr(&deref_target)); + try b.writeXDerefSize(writer, 1); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), stack_machine.stack.popOrNull().?.generic); + + const type_offset: usize = @truncate(0xaabbaabb_aabbaabb); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeAddr(writer, @intFromPtr(&deref_target)); + try b.writeDerefType(writer, 1, type_offset); + _ = try stack_machine.run(program.items, allocator, context, null); + const deref_type = stack_machine.stack.popOrNull().?.regval_type; + try testing.expectEqual(type_offset, deref_type.type_offset); + try testing.expectEqual(@as(u8, 1), deref_type.type_size); + try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), deref_type.value); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeLiteral(writer, 0); + try b.writeAddr(writer, @intFromPtr(&deref_target)); + try b.writeXDerefType(writer, 1, type_offset); + _ = try stack_machine.run(program.items, allocator, context, null); + const xderef_type = stack_machine.stack.popOrNull().?.regval_type; + try testing.expectEqual(type_offset, xderef_type.type_offset); + try testing.expectEqual(@as(u8, 1), xderef_type.type_size); + try testing.expectEqual(@as(usize, @as(*const u8, @ptrCast(&deref_target)).*), xderef_type.value); + + context.object_address = &deref_target; + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeOpcode(writer, OP.push_object_address); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, @intFromPtr(context.object_address.?)), stack_machine.stack.popOrNull().?.generic); + + // TODO: Test OP.form_tls_address + + context.cfa = @truncate(0xccddccdd_ccddccdd); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeOpcode(writer, OP.call_frame_cfa); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(context.cfa.?, stack_machine.stack.popOrNull().?.generic); + } + + // Arithmetic and Logical Operations + { + var context = ExpressionContext{}; + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, i16, -4096); + try b.writeOpcode(writer, OP.abs); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 4096), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 0xff0f); + try b.writeConst(writer, u16, 0xf0ff); + try b.writeOpcode(writer, OP.@"and"); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 0xf00f), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, i16, -404); + try b.writeConst(writer, i16, 100); + try b.writeOpcode(writer, OP.div); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(isize, -404 / 100), @as(isize, @bitCast(stack_machine.stack.popOrNull().?.generic))); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 200); + try b.writeConst(writer, u16, 50); + try b.writeOpcode(writer, OP.minus); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 150), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 123); + try b.writeConst(writer, u16, 100); + try b.writeOpcode(writer, OP.mod); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 23), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 0xff); + try b.writeConst(writer, u16, 0xee); + try b.writeOpcode(writer, OP.mul); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 0xed12), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 5); + try b.writeOpcode(writer, OP.neg); + try b.writeConst(writer, i16, -6); + try b.writeOpcode(writer, OP.neg); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 6), stack_machine.stack.popOrNull().?.generic); + try testing.expectEqual(@as(isize, -5), @as(isize, @bitCast(stack_machine.stack.popOrNull().?.generic))); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 0xff0f); + try b.writeOpcode(writer, OP.not); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(~@as(usize, 0xff0f), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 0xff0f); + try b.writeConst(writer, u16, 0xf0ff); + try b.writeOpcode(writer, OP.@"or"); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 0xffff), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, i16, 402); + try b.writeConst(writer, i16, 100); + try b.writeOpcode(writer, OP.plus); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 502), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 4096); + try b.writePlusUconst(writer, @as(usize, 8192)); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 4096 + 8192), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 0xfff); + try b.writeConst(writer, u16, 1); + try b.writeOpcode(writer, OP.shl); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 0xfff << 1), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 0xfff); + try b.writeConst(writer, u16, 1); + try b.writeOpcode(writer, OP.shr); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 0xfff >> 1), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 0xfff); + try b.writeConst(writer, u16, 1); + try b.writeOpcode(writer, OP.shr); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, @bitCast(@as(isize, 0xfff) >> 1)), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConst(writer, u16, 0xf0ff); + try b.writeConst(writer, u16, 0xff0f); + try b.writeOpcode(writer, OP.xor); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 0x0ff0), stack_machine.stack.popOrNull().?.generic); + } + + // Control Flow Operations + { + var context = ExpressionContext{}; + const expected = .{ + .{ OP.le, 1, 1, 0 }, + .{ OP.ge, 1, 0, 1 }, + .{ OP.eq, 1, 0, 0 }, + .{ OP.lt, 0, 1, 0 }, + .{ OP.gt, 0, 0, 1 }, + .{ OP.ne, 0, 1, 1 }, + }; + + inline for (expected) |e| { + stack_machine.reset(); + program.clearRetainingCapacity(); + + try b.writeConst(writer, u16, 0); + try b.writeConst(writer, u16, 0); + try b.writeOpcode(writer, e[0]); + try b.writeConst(writer, u16, 0); + try b.writeConst(writer, u16, 1); + try b.writeOpcode(writer, e[0]); + try b.writeConst(writer, u16, 1); + try b.writeConst(writer, u16, 0); + try b.writeOpcode(writer, e[0]); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, e[3]), stack_machine.stack.popOrNull().?.generic); + try testing.expectEqual(@as(usize, e[2]), stack_machine.stack.popOrNull().?.generic); + try testing.expectEqual(@as(usize, e[1]), stack_machine.stack.popOrNull().?.generic); + } + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeLiteral(writer, 2); + try b.writeSkip(writer, 1); + try b.writeLiteral(writer, 3); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 2), stack_machine.stack.popOrNull().?.generic); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeLiteral(writer, 2); + try b.writeBra(writer, 1); + try b.writeLiteral(writer, 3); + try b.writeLiteral(writer, 0); + try b.writeBra(writer, 1); + try b.writeLiteral(writer, 4); + try b.writeLiteral(writer, 5); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 5), stack_machine.stack.popOrNull().?.generic); + try testing.expectEqual(@as(usize, 4), stack_machine.stack.popOrNull().?.generic); + try testing.expect(stack_machine.stack.popOrNull() == null); + + // TODO: Test call2, call4, call_ref once implemented + + } + + // Type conversions + { + var context = ExpressionContext{}; + stack_machine.reset(); + program.clearRetainingCapacity(); + + // TODO: Test typed OP.convert once implemented + + const value: usize = @truncate(0xffeeffee_ffeeffee); + var value_bytes: [options.addr_size]u8 = undefined; + mem.writeIntSliceNative(usize, &value_bytes, value); + + // Convert to generic type + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConstType(writer, @as(usize, 0), &value_bytes); + try b.writeConvert(writer, @as(usize, 0)); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(value, stack_machine.stack.popOrNull().?.generic); + + // Reinterpret to generic type + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConstType(writer, @as(usize, 0), &value_bytes); + try b.writeReinterpret(writer, @as(usize, 0)); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(value, stack_machine.stack.popOrNull().?.generic); + + // Reinterpret to new type + const die_offset: usize = 0xffee; + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeConstType(writer, @as(usize, 0), &value_bytes); + try b.writeReinterpret(writer, die_offset); + _ = try stack_machine.run(program.items, allocator, context, null); + const const_type = stack_machine.stack.popOrNull().?.const_type; + try testing.expectEqual(die_offset, const_type.type_offset); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeLiteral(writer, 0); + try b.writeReinterpret(writer, die_offset); + _ = try stack_machine.run(program.items, allocator, context, null); + const regval_type = stack_machine.stack.popOrNull().?.regval_type; + try testing.expectEqual(die_offset, regval_type.type_offset); + } + + // Special operations + { + var context = ExpressionContext{}; + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeOpcode(writer, OP.nop); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expect(stack_machine.stack.popOrNull() == null); + + // Sub-expression + { + var sub_program = std.ArrayList(u8).init(allocator); + defer sub_program.deinit(); + const sub_writer = sub_program.writer(); + try b.writeLiteral(sub_writer, 3); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeEntryValue(writer, sub_program.items); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 3), stack_machine.stack.popOrNull().?.generic); + } + + // Register location description + const reg_context = abi.RegisterContext{ + .eh_frame = true, + .is_macho = builtin.os.tag == .macos, + }; + var thread_context: std.debug.ThreadContext = undefined; + std.debug.relocateContext(&thread_context); + context = ExpressionContext{ + .thread_context = &thread_context, + .reg_context = reg_context, + }; + + if (abi.regBytes(&thread_context, 0, reg_context)) |reg_bytes| { + mem.writeIntSliceNative(usize, reg_bytes, 0xee); + + var sub_program = std.ArrayList(u8).init(allocator); + defer sub_program.deinit(); + const sub_writer = sub_program.writer(); + try b.writeReg(sub_writer, 0); + + stack_machine.reset(); + program.clearRetainingCapacity(); + try b.writeEntryValue(writer, sub_program.items); + _ = try stack_machine.run(program.items, allocator, context, null); + try testing.expectEqual(@as(usize, 0xee), stack_machine.stack.popOrNull().?.generic); + } else |err| { + switch (err) { + error.UnimplementedArch, + error.UnimplementedOs, + error.ThreadContextNotSupported, + => {}, + else => return err, + } + } + } +} diff --git a/lib/std/elf.zig b/lib/std/elf.zig index 3ea136fabe71..004e50896017 100644 --- a/lib/std/elf.zig +++ b/lib/std/elf.zig @@ -371,6 +371,9 @@ pub const SHT_LOUSER = 0x80000000; /// End of application-specific pub const SHT_HIUSER = 0xffffffff; +// Note type for .note.gnu.build_id +pub const NT_GNU_BUILD_ID = 3; + /// Local symbol pub const STB_LOCAL = 0; /// Global symbol @@ -1055,6 +1058,11 @@ pub const Shdr = switch (@sizeOf(usize)) { 8 => Elf64_Shdr, else => @compileError("expected pointer size of 32 or 64"), }; +pub const Chdr = switch (@sizeOf(usize)) { + 4 => Elf32_Chdr, + 8 => Elf64_Chdr, + else => @compileError("expected pointer size of 32 or 64"), +}; pub const Sym = switch (@sizeOf(usize)) { 4 => Elf32_Sym, 8 => Elf64_Sym, diff --git a/lib/std/macho.zig b/lib/std/macho.zig index 1b886e2d903a..d70e3448bd78 100644 --- a/lib/std/macho.zig +++ b/lib/std/macho.zig @@ -2064,3 +2064,64 @@ pub const UNWIND_ARM64_FRAME_D14_D15_PAIR: u32 = 0x00000800; pub const UNWIND_ARM64_FRAMELESS_STACK_SIZE_MASK: u32 = 0x00FFF000; pub const UNWIND_ARM64_DWARF_SECTION_OFFSET: u32 = 0x00FFFFFF; + +pub const CompactUnwindEncoding = packed struct(u32) { + value: packed union { + x86_64: packed union { + frame: packed struct(u24) { + reg4: u3, + reg3: u3, + reg2: u3, + reg1: u3, + reg0: u3, + unused: u1 = 0, + frame_offset: u8, + }, + frameless: packed struct(u24) { + stack_reg_permutation: u10, + stack_reg_count: u3, + stack: packed union { + direct: packed struct(u11) { + _: u3, + stack_size: u8, + }, + indirect: packed struct(u11) { + stack_adjust: u3, + sub_offset: u8, + }, + }, + }, + dwarf: u24, + }, + arm64: packed union { + frame: packed struct(u24) { + x_reg_pairs: packed struct(u5) { + x19_x20: u1, + x21_x22: u1, + x23_x24: u1, + x25_x26: u1, + x27_x28: u1, + }, + d_reg_pairs: packed struct(u4) { + d8_d9: u1, + d10_d11: u1, + d12_d13: u1, + d14_d15: u1, + }, + _: u15, + }, + frameless: packed struct(u24) { + _: u12 = 0, + stack_size: u12, + }, + dwarf: u24, + }, + }, + mode: packed union { + x86_64: UNWIND_X86_64_MODE, + arm64: UNWIND_ARM64_MODE, + }, + personality_index: u2, + has_lsda: u1, + start: u1, +}; diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 6362e9ece1a4..bbcf649f5dd9 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -86,6 +86,7 @@ pub const timeval = arch_bits.timeval; pub const timezone = arch_bits.timezone; pub const ucontext_t = arch_bits.ucontext_t; pub const user_desc = arch_bits.user_desc; +pub const getcontext = arch_bits.getcontext; pub const tls = @import("linux/tls.zig"); pub const pie = @import("linux/start_pie.zig"); diff --git a/lib/std/os/linux/x86.zig b/lib/std/os/linux/x86.zig index 05c012c77cdb..e5d75c1831eb 100644 --- a/lib/std/os/linux/x86.zig +++ b/lib/std/os/linux/x86.zig @@ -389,3 +389,86 @@ pub const SC = struct { pub const recvmmsg = 19; pub const sendmmsg = 20; }; + +fn gpRegisterOffset(comptime reg_index: comptime_int) usize { + return @offsetOf(ucontext_t, "mcontext") + @offsetOf(mcontext_t, "gregs") + @sizeOf(usize) * reg_index; +} + +noinline fn getContextReturnAddress() usize { + return @returnAddress(); +} + +pub fn getContextInternal() callconv(.Naked) void { + asm volatile ( + \\ movl $0, (%[flags_offset])(%%edx) + \\ movl $0, (%[link_offset])(%%edx) + \\ movl %%edi, (%[edi_offset])(%%edx) + \\ movl %%esi, (%[esi_offset])(%%edx) + \\ movl %%ebp, (%[ebp_offset])(%%edx) + \\ movl %%ebx, (%[ebx_offset])(%%edx) + \\ movl %%edx, (%[edx_offset])(%%edx) + \\ movl %%ecx, (%[ecx_offset])(%%edx) + \\ movl %%eax, (%[eax_offset])(%%edx) + \\ movl (%%esp), %%ecx + \\ movl %%ecx, (%[eip_offset])(%%edx) + \\ leal 4(%%esp), %%ecx + \\ movl %%ecx, (%[esp_offset])(%%edx) + \\ xorl %%ecx, %%ecx + \\ movw %%fs, %%cx + \\ movl %%ecx, (%[fs_offset])(%%edx) + \\ leal (%[regspace_offset])(%%edx), %%ecx + \\ movl %%ecx, (%[fpregs_offset])(%%edx) + \\ fnstenv (%%ecx) + \\ fldenv (%%ecx) + \\ pushl %%ebx + \\ pushl %%esi + \\ xorl %%ebx, %%ebx + \\ movl %[sigaltstack], %%eax + \\ leal (%[stack_offset])(%%edx), %%ecx + \\ int $0x80 + \\ cmpl $0, %%eax + \\ jne return + \\ movl %[sigprocmask], %%eax + \\ xorl %%ecx, %%ecx + \\ leal (%[sigmask_offset])(%%edx), %%edx + \\ movl %[sigset_size], %%esi + \\ int $0x80 + \\ return: + \\ popl %%esi + \\ popl %%ebx + : + : [flags_offset] "p" (@offsetOf(ucontext_t, "flags")), + [link_offset] "p" (@offsetOf(ucontext_t, "link")), + [edi_offset] "p" (comptime gpRegisterOffset(REG.EDI)), + [esi_offset] "p" (comptime gpRegisterOffset(REG.ESI)), + [ebp_offset] "p" (comptime gpRegisterOffset(REG.EBP)), + [esp_offset] "p" (comptime gpRegisterOffset(REG.ESP)), + [ebx_offset] "p" (comptime gpRegisterOffset(REG.EBX)), + [edx_offset] "p" (comptime gpRegisterOffset(REG.EDX)), + [ecx_offset] "p" (comptime gpRegisterOffset(REG.ECX)), + [eax_offset] "p" (comptime gpRegisterOffset(REG.EAX)), + [eip_offset] "p" (comptime gpRegisterOffset(REG.EIP)), + [fs_offset] "p" (comptime gpRegisterOffset(REG.FS)), + [fpregs_offset] "p" (@offsetOf(ucontext_t, "mcontext") + @offsetOf(mcontext_t, "fpregs")), + [regspace_offset] "p" (@offsetOf(ucontext_t, "regspace")), + [sigaltstack] "i" (@intFromEnum(linux.SYS.sigaltstack)), + [stack_offset] "p" (@offsetOf(ucontext_t, "stack")), + [sigprocmask] "i" (@intFromEnum(linux.SYS.rt_sigprocmask)), + [sigmask_offset] "p" (@offsetOf(ucontext_t, "sigmask")), + [sigset_size] "i" (linux.NSIG / 8), + : "memory", "eax", "ecx", "edx" + ); +} + +pub inline fn getcontext(context: *ucontext_t) usize { + // This method is used so that getContextInternal can control + // its prologue in order to read ESP from a constant offset. + // The unused &getContextInternal input is required so the function is included in the binary. + return asm volatile ( + \\ call os.linux.x86.getContextInternal + : [ret] "={eax}" (-> usize), + : [context] "{edx}" (context), + [getContextInternal] "X" (&getContextInternal), + : "memory", "ecx" + ); +} diff --git a/lib/std/os/linux/x86_64.zig b/lib/std/os/linux/x86_64.zig index 41c9c9ea46ca..e5febce14d30 100644 --- a/lib/std/os/linux/x86_64.zig +++ b/lib/std/os/linux/x86_64.zig @@ -395,3 +395,97 @@ pub const ucontext_t = extern struct { sigmask: sigset_t, fpregs_mem: [64]usize, }; + +fn gpRegisterOffset(comptime reg_index: comptime_int) usize { + return @offsetOf(ucontext_t, "mcontext") + @offsetOf(mcontext_t, "gregs") + @sizeOf(usize) * reg_index; +} + +fn getContextInternal() callconv(.Naked) void { + // TODO: Read GS/FS registers? + asm volatile ( + \\ movq $0, (%[flags_offset])(%%rdi) + \\ movq $0, (%[link_offset])(%%rdi) + \\ movq %%r8, (%[r8_offset])(%%rdi) + \\ movq %%r9, (%[r9_offset])(%%rdi) + \\ movq %%r10, (%[r10_offset])(%%rdi) + \\ movq %%r11, (%[r11_offset])(%%rdi) + \\ movq %%r12, (%[r12_offset])(%%rdi) + \\ movq %%r13, (%[r13_offset])(%%rdi) + \\ movq %%r14, (%[r14_offset])(%%rdi) + \\ movq %%r15, (%[r15_offset])(%%rdi) + \\ movq %%rdi, (%[rdi_offset])(%%rdi) + \\ movq %%rsi, (%[rsi_offset])(%%rdi) + \\ movq %%rbp, (%[rbp_offset])(%%rdi) + \\ movq %%rbx, (%[rbx_offset])(%%rdi) + \\ movq %%rdx, (%[rdx_offset])(%%rdi) + \\ movq %%rax, (%[rax_offset])(%%rdi) + \\ movq %%rcx, (%[rcx_offset])(%%rdi) + \\ movq (%%rsp), %%rcx + \\ movq %%rcx, (%[rip_offset])(%%rdi) + \\ leaq 8(%%rsp), %%rcx + \\ movq %%rcx, (%[rsp_offset])(%%rdi) + \\ pushfq + \\ popq (%[efl_offset])(%%rdi) + \\ leaq (%[fpmem_offset])(%%rdi), %%rcx + \\ movq %%rcx, (%[fpstate_offset])(%%rdi) + \\ fnstenv (%%rcx) + \\ fldenv (%%rcx) + \\ stmxcsr (%[mxcsr_offset])(%%rdi) + \\ leaq (%[stack_offset])(%%rdi), %%rsi + \\ movq %%rdi, %%r8 + \\ xorq %%rdi, %%rdi + \\ movq %[sigaltstack], %%rax + \\ syscall + \\ cmpq $0, %%rax + \\ jne return + \\ movq %[sigprocmask], %%rax + \\ xorq %%rsi, %%rsi + \\ leaq (%[sigmask_offset])(%%r8), %%rdx + \\ movq %[sigset_size], %%r10 + \\ syscall + \\ return: + : + : [flags_offset] "p" (@offsetOf(ucontext_t, "flags")), + [link_offset] "p" (@offsetOf(ucontext_t, "link")), + [r8_offset] "p" (comptime gpRegisterOffset(REG.R8)), + [r9_offset] "p" (comptime gpRegisterOffset(REG.R9)), + [r10_offset] "p" (comptime gpRegisterOffset(REG.R10)), + [r11_offset] "p" (comptime gpRegisterOffset(REG.R11)), + [r12_offset] "p" (comptime gpRegisterOffset(REG.R12)), + [r13_offset] "p" (comptime gpRegisterOffset(REG.R13)), + [r14_offset] "p" (comptime gpRegisterOffset(REG.R14)), + [r15_offset] "p" (comptime gpRegisterOffset(REG.R15)), + [rdi_offset] "p" (comptime gpRegisterOffset(REG.RDI)), + [rsi_offset] "p" (comptime gpRegisterOffset(REG.RSI)), + [rbp_offset] "p" (comptime gpRegisterOffset(REG.RBP)), + [rbx_offset] "p" (comptime gpRegisterOffset(REG.RBX)), + [rdx_offset] "p" (comptime gpRegisterOffset(REG.RDX)), + [rax_offset] "p" (comptime gpRegisterOffset(REG.RAX)), + [rcx_offset] "p" (comptime gpRegisterOffset(REG.RCX)), + [rsp_offset] "p" (comptime gpRegisterOffset(REG.RSP)), + [rip_offset] "p" (comptime gpRegisterOffset(REG.RIP)), + [efl_offset] "p" (comptime gpRegisterOffset(REG.EFL)), + [fpstate_offset] "p" (@offsetOf(ucontext_t, "mcontext") + @offsetOf(mcontext_t, "fpregs")), + [fpmem_offset] "p" (@offsetOf(ucontext_t, "fpregs_mem")), + [mxcsr_offset] "p" (@offsetOf(ucontext_t, "fpregs_mem") + @offsetOf(fpstate, "mxcsr")), + [sigaltstack] "i" (@intFromEnum(linux.SYS.sigaltstack)), + [stack_offset] "p" (@offsetOf(ucontext_t, "stack")), + [sigprocmask] "i" (@intFromEnum(linux.SYS.rt_sigprocmask)), + [sigmask_offset] "p" (@offsetOf(ucontext_t, "sigmask")), + [sigset_size] "i" (linux.NSIG / 8), + : "memory", "rcx", "rdx", "rdi", "rsi", "r8", "r10", "r11" + ); +} + +pub inline fn getcontext(context: *ucontext_t) usize { + // This method is used so that getContextInternal can control + // its prologue in order to read RSP from a constant offset + // The unused &getContextInternal input is required so the function is included in the binary. + return asm volatile ( + \\ call os.linux.x86_64.getContextInternal + : [ret] "={rax}" (-> usize), + : [context] "{rdi}" (context), + [getContextInternal] "X" (&getContextInternal), + : "memory", "rcx", "rdx", "rdi", "rsi", "r8", "r10", "r11" + ); +} diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 1337efdd341d..9f8aa326a99b 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -3301,6 +3301,35 @@ pub const REGSAM = ACCESS_MASK; pub const ACCESS_MASK = DWORD; pub const LSTATUS = LONG; +pub const SECTION_INHERIT = enum(c_int) { + ViewShare = 0, + ViewUnmap = 1, +}; + +pub const SECTION_QUERY = 0x0001; +pub const SECTION_MAP_WRITE = 0x0002; +pub const SECTION_MAP_READ = 0x0004; +pub const SECTION_MAP_EXECUTE = 0x0008; +pub const SECTION_EXTEND_SIZE = 0x0010; +pub const SECTION_ALL_ACCESS = + STANDARD_RIGHTS_REQUIRED | + SECTION_QUERY | + SECTION_MAP_WRITE | + SECTION_MAP_READ | + SECTION_MAP_EXECUTE | + SECTION_EXTEND_SIZE; + +pub const SEC_64K_PAGES = 0x80000; +pub const SEC_FILE = 0x800000; +pub const SEC_IMAGE = 0x1000000; +pub const SEC_PROTECTED_IMAGE = 0x2000000; +pub const SEC_RESERVE = 0x4000000; +pub const SEC_COMMIT = 0x8000000; +pub const SEC_IMAGE_NO_EXECUTE = SEC_IMAGE | SEC_NOCACHE; +pub const SEC_NOCACHE = 0x10000000; +pub const SEC_WRITECOMBINE = 0x40000000; +pub const SEC_LARGE_PAGES = 0x80000000; + pub const HKEY = *opaque {}; pub const HKEY_LOCAL_MACHINE: HKEY = @as(HKEY, @ptrFromInt(0x80000002)); diff --git a/lib/std/os/windows/ntdll.zig b/lib/std/os/windows/ntdll.zig index 328ecb80f562..8c14f1fad9ed 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -36,6 +36,7 @@ const THREADINFOCLASS = windows.THREADINFOCLASS; const PROCESSINFOCLASS = windows.PROCESSINFOCLASS; const LPVOID = windows.LPVOID; const LPCVOID = windows.LPCVOID; +const SECTION_INHERIT = windows.SECTION_INHERIT; pub extern "ntdll" fn NtQueryInformationProcess( ProcessHandle: HANDLE, @@ -125,6 +126,31 @@ pub extern "ntdll" fn NtCreateFile( EaBuffer: ?*anyopaque, EaLength: ULONG, ) callconv(WINAPI) NTSTATUS; +pub extern "ntdll" fn NtCreateSection( + SectionHandle: *HANDLE, + DesiredAccess: ACCESS_MASK, + ObjectAttributes: ?*OBJECT_ATTRIBUTES, + MaximumSize: ?*LARGE_INTEGER, + SectionPageProtection: ULONG, + AllocationAttributes: ULONG, + FileHandle: ?HANDLE, +) callconv(WINAPI) NTSTATUS; +pub extern "ntdll" fn NtMapViewOfSection( + SectionHandle: HANDLE, + ProcessHandle: HANDLE, + BaseAddress: *PVOID, + ZeroBits: ?*ULONG, + CommitSize: SIZE_T, + SectionOffset: ?*LARGE_INTEGER, + ViewSize: *SIZE_T, + InheritDispostion: SECTION_INHERIT, + AllocationType: ULONG, + Win32Protect: ULONG, +) callconv(WINAPI) NTSTATUS; +pub extern "ntdll" fn NtUnmapViewOfSection( + ProcessHandle: HANDLE, + BaseAddress: PVOID, +) callconv(WINAPI) NTSTATUS; pub extern "ntdll" fn NtDeviceIoControlFile( FileHandle: HANDLE, Event: ?HANDLE, diff --git a/src/Compilation.zig b/src/Compilation.zig index 383b60a66dc6..f6abfae00f75 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -5288,6 +5288,7 @@ pub fn generateBuiltinZigSource(comp: *Compilation, allocator: Allocator) Alloca \\pub const position_independent_executable = {}; \\pub const strip_debug_info = {}; \\pub const code_model = std.builtin.CodeModel.{}; + \\pub const omit_frame_pointer = {}; \\ , .{ std.zig.fmtId(@tagName(target.ofmt)), @@ -5301,6 +5302,7 @@ pub fn generateBuiltinZigSource(comp: *Compilation, allocator: Allocator) Alloca comp.bin_file.options.pie, comp.bin_file.options.strip, std.zig.fmtId(@tagName(comp.bin_file.options.machine_code_model)), + comp.bin_file.options.omit_frame_pointer, }); if (target.os.tag == .wasi) { diff --git a/src/crash_report.zig b/src/crash_report.zig index fc41528321c7..82be4211c7b0 100644 --- a/src/crash_report.zig +++ b/src/crash_report.zig @@ -203,53 +203,11 @@ fn handleSegfaultPosix(sig: i32, info: *const os.siginfo_t, ctx_ptr: ?*const any }; const stack_ctx: StackContext = switch (builtin.cpu.arch) { - .x86 => ctx: { - const ctx: *const os.ucontext_t = @ptrCast(@alignCast(ctx_ptr)); - const ip = @as(usize, @intCast(ctx.mcontext.gregs[os.REG.EIP])); - const bp = @as(usize, @intCast(ctx.mcontext.gregs[os.REG.EBP])); - break :ctx StackContext{ .exception = .{ .bp = bp, .ip = ip } }; - }, - .x86_64 => ctx: { - const ctx: *const os.ucontext_t = @ptrCast(@alignCast(ctx_ptr)); - const ip = switch (builtin.os.tag) { - .linux, .netbsd, .solaris => @as(usize, @intCast(ctx.mcontext.gregs[os.REG.RIP])), - .freebsd => @as(usize, @intCast(ctx.mcontext.rip)), - .openbsd => @as(usize, @intCast(ctx.sc_rip)), - .macos => @as(usize, @intCast(ctx.mcontext.ss.rip)), - else => unreachable, - }; - const bp = switch (builtin.os.tag) { - .linux, .netbsd, .solaris => @as(usize, @intCast(ctx.mcontext.gregs[os.REG.RBP])), - .openbsd => @as(usize, @intCast(ctx.sc_rbp)), - .freebsd => @as(usize, @intCast(ctx.mcontext.rbp)), - .macos => @as(usize, @intCast(ctx.mcontext.ss.rbp)), - else => unreachable, - }; - break :ctx StackContext{ .exception = .{ .bp = bp, .ip = ip } }; - }, - .arm => ctx: { - const ctx: *const os.ucontext_t = @ptrCast(@alignCast(ctx_ptr)); - const ip = @as(usize, @intCast(ctx.mcontext.arm_pc)); - const bp = @as(usize, @intCast(ctx.mcontext.arm_fp)); - break :ctx StackContext{ .exception = .{ .bp = bp, .ip = ip } }; - }, - .aarch64 => ctx: { - const ctx: *const os.ucontext_t = @ptrCast(@alignCast(ctx_ptr)); - const ip = switch (native_os) { - .macos => @as(usize, @intCast(ctx.mcontext.ss.pc)), - .netbsd => @as(usize, @intCast(ctx.mcontext.gregs[os.REG.PC])), - .freebsd => @as(usize, @intCast(ctx.mcontext.gpregs.elr)), - else => @as(usize, @intCast(ctx.mcontext.pc)), - }; - // x29 is the ABI-designated frame pointer - const bp = switch (native_os) { - .macos => @as(usize, @intCast(ctx.mcontext.ss.fp)), - .netbsd => @as(usize, @intCast(ctx.mcontext.gregs[os.REG.FP])), - .freebsd => @as(usize, @intCast(ctx.mcontext.gpregs.x[os.REG.FP])), - else => @as(usize, @intCast(ctx.mcontext.regs[29])), - }; - break :ctx StackContext{ .exception = .{ .bp = bp, .ip = ip } }; - }, + .x86, + .x86_64, + .arm, + .aarch64, + => StackContext{ .exception = @ptrCast(@alignCast(ctx_ptr)) }, else => .not_supported, }; @@ -275,10 +233,9 @@ fn handleSegfaultWindows(info: *os.windows.EXCEPTION_POINTERS) callconv(os.windo fn handleSegfaultWindowsExtra(info: *os.windows.EXCEPTION_POINTERS, comptime msg: WindowsSegfaultMessage) noreturn { PanicSwitch.preDispatch(); - const stack_ctx = if (@hasDecl(os.windows, "CONTEXT")) ctx: { - const regs = info.ContextRecord.getRegs(); - break :ctx StackContext{ .exception = .{ .bp = regs.bp, .ip = regs.ip } }; - } else ctx: { + const stack_ctx = if (@hasDecl(os.windows, "CONTEXT")) + StackContext{ .exception = info.ContextRecord } + else ctx: { const addr = @intFromPtr(info.ExceptionRecord.ExceptionAddress); break :ctx StackContext{ .current = .{ .ret_addr = addr } }; }; @@ -293,7 +250,7 @@ fn handleSegfaultWindowsExtra(info: *os.windows.EXCEPTION_POINTERS, comptime msg }, .illegal_instruction => { const ip: ?usize = switch (stack_ctx) { - .exception => |ex| ex.ip, + .exception => |ex| ex.getRegs().ip, .current => |cur| cur.ret_addr, .not_supported => null, }; @@ -314,10 +271,7 @@ const StackContext = union(enum) { current: struct { ret_addr: ?usize, }, - exception: struct { - bp: usize, - ip: usize, - }, + exception: *const debug.ThreadContext, not_supported: void, pub fn dumpStackTrace(ctx: @This()) void { @@ -325,8 +279,8 @@ const StackContext = union(enum) { .current => |ct| { debug.dumpCurrentStackTrace(ct.ret_addr); }, - .exception => |ex| { - debug.dumpStackTraceFromBase(ex.bp, ex.ip); + .exception => |context| { + debug.dumpStackTraceFromBase(context); }, .not_supported => { const stderr = io.getStdErr().writer(); diff --git a/src/target.zig b/src/target.zig index f07dcc43d21e..030cad6bdc7e 100644 --- a/src/target.zig +++ b/src/target.zig @@ -510,7 +510,7 @@ pub fn clangAssemblerSupportsMcpuArg(target: std.Target) bool { } pub fn needUnwindTables(target: std.Target) bool { - return target.os.tag == .windows; + return target.os.tag == .windows or target.isDarwin(); } pub fn defaultAddressSpace( diff --git a/test/standalone.zig b/test/standalone.zig index cfdb09ea0714..b7dfc9dc944b 100644 --- a/test/standalone.zig +++ b/test/standalone.zig @@ -230,6 +230,14 @@ pub const build_cases = [_]BuildCase{ .build_root = "test/standalone/zerolength_check", .import = @import("standalone/zerolength_check/build.zig"), }, + .{ + .build_root = "test/standalone/stack_iterator", + .import = @import("standalone/stack_iterator/build.zig"), + }, + .{ + .build_root = "test/standalone/coff_dwarf", + .import = @import("standalone/coff_dwarf/build.zig"), + }, }; const std = @import("std"); diff --git a/test/standalone/coff_dwarf/build.zig b/test/standalone/coff_dwarf/build.zig new file mode 100644 index 000000000000..ffd7800a5b6c --- /dev/null +++ b/test/standalone/coff_dwarf/build.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +/// This tests the path where DWARF information is embedded in a COFF binary +pub fn build(b: *std.Build) void { + const test_step = b.step("test", "Test it"); + b.default_step = test_step; + + const optimize: std.builtin.OptimizeMode = .Debug; + const target = b.standardTargetOptions(.{}); + + if (builtin.os.tag != .windows) return; + + const exe = b.addExecutable(.{ + .name = "main", + .root_source_file = .{ .path = "main.zig" }, + .optimize = optimize, + .target = target, + }); + + const lib = b.addSharedLibrary(.{ + .name = "shared_lib", + .optimize = optimize, + .target = target, + }); + lib.addCSourceFile("shared_lib.c", &.{"-gdwarf"}); + lib.linkLibC(); + exe.linkLibrary(lib); + + const run = b.addRunArtifact(exe); + run.expectExitCode(0); + run.skip_foreign_checks = true; + + test_step.dependOn(&run.step); +} diff --git a/test/standalone/coff_dwarf/main.zig b/test/standalone/coff_dwarf/main.zig new file mode 100644 index 000000000000..236aa1c5fa0b --- /dev/null +++ b/test/standalone/coff_dwarf/main.zig @@ -0,0 +1,27 @@ +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +extern fn add(a: u32, b: u32, addr: *usize) u32; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer assert(gpa.deinit() == .ok); + const allocator = gpa.allocator(); + + var debug_info = try std.debug.openSelfDebugInfo(allocator); + defer debug_info.deinit(); + + var add_addr: usize = undefined; + _ = add(1, 2, &add_addr); + + const module = try debug_info.getModuleForAddress(add_addr); + const symbol = try module.getSymbolAtAddress(allocator, add_addr); + defer symbol.deinit(allocator); + + try testing.expectEqualStrings("add", symbol.symbol_name); + try testing.expect(symbol.line_info != null); + try testing.expectEqualStrings("shared_lib.c", std.fs.path.basename(symbol.line_info.?.file_name)); + try testing.expectEqual(@as(u64, 3), symbol.line_info.?.line); + try testing.expectEqual(@as(u64, 0), symbol.line_info.?.column); +} diff --git a/test/standalone/coff_dwarf/shared_lib.c b/test/standalone/coff_dwarf/shared_lib.c new file mode 100644 index 000000000000..0455a6a0ad3a --- /dev/null +++ b/test/standalone/coff_dwarf/shared_lib.c @@ -0,0 +1,6 @@ +#include + +__declspec(dllexport) uint32_t add(uint32_t a, uint32_t b, uintptr_t* addr) { + *addr = (uintptr_t)&add; + return a + b; +} diff --git a/test/standalone/stack_iterator/build.zig b/test/standalone/stack_iterator/build.zig new file mode 100644 index 000000000000..1c5a9673ceca --- /dev/null +++ b/test/standalone/stack_iterator/build.zig @@ -0,0 +1,94 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const test_step = b.step("test", "Test it"); + b.default_step = test_step; + + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Unwinding with a frame pointer + // + // getcontext version: zig std + // + // Unwind info type: + // - ELF: DWARF .debug_frame + // - MachO: __unwind_info encodings: + // - x86_64: RBP_FRAME + // - aarch64: FRAME, DWARF + { + const exe = b.addExecutable(.{ + .name = "unwind_fp", + .root_source_file = .{ .path = "unwind.zig" }, + .target = target, + .optimize = optimize, + }); + + if (target.isDarwin()) exe.unwind_tables = true; + exe.omit_frame_pointer = false; + + const run_cmd = b.addRunArtifact(exe); + test_step.dependOn(&run_cmd.step); + } + + // Unwinding without a frame pointer + // + // getcontext version: zig std + // + // Unwind info type: + // - ELF: DWARF .eh_frame_hdr + .eh_frame + // - MachO: __unwind_info encodings: + // - x86_64: STACK_IMMD, STACK_IND + // - aarch64: FRAMELESS, DWARF + { + const exe = b.addExecutable(.{ + .name = "unwind_nofp", + .root_source_file = .{ .path = "unwind.zig" }, + .target = target, + .optimize = optimize, + }); + + exe.omit_frame_pointer = true; + exe.unwind_tables = true; + + const run_cmd = b.addRunArtifact(exe); + test_step.dependOn(&run_cmd.step); + } + + // Unwinding through a C shared library without a frame pointer (libc) + // + // getcontext version: libc + // + // Unwind info type: + // - ELF: DWARF .eh_frame + .debug_frame + // - MachO: __unwind_info encodings: + // - x86_64: STACK_IMMD, STACK_IND + // - aarch64: FRAMELESS, DWARF + { + const c_shared_lib = b.addSharedLibrary(.{ + .name = "c_shared_lib", + .target = target, + .optimize = optimize, + }); + + if (target.isWindows()) c_shared_lib.defineCMacro("LIB_API", "__declspec(dllexport)"); + + c_shared_lib.strip = false; + c_shared_lib.addCSourceFile("shared_lib.c", &.{"-fomit-frame-pointer"}); + c_shared_lib.linkLibC(); + + const exe = b.addExecutable(.{ + .name = "shared_lib_unwind", + .root_source_file = .{ .path = "shared_lib_unwind.zig" }, + .target = target, + .optimize = optimize, + }); + + if (target.isDarwin()) exe.unwind_tables = true; + exe.omit_frame_pointer = true; + exe.linkLibrary(c_shared_lib); + + const run_cmd = b.addRunArtifact(exe); + test_step.dependOn(&run_cmd.step); + } +} diff --git a/test/standalone/stack_iterator/shared_lib.c b/test/standalone/stack_iterator/shared_lib.c new file mode 100644 index 000000000000..c3170f2dc0f5 --- /dev/null +++ b/test/standalone/stack_iterator/shared_lib.c @@ -0,0 +1,22 @@ +#include + +#ifndef LIB_API +#define LIB_API +#endif + +__attribute__((noinline)) void frame1( + void** expected, + void** unwound, + void (*frame2)(void** expected, void** unwound)) { + expected[3] = __builtin_extract_return_addr(__builtin_return_address(0)); + frame2(expected, unwound); +} + +LIB_API void frame0( + void** expected, + void** unwound, + void (*frame2)(void** expected, void** unwound)) { + expected[4] = __builtin_extract_return_addr(__builtin_return_address(0)); + frame1(expected, unwound, frame2); +} + diff --git a/test/standalone/stack_iterator/shared_lib_unwind.zig b/test/standalone/stack_iterator/shared_lib_unwind.zig new file mode 100644 index 000000000000..50e0421e2ac6 --- /dev/null +++ b/test/standalone/stack_iterator/shared_lib_unwind.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const debug = std.debug; +const testing = std.testing; + +noinline fn frame4(expected: *[5]usize, unwound: *[5]usize) void { + expected[0] = @returnAddress(); + + var context: debug.ThreadContext = undefined; + testing.expect(debug.getContext(&context)) catch @panic("failed to getContext"); + + var debug_info = debug.getSelfDebugInfo() catch @panic("failed to openSelfDebugInfo"); + var it = debug.StackIterator.initWithContext(expected[0], debug_info, &context) catch @panic("failed to initWithContext"); + defer it.deinit(); + + for (unwound) |*addr| { + if (it.next()) |return_address| addr.* = return_address; + } +} + +noinline fn frame3(expected: *[5]usize, unwound: *[5]usize) void { + expected[1] = @returnAddress(); + frame4(expected, unwound); +} + +fn frame2(expected: *[5]usize, unwound: *[5]usize) callconv(.C) void { + expected[2] = @returnAddress(); + frame3(expected, unwound); +} + +extern fn frame0( + expected: *[5]usize, + unwound: *[5]usize, + frame_2: *const fn (expected: *[5]usize, unwound: *[5]usize) callconv(.C) void, +) void; + +pub fn main() !void { + // Disabled until the DWARF unwinder bugs on .aarch64 are solved + if (builtin.omit_frame_pointer and comptime builtin.target.isDarwin() and builtin.cpu.arch == .aarch64) return; + + if (!std.debug.have_ucontext or !std.debug.have_getcontext) return; + + var expected: [5]usize = undefined; + var unwound: [5]usize = undefined; + frame0(&expected, &unwound, &frame2); + try testing.expectEqual(expected, unwound); +} diff --git a/test/standalone/stack_iterator/unwind.zig b/test/standalone/stack_iterator/unwind.zig new file mode 100644 index 000000000000..1280118173e2 --- /dev/null +++ b/test/standalone/stack_iterator/unwind.zig @@ -0,0 +1,99 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const debug = std.debug; +const testing = std.testing; + +noinline fn frame3(expected: *[4]usize, unwound: *[4]usize) void { + expected[0] = @returnAddress(); + + var context: debug.ThreadContext = undefined; + testing.expect(debug.getContext(&context)) catch @panic("failed to getContext"); + + var debug_info = debug.getSelfDebugInfo() catch @panic("failed to openSelfDebugInfo"); + var it = debug.StackIterator.initWithContext(expected[0], debug_info, &context) catch @panic("failed to initWithContext"); + defer it.deinit(); + + for (unwound) |*addr| { + if (it.next()) |return_address| addr.* = return_address; + } +} + +noinline fn frame2(expected: *[4]usize, unwound: *[4]usize) void { + // Excercise different __unwind_info / DWARF CFI encodings by forcing some registers to be restored + if (builtin.target.ofmt != .c) { + switch (builtin.cpu.arch) { + .x86 => { + if (builtin.omit_frame_pointer) { + asm volatile ( + \\movl $3, %%ebx + \\movl $1, %%ecx + \\movl $2, %%edx + \\movl $7, %%edi + \\movl $6, %%esi + \\movl $5, %%ebp + ::: "ebx", "ecx", "edx", "edi", "esi", "ebp"); + } else { + asm volatile ( + \\movl $3, %%ebx + \\movl $1, %%ecx + \\movl $2, %%edx + \\movl $7, %%edi + \\movl $6, %%esi + ::: "ebx", "ecx", "edx", "edi", "esi"); + } + }, + .x86_64 => { + if (builtin.omit_frame_pointer) { + asm volatile ( + \\movq $3, %%rbx + \\movq $12, %%r12 + \\movq $13, %%r13 + \\movq $14, %%r14 + \\movq $15, %%r15 + \\movq $6, %%rbp + ::: "rbx", "r12", "r13", "r14", "r15", "rbp"); + } else { + asm volatile ( + \\movq $3, %%rbx + \\movq $12, %%r12 + \\movq $13, %%r13 + \\movq $14, %%r14 + \\movq $15, %%r15 + ::: "rbx", "r12", "r13", "r14", "r15"); + } + }, + else => {}, + } + } + + expected[1] = @returnAddress(); + frame3(expected, unwound); +} + +noinline fn frame1(expected: *[4]usize, unwound: *[4]usize) void { + expected[2] = @returnAddress(); + + // Use a stack frame that is too big to encode in __unwind_info's stack-immediate encoding + // to exercise the stack-indirect encoding path + var pad: [std.math.maxInt(u8) * @sizeOf(usize) + 1]u8 = undefined; + _ = pad; + + frame2(expected, unwound); +} + +noinline fn frame0(expected: *[4]usize, unwound: *[4]usize) void { + expected[3] = @returnAddress(); + frame1(expected, unwound); +} + +pub fn main() !void { + // Disabled until the DWARF unwinder bugs on .aarch64 are solved + if (builtin.omit_frame_pointer and comptime builtin.target.isDarwin() and builtin.cpu.arch == .aarch64) return; + + if (!std.debug.have_ucontext or !std.debug.have_getcontext) return; + + var expected: [4]usize = undefined; + var unwound: [4]usize = undefined; + frame0(&expected, &unwound); + try testing.expectEqual(expected, unwound); +}