| author | |
| committer | |
| log | c1a30bd0d876330ce7a241fc297c66577ae7e6aa |
| tree | fbc6e50c11746e259fb5366caf57fce40c4d6964 |
| parent | f7980487395b660d5c568ba57891ab371a27102d |
| signature |
This abstraction isn't really tied to DWARF at all! Really, we're just
loading some information from an ELF file which is useful for debugging.
That *includes* DWARF, but it also includes other information. For
instance, the other change here:
Now, if DWARF information is missing, `debug.SelfInfo.ElfModule` will
name symbols by finding a matching symtab entry. We actually already do
this on Mach-O, so it makes obvious sense to do the same on ELF! This
change is what motivated the restructuring to begin with.
The symtab work is derived from #22077.
Co-authored-by: geemili <opensource@geemili.xyz>7 files changed, 627 insertions(+), 421 deletions(-)
lib/std/debug.zig+1| ... | ... | @@ -18,6 +18,7 @@ const root = @import("root"); |
| 18 | 18 | |
| 19 | 19 | pub const Dwarf = @import("debug/Dwarf.zig"); |
| 20 | 20 | pub const Pdb = @import("debug/Pdb.zig"); |
| 21 | pub const ElfFile = @import("debug/ElfFile.zig"); | |
| 21 | 22 | pub const SelfInfo = @import("debug/SelfInfo.zig"); |
| 22 | 23 | pub const Info = @import("debug/Info.zig"); |
| 23 | 24 | pub const Coverage = @import("debug/Coverage.zig"); |
lib/std/debug/Dwarf.zig-1| ... | ... | @@ -30,7 +30,6 @@ pub const expression = @import("Dwarf/expression.zig"); |
| 30 | 30 | pub const abi = @import("Dwarf/abi.zig"); |
| 31 | 31 | pub const call_frame = @import("Dwarf/call_frame.zig"); |
| 32 | 32 | pub const Unwind = @import("Dwarf/Unwind.zig"); |
| 33 | pub const ElfModule = @import("Dwarf/ElfModule.zig"); | |
| 34 | 33 | |
| 35 | 34 | /// Useful to temporarily enable while working on this file. |
| 36 | 35 | const debug_debug_mode = false; |
lib/std/debug/Dwarf/ElfModule.zig deleted-376| ... | ... | @@ -1,376 +0,0 @@ |
| 1 | //! A thin wrapper around `Dwarf` which handles loading debug information from an ELF file. Load the | |
| 2 | //! info with `load`, then directly access the `dwarf` field before finally `deinit`ing. | |
| 3 | ||
| 4 | dwarf: Dwarf, | |
| 5 | ||
| 6 | /// If we encounter a `.eh_frame` section while loading the ELF module, it is stored here and may be | |
| 7 | /// used with `Dwarf.Unwind` for call stack unwinding. | |
| 8 | eh_frame: ?UnwindSection, | |
| 9 | /// If we encounter a `.debug_frame` section while loading the ELF module, it is stored here and may | |
| 10 | /// be used with `Dwarf.Unwind` for call stack unwinding. | |
| 11 | debug_frame: ?UnwindSection, | |
| 12 | ||
| 13 | /// The memory-mapped ELF file, which is referenced by `dwarf`. This field is here only so that | |
| 14 | /// this memory can be unmapped by `ElfModule.deinit`. | |
| 15 | mapped_file: []align(std.heap.page_size_min) const u8, | |
| 16 | /// Sometimes, debug info is stored separately to the main ELF file. In that case, `mapped_file` | |
| 17 | /// is the mapped ELF binary, and `mapped_debug_file` is the mapped debug info file. Both must | |
| 18 | /// be unmapped by `ElfModule.deinit`. | |
| 19 | mapped_debug_file: ?[]align(std.heap.page_size_min) const u8, | |
| 20 | ||
| 21 | pub const UnwindSection = struct { | |
| 22 | vaddr: u64, | |
| 23 | bytes: []const u8, | |
| 24 | owned: bool, | |
| 25 | }; | |
| 26 | ||
| 27 | pub fn deinit(em: *ElfModule, gpa: Allocator) void { | |
| 28 | em.dwarf.deinit(gpa); | |
| 29 | std.posix.munmap(em.mapped_file); | |
| 30 | if (em.mapped_debug_file) |m| std.posix.munmap(m); | |
| 31 | if (em.eh_frame) |s| if (s.owned) gpa.free(s.bytes); | |
| 32 | if (em.debug_frame) |s| if (s.owned) gpa.free(s.bytes); | |
| 33 | } | |
| 34 | ||
| 35 | pub const LoadError = error{ | |
| 36 | InvalidDebugInfo, | |
| 37 | MissingDebugInfo, | |
| 38 | InvalidElfMagic, | |
| 39 | InvalidElfVersion, | |
| 40 | InvalidElfEndian, | |
| 41 | /// TODO: implement this and then remove this error code | |
| 42 | UnimplementedDwarfForeignEndian, | |
| 43 | /// The debug info may be valid but this implementation uses memory | |
| 44 | /// mapping which limits things to usize. If the target debug info is | |
| 45 | /// 64-bit and host is 32-bit, there may be debug info that is not | |
| 46 | /// supportable using this method. | |
| 47 | Overflow, | |
| 48 | ||
| 49 | PermissionDenied, | |
| 50 | LockedMemoryLimitExceeded, | |
| 51 | MemoryMappingNotSupported, | |
| 52 | } || Allocator.Error || std.fs.File.OpenError || Dwarf.OpenError; | |
| 53 | ||
| 54 | /// Reads debug info from an ELF file given its path. | |
| 55 | /// | |
| 56 | /// If the required sections aren't present but a reference to external debug | |
| 57 | /// info is, then this this function will recurse to attempt to load the debug | |
| 58 | /// sections from an external file. | |
| 59 | pub fn load( | |
| 60 | gpa: Allocator, | |
| 61 | elf_file_path: Path, | |
| 62 | build_id: ?[]const u8, | |
| 63 | expected_crc: ?u32, | |
| 64 | parent_sections: ?*Dwarf.SectionArray, | |
| 65 | parent_mapped_mem: ?[]align(std.heap.page_size_min) const u8, | |
| 66 | ) LoadError!ElfModule { | |
| 67 | const mapped_mem: []align(std.heap.page_size_min) const u8 = mapped: { | |
| 68 | const elf_file = try elf_file_path.root_dir.handle.openFile(elf_file_path.sub_path, .{}); | |
| 69 | defer elf_file.close(); | |
| 70 | ||
| 71 | const file_len = std.math.cast( | |
| 72 | usize, | |
| 73 | elf_file.getEndPos() catch return Dwarf.bad(), | |
| 74 | ) orelse return error.Overflow; | |
| 75 | ||
| 76 | break :mapped std.posix.mmap( | |
| 77 | null, | |
| 78 | file_len, | |
| 79 | std.posix.PROT.READ, | |
| 80 | .{ .TYPE = .SHARED }, | |
| 81 | elf_file.handle, | |
| 82 | 0, | |
| 83 | ) catch |err| switch (err) { | |
| 84 | error.MappingAlreadyExists => unreachable, | |
| 85 | else => |e| return e, | |
| 86 | }; | |
| 87 | }; | |
| 88 | errdefer std.posix.munmap(mapped_mem); | |
| 89 | ||
| 90 | if (expected_crc) |crc| if (crc != std.hash.crc.Crc32.hash(mapped_mem)) return error.InvalidDebugInfo; | |
| 91 | ||
| 92 | const hdr: *const elf.Ehdr = @ptrCast(&mapped_mem[0]); | |
| 93 | if (!mem.eql(u8, hdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic; | |
| 94 | if (hdr.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion; | |
| 95 | ||
| 96 | const endian: std.builtin.Endian = switch (hdr.e_ident[elf.EI_DATA]) { | |
| 97 | elf.ELFDATA2LSB => .little, | |
| 98 | elf.ELFDATA2MSB => .big, | |
| 99 | else => return error.InvalidElfEndian, | |
| 100 | }; | |
| 101 | if (endian != native_endian) return error.UnimplementedDwarfForeignEndian; | |
| 102 | ||
| 103 | const shoff = hdr.e_shoff; | |
| 104 | const str_section_off = std.math.cast( | |
| 105 | usize, | |
| 106 | shoff + @as(u64, hdr.e_shentsize) * @as(u64, hdr.e_shstrndx), | |
| 107 | ) orelse return error.Overflow; | |
| 108 | const str_shdr: *const elf.Shdr = @ptrCast(@alignCast(mapped_mem[str_section_off..])); | |
| 109 | const header_strings = mapped_mem[str_shdr.sh_offset..][0..str_shdr.sh_size]; | |
| 110 | const shdrs = @as( | |
| 111 | [*]const elf.Shdr, | |
| 112 | @ptrCast(@alignCast(&mapped_mem[shoff])), | |
| 113 | )[0..hdr.e_shnum]; | |
| 114 | ||
| 115 | var sections: Dwarf.SectionArray = @splat(null); | |
| 116 | // Combine section list. This takes ownership over any owned sections from the parent scope. | |
| 117 | if (parent_sections) |ps| { | |
| 118 | for (ps, &sections) |*parent, *section_elem| { | |
| 119 | if (parent.*) |*p| { | |
| 120 | section_elem.* = p.*; | |
| 121 | p.owned = false; | |
| 122 | } | |
| 123 | } | |
| 124 | } | |
| 125 | errdefer for (sections) |opt_section| if (opt_section) |s| if (s.owned) gpa.free(s.data); | |
| 126 | ||
| 127 | var eh_frame_section: ?UnwindSection = null; | |
| 128 | errdefer if (eh_frame_section) |s| if (s.owned) gpa.free(s.bytes); | |
| 129 | ||
| 130 | var debug_frame_section: ?UnwindSection = null; | |
| 131 | errdefer if (debug_frame_section) |s| if (s.owned) gpa.free(s.bytes); | |
| 132 | ||
| 133 | var separate_debug_filename: ?[]const u8 = null; | |
| 134 | var separate_debug_crc: ?u32 = null; | |
| 135 | ||
| 136 | for (shdrs) |*shdr| { | |
| 137 | if (shdr.sh_type == elf.SHT_NULL or shdr.sh_type == elf.SHT_NOBITS) continue; | |
| 138 | const name = mem.sliceTo(header_strings[shdr.sh_name..], 0); | |
| 139 | ||
| 140 | if (mem.eql(u8, name, ".gnu_debuglink")) { | |
| 141 | if (mapped_mem.len < shdr.sh_offset + shdr.sh_size) return error.InvalidDebugInfo; | |
| 142 | const gnu_debuglink = mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)]; | |
| 143 | const debug_filename = mem.sliceTo(@as([*:0]const u8, @ptrCast(gnu_debuglink.ptr)), 0); | |
| 144 | const crc_offset = mem.alignForward(usize, debug_filename.len + 1, 4); | |
| 145 | const crc_bytes = gnu_debuglink[crc_offset..][0..4]; | |
| 146 | separate_debug_crc = mem.readInt(u32, crc_bytes, endian); | |
| 147 | separate_debug_filename = debug_filename; | |
| 148 | continue; | |
| 149 | } | |
| 150 | ||
| 151 | const section_id: union(enum) { | |
| 152 | dwarf: Dwarf.Section.Id, | |
| 153 | eh_frame, | |
| 154 | debug_frame, | |
| 155 | } = s: { | |
| 156 | inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields) |s| { | |
| 157 | if (mem.eql(u8, "." ++ s.name, name)) { | |
| 158 | break :s .{ .dwarf = @enumFromInt(s.value) }; | |
| 159 | } | |
| 160 | } | |
| 161 | if (mem.eql(u8, ".eh_frame", name)) break :s .eh_frame; | |
| 162 | if (mem.eql(u8, ".debug_frame", name)) break :s .debug_frame; | |
| 163 | continue; | |
| 164 | }; | |
| 165 | ||
| 166 | switch (section_id) { | |
| 167 | .dwarf => |i| if (sections[@intFromEnum(i)] != null) continue, | |
| 168 | .eh_frame => if (eh_frame_section != null) continue, | |
| 169 | .debug_frame => if (debug_frame_section != null) continue, | |
| 170 | } | |
| 171 | ||
| 172 | if (mapped_mem.len < shdr.sh_offset + shdr.sh_size) return error.InvalidDebugInfo; | |
| 173 | const raw_section_bytes = mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)]; | |
| 174 | ||
| 175 | const section_bytes: []const u8, const section_owned: bool = section: { | |
| 176 | if ((shdr.sh_flags & elf.SHF_COMPRESSED) == 0) { | |
| 177 | break :section .{ raw_section_bytes, false }; | |
| 178 | } | |
| 179 | var section_reader: Reader = .fixed(raw_section_bytes); | |
| 180 | const chdr = section_reader.takeStruct(elf.Chdr, endian) catch continue; | |
| 181 | if (chdr.ch_type != .ZLIB) continue; | |
| 182 | ||
| 183 | var decompress: std.compress.flate.Decompress = .init(&section_reader, .zlib, &.{}); | |
| 184 | var decompressed_section: ArrayList(u8) = .empty; | |
| 185 | defer decompressed_section.deinit(gpa); | |
| 186 | decompress.reader.appendRemainingUnlimited(gpa, &decompressed_section) catch { | |
| 187 | Dwarf.invalidDebugInfoDetected(); | |
| 188 | continue; | |
| 189 | }; | |
| 190 | if (chdr.ch_size != decompressed_section.items.len) { | |
| 191 | Dwarf.invalidDebugInfoDetected(); | |
| 192 | continue; | |
| 193 | } | |
| 194 | break :section .{ try decompressed_section.toOwnedSlice(gpa), true }; | |
| 195 | }; | |
| 196 | switch (section_id) { | |
| 197 | .dwarf => |id| sections[@intFromEnum(id)] = .{ | |
| 198 | .data = section_bytes, | |
| 199 | .owned = section_owned, | |
| 200 | }, | |
| 201 | .eh_frame => eh_frame_section = .{ | |
| 202 | .vaddr = shdr.sh_addr, | |
| 203 | .bytes = section_bytes, | |
| 204 | .owned = section_owned, | |
| 205 | }, | |
| 206 | .debug_frame => debug_frame_section = .{ | |
| 207 | .vaddr = shdr.sh_addr, | |
| 208 | .bytes = section_bytes, | |
| 209 | .owned = section_owned, | |
| 210 | }, | |
| 211 | } | |
| 212 | } | |
| 213 | ||
| 214 | const missing_debug_info = | |
| 215 | sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or | |
| 216 | sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or | |
| 217 | sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or | |
| 218 | sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null; | |
| 219 | ||
| 220 | // Attempt to load debug info from an external file | |
| 221 | // See: https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html | |
| 222 | if (missing_debug_info) { | |
| 223 | // Only allow one level of debug info nesting | |
| 224 | if (parent_mapped_mem) |_| { | |
| 225 | return error.MissingDebugInfo; | |
| 226 | } | |
| 227 | ||
| 228 | // $XDG_CACHE_HOME/debuginfod_client/<buildid>/debuginfo | |
| 229 | // This only opportunisticly tries to load from the debuginfod cache, but doesn't try to populate it. | |
| 230 | // One can manually run `debuginfod-find debuginfo PATH` to download the symbols | |
| 231 | debuginfod: { | |
| 232 | const id = build_id orelse break :debuginfod; | |
| 233 | switch (builtin.os.tag) { | |
| 234 | .wasi, .windows => break :debuginfod, | |
| 235 | else => {}, | |
| 236 | } | |
| 237 | const id_dir_path: []u8 = p: { | |
| 238 | if (std.posix.getenv("DEBUGINFOD_CACHE_PATH")) |path| { | |
| 239 | break :p try std.fmt.allocPrint(gpa, "{s}/{x}", .{ path, id }); | |
| 240 | } | |
| 241 | if (std.posix.getenv("XDG_CACHE_HOME")) |cache_path| { | |
| 242 | if (cache_path.len > 0) { | |
| 243 | break :p try std.fmt.allocPrint(gpa, "{s}/debuginfod_client/{x}", .{ cache_path, id }); | |
| 244 | } | |
| 245 | } | |
| 246 | if (std.posix.getenv("HOME")) |home_path| { | |
| 247 | break :p try std.fmt.allocPrint(gpa, "{s}/.cache/debuginfod_client/{x}", .{ home_path, id }); | |
| 248 | } | |
| 249 | break :debuginfod; | |
| 250 | }; | |
| 251 | defer gpa.free(id_dir_path); | |
| 252 | if (!std.fs.path.isAbsolute(id_dir_path)) break :debuginfod; | |
| 253 | ||
| 254 | var id_dir = std.fs.openDirAbsolute(id_dir_path, .{}) catch break :debuginfod; | |
| 255 | defer id_dir.close(); | |
| 256 | ||
| 257 | return load(gpa, .{ | |
| 258 | .root_dir = .{ .path = id_dir_path, .handle = id_dir }, | |
| 259 | .sub_path = "debuginfo", | |
| 260 | }, null, separate_debug_crc, &sections, mapped_mem) catch break :debuginfod; | |
| 261 | } | |
| 262 | ||
| 263 | const global_debug_directories = [_][]const u8{ | |
| 264 | "/usr/lib/debug", | |
| 265 | }; | |
| 266 | ||
| 267 | // <global debug directory>/.build-id/<2-character id prefix>/<id remainder>.debug | |
| 268 | if (build_id) |id| blk: { | |
| 269 | if (id.len < 3) break :blk; | |
| 270 | ||
| 271 | // Either md5 (16 bytes) or sha1 (20 bytes) are used here in practice | |
| 272 | const extension = ".debug"; | |
| 273 | var id_prefix_buf: [2]u8 = undefined; | |
| 274 | var filename_buf: [38 + extension.len]u8 = undefined; | |
| 275 | ||
| 276 | _ = std.fmt.bufPrint(&id_prefix_buf, "{x}", .{id[0..1]}) catch unreachable; | |
| 277 | const filename = std.fmt.bufPrint(&filename_buf, "{x}" ++ extension, .{id[1..]}) catch break :blk; | |
| 278 | ||
| 279 | for (global_debug_directories) |global_directory| { | |
| 280 | const path: Path = .{ | |
| 281 | .root_dir = .cwd(), | |
| 282 | .sub_path = try std.fs.path.join(gpa, &.{ | |
| 283 | global_directory, ".build-id", &id_prefix_buf, filename, | |
| 284 | }), | |
| 285 | }; | |
| 286 | defer gpa.free(path.sub_path); | |
| 287 | ||
| 288 | return load(gpa, path, null, separate_debug_crc, &sections, mapped_mem) catch continue; | |
| 289 | } | |
| 290 | } | |
| 291 | ||
| 292 | // use the path from .gnu_debuglink, in the same search order as gdb | |
| 293 | separate: { | |
| 294 | const separate_filename = separate_debug_filename orelse break :separate; | |
| 295 | if (mem.eql(u8, std.fs.path.basename(elf_file_path.sub_path), separate_filename)) | |
| 296 | return error.MissingDebugInfo; | |
| 297 | ||
| 298 | exe_dir: { | |
| 299 | const exe_dir_path = try std.fs.path.resolve(gpa, &.{ | |
| 300 | elf_file_path.root_dir.path orelse ".", | |
| 301 | std.fs.path.dirname(elf_file_path.sub_path) orelse ".", | |
| 302 | }); | |
| 303 | defer gpa.free(exe_dir_path); | |
| 304 | var exe_dir = std.fs.openDirAbsolute(exe_dir_path, .{}) catch break :exe_dir; | |
| 305 | defer exe_dir.close(); | |
| 306 | ||
| 307 | // <exe_dir>/<gnu_debuglink> | |
| 308 | if (load( | |
| 309 | gpa, | |
| 310 | .{ | |
| 311 | .root_dir = .{ .path = exe_dir_path, .handle = exe_dir }, | |
| 312 | .sub_path = separate_filename, | |
| 313 | }, | |
| 314 | null, | |
| 315 | separate_debug_crc, | |
| 316 | &sections, | |
| 317 | mapped_mem, | |
| 318 | )) |em| { | |
| 319 | return em; | |
| 320 | } else |_| {} | |
| 321 | ||
| 322 | // <exe_dir>/.debug/<gnu_debuglink> | |
| 323 | const path: Path = .{ | |
| 324 | .root_dir = .{ .path = exe_dir_path, .handle = exe_dir }, | |
| 325 | .sub_path = try std.fs.path.join(gpa, &.{ ".debug", separate_filename }), | |
| 326 | }; | |
| 327 | defer gpa.free(path.sub_path); | |
| 328 | ||
| 329 | if (load(gpa, path, null, separate_debug_crc, &sections, mapped_mem)) |em| { | |
| 330 | return em; | |
| 331 | } else |_| {} | |
| 332 | } | |
| 333 | ||
| 334 | var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; | |
| 335 | const cwd_path = std.posix.realpath(".", &cwd_buf) catch break :separate; | |
| 336 | ||
| 337 | // <global debug directory>/<absolute folder of current binary>/<gnu_debuglink> | |
| 338 | for (global_debug_directories) |global_directory| { | |
| 339 | const path: Path = .{ | |
| 340 | .root_dir = .cwd(), | |
| 341 | .sub_path = try std.fs.path.join(gpa, &.{ global_directory, cwd_path, separate_filename }), | |
| 342 | }; | |
| 343 | defer gpa.free(path.sub_path); | |
| 344 | if (load(gpa, path, null, separate_debug_crc, &sections, mapped_mem)) |em| { | |
| 345 | return em; | |
| 346 | } else |_| {} | |
| 347 | } | |
| 348 | } | |
| 349 | ||
| 350 | return error.MissingDebugInfo; | |
| 351 | } | |
| 352 | ||
| 353 | var dwarf: Dwarf = .{ .sections = sections }; | |
| 354 | try dwarf.open(gpa, endian); | |
| 355 | return .{ | |
| 356 | .dwarf = dwarf, | |
| 357 | .eh_frame = eh_frame_section, | |
| 358 | .debug_frame = debug_frame_section, | |
| 359 | .mapped_file = parent_mapped_mem orelse mapped_mem, | |
| 360 | .mapped_debug_file = if (parent_mapped_mem != null) mapped_mem else null, | |
| 361 | }; | |
| 362 | } | |
| 363 | ||
| 364 | const std = @import("../../std.zig"); | |
| 365 | const Allocator = std.mem.Allocator; | |
| 366 | const ArrayList = std.ArrayList; | |
| 367 | const Dwarf = std.debug.Dwarf; | |
| 368 | const Path = std.Build.Cache.Path; | |
| 369 | const Reader = std.Io.Reader; | |
| 370 | const mem = std.mem; | |
| 371 | const elf = std.elf; | |
| 372 | ||
| 373 | const builtin = @import("builtin"); | |
| 374 | const native_endian = builtin.cpu.arch.endian(); | |
| 375 | ||
| 376 | const ElfModule = @This(); |
lib/std/debug/ElfFile.zig created+536| ... | ... | @@ -0,0 +1,536 @@ |
| 1 | //! A helper type for loading an ELF file and collecting its DWARF debug information, unwind | |
| 2 | //! information, and symbol table. | |
| 3 | ||
| 4 | is_64: bool, | |
| 5 | endian: Endian, | |
| 6 | ||
| 7 | /// This is `null` iff any of the required DWARF sections were missing. `ElfFile.load` does *not* | |
| 8 | /// call `Dwarf.open`, `Dwarf.scanAllFunctions`, etc; that is the caller's responsibility. | |
| 9 | dwarf: ?Dwarf, | |
| 10 | ||
| 11 | /// If non-`null`, describes the `.eh_frame` section, which can be used with `Dwarf.Unwind`. | |
| 12 | eh_frame: ?UnwindSection, | |
| 13 | /// If non-`null`, describes the `.debug_frame` section, which can be used with `Dwarf.Unwind`. | |
| 14 | debug_frame: ?UnwindSection, | |
| 15 | ||
| 16 | /// If non-`null`, this is the contents of the `.strtab` section. | |
| 17 | strtab: ?[]const u8, | |
| 18 | /// If non-`null`, describes the `.symtab` section. | |
| 19 | symtab: ?SymtabSection, | |
| 20 | ||
| 21 | /// Binary search table lazily populated by `searchSymtab`. | |
| 22 | symbol_search_table: ?[]u64, | |
| 23 | ||
| 24 | /// The memory-mapped ELF file, which is referenced by `dwarf`. This field is here only so that | |
| 25 | /// this memory can be unmapped by `ElfFile.deinit`. | |
| 26 | mapped_file: []align(std.heap.page_size_min) const u8, | |
| 27 | /// Sometimes, debug info is stored separately to the main ELF file. In that case, `mapped_file` | |
| 28 | /// is the mapped ELF binary, and `mapped_debug_file` is the mapped debug info file. Both must | |
| 29 | /// be unmapped by `ElfFile.deinit`. | |
| 30 | mapped_debug_file: ?[]align(std.heap.page_size_min) const u8, | |
| 31 | ||
| 32 | arena: std.heap.ArenaAllocator.State, | |
| 33 | ||
| 34 | pub const UnwindSection = struct { | |
| 35 | vaddr: u64, | |
| 36 | bytes: []const u8, | |
| 37 | }; | |
| 38 | pub const SymtabSection = struct { | |
| 39 | entry_size: u64, | |
| 40 | bytes: []const u8, | |
| 41 | }; | |
| 42 | ||
| 43 | pub const DebugInfoSearchPaths = struct { | |
| 44 | /// The location of a debuginfod client directory, which acts as a search path for build IDs. If | |
| 45 | /// given, we can load from this directory opportunistically, but make no effort to populate it. | |
| 46 | /// To avoid allocation when building the search paths, this is given as two components which | |
| 47 | /// will be concatenated. | |
| 48 | debuginfod_client: ?[2][]const u8, | |
| 49 | /// All "global debug directories" on the system. These are used as search paths for both debug | |
| 50 | /// links and build IDs. On typical systems this is just "/usr/lib/debug". | |
| 51 | global_debug: []const []const u8, | |
| 52 | /// The path to the dirname of the ELF file, which acts as a search path for debug links. | |
| 53 | exe_dir: ?[]const u8, | |
| 54 | ||
| 55 | pub const none: DebugInfoSearchPaths = .{ | |
| 56 | .debuginfod_client = null, | |
| 57 | .global_debug = &.{}, | |
| 58 | .exe_dir = null, | |
| 59 | }; | |
| 60 | ||
| 61 | pub fn native(exe_path: []const u8) DebugInfoSearchPaths { | |
| 62 | return .{ | |
| 63 | .debuginfod_client = p: { | |
| 64 | if (std.posix.getenv("DEBUGINFOD_CACHE_PATH")) |p| { | |
| 65 | break :p .{ p, "" }; | |
| 66 | } | |
| 67 | if (std.posix.getenv("XDG_CACHE_HOME")) |cache_path| { | |
| 68 | break :p .{ cache_path, "/debuginfod_client" }; | |
| 69 | } | |
| 70 | if (std.posix.getenv("HOME")) |home_path| { | |
| 71 | break :p .{ home_path, "/.cache/debuginfod_client" }; | |
| 72 | } | |
| 73 | break :p null; | |
| 74 | }, | |
| 75 | .global_debug = &.{ | |
| 76 | "/usr/lib/debug", | |
| 77 | }, | |
| 78 | .exe_dir = std.fs.path.dirname(exe_path) orelse ".", | |
| 79 | }; | |
| 80 | } | |
| 81 | }; | |
| 82 | ||
| 83 | pub fn deinit(ef: *ElfFile, gpa: Allocator) void { | |
| 84 | if (ef.dwarf) |*dwarf| dwarf.deinit(gpa); | |
| 85 | if (ef.symbol_search_table) |t| gpa.free(t); | |
| 86 | var arena = ef.arena.promote(gpa); | |
| 87 | arena.deinit(); | |
| 88 | ||
| 89 | std.posix.munmap(ef.mapped_file); | |
| 90 | if (ef.mapped_debug_file) |m| std.posix.munmap(m); | |
| 91 | ||
| 92 | ef.* = undefined; | |
| 93 | } | |
| 94 | ||
| 95 | pub const LoadError = error{ | |
| 96 | OutOfMemory, | |
| 97 | Overflow, | |
| 98 | TruncatedElfFile, | |
| 99 | InvalidCompressedSection, | |
| 100 | InvalidElfMagic, | |
| 101 | InvalidElfVersion, | |
| 102 | InvalidElfClass, | |
| 103 | InvalidElfEndian, | |
| 104 | // The remaining errors all occur when attemping to stat or mmap a file. | |
| 105 | SystemResources, | |
| 106 | MemoryMappingNotSupported, | |
| 107 | AccessDenied, | |
| 108 | LockedMemoryLimitExceeded, | |
| 109 | ProcessFdQuotaExceeded, | |
| 110 | SystemFdQuotaExceeded, | |
| 111 | Unexpected, | |
| 112 | }; | |
| 113 | ||
| 114 | pub fn load( | |
| 115 | gpa: Allocator, | |
| 116 | elf_file: std.fs.File, | |
| 117 | opt_build_id: ?[]const u8, | |
| 118 | di_search_paths: *const DebugInfoSearchPaths, | |
| 119 | ) LoadError!ElfFile { | |
| 120 | var arena_instance: std.heap.ArenaAllocator = .init(gpa); | |
| 121 | errdefer arena_instance.deinit(); | |
| 122 | const arena = arena_instance.allocator(); | |
| 123 | ||
| 124 | var result = loadInner(arena, elf_file, null) catch |err| switch (err) { | |
| 125 | error.CrcMismatch => unreachable, // we passed crc as null | |
| 126 | else => |e| return e, | |
| 127 | }; | |
| 128 | errdefer std.posix.munmap(result.mapped_mem); | |
| 129 | ||
| 130 | // `loadInner` did most of the work, but we might need to load an external debug info file | |
| 131 | ||
| 132 | const di_mapped_mem: ?[]align(std.heap.page_size_min) const u8 = load_di: { | |
| 133 | if (result.sections.get(.debug_info) != null and | |
| 134 | result.sections.get(.debug_abbrev) != null and | |
| 135 | result.sections.get(.debug_str) != null and | |
| 136 | result.sections.get(.debug_line) != null) | |
| 137 | { | |
| 138 | // The info is already loaded from this file alone! | |
| 139 | break :load_di null; | |
| 140 | } | |
| 141 | ||
| 142 | // We're missing some debug info---let's try and load it from a separate file. | |
| 143 | ||
| 144 | build_id: { | |
| 145 | const build_id = opt_build_id orelse break :build_id; | |
| 146 | if (build_id.len < 3) break :build_id; | |
| 147 | ||
| 148 | for (di_search_paths.global_debug) |global_debug| { | |
| 149 | if (try loadSeparateDebugFile(arena, &result, null, "{s}/.build-id/{x}/{x}.debug", .{ | |
| 150 | global_debug, | |
| 151 | build_id[0..1], | |
| 152 | build_id[1..], | |
| 153 | })) |mapped| break :load_di mapped; | |
| 154 | } | |
| 155 | ||
| 156 | if (di_search_paths.debuginfod_client) |components| { | |
| 157 | if (try loadSeparateDebugFile(arena, &result, null, "{s}{s}/{x}/debuginfo", .{ | |
| 158 | components[0], | |
| 159 | components[1], | |
| 160 | build_id, | |
| 161 | })) |mapped| break :load_di mapped; | |
| 162 | } | |
| 163 | } | |
| 164 | ||
| 165 | debug_link: { | |
| 166 | const section = result.sections.get(.gnu_debuglink) orelse break :debug_link; | |
| 167 | const debug_filename = std.mem.sliceTo(section.bytes, 0); | |
| 168 | const crc_offset = std.mem.alignForward(usize, debug_filename.len + 1, 4); | |
| 169 | if (section.bytes.len < crc_offset + 4) break :debug_link; | |
| 170 | const debug_crc = std.mem.readInt(u32, section.bytes[crc_offset..][0..4], result.endian); | |
| 171 | ||
| 172 | const exe_dir = di_search_paths.exe_dir orelse break :debug_link; | |
| 173 | ||
| 174 | if (try loadSeparateDebugFile(arena, &result, debug_crc, "{s}/{s}", .{ | |
| 175 | exe_dir, | |
| 176 | debug_filename, | |
| 177 | })) |mapped| break :load_di mapped; | |
| 178 | if (try loadSeparateDebugFile(arena, &result, debug_crc, "{s}/.debug/{s}", .{ | |
| 179 | exe_dir, | |
| 180 | debug_filename, | |
| 181 | })) |mapped| break :load_di mapped; | |
| 182 | for (di_search_paths.global_debug) |global_debug| { | |
| 183 | // This looks like a bug; it isn't. They really do embed the absolute path to the | |
| 184 | // exe's dirname, *under* the global debug path. | |
| 185 | if (try loadSeparateDebugFile(arena, &result, debug_crc, "{s}/{s}/{s}", .{ | |
| 186 | global_debug, | |
| 187 | exe_dir, | |
| 188 | debug_filename, | |
| 189 | })) |mapped| break :load_di mapped; | |
| 190 | } | |
| 191 | } | |
| 192 | ||
| 193 | break :load_di null; | |
| 194 | }; | |
| 195 | errdefer comptime unreachable; | |
| 196 | ||
| 197 | return .{ | |
| 198 | .is_64 = result.is_64, | |
| 199 | .endian = result.endian, | |
| 200 | .dwarf = dwarf: { | |
| 201 | if (result.sections.get(.debug_info) == null or | |
| 202 | result.sections.get(.debug_abbrev) == null or | |
| 203 | result.sections.get(.debug_str) == null or | |
| 204 | result.sections.get(.debug_line) == null) | |
| 205 | { | |
| 206 | break :dwarf null; // debug info not present | |
| 207 | } | |
| 208 | var sections: Dwarf.SectionArray = @splat(null); | |
| 209 | inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields) |f| { | |
| 210 | if (result.sections.get(@field(Section.Id, f.name))) |s| { | |
| 211 | sections[f.value] = .{ .data = s.bytes, .owned = false }; | |
| 212 | } | |
| 213 | } | |
| 214 | break :dwarf .{ .sections = sections }; | |
| 215 | }, | |
| 216 | .eh_frame = if (result.sections.get(.eh_frame)) |s| .{ | |
| 217 | .vaddr = s.header.sh_addr, | |
| 218 | .bytes = s.bytes, | |
| 219 | } else null, | |
| 220 | .debug_frame = if (result.sections.get(.debug_frame)) |s| .{ | |
| 221 | .vaddr = s.header.sh_addr, | |
| 222 | .bytes = s.bytes, | |
| 223 | } else null, | |
| 224 | .strtab = if (result.sections.get(.strtab)) |s| s.bytes else null, | |
| 225 | .symtab = if (result.sections.get(.symtab)) |s| .{ | |
| 226 | .entry_size = s.header.sh_entsize, | |
| 227 | .bytes = s.bytes, | |
| 228 | } else null, | |
| 229 | .symbol_search_table = null, | |
| 230 | .mapped_file = result.mapped_mem, | |
| 231 | .mapped_debug_file = di_mapped_mem, | |
| 232 | .arena = arena_instance.state, | |
| 233 | }; | |
| 234 | } | |
| 235 | ||
| 236 | pub fn searchSymtab(ef: *ElfFile, gpa: Allocator, vaddr: u64) error{ | |
| 237 | NoSymtab, | |
| 238 | NoStrtab, | |
| 239 | BadSymtab, | |
| 240 | OutOfMemory, | |
| 241 | }!std.debug.Symbol { | |
| 242 | const symtab = ef.symtab orelse return error.NoSymtab; | |
| 243 | const strtab = ef.strtab orelse return error.NoStrtab; | |
| 244 | ||
| 245 | if (symtab.bytes.len % symtab.entry_size != 0) return error.BadSymtab; | |
| 246 | ||
| 247 | const swap_endian = ef.endian != @import("builtin").cpu.arch.endian(); | |
| 248 | ||
| 249 | switch (ef.is_64) { | |
| 250 | inline true, false => |is_64| { | |
| 251 | const Sym = if (is_64) elf.Elf64_Sym else elf.Elf32_Sym; | |
| 252 | if (symtab.entry_size != @sizeOf(Sym)) return error.BadSymtab; | |
| 253 | const symbols: []align(1) const Sym = @ptrCast(symtab.bytes); | |
| 254 | if (ef.symbol_search_table == null) { | |
| 255 | ef.symbol_search_table = try buildSymbolSearchTable(gpa, ef.endian, Sym, symbols); | |
| 256 | } | |
| 257 | const search_table = ef.symbol_search_table.?; | |
| 258 | const SearchContext = struct { | |
| 259 | swap_endian: bool, | |
| 260 | target: u64, | |
| 261 | symbols: []align(1) const Sym, | |
| 262 | fn predicate(ctx: @This(), sym_index: u64) bool { | |
| 263 | // We need to return `true` for the first N items, then `false` for the rest -- | |
| 264 | // the index we'll get out is the first `false` one. So, we'll return `true` iff | |
| 265 | // the target address is after the *end* of this symbol. This synchronizes with | |
| 266 | // the logic in `buildSymbolSearchTable` which sorts by *end* address. | |
| 267 | var sym = ctx.symbols[sym_index]; | |
| 268 | if (ctx.swap_endian) std.mem.byteSwapAllFields(Sym, &sym); | |
| 269 | const sym_end = sym.st_value + sym.st_size; | |
| 270 | return ctx.target >= sym_end; | |
| 271 | } | |
| 272 | }; | |
| 273 | const sym_index_index = std.sort.partitionPoint(u64, search_table, @as(SearchContext, .{ | |
| 274 | .swap_endian = swap_endian, | |
| 275 | .target = vaddr, | |
| 276 | .symbols = symbols, | |
| 277 | }), SearchContext.predicate); | |
| 278 | if (sym_index_index == search_table.len) return .unknown; | |
| 279 | var sym = symbols[search_table[sym_index_index]]; | |
| 280 | if (swap_endian) std.mem.byteSwapAllFields(Sym, &sym); | |
| 281 | if (vaddr < sym.st_value or vaddr >= sym.st_value + sym.st_size) return .unknown; | |
| 282 | return .{ | |
| 283 | .name = std.mem.sliceTo(strtab[sym.st_name..], 0), | |
| 284 | .compile_unit_name = null, | |
| 285 | .source_location = null, | |
| 286 | }; | |
| 287 | }, | |
| 288 | } | |
| 289 | } | |
| 290 | ||
| 291 | fn buildSymbolSearchTable(gpa: Allocator, endian: Endian, comptime Sym: type, symbols: []align(1) const Sym) error{ | |
| 292 | OutOfMemory, | |
| 293 | BadSymtab, | |
| 294 | }![]u64 { | |
| 295 | var result: std.ArrayList(u64) = .empty; | |
| 296 | defer result.deinit(gpa); | |
| 297 | ||
| 298 | const swap_endian = endian != @import("builtin").cpu.arch.endian(); | |
| 299 | ||
| 300 | for (symbols, 0..) |sym_orig, sym_index| { | |
| 301 | var sym = sym_orig; | |
| 302 | if (swap_endian) std.mem.byteSwapAllFields(Sym, &sym); | |
| 303 | if (sym.st_name == 0) continue; | |
| 304 | if (sym.st_shndx == elf.SHN_UNDEF) continue; | |
| 305 | try result.append(gpa, sym_index); | |
| 306 | } | |
| 307 | ||
| 308 | const SortContext = struct { | |
| 309 | swap_endian: bool, | |
| 310 | symbols: []align(1) const Sym, | |
| 311 | fn lessThan(ctx: @This(), lhs_sym_index: u64, rhs_sym_index: u64) bool { | |
| 312 | // We sort by *end* address, not start address. This matches up with logic in `searchSymtab`. | |
| 313 | var lhs_sym = ctx.symbols[lhs_sym_index]; | |
| 314 | var rhs_sym = ctx.symbols[rhs_sym_index]; | |
| 315 | if (ctx.swap_endian) { | |
| 316 | std.mem.byteSwapAllFields(Sym, &lhs_sym); | |
| 317 | std.mem.byteSwapAllFields(Sym, &rhs_sym); | |
| 318 | } | |
| 319 | const lhs_val = lhs_sym.st_value + lhs_sym.st_size; | |
| 320 | const rhs_val = rhs_sym.st_value + rhs_sym.st_size; | |
| 321 | return lhs_val < rhs_val; | |
| 322 | } | |
| 323 | }; | |
| 324 | std.mem.sort(u64, result.items, @as(SortContext, .{ | |
| 325 | .swap_endian = swap_endian, | |
| 326 | .symbols = symbols, | |
| 327 | }), SortContext.lessThan); | |
| 328 | ||
| 329 | return result.toOwnedSlice(gpa); | |
| 330 | } | |
| 331 | ||
| 332 | /// Only used locally, during `load`. | |
| 333 | const Section = struct { | |
| 334 | header: elf.Elf64_Shdr, | |
| 335 | bytes: []const u8, | |
| 336 | const Id = enum { | |
| 337 | // DWARF sections: see `Dwarf.Section.Id`. | |
| 338 | debug_info, | |
| 339 | debug_abbrev, | |
| 340 | debug_str, | |
| 341 | debug_str_offsets, | |
| 342 | debug_line, | |
| 343 | debug_line_str, | |
| 344 | debug_ranges, | |
| 345 | debug_loclists, | |
| 346 | debug_rnglists, | |
| 347 | debug_addr, | |
| 348 | debug_names, | |
| 349 | // Then anything else we're interested in. | |
| 350 | gnu_debuglink, | |
| 351 | eh_frame, | |
| 352 | debug_frame, | |
| 353 | symtab, | |
| 354 | strtab, | |
| 355 | }; | |
| 356 | const Array = std.enums.EnumArray(Section.Id, ?Section); | |
| 357 | }; | |
| 358 | ||
| 359 | fn loadSeparateDebugFile(arena: Allocator, main_loaded: *LoadInnerResult, opt_crc: ?u32, comptime fmt: []const u8, args: anytype) Allocator.Error!?[]align(std.heap.page_size_min) const u8 { | |
| 360 | const path = try std.fmt.allocPrint(arena, fmt, args); | |
| 361 | const elf_file = std.fs.cwd().openFile(path, .{}) catch return null; | |
| 362 | defer elf_file.close(); | |
| 363 | ||
| 364 | const result = loadInner(arena, elf_file, opt_crc) catch |err| switch (err) { | |
| 365 | error.OutOfMemory => |e| return e, | |
| 366 | error.CrcMismatch => return null, | |
| 367 | else => return null, | |
| 368 | }; | |
| 369 | errdefer comptime unreachable; | |
| 370 | ||
| 371 | const have_debug_sections = inline for (@as([]const []const u8, &.{ | |
| 372 | "debug_info", | |
| 373 | "debug_abbrev", | |
| 374 | "debug_str", | |
| 375 | "debug_line", | |
| 376 | })) |name| { | |
| 377 | const s = @field(Section.Id, name); | |
| 378 | if (main_loaded.sections.get(s) == null and result.sections.get(s) != null) { | |
| 379 | break false; | |
| 380 | } | |
| 381 | } else true; | |
| 382 | ||
| 383 | if (result.is_64 != main_loaded.is_64 or | |
| 384 | result.endian != main_loaded.endian or | |
| 385 | !have_debug_sections) | |
| 386 | { | |
| 387 | std.posix.munmap(result.mapped_mem); | |
| 388 | return null; | |
| 389 | } | |
| 390 | ||
| 391 | inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields) |f| { | |
| 392 | const id = @field(Section.Id, f.name); | |
| 393 | if (main_loaded.sections.get(id) == null) { | |
| 394 | main_loaded.sections.set(id, result.sections.get(id)); | |
| 395 | } | |
| 396 | } | |
| 397 | ||
| 398 | return result.mapped_mem; | |
| 399 | } | |
| 400 | ||
| 401 | const LoadInnerResult = struct { | |
| 402 | is_64: bool, | |
| 403 | endian: Endian, | |
| 404 | sections: Section.Array, | |
| 405 | mapped_mem: []align(std.heap.page_size_min) const u8, | |
| 406 | }; | |
| 407 | fn loadInner( | |
| 408 | arena: Allocator, | |
| 409 | elf_file: std.fs.File, | |
| 410 | opt_crc: ?u32, | |
| 411 | ) (LoadError || error{CrcMismatch})!LoadInnerResult { | |
| 412 | const mapped_mem: []align(std.heap.page_size_min) const u8 = mapped: { | |
| 413 | const file_len = std.math.cast( | |
| 414 | usize, | |
| 415 | elf_file.getEndPos() catch |err| switch (err) { | |
| 416 | error.PermissionDenied => unreachable, // not asking for PROT_EXEC | |
| 417 | else => |e| return e, | |
| 418 | }, | |
| 419 | ) orelse return error.Overflow; | |
| 420 | ||
| 421 | break :mapped std.posix.mmap( | |
| 422 | null, | |
| 423 | file_len, | |
| 424 | std.posix.PROT.READ, | |
| 425 | .{ .TYPE = .SHARED }, | |
| 426 | elf_file.handle, | |
| 427 | 0, | |
| 428 | ) catch |err| switch (err) { | |
| 429 | error.MappingAlreadyExists => unreachable, // not using FIXED_NOREPLACE | |
| 430 | error.PermissionDenied => unreachable, // not asking for PROT_EXEC | |
| 431 | else => |e| return e, | |
| 432 | }; | |
| 433 | }; | |
| 434 | ||
| 435 | if (opt_crc) |crc| { | |
| 436 | if (std.hash.crc.Crc32.hash(mapped_mem) != crc) { | |
| 437 | return error.CrcMismatch; | |
| 438 | } | |
| 439 | } | |
| 440 | errdefer std.posix.munmap(mapped_mem); | |
| 441 | ||
| 442 | var fr: std.Io.Reader = .fixed(mapped_mem); | |
| 443 | ||
| 444 | const header = elf.Header.read(&fr) catch |err| switch (err) { | |
| 445 | error.ReadFailed => unreachable, | |
| 446 | error.EndOfStream => return error.TruncatedElfFile, | |
| 447 | ||
| 448 | error.InvalidElfMagic, | |
| 449 | error.InvalidElfVersion, | |
| 450 | error.InvalidElfClass, | |
| 451 | error.InvalidElfEndian, | |
| 452 | => |e| return e, | |
| 453 | }; | |
| 454 | const endian = header.endian; | |
| 455 | ||
| 456 | const shstrtab_shdr_off = try std.math.add( | |
| 457 | u64, | |
| 458 | header.shoff, | |
| 459 | try std.math.mul(u64, header.shstrndx, header.shentsize), | |
| 460 | ); | |
| 461 | fr.seek = std.math.cast(usize, shstrtab_shdr_off) orelse return error.Overflow; | |
| 462 | const shstrtab: []const u8 = if (header.is_64) shstrtab: { | |
| 463 | const shdr = fr.takeStruct(elf.Elf64_Shdr, endian) catch return error.TruncatedElfFile; | |
| 464 | if (shdr.sh_offset + shdr.sh_size > mapped_mem.len) return error.TruncatedElfFile; | |
| 465 | break :shstrtab mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)]; | |
| 466 | } else shstrtab: { | |
| 467 | const shdr = fr.takeStruct(elf.Elf32_Shdr, endian) catch return error.TruncatedElfFile; | |
| 468 | if (shdr.sh_offset + shdr.sh_size > mapped_mem.len) return error.TruncatedElfFile; | |
| 469 | break :shstrtab mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)]; | |
| 470 | }; | |
| 471 | ||
| 472 | var sections: Section.Array = .initFill(null); | |
| 473 | ||
| 474 | var it = header.iterateSectionHeadersBuffer(mapped_mem); | |
| 475 | while (it.next() catch return error.TruncatedElfFile) |shdr| { | |
| 476 | if (shdr.sh_type == elf.SHT_NULL or shdr.sh_type == elf.SHT_NOBITS) continue; | |
| 477 | if (shdr.sh_name > shstrtab.len) return error.TruncatedElfFile; | |
| 478 | const name = std.mem.sliceTo(shstrtab[@intCast(shdr.sh_name)..], 0); | |
| 479 | ||
| 480 | const section_id: Section.Id = inline for (@typeInfo(Section.Id).@"enum".fields) |s| { | |
| 481 | if (std.mem.eql(u8, "." ++ s.name, name)) { | |
| 482 | break @enumFromInt(s.value); | |
| 483 | } | |
| 484 | } else continue; | |
| 485 | ||
| 486 | if (sections.get(section_id) != null) continue; | |
| 487 | ||
| 488 | if (shdr.sh_offset + shdr.sh_size > mapped_mem.len) return error.TruncatedElfFile; | |
| 489 | const raw_section_bytes = mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)]; | |
| 490 | const section_bytes: []const u8 = bytes: { | |
| 491 | if ((shdr.sh_flags & elf.SHF_COMPRESSED) == 0) break :bytes raw_section_bytes; | |
| 492 | ||
| 493 | var section_reader: std.Io.Reader = .fixed(raw_section_bytes); | |
| 494 | const ch_type: elf.COMPRESS, const ch_size: u64 = if (header.is_64) ch: { | |
| 495 | const chdr = section_reader.takeStruct(elf.Elf64_Chdr, endian) catch return error.InvalidCompressedSection; | |
| 496 | break :ch .{ chdr.ch_type, chdr.ch_size }; | |
| 497 | } else ch: { | |
| 498 | const chdr = section_reader.takeStruct(elf.Elf32_Chdr, endian) catch return error.InvalidCompressedSection; | |
| 499 | break :ch .{ chdr.ch_type, chdr.ch_size }; | |
| 500 | }; | |
| 501 | if (ch_type != .ZLIB) { | |
| 502 | // The compression algorithm is unsupported, but don't make that a hard error; the | |
| 503 | // file might still be valid, and we might still be okay without this section. | |
| 504 | continue; | |
| 505 | } | |
| 506 | ||
| 507 | const buf = try arena.alloc(u8, ch_size); | |
| 508 | var fw: std.Io.Writer = .fixed(buf); | |
| 509 | var decompress: std.compress.flate.Decompress = .init(&section_reader, .zlib, &.{}); | |
| 510 | const n = decompress.reader.streamRemaining(&fw) catch |err| switch (err) { | |
| 511 | // If a write failed, then `buf` filled up, so `ch_size` was incorrect | |
| 512 | error.WriteFailed => return error.InvalidCompressedSection, | |
| 513 | // If a read failed, flate expected the section to have more data | |
| 514 | error.ReadFailed => return error.InvalidCompressedSection, | |
| 515 | }; | |
| 516 | // It's also an error if the data is shorter than expected. | |
| 517 | if (n != buf.len) return error.InvalidCompressedSection; | |
| 518 | break :bytes buf; | |
| 519 | }; | |
| 520 | sections.set(section_id, .{ .header = shdr, .bytes = section_bytes }); | |
| 521 | } | |
| 522 | ||
| 523 | return .{ | |
| 524 | .is_64 = header.is_64, | |
| 525 | .endian = endian, | |
| 526 | .sections = sections, | |
| 527 | .mapped_mem = mapped_mem, | |
| 528 | }; | |
| 529 | } | |
| 530 | ||
| 531 | const std = @import("std"); | |
| 532 | const Endian = std.builtin.Endian; | |
| 533 | const Dwarf = std.debug.Dwarf; | |
| 534 | const ElfFile = @This(); | |
| 535 | const Allocator = std.mem.Allocator; | |
| 536 | const elf = std.elf; |
lib/std/debug/Info.zig+19-14| ... | ... | @@ -9,7 +9,7 @@ |
| 9 | 9 | const std = @import("../std.zig"); |
| 10 | 10 | const Allocator = std.mem.Allocator; |
| 11 | 11 | const Path = std.Build.Cache.Path; |
| 12 | const Dwarf = std.debug.Dwarf; | |
| 12 | const ElfFile = std.debug.ElfFile; | |
| 13 | 13 | const assert = std.debug.assert; |
| 14 | 14 | const Coverage = std.debug.Coverage; |
| 15 | 15 | const SourceLocation = std.debug.Coverage.SourceLocation; |
| ... | ... | @@ -17,28 +17,35 @@ const SourceLocation = std.debug.Coverage.SourceLocation; |
| 17 | 17 | const Info = @This(); |
| 18 | 18 | |
| 19 | 19 | /// Sorted by key, ascending. |
| 20 | address_map: std.AutoArrayHashMapUnmanaged(u64, Dwarf.ElfModule), | |
| 20 | address_map: std.AutoArrayHashMapUnmanaged(u64, ElfFile), | |
| 21 | 21 | /// Externally managed, outlives this `Info` instance. |
| 22 | 22 | coverage: *Coverage, |
| 23 | 23 | |
| 24 | pub const LoadError = Dwarf.ElfModule.LoadError; | |
| 24 | pub const LoadError = std.fs.File.OpenError || ElfFile.LoadError || std.debug.Dwarf.ScanError || error{MissingDebugInfo}; | |
| 25 | 25 | |
| 26 | 26 | pub fn load(gpa: Allocator, path: Path, coverage: *Coverage) LoadError!Info { |
| 27 | var elf_module = try Dwarf.ElfModule.load(gpa, path, null, null, null, null); | |
| 28 | // This is correct because `Dwarf.ElfModule` currently only supports native-endian ELF files. | |
| 29 | const endian = @import("builtin").target.cpu.arch.endian(); | |
| 30 | try elf_module.dwarf.populateRanges(gpa, endian); | |
| 27 | var file = try path.root_dir.handle.openFile(path.sub_path, .{}); | |
| 28 | defer file.close(); | |
| 29 | ||
| 30 | var elf_file: ElfFile = try .load(gpa, file, null, &.none); | |
| 31 | errdefer elf_file.deinit(gpa); | |
| 32 | ||
| 33 | if (elf_file.dwarf == null) return error.MissingDebugInfo; | |
| 34 | try elf_file.dwarf.?.open(gpa, elf_file.endian); | |
| 35 | try elf_file.dwarf.?.populateRanges(gpa, elf_file.endian); | |
| 36 | ||
| 31 | 37 | var info: Info = .{ |
| 32 | 38 | .address_map = .{}, |
| 33 | 39 | .coverage = coverage, |
| 34 | 40 | }; |
| 35 | try info.address_map.put(gpa, 0, elf_module); | |
| 41 | try info.address_map.put(gpa, 0, elf_file); | |
| 42 | errdefer comptime unreachable; // elf_file is owned by the map now | |
| 36 | 43 | return info; |
| 37 | 44 | } |
| 38 | 45 | |
| 39 | 46 | pub fn deinit(info: *Info, gpa: Allocator) void { |
| 40 | for (info.address_map.values()) |*elf_module| { | |
| 41 | elf_module.dwarf.deinit(gpa); | |
| 47 | for (info.address_map.values()) |*elf_file| { | |
| 48 | elf_file.dwarf.?.deinit(gpa); | |
| 42 | 49 | } |
| 43 | 50 | info.address_map.deinit(gpa); |
| 44 | 51 | info.* = undefined; |
| ... | ... | @@ -58,8 +65,6 @@ pub fn resolveAddresses( |
| 58 | 65 | ) ResolveAddressesError!void { |
| 59 | 66 | assert(sorted_pc_addrs.len == output.len); |
| 60 | 67 | if (info.address_map.entries.len != 1) @panic("TODO"); |
| 61 | const elf_module = &info.address_map.values()[0]; | |
| 62 | // This is correct because `Dwarf.ElfModule` currently only supports native-endian ELF files. | |
| 63 | const endian = @import("builtin").target.cpu.arch.endian(); | |
| 64 | return info.coverage.resolveAddressesDwarf(gpa, endian, sorted_pc_addrs, output, &elf_module.dwarf); | |
| 68 | const elf_file = &info.address_map.values()[0]; | |
| 69 | return info.coverage.resolveAddressesDwarf(gpa, elf_file.endian, sorted_pc_addrs, output, &elf_file.dwarf.?); | |
| 65 | 70 | } |
lib/std/debug/SelfInfo.zig+1| ... | ... | @@ -78,6 +78,7 @@ pub fn getSymbolAtAddress(self: *SelfInfo, gpa: Allocator, address: usize) Error |
| 78 | 78 | pub fn getModuleNameForAddress(self: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 { |
| 79 | 79 | comptime assert(target_supported); |
| 80 | 80 | const module: Module = try .lookup(&self.lookup_cache, gpa, address); |
| 81 | if (module.name.len == 0) return error.MissingDebugInfo; | |
| 81 | 82 | return module.name; |
| 82 | 83 | } |
| 83 | 84 |
lib/std/debug/SelfInfo/ElfModule.zig+70-30| ... | ... | @@ -7,10 +7,12 @@ gnu_eh_frame: ?[]const u8, |
| 7 | 7 | pub const LookupCache = void; |
| 8 | 8 | |
| 9 | 9 | pub const DebugInfo = struct { |
| 10 | loaded_elf: ?Dwarf.ElfModule, | |
| 10 | loaded_elf: ?ElfFile, | |
| 11 | scanned_dwarf: bool, | |
| 11 | 12 | unwind: [2]?Dwarf.Unwind, |
| 12 | 13 | pub const init: DebugInfo = .{ |
| 13 | 14 | .loaded_elf = null, |
| 15 | .scanned_dwarf = false, | |
| 14 | 16 | .unwind = @splat(null), |
| 15 | 17 | }; |
| 16 | 18 | pub fn deinit(di: *DebugInfo, gpa: Allocator) void { |
| ... | ... | @@ -92,55 +94,92 @@ pub fn lookup(cache: *LookupCache, gpa: Allocator, address: usize) Error!ElfModu |
| 92 | 94 | }; |
| 93 | 95 | return error.MissingDebugInfo; |
| 94 | 96 | } |
| 95 | fn loadDwarf(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Error!void { | |
| 97 | fn loadElf(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Error!void { | |
| 98 | std.debug.assert(di.loaded_elf == null); | |
| 99 | std.debug.assert(!di.scanned_dwarf); | |
| 100 | ||
| 96 | 101 | const load_result = if (module.name.len > 0) res: { |
| 97 | break :res Dwarf.ElfModule.load(gpa, .{ | |
| 98 | .root_dir = .cwd(), | |
| 99 | .sub_path = module.name, | |
| 100 | }, module.build_id, null, null, null); | |
| 102 | var file = std.fs.cwd().openFile(module.name, .{}) catch return error.MissingDebugInfo; | |
| 103 | defer file.close(); | |
| 104 | break :res ElfFile.load(gpa, file, module.build_id, &.native(module.name)); | |
| 101 | 105 | } else res: { |
| 102 | 106 | const path = std.fs.selfExePathAlloc(gpa) catch |err| switch (err) { |
| 103 | 107 | error.OutOfMemory => |e| return e, |
| 104 | 108 | else => return error.ReadFailed, |
| 105 | 109 | }; |
| 106 | 110 | defer gpa.free(path); |
| 107 | break :res Dwarf.ElfModule.load(gpa, .{ | |
| 108 | .root_dir = .cwd(), | |
| 109 | .sub_path = path, | |
| 110 | }, module.build_id, null, null, null); | |
| 111 | var file = std.fs.cwd().openFile(path, .{}) catch return error.MissingDebugInfo; | |
| 112 | defer file.close(); | |
| 113 | break :res ElfFile.load(gpa, file, module.build_id, &.native(path)); | |
| 111 | 114 | }; |
| 112 | 115 | di.loaded_elf = load_result catch |err| switch (err) { |
| 113 | error.FileNotFound => return error.MissingDebugInfo, | |
| 114 | ||
| 115 | 116 | error.OutOfMemory, |
| 116 | error.InvalidDebugInfo, | |
| 117 | error.MissingDebugInfo, | |
| 118 | 117 | error.Unexpected, |
| 119 | 118 | => |e| return e, |
| 120 | 119 | |
| 121 | error.InvalidElfEndian, | |
| 120 | error.Overflow, | |
| 121 | error.TruncatedElfFile, | |
| 122 | error.InvalidCompressedSection, | |
| 122 | 123 | error.InvalidElfMagic, |
| 123 | 124 | error.InvalidElfVersion, |
| 124 | error.InvalidUtf8, | |
| 125 | error.InvalidWtf8, | |
| 126 | error.EndOfStream, | |
| 127 | error.Overflow, | |
| 128 | error.UnimplementedDwarfForeignEndian, // this should be impossible as we're looking at the debug info for this process | |
| 125 | error.InvalidElfClass, | |
| 126 | error.InvalidElfEndian, | |
| 129 | 127 | => return error.InvalidDebugInfo, |
| 130 | 128 | |
| 131 | else => return error.ReadFailed, | |
| 129 | error.SystemResources, | |
| 130 | error.MemoryMappingNotSupported, | |
| 131 | error.AccessDenied, | |
| 132 | error.LockedMemoryLimitExceeded, | |
| 133 | error.ProcessFdQuotaExceeded, | |
| 134 | error.SystemFdQuotaExceeded, | |
| 135 | => return error.ReadFailed, | |
| 132 | 136 | }; |
| 137 | ||
| 138 | const matches_native = | |
| 139 | di.loaded_elf.?.endian == native_endian and | |
| 140 | di.loaded_elf.?.is_64 == (@sizeOf(usize) == 8); | |
| 141 | ||
| 142 | if (!matches_native) { | |
| 143 | di.loaded_elf.?.deinit(gpa); | |
| 144 | di.loaded_elf = null; | |
| 145 | return error.InvalidDebugInfo; | |
| 146 | } | |
| 133 | 147 | } |
| 134 | 148 | pub fn getSymbolAtAddress(module: *const ElfModule, gpa: Allocator, di: *DebugInfo, address: usize) Error!std.debug.Symbol { |
| 135 | if (di.loaded_elf == null) try module.loadDwarf(gpa, di); | |
| 149 | if (di.loaded_elf == null) try module.loadElf(gpa, di); | |
| 136 | 150 | const vaddr = address - module.load_offset; |
| 137 | return di.loaded_elf.?.dwarf.getSymbol(gpa, native_endian, vaddr) catch |err| switch (err) { | |
| 138 | error.InvalidDebugInfo, error.MissingDebugInfo, error.OutOfMemory => |e| return e, | |
| 139 | error.ReadFailed, | |
| 140 | error.EndOfStream, | |
| 141 | error.Overflow, | |
| 142 | error.StreamTooLong, | |
| 143 | => return error.InvalidDebugInfo, | |
| 151 | if (di.loaded_elf.?.dwarf) |*dwarf| { | |
| 152 | if (!di.scanned_dwarf) { | |
| 153 | dwarf.open(gpa, native_endian) catch |err| switch (err) { | |
| 154 | error.InvalidDebugInfo, | |
| 155 | error.MissingDebugInfo, | |
| 156 | error.OutOfMemory, | |
| 157 | => |e| return e, | |
| 158 | error.EndOfStream, | |
| 159 | error.Overflow, | |
| 160 | error.ReadFailed, | |
| 161 | error.StreamTooLong, | |
| 162 | => return error.InvalidDebugInfo, | |
| 163 | }; | |
| 164 | di.scanned_dwarf = true; | |
| 165 | } | |
| 166 | return dwarf.getSymbol(gpa, native_endian, vaddr) catch |err| switch (err) { | |
| 167 | error.InvalidDebugInfo, | |
| 168 | error.MissingDebugInfo, | |
| 169 | error.OutOfMemory, | |
| 170 | => |e| return e, | |
| 171 | error.ReadFailed, | |
| 172 | error.EndOfStream, | |
| 173 | error.Overflow, | |
| 174 | error.StreamTooLong, | |
| 175 | => return error.InvalidDebugInfo, | |
| 176 | }; | |
| 177 | } | |
| 178 | // When there's no DWARF available, fall back to searching the symtab. | |
| 179 | return di.loaded_elf.?.searchSymtab(gpa, vaddr) catch |err| switch (err) { | |
| 180 | error.NoSymtab, error.NoStrtab => return error.MissingDebugInfo, | |
| 181 | error.BadSymtab => return error.InvalidDebugInfo, | |
| 182 | error.OutOfMemory => |e| return e, | |
| 144 | 183 | }; |
| 145 | 184 | } |
| 146 | 185 | fn prepareUnwindLookup(unwind: *Dwarf.Unwind, gpa: Allocator) Error!void { |
| ... | ... | @@ -166,7 +205,7 @@ fn loadUnwindInfo(module: *const ElfModule, gpa: Allocator, di: *DebugInfo) Erro |
| 166 | 205 | } else unwinds: { |
| 167 | 206 | // There is no `.eh_frame_hdr` section. There may still be an `.eh_frame` or `.debug_frame` |
| 168 | 207 | // section, but we'll have to load the binary to get at it. |
| 169 | try module.loadDwarf(gpa, di); | |
| 208 | try module.loadElf(gpa, di); | |
| 170 | 209 | const opt_debug_frame = &di.loaded_elf.?.debug_frame; |
| 171 | 210 | const opt_eh_frame = &di.loaded_elf.?.eh_frame; |
| 172 | 211 | // If both are present, we can't just pick one -- the info could be split between them. |
| ... | ... | @@ -232,6 +271,7 @@ const ElfModule = @This(); |
| 232 | 271 | const std = @import("../../std.zig"); |
| 233 | 272 | const Allocator = std.mem.Allocator; |
| 234 | 273 | const Dwarf = std.debug.Dwarf; |
| 274 | const ElfFile = std.debug.ElfFile; | |
| 235 | 275 | const elf = std.elf; |
| 236 | 276 | const mem = std.mem; |
| 237 | 277 | const Error = std.debug.SelfInfo.Error; |