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...@@ -684,12 +684,16 @@ fn printUnknownSource(debug_info: *DebugInfo, out_stream: anytype, address: usiz
684pub fn printSourceAtAddress(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: io.tty.Config) !void {684pub fn printSourceAtAddress(debug_info: *DebugInfo, out_stream: anytype, address: usize, tty_config: io.tty.Config) !void {
685 const module = debug_info.getModuleForAddress(address) catch |err| switch (err) {685 const module = debug_info.getModuleForAddress(address) catch |err| switch (err) {
686 error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),686 error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),
687 else => return err,687 else => {
688 return err;
689 },
688 };690 };
689691
690 const symbol_info = module.getSymbolAtAddress(debug_info.allocator, address) catch |err| switch (err) {692 const symbol_info = module.getSymbolAtAddress(debug_info.allocator, address) catch |err| switch (err) {
691 error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),693 error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),
692 else => return err,694 else => {
695 return err;
696 },
693 };697 };
694 defer symbol_info.deinit(debug_info.allocator);698 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...@@ -877,13 +881,29 @@ fn chopSlice(ptr: []const u8, offset: u64, size: u64) error{Overflow}![]const u8
877 return ptr[start..end];881 return ptr[start..end];
878}882}
879883
880/// This takes ownership of elf_file: users of this function should not close884pub fn readElfDebugInfo(
881/// it themselves, even on error.885 allocator: mem.Allocator,
882/// TODO it's weird to take ownership even on error, rework this code.886 elf_filename: ?[]const u8,
883pub fn readElfDebugInfo(allocator: mem.Allocator, elf_file: File) !ModuleDebugInfo {887 build_id: ?[]const u8,
888 expected_crc: ?u32,
889) !ModuleDebugInfo {
884 nosuspend {890 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
885 const mapped_mem = try mapWholeFile(elf_file);903 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]);
887 if (!mem.eql(u8, hdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic;907 if (!mem.eql(u8, hdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic;
888 if (hdr.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion;908 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...@@ -918,44 +938,147 @@ pub fn readElfDebugInfo(allocator: mem.Allocator, elf_file: File) !ModuleDebugIn
918 var opt_debug_names: ?[]const u8 = null;938 var opt_debug_names: ?[]const u8 = null;
919 var opt_debug_frame: ?[]const u8 = null;939 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
921 for (shdrs) |*shdr| {947 for (shdrs) |*shdr| {
922 if (shdr.sh_type == elf.SHT_NULL) continue;948 if (shdr.sh_type == elf.SHT_NULL) continue;
923
924 const name = mem.sliceTo(header_strings[shdr.sh_name..], 0);949 const name = mem.sliceTo(header_strings[shdr.sh_name..], 0);
925 if (mem.eql(u8, name, ".debug_info")) {950
926 opt_debug_info = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);951 if (mem.eql(u8, name, ".gnu_debuglink")) {
927 } else if (mem.eql(u8, name, ".debug_abbrev")) {952 const gnu_debuglink = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
928 opt_debug_abbrev = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);953 const debug_filename = mem.sliceTo(@ptrCast([*:0]const u8, gnu_debuglink.ptr), 0);
929 } else if (mem.eql(u8, name, ".debug_str")) {954 const crc_offset = mem.alignForward(@ptrToInt(&debug_filename[debug_filename.len]) + 1, 4) - @ptrToInt(gnu_debuglink.ptr);
930 opt_debug_str = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);955 const crc_bytes = gnu_debuglink[crc_offset .. crc_offset + 4];
931 } else if (mem.eql(u8, name, ".debug_str_offsets")) {956 separate_debug_crc = mem.readIntSliceNative(u32, crc_bytes);
932 opt_debug_str_offsets = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);957 separate_debug_filename = debug_filename;
933 } else if (mem.eql(u8, name, ".debug_line")) {958 continue;
934 opt_debug_line = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);959 }
935 } else if (mem.eql(u8, name, ".debug_line_str")) {960
936 opt_debug_line_str = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);961 const sections = [_]struct { name: []const u8, out: *?[]const u8 }{
937 } else if (mem.eql(u8, name, ".debug_ranges")) {962 .{ .name = ".debug_info", .out = &opt_debug_info },
938 opt_debug_ranges = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);963 .{ .name = ".debug_abbrev", .out = &opt_debug_abbrev },
939 } else if (mem.eql(u8, name, ".debug_loclists")) {964 .{ .name = ".debug_str", .out = &opt_debug_str },
940 opt_debug_loclists = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);965 .{ .name = ".debug_str_offsets", .out = &opt_debug_str_offsets },
941 } else if (mem.eql(u8, name, ".debug_rnglists")) {966 .{ .name = ".debug_line", .out = &opt_debug_line },
942 opt_debug_rnglists = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);967 .{ .name = ".debug_line_str", .out = &opt_debug_line_str },
943 } else if (mem.eql(u8, name, ".debug_addr")) {968 .{ .name = ".debug_ranges", .out = &opt_debug_ranges },
944 opt_debug_addr = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);969 .{ .name = ".debug_loclists", .out = &opt_debug_loclists },
945 } else if (mem.eql(u8, name, ".debug_names")) {970 .{ .name = ".debug_rnglists", .out = &opt_debug_rnglists },
946 opt_debug_names = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);971 .{ .name = ".debug_addr", .out = &opt_debug_addr },
947 } else if (mem.eql(u8, name, ".debug_frame")) {972 .{ .name = ".debug_names", .out = &opt_debug_names },
948 opt_debug_frame = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);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 }
949 }1070 }
1071
1072 return error.MissingDebugInfo;
950 }1073 }
9511074
952 var di = DW.DwarfInfo{1075 var di = DW.DwarfInfo{
953 .endian = endian,1076 .endian = endian,
954 .debug_info = opt_debug_info orelse return error.MissingDebugInfo,1077 .debug_info = opt_debug_info.?,
955 .debug_abbrev = opt_debug_abbrev orelse return error.MissingDebugInfo,1078 .debug_abbrev = opt_debug_abbrev.?,
956 .debug_str = opt_debug_str orelse return error.MissingDebugInfo,1079 .debug_str = opt_debug_str.?,
957 .debug_str_offsets = opt_debug_str_offsets,1080 .debug_str_offsets = opt_debug_str_offsets,
958 .debug_line = opt_debug_line orelse return error.MissingDebugInfo,1081 .debug_line = opt_debug_line.?,
959 .debug_line_str = opt_debug_line_str,1082 .debug_line_str = opt_debug_line_str,
960 .debug_ranges = opt_debug_ranges,1083 .debug_ranges = opt_debug_ranges,
961 .debug_loclists = opt_debug_loclists,1084 .debug_loclists = opt_debug_loclists,
...@@ -971,6 +1094,7 @@ pub fn readElfDebugInfo(allocator: mem.Allocator, elf_file: File) !ModuleDebugIn...@@ -971,6 +1094,7 @@ pub fn readElfDebugInfo(allocator: mem.Allocator, elf_file: File) !ModuleDebugIn
971 .base_address = undefined,1094 .base_address = undefined,
972 .dwarf = di,1095 .dwarf = di,
973 .mapped_memory = mapped_mem,1096 .mapped_memory = mapped_mem,
1097 .owned_sections = owned_sections,
974 };1098 };
975 }1099 }
976}1100}
...@@ -1359,6 +1483,7 @@ pub const DebugInfo = struct {...@@ -1359,6 +1483,7 @@ pub const DebugInfo = struct {
1359 // Output1483 // Output
1360 base_address: usize = undefined,1484 base_address: usize = undefined,
1361 name: []const u8 = undefined,1485 name: []const u8 = undefined,
1486 build_id: ?[]const u8 = undefined,
1362 } = .{ .address = address };1487 } = .{ .address = address };
1363 const CtxTy = @TypeOf(ctx);1488 const CtxTy = @TypeOf(ctx);
13641489
...@@ -1375,16 +1500,30 @@ pub const DebugInfo = struct {...@@ -1375,16 +1500,30 @@ pub const DebugInfo = struct {
13751500
1376 const seg_start = info.dlpi_addr + phdr.p_vaddr;1501 const seg_start = info.dlpi_addr + phdr.p_vaddr;
1377 const seg_end = seg_start + phdr.p_memsz;1502 const seg_end = seg_start + phdr.p_memsz;
1378
1379 if (context.address >= seg_start and context.address < seg_end) {1503 if (context.address >= seg_start and context.address < seg_end) {
1380 // Android libc uses NULL instead of an empty string to mark the1504 // Android libc uses NULL instead of an empty string to mark the
1381 // main program1505 // main program
1382 context.name = mem.sliceTo(info.dlpi_name, 0) orelse "";1506 context.name = mem.sliceTo(info.dlpi_name, 0) orelse "";
1383 context.base_address = info.dlpi_addr;1507 context.base_address = info.dlpi_addr;
1384 // Stop the iteration1508 break;
1385 return error.Found;
1386 }1509 }
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];
1387 }1523 }
1524
1525 // Stop the iteration
1526 return error.Found;
1388 }1527 }
1389 }.callback)) {1528 }.callback)) {
1390 return error.MissingDebugInfo;1529 return error.MissingDebugInfo;
...@@ -1399,18 +1538,7 @@ pub const DebugInfo = struct {...@@ -1399,18 +1538,7 @@ pub const DebugInfo = struct {
1399 const obj_di = try self.allocator.create(ModuleDebugInfo);1538 const obj_di = try self.allocator.create(ModuleDebugInfo);
1400 errdefer self.allocator.destroy(obj_di);1539 errdefer self.allocator.destroy(obj_di);
14011540
1402 // TODO https://github.com/ziglang/zig/issues/55251541 obj_di.* = try readElfDebugInfo(self.allocator, if (ctx.name.len > 0) ctx.name else null, ctx.build_id, null);
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);
1414 obj_di.base_address = ctx.base_address;1542 obj_di.base_address = ctx.base_address;
14151543
1416 try self.address_map.putNoClobber(ctx.base_address, obj_di);1544 try self.address_map.putNoClobber(ctx.base_address, obj_di);
...@@ -1752,9 +1880,13 @@ pub const ModuleDebugInfo = switch (native_os) {...@@ -1752,9 +1880,13 @@ pub const ModuleDebugInfo = switch (native_os) {
1752 base_address: usize,1880 base_address: usize,
1753 dwarf: DW.DwarfInfo,1881 dwarf: DW.DwarfInfo,
1754 mapped_memory: []align(mem.page_size) const u8,1882 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
1756 fn deinit(self: *@This(), allocator: mem.Allocator) void {1887 fn deinit(self: *@This(), allocator: mem.Allocator) void {
1757 self.dwarf.deinit(allocator);1888 self.dwarf.deinit(allocator);
1889 for (self.owned_sections) |section| allocator.free(section);
1758 os.munmap(self.mapped_memory);1890 os.munmap(self.mapped_memory);
1759 }1891 }
17601892
lib/std/elf.zig+8
...@@ -371,6 +371,9 @@ pub const SHT_LOUSER = 0x80000000;...@@ -371,6 +371,9 @@ pub const SHT_LOUSER = 0x80000000;
371/// End of application-specific371/// End of application-specific
372pub const SHT_HIUSER = 0xffffffff;372pub const SHT_HIUSER = 0xffffffff;
373373
374// Note type for .note.gnu.build_id
375pub const NT_GNU_BUILD_ID = 3;
376
374/// Local symbol377/// Local symbol
375pub const STB_LOCAL = 0;378pub const STB_LOCAL = 0;
376/// Global symbol379/// Global symbol
...@@ -1055,6 +1058,11 @@ pub const Shdr = switch (@sizeOf(usize)) {...@@ -1055,6 +1058,11 @@ pub const Shdr = switch (@sizeOf(usize)) {
1055 8 => Elf64_Shdr,1058 8 => Elf64_Shdr,
1056 else => @compileError("expected pointer size of 32 or 64"),1059 else => @compileError("expected pointer size of 32 or 64"),
1057};1060};
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};
1058pub const Sym = switch (@sizeOf(usize)) {1066pub const Sym = switch (@sizeOf(usize)) {
1059 4 => Elf32_Sym,1067 4 => Elf32_Sym,
1060 8 => Elf64_Sym,1068 8 => Elf64_Sym,