mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 06:13:07 +00:00
2239 lines
88 KiB
Zig
2239 lines
88 KiB
Zig
//! Cross-platform abstraction for this binary's own debug information, with a
|
|
//! goal of minimal code bloat and compilation speed penalty.
|
|
|
|
const builtin = @import("builtin");
|
|
const native_os = builtin.os.tag;
|
|
const native_endian = native_arch.endian();
|
|
const native_arch = builtin.cpu.arch;
|
|
|
|
const std = @import("../std.zig");
|
|
const mem = std.mem;
|
|
const Allocator = std.mem.Allocator;
|
|
const windows = std.os.windows;
|
|
const macho = std.macho;
|
|
const fs = std.fs;
|
|
const coff = std.coff;
|
|
const pdb = std.pdb;
|
|
const assert = std.debug.assert;
|
|
const posix = std.posix;
|
|
const elf = std.elf;
|
|
const Dwarf = std.debug.Dwarf;
|
|
const Pdb = std.debug.Pdb;
|
|
const File = std.fs.File;
|
|
const math = std.math;
|
|
const testing = std.testing;
|
|
const StackIterator = std.debug.StackIterator;
|
|
const regBytes = Dwarf.abi.regBytes;
|
|
const regValueNative = Dwarf.abi.regValueNative;
|
|
|
|
const SelfInfo = @This();
|
|
|
|
const root = @import("root");
|
|
|
|
allocator: Allocator,
|
|
address_map: std.AutoHashMap(usize, *Module),
|
|
modules: if (native_os == .windows) std.ArrayListUnmanaged(WindowsModule) else void,
|
|
|
|
pub const OpenError = error{
|
|
MissingDebugInfo,
|
|
UnsupportedOperatingSystem,
|
|
} || @typeInfo(@typeInfo(@TypeOf(SelfInfo.init)).@"fn".return_type.?).error_union.error_set;
|
|
|
|
pub fn open(allocator: Allocator) OpenError!SelfInfo {
|
|
nosuspend {
|
|
if (builtin.strip_debug_info)
|
|
return error.MissingDebugInfo;
|
|
switch (native_os) {
|
|
.linux,
|
|
.freebsd,
|
|
.netbsd,
|
|
.dragonfly,
|
|
.openbsd,
|
|
.macos,
|
|
.solaris,
|
|
.illumos,
|
|
.windows,
|
|
=> return try SelfInfo.init(allocator),
|
|
else => return error.UnsupportedOperatingSystem,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn init(allocator: Allocator) !SelfInfo {
|
|
var debug_info: SelfInfo = .{
|
|
.allocator = allocator,
|
|
.address_map = std.AutoHashMap(usize, *Module).init(allocator),
|
|
.modules = if (native_os == .windows) .{} else {},
|
|
};
|
|
|
|
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.GetLastError()) {
|
|
else => |err| return windows.unexpectedError(err),
|
|
}
|
|
}
|
|
defer windows.CloseHandle(handle);
|
|
|
|
var module_entry: windows.MODULEENTRY32 = undefined;
|
|
module_entry.dwSize = @sizeOf(windows.MODULEENTRY32);
|
|
if (windows.kernel32.Module32First(handle, &module_entry) == 0) {
|
|
return error.MissingDebugInfo;
|
|
}
|
|
|
|
var module_valid = true;
|
|
while (module_valid) {
|
|
const module_info = try debug_info.modules.addOne(allocator);
|
|
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;
|
|
}
|
|
}
|
|
|
|
return debug_info;
|
|
}
|
|
|
|
pub fn deinit(self: *SelfInfo) void {
|
|
var it = self.address_map.iterator();
|
|
while (it.next()) |entry| {
|
|
const mdi = entry.value_ptr.*;
|
|
mdi.deinit(self.allocator);
|
|
self.allocator.destroy(mdi);
|
|
}
|
|
self.address_map.deinit();
|
|
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);
|
|
}
|
|
}
|
|
|
|
pub fn getModuleForAddress(self: *SelfInfo, address: usize) !*Module {
|
|
if (builtin.target.os.tag.isDarwin()) {
|
|
return self.lookupModuleDyld(address);
|
|
} else if (native_os == .windows) {
|
|
return self.lookupModuleWin32(address);
|
|
} else if (native_os == .haiku) {
|
|
return self.lookupModuleHaiku(address);
|
|
} else if (builtin.target.cpu.arch.isWasm()) {
|
|
return self.lookupModuleWasm(address);
|
|
} else {
|
|
return self.lookupModuleDl(address);
|
|
}
|
|
}
|
|
|
|
// 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: *SelfInfo, address: usize) ?[]const u8 {
|
|
if (builtin.target.os.tag.isDarwin()) {
|
|
return self.lookupModuleNameDyld(address);
|
|
} else if (native_os == .windows) {
|
|
return self.lookupModuleNameWin32(address);
|
|
} else if (native_os == .haiku) {
|
|
return null;
|
|
} else if (builtin.target.cpu.arch.isWasm()) {
|
|
return null;
|
|
} else {
|
|
return self.lookupModuleNameDl(address);
|
|
}
|
|
}
|
|
|
|
fn lookupModuleDyld(self: *SelfInfo, address: usize) !*Module {
|
|
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]),
|
|
};
|
|
|
|
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).?;
|
|
if (!mem.eql(u8, "__TEXT", segment_cmd.segName())) continue;
|
|
|
|
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| {
|
|
const sect_addr: usize = @intCast(sect.addr);
|
|
const sect_size: usize = @intCast(sect.size);
|
|
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(Module);
|
|
errdefer self.allocator.destroy(obj_di);
|
|
|
|
const macho_path = mem.sliceTo(std.c._dyld_get_image_name(i), 0);
|
|
const macho_file = fs.cwd().openFile(macho_path, .{}) catch |err| switch (err) {
|
|
error.FileNotFound => return error.MissingDebugInfo,
|
|
else => return err,
|
|
};
|
|
obj_di.* = try readMachODebugInfo(self.allocator, macho_file);
|
|
obj_di.base_address = base_address;
|
|
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);
|
|
|
|
return obj_di;
|
|
}
|
|
},
|
|
else => {},
|
|
};
|
|
}
|
|
|
|
return error.MissingDebugInfo;
|
|
}
|
|
|
|
fn lookupModuleNameDyld(self: *SelfInfo, 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: *SelfInfo, address: usize) !*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 obj_di = try self.allocator.create(Module);
|
|
errdefer self.allocator.destroy(obj_di);
|
|
|
|
const mapped_module = @as([*]const u8, @ptrFromInt(module.base_address))[0..module.size];
|
|
var coff_obj = try coff.Coff.init(mapped_module, true);
|
|
|
|
// 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
|
|
@memcpy(name_buffer[0..4], &[_]u16{ '\\', '?', '?', '\\' });
|
|
|
|
const process_handle = windows.GetCurrentProcess();
|
|
const len = windows.kernel32.GetModuleFileNameExW(
|
|
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, false);
|
|
|
|
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);
|
|
return obj_di;
|
|
}
|
|
}
|
|
|
|
return error.MissingDebugInfo;
|
|
}
|
|
|
|
fn lookupModuleNameWin32(self: *SelfInfo, address: usize) ?[]const u8 {
|
|
for (self.modules.items) |module| {
|
|
if (address >= module.base_address and address < module.base_address + module.size) {
|
|
return module.name;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn lookupModuleNameDl(self: *SelfInfo, address: usize) ?[]const u8 {
|
|
_ = self;
|
|
|
|
var ctx: struct {
|
|
// Input
|
|
address: usize,
|
|
// Output
|
|
name: []const u8 = "",
|
|
} = .{ .address = address };
|
|
const CtxTy = @TypeOf(ctx);
|
|
|
|
if (posix.dl_iterate_phdr(&ctx, error{Found}, struct {
|
|
fn callback(info: *posix.dl_phdr_info, size: usize, context: *CtxTy) !void {
|
|
_ = size;
|
|
if (context.address < info.addr) return;
|
|
const phdrs = info.phdr[0..info.phnum];
|
|
for (phdrs) |*phdr| {
|
|
if (phdr.p_type != elf.PT_LOAD) continue;
|
|
|
|
const seg_start = info.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.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: *SelfInfo, address: usize) !*Module {
|
|
var ctx: struct {
|
|
// Input
|
|
address: usize,
|
|
// 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);
|
|
|
|
if (posix.dl_iterate_phdr(&ctx, error{Found}, struct {
|
|
fn callback(info: *posix.dl_phdr_info, size: usize, context: *CtxTy) !void {
|
|
_ = size;
|
|
// The base address is too high
|
|
if (context.address < info.addr)
|
|
return;
|
|
|
|
const phdrs = info.phdr[0..info.phnum];
|
|
for (phdrs) |*phdr| {
|
|
if (phdr.p_type != elf.PT_LOAD) continue;
|
|
|
|
// Overflowing addition is used to handle the case of VSDOs having a p_vaddr = 0xffffffffff700000
|
|
const seg_start = info.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.name, 0) orelse "";
|
|
context.base_address = info.addr;
|
|
break;
|
|
}
|
|
} else return;
|
|
|
|
for (info.phdr[0..info.phnum]) |phdr| {
|
|
switch (phdr.p_type) {
|
|
elf.PT_NOTE => {
|
|
// Look for .note.gnu.build-id
|
|
const note_bytes = @as([*]const u8, @ptrFromInt(info.addr + phdr.p_vaddr))[0..phdr.p_memsz];
|
|
const name_size = mem.readInt(u32, note_bytes[0..4], native_endian);
|
|
if (name_size != 4) continue;
|
|
const desc_size = mem.readInt(u32, note_bytes[4..8], native_endian);
|
|
const note_type = mem.readInt(u32, note_bytes[8..12], native_endian);
|
|
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.addr + phdr.p_vaddr))[0..phdr.p_memsz];
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
// Stop the iteration
|
|
return error.Found;
|
|
}
|
|
}.callback)) {
|
|
return error.MissingDebugInfo;
|
|
} else |err| switch (err) {
|
|
error.Found => {},
|
|
}
|
|
|
|
if (self.address_map.get(ctx.base_address)) |obj_di| {
|
|
return obj_di;
|
|
}
|
|
|
|
const obj_di = try self.allocator.create(Module);
|
|
errdefer self.allocator.destroy(obj_di);
|
|
|
|
var sections: Dwarf.SectionArray = Dwarf.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(Dwarf.Section.Id.eh_frame_hdr)] = .{
|
|
.data = eh_frame_hdr,
|
|
.owned = false,
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
fn lookupModuleHaiku(self: *SelfInfo, address: usize) !*Module {
|
|
_ = self;
|
|
_ = address;
|
|
@panic("TODO implement lookup module for Haiku");
|
|
}
|
|
|
|
fn lookupModuleWasm(self: *SelfInfo, address: usize) !*Module {
|
|
_ = self;
|
|
_ = address;
|
|
@panic("TODO implement lookup module for Wasm");
|
|
}
|
|
|
|
pub const Module = switch (native_os) {
|
|
.macos, .ios, .watchos, .tvos, .visionos => struct {
|
|
base_address: usize,
|
|
vmaddr_slide: usize,
|
|
mapped_memory: []align(std.heap.page_size_min) 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: Dwarf,
|
|
addr_table: std.StringHashMap(u64),
|
|
};
|
|
|
|
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
|
var it = self.ofiles.iterator();
|
|
while (it.next()) |entry| {
|
|
const ofile = entry.value_ptr;
|
|
ofile.di.deinit(allocator);
|
|
ofile.addr_table.deinit();
|
|
}
|
|
self.ofiles.deinit();
|
|
allocator.free(self.symbols);
|
|
posix.munmap(self.mapped_memory);
|
|
}
|
|
|
|
fn loadOFile(self: *@This(), allocator: Allocator, o_file_path: []const u8) !*OFileInfo {
|
|
const o_file = try fs.cwd().openFile(o_file_path, .{});
|
|
const mapped_mem = try mapWholeFile(o_file);
|
|
|
|
const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_mem.ptr));
|
|
if (hdr.magic != std.macho.MH_MAGIC_64)
|
|
return error.InvalidDebugInfo;
|
|
|
|
var segcmd: ?macho.LoadCommandIterator.LoadCommand = null;
|
|
var symtabcmd: ?macho.symtab_command = null;
|
|
var it = macho.LoadCommandIterator{
|
|
.ncmds = hdr.ncmds,
|
|
.buffer = mapped_mem[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
|
|
};
|
|
while (it.next()) |cmd| switch (cmd.cmd()) {
|
|
.SEGMENT_64 => segcmd = cmd,
|
|
.SYMTAB => symtabcmd = cmd.cast(macho.symtab_command).?,
|
|
else => {},
|
|
};
|
|
|
|
if (segcmd == null or symtabcmd == null) return error.MissingDebugInfo;
|
|
|
|
// Parse symbols
|
|
const strtab = @as(
|
|
[*]const u8,
|
|
@ptrCast(&mapped_mem[symtabcmd.?.stroff]),
|
|
)[0 .. symtabcmd.?.strsize - 1 :0];
|
|
const symtab = @as(
|
|
[*]const macho.nlist_64,
|
|
@ptrCast(@alignCast(&mapped_mem[symtabcmd.?.symoff])),
|
|
)[0..symtabcmd.?.nsyms];
|
|
|
|
// TODO handle tentative (common) symbols
|
|
var addr_table = std.StringHashMap(u64).init(allocator);
|
|
try addr_table.ensureTotalCapacity(@as(u32, @intCast(symtab.len)));
|
|
for (symtab) |sym| {
|
|
if (sym.n_strx == 0) continue;
|
|
if (sym.undf() or sym.tentative() or sym.abs()) continue;
|
|
const sym_name = mem.sliceTo(strtab[sym.n_strx..], 0);
|
|
// TODO is it possible to have a symbol collision?
|
|
addr_table.putAssumeCapacityNoClobber(sym_name, sym.n_value);
|
|
}
|
|
|
|
var sections: Dwarf.SectionArray = Dwarf.null_section_array;
|
|
if (self.eh_frame) |eh_frame| sections[@intFromEnum(Dwarf.Section.Id.eh_frame)] = .{
|
|
.data = eh_frame,
|
|
.owned = false,
|
|
};
|
|
|
|
for (segcmd.?.getSections()) |sect| {
|
|
if (!std.mem.eql(u8, "__DWARF", sect.segName())) continue;
|
|
|
|
var section_index: ?usize = null;
|
|
inline for (@typeInfo(Dwarf.Section.Id).@"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 Dwarf.chopSlice(mapped_mem, sect.offset, sect.size);
|
|
sections[section_index.?] = .{
|
|
.data = section_bytes,
|
|
.virtual_address = @intCast(sect.addr),
|
|
.owned = false,
|
|
};
|
|
}
|
|
|
|
const missing_debug_info =
|
|
sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
|
|
sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
|
|
sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
|
|
sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
|
|
if (missing_debug_info) return error.MissingDebugInfo;
|
|
|
|
var di: Dwarf = .{
|
|
.endian = .little,
|
|
.sections = sections,
|
|
.is_macho = true,
|
|
};
|
|
|
|
try Dwarf.open(&di, allocator);
|
|
const info = OFileInfo{
|
|
.di = di,
|
|
.addr_table = addr_table,
|
|
};
|
|
|
|
// Add the debug info to the cache
|
|
const result = try self.ofiles.getOrPut(o_file_path);
|
|
assert(!result.found_existing);
|
|
result.value_ptr.* = info;
|
|
|
|
return result.value_ptr;
|
|
}
|
|
|
|
pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !std.debug.Symbol {
|
|
nosuspend {
|
|
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[result.symbol.?.strx..], 0);
|
|
if (result.o_file_info == null) return .{ .name = stab_symbol };
|
|
|
|
// Translate again the address, this time into an address inside the
|
|
// .o file
|
|
const relocated_address_o = result.o_file_info.?.addr_table.get(stab_symbol) orelse return .{
|
|
.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 .{
|
|
.name = o_file_di.getSymbolName(relocated_address_o) orelse "???",
|
|
.compile_unit_name = compile_unit.die.getAttrString(
|
|
o_file_di,
|
|
std.dwarf.AT.name,
|
|
o_file_di.section(.debug_str),
|
|
compile_unit.*,
|
|
) catch |err| switch (err) {
|
|
error.MissingDebugInfo, error.InvalidDebugInfo => "???",
|
|
},
|
|
.source_location = o_file_di.getLineNumberInfo(
|
|
allocator,
|
|
compile_unit,
|
|
relocated_address_o + addr_off,
|
|
) catch |err| switch (err) {
|
|
error.MissingDebugInfo, error.InvalidDebugInfo => null,
|
|
else => return err,
|
|
},
|
|
};
|
|
} else |err| switch (err) {
|
|
error.MissingDebugInfo, error.InvalidDebugInfo => {
|
|
return .{ .name = stab_symbol };
|
|
},
|
|
else => return err,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn getOFileInfoForAddress(self: *@This(), allocator: 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);
|
|
const 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,
|
|
});
|
|
|
|
return .{
|
|
.relocated_address = relocated_address,
|
|
.symbol = symbol,
|
|
.o_file_info = o_file_info,
|
|
};
|
|
}
|
|
}
|
|
|
|
pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*Dwarf {
|
|
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,
|
|
pdb: ?Pdb,
|
|
dwarf: ?Dwarf,
|
|
coff_image_base: u64,
|
|
|
|
/// Only used if pdb is non-null
|
|
coff_section_headers: []coff.SectionHeader,
|
|
|
|
pub fn deinit(self: *@This(), gpa: Allocator) void {
|
|
if (self.dwarf) |*dwarf| {
|
|
dwarf.deinit(gpa);
|
|
}
|
|
|
|
if (self.pdb) |*p| {
|
|
gpa.free(p.file_reader.interface.buffer);
|
|
gpa.destroy(p.file_reader);
|
|
p.deinit();
|
|
gpa.free(self.coff_section_headers);
|
|
}
|
|
|
|
self.* = undefined;
|
|
}
|
|
|
|
fn getSymbolFromPdb(self: *@This(), relocated_address: usize) !?std.debug.Symbol {
|
|
var coff_section: *align(1) const coff.SectionHeader = undefined;
|
|
const mod_index = for (self.pdb.?.sect_contribs) |sect_contrib| {
|
|
if (sect_contrib.section > self.coff_section_headers.len) continue;
|
|
// Remember that SectionContribEntry.Section is 1-based.
|
|
coff_section = &self.coff_section_headers[sect_contrib.section - 1];
|
|
|
|
const vaddr_start = coff_section.virtual_address + sect_contrib.offset;
|
|
const vaddr_end = vaddr_start + sect_contrib.size;
|
|
if (relocated_address >= vaddr_start and relocated_address < vaddr_end) {
|
|
break sect_contrib.module_index;
|
|
}
|
|
} else {
|
|
// we have no information to add to the address
|
|
return null;
|
|
};
|
|
|
|
const module = (try self.pdb.?.getModule(mod_index)) orelse
|
|
return error.InvalidDebugInfo;
|
|
const obj_basename = fs.path.basename(module.obj_file_name);
|
|
|
|
const symbol_name = self.pdb.?.getSymbolName(
|
|
module,
|
|
relocated_address - coff_section.virtual_address,
|
|
) orelse "???";
|
|
const opt_line_info = try self.pdb.?.getLineNumberInfo(
|
|
module,
|
|
relocated_address - coff_section.virtual_address,
|
|
);
|
|
|
|
return .{
|
|
.name = symbol_name,
|
|
.compile_unit_name = obj_basename,
|
|
.source_location = opt_line_info,
|
|
};
|
|
}
|
|
|
|
pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !std.debug.Symbol {
|
|
// Translate the VA into an address into this object
|
|
const relocated_address = address - self.base_address;
|
|
|
|
if (self.pdb != null) {
|
|
if (try self.getSymbolFromPdb(relocated_address)) |symbol| return symbol;
|
|
}
|
|
|
|
if (self.dwarf) |*dwarf| {
|
|
const dwarf_address = relocated_address + self.coff_image_base;
|
|
return dwarf.getSymbol(allocator, dwarf_address);
|
|
}
|
|
|
|
return .{};
|
|
}
|
|
|
|
pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*Dwarf {
|
|
_ = allocator;
|
|
_ = address;
|
|
|
|
return switch (self.debug_data) {
|
|
.dwarf => |*dwarf| dwarf,
|
|
else => null,
|
|
};
|
|
}
|
|
},
|
|
.linux, .netbsd, .freebsd, .dragonfly, .openbsd, .haiku, .solaris, .illumos => Dwarf.ElfModule,
|
|
.wasi, .emscripten => struct {
|
|
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
|
_ = self;
|
|
_ = allocator;
|
|
}
|
|
|
|
pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !std.debug.Symbol {
|
|
_ = self;
|
|
_ = allocator;
|
|
_ = address;
|
|
return .{};
|
|
}
|
|
|
|
pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*Dwarf {
|
|
_ = self;
|
|
_ = allocator;
|
|
_ = address;
|
|
return null;
|
|
}
|
|
},
|
|
else => Dwarf,
|
|
};
|
|
|
|
/// How is this different than `Module` when the host is Windows?
|
|
/// Why are both stored in the `SelfInfo` struct?
|
|
/// Boy, it sure would be nice if someone added documentation comments for this
|
|
/// struct explaining it.
|
|
pub const WindowsModule = 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.GetCurrentProcess();
|
|
assert(windows.ntdll.NtUnmapViewOfSection(process_handle, @ptrCast(@constCast(self.section_view.ptr))) == .SUCCESS);
|
|
windows.CloseHandle(self.section_handle);
|
|
self.file.close();
|
|
}
|
|
} = null,
|
|
};
|
|
|
|
/// This takes ownership of macho_file: users of this function should not close
|
|
/// it themselves, even on error.
|
|
/// TODO it's weird to take ownership even on error, rework this code.
|
|
fn readMachODebugInfo(allocator: Allocator, macho_file: File) !Module {
|
|
const mapped_mem = try mapWholeFile(macho_file);
|
|
|
|
const hdr: *const macho.mach_header_64 = @ptrCast(@alignCast(mapped_mem.ptr));
|
|
if (hdr.magic != macho.MH_MAGIC_64)
|
|
return error.InvalidDebugInfo;
|
|
|
|
var it = macho.LoadCommandIterator{
|
|
.ncmds = hdr.ncmds,
|
|
.buffer = mapped_mem[@sizeOf(macho.mach_header_64)..][0..hdr.sizeofcmds],
|
|
};
|
|
const symtab = while (it.next()) |cmd| switch (cmd.cmd()) {
|
|
.SYMTAB => break cmd.cast(macho.symtab_command).?,
|
|
else => {},
|
|
} else return error.MissingDebugInfo;
|
|
|
|
const syms = @as(
|
|
[*]const macho.nlist_64,
|
|
@ptrCast(@alignCast(&mapped_mem[symtab.symoff])),
|
|
)[0..symtab.nsyms];
|
|
const strings = mapped_mem[symtab.stroff..][0 .. symtab.strsize - 1 :0];
|
|
|
|
const symbols_buf = try allocator.alloc(MachoSymbol, syms.len);
|
|
|
|
var ofile: u32 = undefined;
|
|
var last_sym: MachoSymbol = undefined;
|
|
var symbol_index: usize = 0;
|
|
var state: enum {
|
|
init,
|
|
oso_open,
|
|
oso_close,
|
|
bnsym,
|
|
fun_strx,
|
|
fun_size,
|
|
ensym,
|
|
} = .init;
|
|
|
|
for (syms) |*sym| {
|
|
if (!sym.stab()) continue;
|
|
|
|
// TODO handle globals N_GSYM, and statics N_STSYM
|
|
switch (sym.n_type) {
|
|
macho.N_OSO => {
|
|
switch (state) {
|
|
.init, .oso_close => {
|
|
state = .oso_open;
|
|
ofile = sym.n_strx;
|
|
},
|
|
else => return error.InvalidDebugInfo,
|
|
}
|
|
},
|
|
macho.N_BNSYM => {
|
|
switch (state) {
|
|
.oso_open, .ensym => {
|
|
state = .bnsym;
|
|
last_sym = .{
|
|
.strx = 0,
|
|
.addr = sym.n_value,
|
|
.size = 0,
|
|
.ofile = ofile,
|
|
};
|
|
},
|
|
else => return error.InvalidDebugInfo,
|
|
}
|
|
},
|
|
macho.N_FUN => {
|
|
switch (state) {
|
|
.bnsym => {
|
|
state = .fun_strx;
|
|
last_sym.strx = sym.n_strx;
|
|
},
|
|
.fun_strx => {
|
|
state = .fun_size;
|
|
last_sym.size = @as(u32, @intCast(sym.n_value));
|
|
},
|
|
else => return error.InvalidDebugInfo,
|
|
}
|
|
},
|
|
macho.N_ENSYM => {
|
|
switch (state) {
|
|
.fun_size => {
|
|
state = .ensym;
|
|
symbols_buf[symbol_index] = last_sym;
|
|
symbol_index += 1;
|
|
},
|
|
else => return error.InvalidDebugInfo,
|
|
}
|
|
},
|
|
macho.N_SO => {
|
|
switch (state) {
|
|
.init, .oso_close => {},
|
|
.oso_open, .ensym => {
|
|
state = .oso_close;
|
|
},
|
|
else => return error.InvalidDebugInfo,
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
switch (state) {
|
|
.init => return error.MissingDebugInfo,
|
|
.oso_close => {},
|
|
else => return error.InvalidDebugInfo,
|
|
}
|
|
|
|
const symbols = try allocator.realloc(symbols_buf, symbol_index);
|
|
|
|
// Even though lld emits symbols in ascending order, this debug code
|
|
// should work for programs linked in any valid way.
|
|
// This sort is so that we can binary search later.
|
|
mem.sort(MachoSymbol, symbols, {}, MachoSymbol.addressLessThan);
|
|
|
|
return .{
|
|
.base_address = undefined,
|
|
.vmaddr_slide = undefined,
|
|
.mapped_memory = mapped_mem,
|
|
.ofiles = Module.OFileTable.init(allocator),
|
|
.symbols = symbols,
|
|
.strings = strings,
|
|
};
|
|
}
|
|
|
|
fn readCoffDebugInfo(gpa: Allocator, coff_obj: *coff.Coff) !Module {
|
|
nosuspend {
|
|
var di: Module = .{
|
|
.base_address = undefined,
|
|
.coff_image_base = coff_obj.getImageBase(),
|
|
.coff_section_headers = undefined,
|
|
.pdb = null,
|
|
.dwarf = null,
|
|
};
|
|
|
|
if (coff_obj.getSectionByName(".debug_info")) |_| {
|
|
// This coff file has embedded DWARF debug info
|
|
var sections: Dwarf.SectionArray = Dwarf.null_section_array;
|
|
errdefer for (sections) |section| if (section) |s| if (s.owned) gpa.free(s.data);
|
|
|
|
inline for (@typeInfo(Dwarf.Section.Id).@"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, gpa),
|
|
.virtual_address = section_header.virtual_address,
|
|
.owned = true,
|
|
};
|
|
} else null;
|
|
}
|
|
|
|
var dwarf: Dwarf = .{
|
|
.endian = native_endian,
|
|
.sections = sections,
|
|
.is_macho = false,
|
|
};
|
|
|
|
try Dwarf.open(&dwarf, gpa);
|
|
di.dwarf = dwarf;
|
|
}
|
|
|
|
const raw_path = try coff_obj.getPdbPath() orelse return di;
|
|
const path = blk: {
|
|
if (fs.path.isAbsolute(raw_path)) {
|
|
break :blk raw_path;
|
|
} else {
|
|
const self_dir = try fs.selfExeDirPathAlloc(gpa);
|
|
defer gpa.free(self_dir);
|
|
break :blk try fs.path.join(gpa, &.{ self_dir, raw_path });
|
|
}
|
|
};
|
|
defer if (path.ptr != raw_path.ptr) gpa.free(path);
|
|
|
|
const pdb_file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
|
|
error.FileNotFound, error.IsDir => {
|
|
if (di.dwarf == null) return error.MissingDebugInfo;
|
|
return di;
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
errdefer pdb_file.close();
|
|
|
|
const pdb_file_reader_buffer = try gpa.alloc(u8, 4096);
|
|
errdefer gpa.free(pdb_file_reader_buffer);
|
|
|
|
const pdb_file_reader = try gpa.create(File.Reader);
|
|
errdefer gpa.destroy(pdb_file_reader);
|
|
|
|
pdb_file_reader.* = pdb_file.reader(pdb_file_reader_buffer);
|
|
|
|
di.pdb = try Pdb.init(gpa, pdb_file_reader);
|
|
try di.pdb.?.parseInfoStream();
|
|
try di.pdb.?.parseDbiStream();
|
|
|
|
if (!mem.eql(u8, &coff_obj.guid, &di.pdb.?.guid) or coff_obj.age != di.pdb.?.age)
|
|
return error.InvalidDebugInfo;
|
|
|
|
// Only used by the pdb path
|
|
di.coff_section_headers = try coff_obj.getSectionHeadersAlloc(gpa);
|
|
errdefer gpa.free(di.coff_section_headers);
|
|
|
|
return di;
|
|
}
|
|
}
|
|
|
|
/// 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: Allocator,
|
|
elf_filename: ?[]const u8,
|
|
build_id: ?[]const u8,
|
|
expected_crc: ?u32,
|
|
parent_sections: *Dwarf.SectionArray,
|
|
parent_mapped_mem: ?[]align(std.heap.page_size_min) const u8,
|
|
) !Dwarf.ElfModule {
|
|
nosuspend {
|
|
const elf_file = (if (elf_filename) |filename| blk: {
|
|
break :blk fs.cwd().openFile(filename, .{});
|
|
} else fs.openSelfExe(.{})) catch |err| switch (err) {
|
|
error.FileNotFound => return error.MissingDebugInfo,
|
|
else => return err,
|
|
};
|
|
|
|
const mapped_mem = try mapWholeFile(elf_file);
|
|
return Dwarf.ElfModule.load(
|
|
allocator,
|
|
mapped_mem,
|
|
build_id,
|
|
expected_crc,
|
|
parent_sections,
|
|
parent_mapped_mem,
|
|
elf_filename,
|
|
);
|
|
}
|
|
}
|
|
|
|
const MachoSymbol = struct {
|
|
strx: u32,
|
|
addr: u64,
|
|
size: u32,
|
|
ofile: u32,
|
|
|
|
/// Returns the address from the macho file
|
|
fn address(self: MachoSymbol) u64 {
|
|
return self.addr;
|
|
}
|
|
|
|
fn addressLessThan(context: void, lhs: MachoSymbol, rhs: MachoSymbol) bool {
|
|
_ = context;
|
|
return lhs.addr < rhs.addr;
|
|
}
|
|
};
|
|
|
|
/// Takes ownership of file, even on error.
|
|
/// TODO it's weird to take ownership even on error, rework this code.
|
|
fn mapWholeFile(file: File) ![]align(std.heap.page_size_min) const u8 {
|
|
nosuspend {
|
|
defer file.close();
|
|
|
|
const file_len = math.cast(usize, try file.getEndPos()) orelse math.maxInt(usize);
|
|
const mapped_mem = try posix.mmap(
|
|
null,
|
|
file_len,
|
|
posix.PROT.READ,
|
|
.{ .TYPE = .SHARED },
|
|
file.handle,
|
|
0,
|
|
);
|
|
errdefer posix.munmap(mapped_mem);
|
|
|
|
return mapped_mem;
|
|
}
|
|
}
|
|
|
|
fn machoSearchSymbols(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
|
|
var min: usize = 0;
|
|
var max: usize = symbols.len - 1;
|
|
while (min < max) {
|
|
const mid = min + (max - min) / 2;
|
|
const curr = &symbols[mid];
|
|
const next = &symbols[mid + 1];
|
|
if (address >= next.address()) {
|
|
min = mid + 1;
|
|
} else if (address < curr.address()) {
|
|
max = mid;
|
|
} else {
|
|
return curr;
|
|
}
|
|
}
|
|
|
|
const max_sym = &symbols[symbols.len - 1];
|
|
if (address >= max_sym.address())
|
|
return max_sym;
|
|
|
|
return null;
|
|
}
|
|
|
|
test machoSearchSymbols {
|
|
const symbols = [_]MachoSymbol{
|
|
.{ .addr = 100, .strx = undefined, .size = undefined, .ofile = undefined },
|
|
.{ .addr = 200, .strx = undefined, .size = undefined, .ofile = undefined },
|
|
.{ .addr = 300, .strx = undefined, .size = undefined, .ofile = undefined },
|
|
};
|
|
|
|
try testing.expectEqual(null, machoSearchSymbols(&symbols, 0));
|
|
try testing.expectEqual(null, machoSearchSymbols(&symbols, 99));
|
|
try testing.expectEqual(&symbols[0], machoSearchSymbols(&symbols, 100).?);
|
|
try testing.expectEqual(&symbols[0], machoSearchSymbols(&symbols, 150).?);
|
|
try testing.expectEqual(&symbols[0], machoSearchSymbols(&symbols, 199).?);
|
|
|
|
try testing.expectEqual(&symbols[1], machoSearchSymbols(&symbols, 200).?);
|
|
try testing.expectEqual(&symbols[1], machoSearchSymbols(&symbols, 250).?);
|
|
try testing.expectEqual(&symbols[1], machoSearchSymbols(&symbols, 299).?);
|
|
|
|
try testing.expectEqual(&symbols[2], machoSearchSymbols(&symbols, 300).?);
|
|
try testing.expectEqual(&symbols[2], machoSearchSymbols(&symbols, 301).?);
|
|
try testing.expectEqual(&symbols[2], machoSearchSymbols(&symbols, 5000).?);
|
|
}
|
|
|
|
/// 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(
|
|
allocator: Allocator,
|
|
base_address: usize,
|
|
context: *UnwindContext,
|
|
unwind_info: []const u8,
|
|
eh_frame: ?[]const u8,
|
|
) !usize {
|
|
const header = std.mem.bytesAsValue(
|
|
macho.unwind_info_section_header,
|
|
unwind_info[0..@sizeOf(macho.unwind_info_section_header)],
|
|
);
|
|
const indices = std.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 - 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 = std.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 = std.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 = std.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 = std.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 = std.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 = std.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 math.sub(
|
|
u8,
|
|
entry.encodingIndex,
|
|
math.cast(u8, header.commonEncodingsArrayCount) orelse return error.InvalidUnwindInfo,
|
|
);
|
|
const local_encodings = std.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 = Dwarf.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 regValueNative(context.thread_context, fpRegNum(reg_context), reg_context)).*;
|
|
const new_sp = fp + 2 * @sizeOf(usize);
|
|
|
|
const ip_ptr = fp + @sizeOf(usize);
|
|
const new_ip = @as(*const usize, @ptrFromInt(ip_ptr)).*;
|
|
const new_fp = @as(*const usize, @ptrFromInt(fp)).*;
|
|
|
|
(try regValueNative(context.thread_context, fpRegNum(reg_context), reg_context)).* = new_fp;
|
|
(try regValueNative(context.thread_context, spRegNum(reg_context), reg_context)).* = new_sp;
|
|
(try regValueNative(context.thread_context, ip_reg_num, 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 Dwarf.compactUnwindToDwarfRegNumber(reg);
|
|
(try regValueNative(context.thread_context, reg_number, reg_context)).* = @as(*const usize, @ptrFromInt(addr)).*;
|
|
}
|
|
|
|
break :blk new_ip;
|
|
},
|
|
.STACK_IMMD,
|
|
.STACK_IND,
|
|
=> blk: {
|
|
const sp = (try regValueNative(context.thread_context, 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 =
|
|
base_address +
|
|
entry.function_offset +
|
|
encoding.value.x86_64.frameless.stack.indirect.sub_offset;
|
|
|
|
// `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);
|
|
for (0..reg_count) |i| {
|
|
const reg_number = try Dwarf.compactUnwindToDwarfRegNumber(registers[i]);
|
|
(try regValueNative(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);
|
|
|
|
(try regValueNative(context.thread_context, spRegNum(reg_context), reg_context)).* = new_sp;
|
|
(try regValueNative(context.thread_context, ip_reg_num, reg_context)).* = new_ip;
|
|
|
|
break :blk new_ip;
|
|
},
|
|
.DWARF => {
|
|
return unwindFrameMachODwarf(allocator, base_address, context, eh_frame orelse return error.MissingEhFrame, @intCast(encoding.value.x86_64.dwarf));
|
|
},
|
|
},
|
|
.aarch64, .aarch64_be => switch (encoding.mode.arm64) {
|
|
.OLD => return error.UnimplementedUnwindEncoding,
|
|
.FRAMELESS => blk: {
|
|
const sp = (try regValueNative(context.thread_context, spRegNum(reg_context), reg_context)).*;
|
|
const new_sp = sp + encoding.value.arm64.frameless.stack_size * 16;
|
|
const new_ip = (try regValueNative(context.thread_context, 30, reg_context)).*;
|
|
(try regValueNative(context.thread_context, spRegNum(reg_context), reg_context)).* = new_sp;
|
|
break :blk new_ip;
|
|
},
|
|
.DWARF => {
|
|
return unwindFrameMachODwarf(allocator, base_address, context, eh_frame orelse return error.MissingEhFrame, @intCast(encoding.value.arm64.dwarf));
|
|
},
|
|
.FRAME => blk: {
|
|
const fp = (try regValueNative(context.thread_context, fpRegNum(reg_context), reg_context)).*;
|
|
const ip_ptr = fp + @sizeOf(usize);
|
|
|
|
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 regValueNative(context.thread_context, 19 + i, reg_context)).* = @as(*const usize, @ptrFromInt(reg_addr)).*;
|
|
reg_addr += @sizeOf(usize);
|
|
(try regValueNative(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 regBytes(context.thread_context, 64 + 8 + i, context.reg_context),
|
|
std.mem.asBytes(@as(*const usize, @ptrFromInt(reg_addr))),
|
|
);
|
|
reg_addr += @sizeOf(usize);
|
|
@memcpy(
|
|
try regBytes(context.thread_context, 64 + 9 + i, context.reg_context),
|
|
std.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 regValueNative(context.thread_context, fpRegNum(reg_context), reg_context)).* = new_fp;
|
|
(try regValueNative(context.thread_context, ip_reg_num, reg_context)).* = new_ip;
|
|
|
|
break :blk new_ip;
|
|
},
|
|
},
|
|
else => return error.UnimplementedArch,
|
|
};
|
|
|
|
context.pc = stripInstructionPtrAuthCode(new_ip);
|
|
if (context.pc > 0) context.pc -= 1;
|
|
return new_ip;
|
|
}
|
|
|
|
pub const UnwindContext = struct {
|
|
allocator: Allocator,
|
|
cfa: ?usize,
|
|
pc: usize,
|
|
thread_context: *std.debug.ThreadContext,
|
|
reg_context: Dwarf.abi.RegisterContext,
|
|
vm: VirtualMachine,
|
|
stack_machine: Dwarf.expression.StackMachine(.{ .call_frame_context = true }),
|
|
|
|
pub fn init(
|
|
allocator: Allocator,
|
|
thread_context: *std.debug.ThreadContext,
|
|
) !UnwindContext {
|
|
comptime assert(supports_unwinding);
|
|
|
|
const pc = stripInstructionPtrAuthCode(
|
|
(try regValueNative(thread_context, ip_reg_num, null)).*,
|
|
);
|
|
|
|
const context_copy = try allocator.create(std.debug.ThreadContext);
|
|
std.debug.copyContext(thread_context, context_copy);
|
|
|
|
return .{
|
|
.allocator = allocator,
|
|
.cfa = null,
|
|
.pc = pc,
|
|
.thread_context = context_copy,
|
|
.reg_context = undefined,
|
|
.vm = .{},
|
|
.stack_machine = .{},
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *UnwindContext) void {
|
|
self.vm.deinit(self.allocator);
|
|
self.stack_machine.deinit(self.allocator);
|
|
self.allocator.destroy(self.thread_context);
|
|
self.* = undefined;
|
|
}
|
|
|
|
pub fn getFp(self: *const UnwindContext) !usize {
|
|
return (try regValueNative(self.thread_context, fpRegNum(self.reg_context), self.reg_context)).*;
|
|
}
|
|
};
|
|
|
|
/// 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 (native_arch.isAARCH64()) {
|
|
// `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 = true });
|
|
}
|
|
|
|
return ptr;
|
|
}
|
|
|
|
/// Unwind a stack frame using DWARF unwinding info, updating the register context.
|
|
///
|
|
/// If `.eh_frame_hdr` is available and complete, 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. The latter
|
|
/// may require lazily loading the data in those sections.
|
|
///
|
|
/// `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 unwindFrameDwarf(
|
|
allocator: Allocator,
|
|
di: *Dwarf,
|
|
base_address: usize,
|
|
context: *UnwindContext,
|
|
explicit_fde_offset: ?usize,
|
|
) !usize {
|
|
if (!supports_unwinding) return error.UnsupportedCpuArchitecture;
|
|
if (context.pc == 0) return 0;
|
|
|
|
const endian = di.endian;
|
|
|
|
// Find the FDE and CIE
|
|
const cie, const fde = if (explicit_fde_offset) |fde_offset| blk: {
|
|
const dwarf_section: Dwarf.Section.Id = .eh_frame;
|
|
const frame_section = di.section(dwarf_section) orelse return error.MissingFDE;
|
|
if (fde_offset >= frame_section.len) return error.MissingFDE;
|
|
|
|
var fbr: std.Io.Reader = .fixed(frame_section);
|
|
fbr.seek = fde_offset;
|
|
|
|
const fde_entry_header = try Dwarf.EntryHeader.read(&fbr, dwarf_section, endian);
|
|
if (fde_entry_header.type != .fde) return error.MissingFDE;
|
|
|
|
const cie_offset = fde_entry_header.type.fde;
|
|
fbr.seek = @intCast(cie_offset);
|
|
|
|
const cie_entry_header = try Dwarf.EntryHeader.read(&fbr, dwarf_section, endian);
|
|
if (cie_entry_header.type != .cie) return Dwarf.bad();
|
|
|
|
const cie = try Dwarf.CommonInformationEntry.parse(
|
|
cie_entry_header.entry_bytes,
|
|
0,
|
|
true,
|
|
cie_entry_header.format,
|
|
dwarf_section,
|
|
cie_entry_header.length_offset,
|
|
@sizeOf(usize),
|
|
native_endian,
|
|
);
|
|
const fde = try Dwarf.FrameDescriptionEntry.parse(
|
|
fde_entry_header.entry_bytes,
|
|
0,
|
|
true,
|
|
cie,
|
|
@sizeOf(usize),
|
|
native_endian,
|
|
);
|
|
|
|
break :blk .{ cie, fde };
|
|
} else blk: {
|
|
// `.eh_frame_hdr` may be incomplete. We'll try it first, but if the lookup fails, we fall
|
|
// back to loading `.eh_frame`/`.debug_frame` and using those from that point on.
|
|
|
|
if (di.eh_frame_hdr) |header| hdr: {
|
|
const eh_frame_len = if (di.section(.eh_frame)) |eh_frame| eh_frame.len else {
|
|
try di.scanCieFdeInfo(allocator, base_address);
|
|
di.eh_frame_hdr = null;
|
|
break :hdr;
|
|
};
|
|
|
|
var cie: Dwarf.CommonInformationEntry = undefined;
|
|
var fde: Dwarf.FrameDescriptionEntry = undefined;
|
|
|
|
header.findEntry(
|
|
eh_frame_len,
|
|
@intFromPtr(di.section(.eh_frame_hdr).?.ptr),
|
|
context.pc,
|
|
&cie,
|
|
&fde,
|
|
endian,
|
|
) catch |err| switch (err) {
|
|
error.MissingDebugInfo => {
|
|
// `.eh_frame_hdr` appears to be incomplete, so go ahead and populate `cie_map`
|
|
// and `fde_list`, and fall back to the binary search logic below.
|
|
try di.scanCieFdeInfo(allocator, base_address);
|
|
|
|
// Since `.eh_frame_hdr` is incomplete, we're very likely to get more lookup
|
|
// failures using it, and we've just built a complete, sorted list of FDEs
|
|
// anyway, so just stop using `.eh_frame_hdr` altogether.
|
|
di.eh_frame_hdr = null;
|
|
|
|
break :hdr;
|
|
},
|
|
else => return err,
|
|
};
|
|
|
|
break :blk .{ cie, fde };
|
|
}
|
|
|
|
const index = std.sort.binarySearch(Dwarf.FrameDescriptionEntry, di.fde_list.items, context.pc, struct {
|
|
pub fn compareFn(pc: usize, item: Dwarf.FrameDescriptionEntry) std.math.Order {
|
|
if (pc < item.pc_begin) return .lt;
|
|
|
|
const range_end = item.pc_begin + item.pc_range;
|
|
if (pc < range_end) return .eq;
|
|
|
|
return .gt;
|
|
}
|
|
}.compareFn);
|
|
|
|
const fde = if (index) |i| di.fde_list.items[i] else return error.MissingFDE;
|
|
const cie = di.cie_map.get(fde.cie_length_offset) orelse return error.MissingCIE;
|
|
|
|
break :blk .{ cie, fde };
|
|
};
|
|
|
|
var expression_context: Dwarf.expression.Context = .{
|
|
.format = cie.format,
|
|
.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.readInt(usize, (try regBytes(context.thread_context, register, context.reg_context))[0..@sizeOf(usize)], native_endian);
|
|
break :blk try applyOffset(value, offset);
|
|
},
|
|
.expression => |expr| blk: {
|
|
context.stack_machine.reset();
|
|
const value = try context.stack_machine.run(
|
|
expr,
|
|
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,
|
|
};
|
|
|
|
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 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 regValueNative(context.thread_context, 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 = stripInstructionPtrAuthCode(mem.readInt(usize, (try regBytes(
|
|
context.thread_context,
|
|
cie.return_address_register,
|
|
context.reg_context,
|
|
))[0..@sizeOf(usize)], native_endian));
|
|
} else {
|
|
context.pc = 0;
|
|
}
|
|
|
|
(try regValueNative(context.thread_context, ip_reg_num, 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;
|
|
}
|
|
|
|
fn fpRegNum(reg_context: Dwarf.abi.RegisterContext) u8 {
|
|
return Dwarf.abi.fpRegNum(native_arch, reg_context);
|
|
}
|
|
|
|
fn spRegNum(reg_context: Dwarf.abi.RegisterContext) u8 {
|
|
return Dwarf.abi.spRegNum(native_arch, reg_context);
|
|
}
|
|
|
|
const ip_reg_num = Dwarf.abi.ipRegNum(native_arch).?;
|
|
|
|
/// Tells whether unwinding for the host is implemented.
|
|
pub const supports_unwinding = supportsUnwinding(&builtin.target);
|
|
|
|
comptime {
|
|
if (supports_unwinding) assert(Dwarf.abi.supportsUnwinding(&builtin.target));
|
|
}
|
|
|
|
/// Tells whether unwinding for this target is *implemented* here in the Zig
|
|
/// standard library.
|
|
///
|
|
/// See also `Dwarf.abi.supportsUnwinding` which tells whether Dwarf supports
|
|
/// unwinding on that target *in theory*.
|
|
pub fn supportsUnwinding(target: *const std.Target) bool {
|
|
return switch (target.cpu.arch) {
|
|
.x86 => switch (target.os.tag) {
|
|
.linux, .netbsd, .solaris, .illumos => true,
|
|
else => false,
|
|
},
|
|
.x86_64 => switch (target.os.tag) {
|
|
.linux, .netbsd, .freebsd, .openbsd, .macos, .ios, .solaris, .illumos => true,
|
|
else => false,
|
|
},
|
|
.arm, .armeb, .thumb, .thumbeb => switch (target.os.tag) {
|
|
.linux => true,
|
|
else => false,
|
|
},
|
|
.aarch64, .aarch64_be => switch (target.os.tag) {
|
|
.linux, .netbsd, .freebsd, .macos, .ios => true,
|
|
else => false,
|
|
},
|
|
// Unwinding is possible on other targets but this implementation does
|
|
// not support them...yet!
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
fn unwindFrameMachODwarf(
|
|
allocator: Allocator,
|
|
base_address: usize,
|
|
context: *UnwindContext,
|
|
eh_frame: []const u8,
|
|
fde_offset: usize,
|
|
) !usize {
|
|
var di: Dwarf = .{
|
|
.endian = native_endian,
|
|
.is_macho = true,
|
|
};
|
|
defer di.deinit(context.allocator);
|
|
|
|
di.sections[@intFromEnum(Dwarf.Section.Id.eh_frame)] = .{
|
|
.data = eh_frame,
|
|
.owned = false,
|
|
};
|
|
|
|
return unwindFrameDwarf(allocator, &di, base_address, context, fde_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 regBytes)
|
|
pub fn resolveValue(
|
|
self: Column,
|
|
context: *SelfInfo.UnwindContext,
|
|
expression_context: std.debug.Dwarf.expression.Context,
|
|
out: []u8,
|
|
) !void {
|
|
switch (self.rule) {
|
|
.default => {
|
|
const register = self.register orelse return error.InvalidRegister;
|
|
try 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 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);
|
|
const ptr: *const usize = @ptrFromInt(addr);
|
|
mem.writeInt(usize, out[0..@sizeOf(usize)], ptr.*, native_endian);
|
|
} else return error.InvalidCFA;
|
|
},
|
|
.val_offset => |offset| {
|
|
if (context.cfa) |cfa| {
|
|
mem.writeInt(usize, out[0..@sizeOf(usize)], try applyOffset(cfa, offset), native_endian);
|
|
} else return error.InvalidCFA;
|
|
},
|
|
.register => |register| {
|
|
const src = try regBytes(context.thread_context, register, context.reg_context);
|
|
if (src.len != out.len) return error.RegisterSizeMismatch;
|
|
@memcpy(out, try 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;
|
|
|
|
const ptr: *usize = @ptrFromInt(addr);
|
|
mem.writeInt(usize, out[0..@sizeOf(usize)], ptr.*, native_endian);
|
|
},
|
|
.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.writeInt(usize, out[0..@sizeOf(usize)], v.generic, native_endian);
|
|
} 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) = .empty,
|
|
stack: std.ArrayListUnmanaged(ColumnRange) = .empty,
|
|
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 {
|
|
if (row.columns.len == 0) return &.{};
|
|
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: std.debug.Dwarf.CommonInformationEntry,
|
|
fde: std.debug.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.Reader = .fixed(cie.initial_instructions);
|
|
var fde_stream: std.Io.Reader = .fixed(fde.instructions);
|
|
const streams = [_]*std.Io.Reader{ &cie_stream, &fde_stream };
|
|
|
|
for (&streams, 0..) |stream, i| {
|
|
while (stream.seek < stream.buffer.len) {
|
|
const instruction = try std.debug.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: std.debug.Dwarf.CommonInformationEntry,
|
|
fde: std.debug.Dwarf.FrameDescriptionEntry,
|
|
) !Row {
|
|
return self.runTo(allocator, pc, cie, fde, @sizeOf(usize), native_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: std.debug.Dwarf.CommonInformationEntry,
|
|
is_initial: bool,
|
|
instruction: Dwarf.call_frame.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.address <= self.current_row.offset) return error.InvalidOperation;
|
|
// TODO: Check cie.segment_selector_size != 0 for DWARFV4
|
|
self.current_row.offset = i.address;
|
|
},
|
|
inline .advance_loc,
|
|
.advance_loc1,
|
|
.advance_loc2,
|
|
.advance_loc4,
|
|
=> |i| {
|
|
self.current_row.offset += i.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.register);
|
|
column.rule = .{ .offset = @as(i64, @intCast(i.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.register);
|
|
column.rule = for (self.rowColumns(cie_row)) |cie_column| {
|
|
if (cie_column.register == i.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.register);
|
|
column.rule = .{ .undefined = {} };
|
|
},
|
|
.same_value => |i| {
|
|
try self.resolveCopyOnWrite(allocator);
|
|
const column = try self.getOrAddColumn(allocator, i.register);
|
|
column.rule = .{ .same_value = {} };
|
|
},
|
|
.register => |i| {
|
|
try self.resolveCopyOnWrite(allocator);
|
|
const column = try self.getOrAddColumn(allocator, i.register);
|
|
column.rule = .{ .register = i.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.pop() 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.register,
|
|
.rule = .{ .val_offset = @intCast(i.offset) },
|
|
};
|
|
},
|
|
.def_cfa_sf => |i| {
|
|
try self.resolveCopyOnWrite(allocator);
|
|
self.current_row.cfa = .{
|
|
.register = i.register,
|
|
.rule = .{ .val_offset = i.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.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.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.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.block,
|
|
};
|
|
},
|
|
.expression => |i| {
|
|
try self.resolveCopyOnWrite(allocator);
|
|
const column = try self.getOrAddColumn(allocator, i.register);
|
|
column.rule = .{
|
|
.expression = i.block,
|
|
};
|
|
},
|
|
.val_offset => |i| {
|
|
try self.resolveCopyOnWrite(allocator);
|
|
const column = try self.getOrAddColumn(allocator, i.register);
|
|
column.rule = .{
|
|
.val_offset = @as(i64, @intCast(i.offset)) * cie.data_alignment_factor,
|
|
};
|
|
},
|
|
.val_offset_sf => |i| {
|
|
try self.resolveCopyOnWrite(allocator);
|
|
const column = try self.getOrAddColumn(allocator, i.register);
|
|
column.rule = .{
|
|
.val_offset = i.offset * cie.data_alignment_factor,
|
|
};
|
|
},
|
|
.val_expression => |i| {
|
|
try self.resolveCopyOnWrite(allocator);
|
|
const column = try self.getOrAddColumn(allocator, i.register);
|
|
column.rule = .{
|
|
.val_expression = i.block,
|
|
};
|
|
},
|
|
}
|
|
|
|
return prev_row;
|
|
}
|
|
};
|
|
|
|
/// 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.
|
|
fn getRegDefaultValue(reg_number: u8, context: *UnwindContext, out: []u8) !void {
|
|
switch (builtin.cpu.arch) {
|
|
.aarch64, .aarch64_be => {
|
|
// 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);
|
|
}
|
|
|
|
/// 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.
|
|
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)));
|
|
}
|