1//! A helper type for loading an ELF file and collecting its DWARF debug information, unwind
2//! information, and symbol table.
3const ElfFile = @This();
4
5const std = @import("std");
6const Io = std.Io;
7const Endian = std.builtin.Endian;
8const Dwarf = std.debug.Dwarf;
9const Allocator = std.mem.Allocator;
10const elf = std.elf;
11
12is_64: bool,
13endian: Endian,
14
15/// This is `null` iff any of the required DWARF sections were missing. `ElfFile.load` does *not*
16/// call `Dwarf.open`, `Dwarf.scanAllFunctions`, etc; that is the caller's responsibility.
17dwarf: ?Dwarf,
18
19/// If non-`null`, describes the `.eh_frame` section, which can be used with `Dwarf.Unwind`.
20eh_frame: ?UnwindSection,
21/// If non-`null`, describes the `.debug_frame` section, which can be used with `Dwarf.Unwind`.
22debug_frame: ?UnwindSection,
23
24/// If non-`null`, this is the contents of the `.strtab` section.
25strtab: ?[]const u8,
26/// If non-`null`, describes the `.symtab` section.
27symtab: ?SymtabSection,
28
29/// Binary search table lazily populated by `searchSymtab`.
30symbol_search_table: ?[]usize,
31
32/// The memory-mapped ELF file, which is referenced by `dwarf`. This field is here only so that
33/// this memory can be unmapped by `ElfFile.deinit`.
34mapped_file: []align(std.heap.page_size_min) const u8,
35/// Sometimes, debug info is stored separately to the main ELF file. In that case, `mapped_file`
36/// is the mapped ELF binary, and `mapped_debug_file` is the mapped debug info file. Both must
37/// be unmapped by `ElfFile.deinit`.
38mapped_debug_file: ?[]align(std.heap.page_size_min) const u8,
39
40arena: std.heap.ArenaAllocator.State,
41
42pub const UnwindSection = struct {
43 vaddr: u64,
44 bytes: []const u8,
45};
46pub const SymtabSection = struct {
47 entry_size: u64,
48 bytes: []const u8,
49};
50
51pub const DebugInfoSearchPaths = struct {
52 /// The location of a debuginfod client directory, which acts as a search path for build IDs. If
53 /// given, we can load from this directory opportunistically, but make no effort to populate it.
54 /// To avoid allocation when building the search paths, this is given as two components which
55 /// will be concatenated.
56 debuginfod_client: ?[2][]const u8,
57 /// All "global debug directories" on the system. These are used as search paths for both debug
58 /// links and build IDs. On typical systems this is just "/usr/lib/debug".
59 global_debug: []const []const u8,
60 /// The path to the dirname of the ELF file, which acts as a search path for debug links.
61 exe_dir: ?[]const u8,
62
63 pub const none: DebugInfoSearchPaths = .{
64 .debuginfod_client = null,
65 .global_debug = &.{},
66 .exe_dir = null,
67 };
68
69 pub fn native(exe_path: []const u8) DebugInfoSearchPaths {
70 if (std.Options.elf_debug_info_search_paths) |f| return f(exe_path);
71 if (std.Options.debug_threaded_io) |t| return .{
72 .debuginfod_client = p: {
73 if (t.environString("DEBUGINFOD_CACHE_PATH")) |p| {
74 break :p .{ p, "" };
75 }
76 if (t.environString("XDG_CACHE_HOME")) |cache_path| {
77 break :p .{ cache_path, "/debuginfod_client" };
78 }
79 if (t.environString("HOME")) |home_path| {
80 break :p .{ home_path, "/.cache/debuginfod_client" };
81 }
82 break :p null;
83 },
84 .global_debug = &.{
85 "/usr/lib/debug",
86 },
87 .exe_dir = std.fs.path.dirname(exe_path) orelse ".",
88 };
89 @compileError("std.Options.elf_debug_info_search_paths must be provided");
90 }
91};
92
93pub fn deinit(ef: *ElfFile, gpa: Allocator) void {
94 if (ef.dwarf) |*dwarf| dwarf.deinit(gpa);
95 if (ef.symbol_search_table) |t| gpa.free(t);
96 var arena = ef.arena.promote(gpa);
97 arena.deinit();
98
99 std.posix.munmap(ef.mapped_file);
100 if (ef.mapped_debug_file) |m| std.posix.munmap(m);
101
102 ef.* = undefined;
103}
104
105pub const LoadError = error{
106 OutOfMemory,
107 Overflow,
108 TruncatedElfFile,
109 InvalidCompressedSection,
110 InvalidElfMagic,
111 InvalidElfVersion,
112 InvalidElfClass,
113 InvalidElfEndian,
114 // The remaining errors all occur when attemping to stat or mmap a file.
115 SystemResources,
116 MemoryMappingNotSupported,
117 AccessDenied,
118 LockedMemoryLimitExceeded,
119 ProcessFdQuotaExceeded,
120 SystemFdQuotaExceeded,
121 Streaming,
122 Canceled,
123 Unexpected,
124};
125
126pub fn load(
127 gpa: Allocator,
128 io: Io,
129 elf_file: Io.File,
130 opt_build_id: ?[]const u8,
131 di_search_paths: *const DebugInfoSearchPaths,
132) LoadError!ElfFile {
133 var arena_instance: std.heap.ArenaAllocator = .init(gpa);
134 errdefer arena_instance.deinit();
135 const arena = arena_instance.allocator();
136
137 var result = loadInner(arena, io, elf_file, null) catch |err| switch (err) {
138 error.CrcMismatch => unreachable, // we passed crc as null
139 else => |e| return e,
140 };
141 errdefer std.posix.munmap(result.mapped_mem);
142
143 // `loadInner` did most of the work, but we might need to load an external debug info file
144
145 const di_mapped_mem: ?[]align(std.heap.page_size_min) const u8 = load_di: {
146 if (result.sections.get(.debug_info) != null and
147 result.sections.get(.debug_abbrev) != null and
148 result.sections.get(.debug_str) != null and
149 result.sections.get(.debug_line) != null)
150 {
151 // The info is already loaded from this file alone!
152 break :load_di null;
153 }
154
155 // We're missing some debug info---let's try and load it from a separate file.
156
157 build_id: {
158 const build_id = opt_build_id orelse break :build_id;
159 if (build_id.len < 3) break :build_id;
160
161 for (di_search_paths.global_debug) |global_debug| {
162 if (try loadSeparateDebugFile(arena, io, &result, null, "{s}/.build-id/{x}/{x}.debug", .{
163 global_debug,
164 build_id[0..1],
165 build_id[1..],
166 })) |mapped| break :load_di mapped;
167 }
168
169 if (di_search_paths.debuginfod_client) |components| {
170 if (try loadSeparateDebugFile(arena, io, &result, null, "{s}{s}/{x}/debuginfo", .{
171 components[0],
172 components[1],
173 build_id,
174 })) |mapped| break :load_di mapped;
175 }
176 }
177
178 debug_link: {
179 const section = result.sections.get(.gnu_debuglink) orelse break :debug_link;
180 const debug_filename = std.mem.sliceTo(section.bytes, 0);
181 const crc_offset = std.mem.alignForward(usize, debug_filename.len + 1, 4);
182 if (section.bytes.len < crc_offset + 4) break :debug_link;
183 const debug_crc = std.mem.readInt(u32, section.bytes[crc_offset..][0..4], result.endian);
184
185 const exe_dir = di_search_paths.exe_dir orelse break :debug_link;
186
187 if (try loadSeparateDebugFile(arena, io, &result, debug_crc, "{s}/{s}", .{
188 exe_dir,
189 debug_filename,
190 })) |mapped| break :load_di mapped;
191 if (try loadSeparateDebugFile(arena, io, &result, debug_crc, "{s}/.debug/{s}", .{
192 exe_dir,
193 debug_filename,
194 })) |mapped| break :load_di mapped;
195 for (di_search_paths.global_debug) |global_debug| {
196 // This looks like a bug; it isn't. They really do embed the absolute path to the
197 // exe's dirname, *under* the global debug path.
198 if (try loadSeparateDebugFile(arena, io, &result, debug_crc, "{s}/{s}/{s}", .{
199 global_debug,
200 exe_dir,
201 debug_filename,
202 })) |mapped| break :load_di mapped;
203 }
204 }
205
206 break :load_di null;
207 };
208 errdefer comptime unreachable;
209
210 return .{
211 .is_64 = result.is_64,
212 .endian = result.endian,
213 .dwarf = dwarf: {
214 if (result.sections.get(.debug_info) == null or
215 result.sections.get(.debug_abbrev) == null or
216 result.sections.get(.debug_str) == null or
217 result.sections.get(.debug_line) == null)
218 {
219 break :dwarf null; // debug info not present
220 }
221 var sections: Dwarf.SectionArray = @splat(null);
222 const info = @typeInfo(Dwarf.Section.Id).@"enum";
223 inline for (info.field_names, info.field_values) |f_name, f_value| {
224 if (result.sections.get(@field(Section.Id, f_name))) |s| {
225 sections[f_value] = .{ .data = s.bytes, .owned = false };
226 }
227 }
228 break :dwarf .{ .sections = sections };
229 },
230 .eh_frame = if (result.sections.get(.eh_frame)) |s| .{
231 .vaddr = s.header.sh_addr,
232 .bytes = s.bytes,
233 } else null,
234 .debug_frame = if (result.sections.get(.debug_frame)) |s| .{
235 .vaddr = s.header.sh_addr,
236 .bytes = s.bytes,
237 } else null,
238 .strtab = if (result.sections.get(.strtab)) |s| s.bytes else null,
239 .symtab = if (result.sections.get(.symtab)) |s| .{
240 .entry_size = s.header.sh_entsize,
241 .bytes = s.bytes,
242 } else null,
243 .symbol_search_table = null,
244 .mapped_file = result.mapped_mem,
245 .mapped_debug_file = di_mapped_mem,
246 .arena = arena_instance.state,
247 };
248}
249
250pub fn searchSymtab(ef: *ElfFile, gpa: Allocator, vaddr: u64) error{
251 NoSymtab,
252 NoStrtab,
253 BadSymtab,
254 OutOfMemory,
255}!std.debug.Symbol {
256 const symtab = ef.symtab orelse return error.NoSymtab;
257 const strtab = ef.strtab orelse return error.NoStrtab;
258
259 if (symtab.bytes.len % symtab.entry_size != 0) return error.BadSymtab;
260
261 const swap_endian = ef.endian != @import("builtin").cpu.arch.endian();
262
263 switch (ef.is_64) {
264 inline true, false => |is_64| {
265 const Sym = if (is_64) elf.Elf64_Sym else elf.Elf32_Sym;
266 if (symtab.entry_size != @sizeOf(Sym)) return error.BadSymtab;
267 const symbols: []align(1) const Sym = @ptrCast(symtab.bytes);
268 if (ef.symbol_search_table == null) {
269 ef.symbol_search_table = try buildSymbolSearchTable(gpa, ef.endian, Sym, symbols);
270 }
271 const search_table = ef.symbol_search_table.?;
272 const SearchContext = struct {
273 swap_endian: bool,
274 target: u64,
275 symbols: []align(1) const Sym,
276 fn predicate(ctx: @This(), sym_index: usize) bool {
277 // We need to return `true` for the first N items, then `false` for the rest --
278 // the index we'll get out is the first `false` one. So, we'll return `true` iff
279 // the target address is after the *end* of this symbol. This synchronizes with
280 // the logic in `buildSymbolSearchTable` which sorts by *end* address.
281 var sym = ctx.symbols[sym_index];
282 if (ctx.swap_endian) std.mem.byteSwapAllFields(Sym, &sym);
283 const sym_end = sym.st_value + sym.st_size;
284 return ctx.target >= sym_end;
285 }
286 };
287 const sym_index_index = std.sort.partitionPoint(usize, search_table, @as(SearchContext, .{
288 .swap_endian = swap_endian,
289 .target = vaddr,
290 .symbols = symbols,
291 }), SearchContext.predicate);
292 if (sym_index_index == search_table.len) return .unknown;
293 var sym = symbols[search_table[sym_index_index]];
294 if (swap_endian) std.mem.byteSwapAllFields(Sym, &sym);
295 if (vaddr < sym.st_value or vaddr >= sym.st_value + sym.st_size) return .unknown;
296 return .{
297 .name = std.mem.sliceTo(strtab[sym.st_name..], 0),
298 .compile_unit_name = null,
299 .source_location = null,
300 };
301 },
302 }
303}
304
305fn buildSymbolSearchTable(gpa: Allocator, endian: Endian, comptime Sym: type, symbols: []align(1) const Sym) error{
306 OutOfMemory,
307 BadSymtab,
308}![]usize {
309 var result: std.ArrayList(usize) = .empty;
310 defer result.deinit(gpa);
311
312 const swap_endian = endian != @import("builtin").cpu.arch.endian();
313
314 for (symbols, 0..) |sym_orig, sym_index| {
315 var sym = sym_orig;
316 if (swap_endian) std.mem.byteSwapAllFields(Sym, &sym);
317 if (sym.st_name == 0) continue;
318 if (sym.st_shndx == elf.SHN_UNDEF) continue;
319 try result.append(gpa, sym_index);
320 }
321
322 const SortContext = struct {
323 swap_endian: bool,
324 symbols: []align(1) const Sym,
325 fn lessThan(ctx: @This(), lhs_sym_index: usize, rhs_sym_index: usize) bool {
326 // We sort by *end* address, not start address. This matches up with logic in `searchSymtab`.
327 var lhs_sym = ctx.symbols[lhs_sym_index];
328 var rhs_sym = ctx.symbols[rhs_sym_index];
329 if (ctx.swap_endian) {
330 std.mem.byteSwapAllFields(Sym, &lhs_sym);
331 std.mem.byteSwapAllFields(Sym, &rhs_sym);
332 }
333 const lhs_val = lhs_sym.st_value + lhs_sym.st_size;
334 const rhs_val = rhs_sym.st_value + rhs_sym.st_size;
335 return lhs_val < rhs_val;
336 }
337 };
338 std.mem.sort(usize, result.items, @as(SortContext, .{
339 .swap_endian = swap_endian,
340 .symbols = symbols,
341 }), SortContext.lessThan);
342
343 return result.toOwnedSlice(gpa);
344}
345
346/// Only used locally, during `load`.
347const Section = struct {
348 header: elf.Elf64_Shdr,
349 bytes: []const u8,
350 const Id = enum {
351 // DWARF sections: see `Dwarf.Section.Id`.
352 debug_info,
353 debug_abbrev,
354 debug_str,
355 debug_str_offsets,
356 debug_line,
357 debug_line_str,
358 debug_ranges,
359 debug_loclists,
360 debug_rnglists,
361 debug_addr,
362 debug_names,
363 // Then anything else we're interested in.
364 gnu_debuglink,
365 eh_frame,
366 debug_frame,
367 symtab,
368 strtab,
369 };
370 const Array = std.enums.EnumArray(Section.Id, ?Section);
371};
372
373fn loadSeparateDebugFile(
374 arena: Allocator,
375 io: Io,
376 main_loaded: *LoadInnerResult,
377 opt_crc: ?u32,
378 comptime fmt: []const u8,
379 args: anytype,
380) Allocator.Error!?[]align(std.heap.page_size_min) const u8 {
381 const path = try std.fmt.allocPrint(arena, fmt, args);
382 const elf_file = Io.Dir.cwd().openFile(io, path, .{}) catch return null;
383 defer elf_file.close(io);
384
385 const result = loadInner(arena, io, elf_file, opt_crc) catch |err| switch (err) {
386 error.OutOfMemory => |e| return e,
387 error.CrcMismatch => return null,
388 else => return null,
389 };
390 errdefer comptime unreachable;
391
392 const have_debug_sections = inline for (@as([]const []const u8, &.{
393 "debug_info",
394 "debug_abbrev",
395 "debug_str",
396 "debug_line",
397 })) |name| {
398 const s = @field(Section.Id, name);
399 if (main_loaded.sections.get(s) == null and result.sections.get(s) == null) {
400 break false;
401 }
402 } else true;
403
404 if (result.is_64 != main_loaded.is_64 or
405 result.endian != main_loaded.endian or
406 !have_debug_sections)
407 {
408 std.posix.munmap(result.mapped_mem);
409 return null;
410 }
411
412 inline for (@typeInfo(Dwarf.Section.Id).@"enum".field_names) |f_name| {
413 const id = @field(Section.Id, f_name);
414 if (main_loaded.sections.get(id) == null) {
415 main_loaded.sections.set(id, result.sections.get(id));
416 }
417 }
418
419 return result.mapped_mem;
420}
421
422const LoadInnerResult = struct {
423 is_64: bool,
424 endian: Endian,
425 sections: Section.Array,
426 mapped_mem: []align(std.heap.page_size_min) const u8,
427};
428fn loadInner(
429 arena: Allocator,
430 io: Io,
431 elf_file: Io.File,
432 opt_crc: ?u32,
433) (LoadError || error{ CrcMismatch, Streaming, Canceled })!LoadInnerResult {
434 const mapped_mem: []align(std.heap.page_size_min) const u8 = mapped: {
435 const file_len = std.math.cast(
436 usize,
437 elf_file.length(io) catch |err| switch (err) {
438 error.PermissionDenied => unreachable, // not asking for PROT_EXEC
439 else => |e| return e,
440 },
441 ) orelse return error.Overflow;
442
443 break :mapped std.posix.mmap(
444 null,
445 file_len,
446 .{ .READ = true },
447 .{ .TYPE = .SHARED },
448 elf_file.handle,
449 0,
450 ) catch |err| switch (err) {
451 error.MappingAlreadyExists => unreachable, // not using FIXED_NOREPLACE
452 error.PermissionDenied => unreachable, // not asking for PROT_EXEC
453 else => |e| return e,
454 };
455 };
456
457 if (opt_crc) |crc| {
458 if (std.hash.Crc32.hash(mapped_mem) != crc) {
459 return error.CrcMismatch;
460 }
461 }
462 errdefer std.posix.munmap(mapped_mem);
463
464 var fr: std.Io.Reader = .fixed(mapped_mem);
465
466 const header = elf.Header.read(&fr) catch |err| switch (err) {
467 error.ReadFailed => unreachable,
468 error.EndOfStream => return error.TruncatedElfFile,
469
470 error.InvalidElfMagic,
471 error.InvalidElfVersion,
472 error.InvalidElfClass,
473 error.InvalidElfEndian,
474 => |e| return e,
475 };
476 const endian = header.endian;
477
478 const shstrtab_shdr_off = try std.math.add(
479 u64,
480 header.shoff,
481 try std.math.mul(u64, header.shstrndx, header.shentsize),
482 );
483 fr.seek = std.math.cast(usize, shstrtab_shdr_off) orelse return error.Overflow;
484 const shstrtab: []const u8 = if (header.is_64) shstrtab: {
485 const shdr = fr.takeStruct(elf.Elf64_Shdr, endian) catch return error.TruncatedElfFile;
486 if (shdr.sh_offset + shdr.sh_size > mapped_mem.len) return error.TruncatedElfFile;
487 break :shstrtab mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)];
488 } else shstrtab: {
489 const shdr = fr.takeStruct(elf.Elf32_Shdr, endian) catch return error.TruncatedElfFile;
490 if (shdr.sh_offset + shdr.sh_size > mapped_mem.len) return error.TruncatedElfFile;
491 break :shstrtab mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)];
492 };
493
494 var sections: Section.Array = .initFill(null);
495
496 var it = header.iterateSectionHeadersBuffer(mapped_mem);
497 while (it.next() catch return error.TruncatedElfFile) |shdr| {
498 if (shdr.sh_type == elf.SHT_NULL or shdr.sh_type == elf.SHT_NOBITS) continue;
499 if (shdr.sh_name > shstrtab.len) return error.TruncatedElfFile;
500 const name = std.mem.sliceTo(shstrtab[@intCast(shdr.sh_name)..], 0);
501
502 const section_id: Section.Id = inline for (
503 @typeInfo(Section.Id).@"enum".field_names,
504 @typeInfo(Section.Id).@"enum".field_values,
505 ) |s_name, s_value| {
506 if (std.mem.eql(u8, "." ++ s_name, name)) {
507 break @fromBackingInt(@intCast(s_value));
508 }
509 } else continue;
510
511 if (sections.get(section_id) != null) continue;
512
513 if (shdr.sh_offset + shdr.sh_size > mapped_mem.len) return error.TruncatedElfFile;
514 const raw_section_bytes = mapped_mem[@intCast(shdr.sh_offset)..][0..@intCast(shdr.sh_size)];
515 const section_bytes: []const u8 = bytes: {
516 if ((shdr.sh_flags & elf.SHF_COMPRESSED) == 0) break :bytes raw_section_bytes;
517
518 var section_reader: std.Io.Reader = .fixed(raw_section_bytes);
519 const ch_type: elf.COMPRESS, const ch_size: u64 = if (header.is_64) ch: {
520 const chdr = section_reader.takeStruct(elf.Elf64_Chdr, endian) catch return error.InvalidCompressedSection;
521 break :ch .{ chdr.ch_type, chdr.ch_size };
522 } else ch: {
523 const chdr = section_reader.takeStruct(elf.Elf32_Chdr, endian) catch return error.InvalidCompressedSection;
524 break :ch .{ chdr.ch_type, chdr.ch_size };
525 };
526 if (ch_type != .ZLIB) {
527 // The compression algorithm is unsupported, but don't make that a hard error; the
528 // file might still be valid, and we might still be okay without this section.
529 continue;
530 }
531
532 const buf = try arena.alloc(u8, std.math.cast(usize, ch_size) orelse return error.Overflow);
533 var fw: std.Io.Writer = .fixed(buf);
534 var decompress: std.compress.flate.Decompress = .init(&section_reader, .zlib, &.{});
535 const n = decompress.reader.streamRemaining(&fw) catch |err| switch (err) {
536 // If a write failed, then `buf` filled up, so `ch_size` was incorrect
537 error.WriteFailed => return error.InvalidCompressedSection,
538 // If a read failed, flate expected the section to have more data
539 error.ReadFailed => return error.InvalidCompressedSection,
540 };
541 // It's also an error if the data is shorter than expected.
542 if (n != buf.len) return error.InvalidCompressedSection;
543 break :bytes buf;
544 };
545 sections.set(section_id, .{ .header = shdr, .bytes = section_bytes });
546 }
547
548 return .{
549 .is_64 = header.is_64,
550 .endian = endian,
551 .sections = sections,
552 .mapped_mem = mapped_mem,
553 };
554}