authorgravatar for kcbanner@gmail.comCasey Banner <kcbanner@gmail.com> 2023-05-01 00:46:28-04:00
committergravatar for kcbanner@gmail.comCasey Banner <kcbanner@gmail.com> 2023-07-20 22:58:12-04:00
logea9917d9bd921f9fc14028a8dbf9e3f0f2579120
tree5128a49e9c364c4b72b11c6d4afb14c35dcbcc6b
parent8a18abfd60392a3adcfc4e6cfa712f63ecf2bf67

debug: support loading elf debug info from external files

Some distributions (ie. Ubuntu) have their libc debug info in separate files. This change allows the stack walking code to read that debug info. - add support for reading compressed ELF sections - support reading the build-id from the elf headers in order to lookup external debug info - support reading the .gnu_debuglink section to look up external debug info

2 files changed, 191 insertions(+), 51 deletions(-)

lib/std/debug.zig+183-51
......@@ -684,12 +684,16 @@ fn printUnknownSource(debug_info: *DebugInfo, out_stream: anytype, address: usiz
684684pub fn printSourceAtAddress(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: io.tty.Config) !void {
685685 const module = debug_info.getModuleForAddress(address) catch |err| switch (err) {
686686 error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),
687 else => return err,
687 else => {
688 return err;
689 },
688690 };
689691
690692 const symbol_info = module.getSymbolAtAddress(debug_info.allocator, address) catch |err| switch (err) {
691693 error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),
692 else => return err,
694 else => {
695 return err;
696 },
693697 };
694698 defer symbol_info.deinit(debug_info.allocator);
695699
......@@ -877,13 +881,29 @@ fn chopSlice(ptr: []const u8, offset: u64, size: u64) error{Overflow}![]const u8
877881 return ptr[start..end];
878882}
879883
880/// This takes ownership of elf_file: users of this function should not close
881/// it themselves, even on error.
882/// TODO it's weird to take ownership even on error, rework this code.
883pub fn readElfDebugInfo(allocator: mem.Allocator, elf_file: File) !ModuleDebugInfo {
884pub fn readElfDebugInfo(
885 allocator: mem.Allocator,
886 elf_filename: ?[]const u8,
887 build_id: ?[]const u8,
888 expected_crc: ?u32,
889) !ModuleDebugInfo {
884890 nosuspend {
891
892 // TODO https://github.com/ziglang/zig/issues/5525
893 const elf_file = (if (elf_filename) |filename| blk: {
894 break :blk if (fs.path.isAbsolute(filename))
895 fs.openFileAbsolute(filename, .{ .intended_io_mode = .blocking })
896 else
897 fs.cwd().openFile(filename, .{ .intended_io_mode = .blocking });
898 } else fs.openSelfExe(.{ .intended_io_mode = .blocking })) catch |err| switch (err) {
899 error.FileNotFound => return error.MissingDebugInfo,
900 else => return err,
901 };
902
885903 const mapped_mem = try mapWholeFile(elf_file);
886 const hdr = @as(*const elf.Ehdr, @ptrCast(&mapped_mem[0]));
904 if (expected_crc) |crc| if (crc != std.hash.crc.Crc32SmallWithPoly(.IEEE).hash(mapped_mem)) return error.MissingDebugInfo;
905
906 const hdr: *const elf.Ehdr = @ptrCast(&mapped_mem[0]);
887907 if (!mem.eql(u8, hdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic;
888908 if (hdr.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion;
889909
......@@ -918,44 +938,147 @@ pub fn readElfDebugInfo(allocator: mem.Allocator, elf_file: File) !ModuleDebugIn
918938 var opt_debug_names: ?[]const u8 = null;
919939 var opt_debug_frame: ?[]const u8 = null;
920940
941 var owned_sections: [ModuleDebugInfo.num_sections][]const u8 = [_][]const u8{&.{}} ** ModuleDebugInfo.num_sections;
942 errdefer for (owned_sections) |section| allocator.free(section);
943
944 var separate_debug_filename: ?[]const u8 = null;
945 var separate_debug_crc: ?u32 = null;
946
921947 for (shdrs) |*shdr| {
922948 if (shdr.sh_type == elf.SHT_NULL) continue;
923
924949 const name = mem.sliceTo(header_strings[shdr.sh_name..], 0);
925 if (mem.eql(u8, name, ".debug_info")) {
926 opt_debug_info = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
927 } else if (mem.eql(u8, name, ".debug_abbrev")) {
928 opt_debug_abbrev = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
929 } else if (mem.eql(u8, name, ".debug_str")) {
930 opt_debug_str = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
931 } else if (mem.eql(u8, name, ".debug_str_offsets")) {
932 opt_debug_str_offsets = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
933 } else if (mem.eql(u8, name, ".debug_line")) {
934 opt_debug_line = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
935 } else if (mem.eql(u8, name, ".debug_line_str")) {
936 opt_debug_line_str = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
937 } else if (mem.eql(u8, name, ".debug_ranges")) {
938 opt_debug_ranges = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
939 } else if (mem.eql(u8, name, ".debug_loclists")) {
940 opt_debug_loclists = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
941 } else if (mem.eql(u8, name, ".debug_rnglists")) {
942 opt_debug_rnglists = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
943 } else if (mem.eql(u8, name, ".debug_addr")) {
944 opt_debug_addr = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
945 } else if (mem.eql(u8, name, ".debug_names")) {
946 opt_debug_names = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
947 } else if (mem.eql(u8, name, ".debug_frame")) {
948 opt_debug_frame = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
950
951 if (mem.eql(u8, name, ".gnu_debuglink")) {
952 const gnu_debuglink = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
953 const debug_filename = mem.sliceTo(@ptrCast([*:0]const u8, gnu_debuglink.ptr), 0);
954 const crc_offset = mem.alignForward(@ptrToInt(&debug_filename[debug_filename.len]) + 1, 4) - @ptrToInt(gnu_debuglink.ptr);
955 const crc_bytes = gnu_debuglink[crc_offset .. crc_offset + 4];
956 separate_debug_crc = mem.readIntSliceNative(u32, crc_bytes);
957 separate_debug_filename = debug_filename;
958 continue;
959 }
960
961 const sections = [_]struct { name: []const u8, out: *?[]const u8 }{
962 .{ .name = ".debug_info", .out = &opt_debug_info },
963 .{ .name = ".debug_abbrev", .out = &opt_debug_abbrev },
964 .{ .name = ".debug_str", .out = &opt_debug_str },
965 .{ .name = ".debug_str_offsets", .out = &opt_debug_str_offsets },
966 .{ .name = ".debug_line", .out = &opt_debug_line },
967 .{ .name = ".debug_line_str", .out = &opt_debug_line_str },
968 .{ .name = ".debug_ranges", .out = &opt_debug_ranges },
969 .{ .name = ".debug_loclists", .out = &opt_debug_loclists },
970 .{ .name = ".debug_rnglists", .out = &opt_debug_rnglists },
971 .{ .name = ".debug_addr", .out = &opt_debug_addr },
972 .{ .name = ".debug_names", .out = &opt_debug_names },
973 .{ .name = ".debug_frame", .out = &opt_debug_frame },
974 };
975
976 var section_index = for (sections, 0..) |section, i| {
977 if (mem.eql(u8, section.name, name)) {
978 break i;
979 }
980 } else continue;
981
982 const section_bytes = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
983 if ((shdr.sh_flags & elf.SHF_COMPRESSED) > 0) {
984 var section_stream = io.fixedBufferStream(section_bytes);
985 var section_reader = section_stream.reader();
986 const chdr = section_reader.readStruct(elf.Chdr) catch continue;
987
988 // TODO: Support ZSTD
989 if (chdr.ch_type != .ZLIB) continue;
990
991 var zlib_stream = std.compress.zlib.zlibStream(allocator, section_stream.reader()) catch continue;
992 defer zlib_stream.deinit();
993
994 var decompressed_section = try allocator.alloc(u8, chdr.ch_size);
995 errdefer allocator.free(decompressed_section);
996
997 const read = zlib_stream.reader().readAll(decompressed_section) catch continue;
998 assert(read == decompressed_section.len);
999
1000 sections[section_index].out.* = decompressed_section;
1001 owned_sections[section_index] = decompressed_section;
1002 } else {
1003 sections[section_index].out.* = section_bytes;
1004 }
1005 }
1006
1007 const missing_debug_info =
1008 opt_debug_info == null or
1009 opt_debug_abbrev == null or
1010 opt_debug_str == null or
1011 opt_debug_line == null;
1012
1013 // Attempt to load debug info from an external file
1014 // See: https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
1015 if (missing_debug_info) {
1016 const global_debug_directories = [_][]const u8{
1017 "/usr/lib/debug",
1018 // TODO: Determine the set of directories used by most distros for this path (check GDB sources)
1019 };
1020
1021 // <global debug directory>/.build-id/<2-character id prefix>/<id remainder>.debug
1022 if (build_id) |id| blk: {
1023 if (id.len < 3) break :blk;
1024
1025 // Either md5 (16 bytes) or sha1 (20 bytes) are used here in practice
1026 const extension = ".debug";
1027 var id_prefix_buf: [2]u8 = undefined;
1028 var filename_buf: [38 + extension.len]u8 = undefined;
1029
1030 _ = std.fmt.bufPrint(&id_prefix_buf, "{s}", .{std.fmt.fmtSliceHexLower(id[0..1])}) catch unreachable;
1031 const filename = std.fmt.bufPrint(
1032 &filename_buf,
1033 "{s}" ++ extension,
1034 .{std.fmt.fmtSliceHexLower(id[1..])},
1035 ) catch break :blk;
1036
1037 for (global_debug_directories) |global_directory| {
1038 // TODO: joinBuf would be ideal (with a fs.MAX_PATH_BYTES buffer)
1039 const path = try fs.path.join(allocator, &.{ global_directory, ".build-id", &id_prefix_buf, filename });
1040 defer allocator.free(path);
1041 std.debug.print(" Loading external debug info from {s}\n", .{path});
1042 return readElfDebugInfo(allocator, path, null, separate_debug_crc) catch continue;
1043 }
1044 }
1045
1046 // use the path from .gnu_debuglink, in the search order as gdb
1047 if (separate_debug_filename) |separate_filename| blk: {
1048 if (elf_filename != null and mem.eql(u8, elf_filename.?, separate_filename)) return error.MissingDebugInfo;
1049
1050 // <cwd>/<gnu_debuglink>
1051 if (readElfDebugInfo(allocator, separate_filename, null, separate_debug_crc)) |debug_info| return debug_info else |_| {}
1052
1053 // <cwd>/.debug/<gnu_debuglink>
1054 {
1055 const path = try fs.path.join(allocator, &.{ ".debug", separate_filename });
1056 defer allocator.free(path);
1057
1058 if (readElfDebugInfo(allocator, path, null, separate_debug_crc)) |debug_info| return debug_info else |_| {}
1059 }
1060
1061 var cwd_buf: [fs.MAX_PATH_BYTES]u8 = undefined;
1062 const cwd_path = fs.cwd().realpath("", &cwd_buf) catch break :blk;
1063
1064 // <global debug directory>/<absolute folder of current binary>/<gnu_debuglink>
1065 for (global_debug_directories) |global_directory| {
1066 const path = try fs.path.join(allocator, &.{ global_directory, cwd_path, separate_filename });
1067 defer allocator.free(path);
1068 if (readElfDebugInfo(allocator, path, null, separate_debug_crc)) |debug_info| return debug_info else |_| {}
1069 }
9491070 }
1071
1072 return error.MissingDebugInfo;
9501073 }
9511074
9521075 var di = DW.DwarfInfo{
9531076 .endian = endian,
954 .debug_info = opt_debug_info orelse return error.MissingDebugInfo,
955 .debug_abbrev = opt_debug_abbrev orelse return error.MissingDebugInfo,
956 .debug_str = opt_debug_str orelse return error.MissingDebugInfo,
1077 .debug_info = opt_debug_info.?,
1078 .debug_abbrev = opt_debug_abbrev.?,
1079 .debug_str = opt_debug_str.?,
9571080 .debug_str_offsets = opt_debug_str_offsets,
958 .debug_line = opt_debug_line orelse return error.MissingDebugInfo,
1081 .debug_line = opt_debug_line.?,
9591082 .debug_line_str = opt_debug_line_str,
9601083 .debug_ranges = opt_debug_ranges,
9611084 .debug_loclists = opt_debug_loclists,
......@@ -971,6 +1094,7 @@ pub fn readElfDebugInfo(allocator: mem.Allocator, elf_file: File) !ModuleDebugIn
9711094 .base_address = undefined,
9721095 .dwarf = di,
9731096 .mapped_memory = mapped_mem,
1097 .owned_sections = owned_sections,
9741098 };
9751099 }
9761100}
......@@ -1359,6 +1483,7 @@ pub const DebugInfo = struct {
13591483 // Output
13601484 base_address: usize = undefined,
13611485 name: []const u8 = undefined,
1486 build_id: ?[]const u8 = undefined,
13621487 } = .{ .address = address };
13631488 const CtxTy = @TypeOf(ctx);
13641489
......@@ -1375,16 +1500,30 @@ pub const DebugInfo = struct {
13751500
13761501 const seg_start = info.dlpi_addr + phdr.p_vaddr;
13771502 const seg_end = seg_start + phdr.p_memsz;
1378
13791503 if (context.address >= seg_start and context.address < seg_end) {
13801504 // Android libc uses NULL instead of an empty string to mark the
13811505 // main program
13821506 context.name = mem.sliceTo(info.dlpi_name, 0) orelse "";
13831507 context.base_address = info.dlpi_addr;
1384 // Stop the iteration
1385 return error.Found;
1508 break;
13861509 }
1510 } else return;
1511
1512 for (info.dlpi_phdr[0..info.dlpi_phnum]) |phdr| {
1513 if (phdr.p_type != elf.PT_NOTE) continue;
1514
1515 const note_bytes = @intToPtr([*]const u8, info.dlpi_addr + phdr.p_vaddr)[0..phdr.p_memsz];
1516 const name_size = mem.readIntSliceNative(u32, note_bytes[0..4]);
1517 if (name_size != 4) continue;
1518 const desc_size = mem.readIntSliceNative(u32, note_bytes[4..8]);
1519 const note_type = mem.readIntSliceNative(u32, note_bytes[8..12]);
1520 if (note_type != elf.NT_GNU_BUILD_ID) continue;
1521 if (!mem.eql(u8, "GNU\x00", note_bytes[12..16])) continue;
1522 context.build_id = note_bytes[16 .. 16 + desc_size];
13871523 }
1524
1525 // Stop the iteration
1526 return error.Found;
13881527 }
13891528 }.callback)) {
13901529 return error.MissingDebugInfo;
......@@ -1399,18 +1538,7 @@ pub const DebugInfo = struct {
13991538 const obj_di = try self.allocator.create(ModuleDebugInfo);
14001539 errdefer self.allocator.destroy(obj_di);
14011540
1402 // TODO https://github.com/ziglang/zig/issues/5525
1403 const copy = if (ctx.name.len > 0)
1404 fs.cwd().openFile(ctx.name, .{ .intended_io_mode = .blocking })
1405 else
1406 fs.openSelfExe(.{ .intended_io_mode = .blocking });
1407
1408 const elf_file = copy catch |err| switch (err) {
1409 error.FileNotFound => return error.MissingDebugInfo,
1410 else => return err,
1411 };
1412
1413 obj_di.* = try readElfDebugInfo(self.allocator, elf_file);
1541 obj_di.* = try readElfDebugInfo(self.allocator, if (ctx.name.len > 0) ctx.name else null, ctx.build_id, null);
14141542 obj_di.base_address = ctx.base_address;
14151543
14161544 try self.address_map.putNoClobber(ctx.base_address, obj_di);
......@@ -1752,9 +1880,13 @@ pub const ModuleDebugInfo = switch (native_os) {
17521880 base_address: usize,
17531881 dwarf: DW.DwarfInfo,
17541882 mapped_memory: []align(mem.page_size) const u8,
1883 owned_sections: [num_sections][]const u8 = [_][]const u8{&.{}} ** num_sections,
1884
1885 const num_sections = 12;
17551886
17561887 fn deinit(self: *@This(), allocator: mem.Allocator) void {
17571888 self.dwarf.deinit(allocator);
1889 for (self.owned_sections) |section| allocator.free(section);
17581890 os.munmap(self.mapped_memory);
17591891 }
17601892
lib/std/elf.zig+8
......@@ -371,6 +371,9 @@ pub const SHT_LOUSER = 0x80000000;
371371/// End of application-specific
372372pub const SHT_HIUSER = 0xffffffff;
373373
374// Note type for .note.gnu.build_id
375pub const NT_GNU_BUILD_ID = 3;
376
374377/// Local symbol
375378pub const STB_LOCAL = 0;
376379/// Global symbol
......@@ -1055,6 +1058,11 @@ pub const Shdr = switch (@sizeOf(usize)) {
10551058 8 => Elf64_Shdr,
10561059 else => @compileError("expected pointer size of 32 or 64"),
10571060};
1061pub const Chdr = switch (@sizeOf(usize)) {
1062 4 => Elf32_Chdr,
1063 8 => Elf64_Chdr,
1064 else => @compileError("expected pointer size of 32 or 64"),
1065};
10581066pub const Sym = switch (@sizeOf(usize)) {
10591067 4 => Elf32_Sym,
10601068 8 => Elf64_Sym,