DoxigAlpha

load

Reads debug info from an already mapped ELF file.

If the required sections aren't present but a reference to external debug info is, then this this function will recurse to attempt to load the debug sections from an external file.

Function parameters

Parameters

#
mapped_mem:[]align(std.heap.page_size_min) const u8
build_id:?[]const u8
expected_crc:?u32
parent_sections:*Dwarf.SectionArray
parent_mapped_mem:?[]align(std.heap.page_size_min) const u8
elf_filename:?[]const u8

Type definitions in this namespace

Types

#
ExceptionFrameHeader
This represents the decoded .eh_frame_hdr header

Initialize DWARF info.

Functions

#
open
Initialize DWARF info.
findCompileUnit
TODO: change this to binary searching the sorted compile unit list
scanAllUnwindInfo
If `.eh_frame_hdr` is present, then only the header needs to be parsed.
scanCieFdeInfo
Scan `.eh_frame` and `.debug_frame` and build a sorted list of FDEs for binary searching during
compactUnwindToDwarfRegNumber
Returns the DWARF register number for an x86_64 register number found in compact unwind info
bad
This function is to make it handy to comment out the return and make it

Error sets in this namespace

Error Sets

#

= [_]?Section{null} ** num_sections

Values

#
null_section_array
= [_]?Section{null} ** num_sections

Source

Implementation

#
pub fn load(
    gpa: Allocator,
    mapped_mem: []align(std.heap.page_size_min) const u8,
    build_id: ?[]const u8,
    expected_crc: ?u32,
    parent_sections: *Dwarf.SectionArray,
    parent_mapped_mem: ?[]align(std.heap.page_size_min) const u8,
    elf_filename: ?[]const u8,
) LoadError!Dwarf.ElfModule {
    if (expected_crc) |crc| if (crc != std.hash.crc.Crc32.hash(mapped_mem)) return error.InvalidDebugInfo;

    const hdr: *const elf.Ehdr = @ptrCast(&mapped_mem[0]);
    if (!mem.eql(u8, hdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic;
    if (hdr.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion;

    const endian: std.builtin.Endian = switch (hdr.e_ident[elf.EI_DATA]) {
        elf.ELFDATA2LSB => .little,
        elf.ELFDATA2MSB => .big,
        else => return error.InvalidElfEndian,
    };
    if (endian != native_endian) return error.UnimplementedDwarfForeignEndian;

    const shoff = hdr.e_shoff;
    const str_section_off = shoff + @as(u64, hdr.e_shentsize) * @as(u64, hdr.e_shstrndx);
    const str_shdr: *const elf.Shdr = @ptrCast(@alignCast(&mapped_mem[cast(usize, str_section_off) orelse return error.Overflow]));
    const header_strings = mapped_mem[str_shdr.sh_offset..][0..str_shdr.sh_size];
    const shdrs = @as(
        [*]const elf.Shdr,
        @ptrCast(@alignCast(&mapped_mem[shoff])),
    )[0..hdr.e_shnum];

    var sections: Dwarf.SectionArray = Dwarf.null_section_array;

    // Combine section list. This takes ownership over any owned sections from the parent scope.
    for (parent_sections, &sections) |*parent, *section_elem| {
        if (parent.*) |*p| {
            section_elem.* = p.*;
            p.owned = false;
        }
    }
    errdefer for (sections) |opt_section| if (opt_section) |s| if (s.owned) gpa.free(s.data);

    var separate_debug_filename: ?[]const u8 = null;
    var separate_debug_crc: ?u32 = null;

    for (shdrs) |*shdr| {
        if (shdr.sh_type == elf.SHT_NULL or shdr.sh_type == elf.SHT_NOBITS) continue;
        const name = mem.sliceTo(header_strings[shdr.sh_name..], 0);

        if (mem.eql(u8, name, ".gnu_debuglink")) {
            const gnu_debuglink = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
            const debug_filename = mem.sliceTo(@as([*:0]const u8, @ptrCast(gnu_debuglink.ptr)), 0);
            const crc_offset = mem.alignForward(usize, debug_filename.len + 1, 4);
            const crc_bytes = gnu_debuglink[crc_offset..][0..4];
            separate_debug_crc = mem.readInt(u32, crc_bytes, native_endian);
            separate_debug_filename = debug_filename;
            continue;
        }

        var section_index: ?usize = null;
        inline for (@typeInfo(Dwarf.Section.Id).@"enum".fields, 0..) |sect, i| {
            if (mem.eql(u8, "." ++ sect.name, name)) section_index = i;
        }
        if (section_index == null) continue;
        if (sections[section_index.?] != null) continue;

        const section_bytes = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
        sections[section_index.?] = if ((shdr.sh_flags & elf.SHF_COMPRESSED) > 0) blk: {
            var section_reader: std.Io.Reader = .fixed(section_bytes);
            const chdr = section_reader.takeStruct(elf.Chdr, endian) catch continue;
            if (chdr.ch_type != .ZLIB) continue;

            var decompress: std.compress.flate.Decompress = .init(&section_reader, .zlib, &.{});
            var decompressed_section: ArrayList(u8) = .empty;
            defer decompressed_section.deinit(gpa);
            decompress.reader.appendRemainingUnlimited(gpa, &decompressed_section) catch {
                invalidDebugInfoDetected();
                continue;
            };
            if (chdr.ch_size != decompressed_section.items.len) {
                invalidDebugInfoDetected();
                continue;
            }
            break :blk .{
                .data = try decompressed_section.toOwnedSlice(gpa),
                .virtual_address = shdr.sh_addr,
                .owned = true,
            };
        } else .{
            .data = section_bytes,
            .virtual_address = shdr.sh_addr,
            .owned = false,
        };
    }

    const missing_debug_info =
        sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
        sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
        sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
        sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;

    // Attempt to load debug info from an external file
    // See: https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
    if (missing_debug_info) {

        // Only allow one level of debug info nesting
        if (parent_mapped_mem) |_| {
            return error.MissingDebugInfo;
        }

        // $XDG_CACHE_HOME/debuginfod_client/<buildid>/debuginfo
        // This only opportunisticly tries to load from the debuginfod cache, but doesn't try to populate it.
        // One can manually run `debuginfod-find debuginfo PATH` to download the symbols
        if (build_id) |id| blk: {
            var debuginfod_dir: std.fs.Dir = switch (builtin.os.tag) {
                .wasi, .windows => break :blk,
                else => dir: {
                    if (std.posix.getenv("DEBUGINFOD_CACHE_PATH")) |path| {
                        break :dir std.fs.openDirAbsolute(path, .{}) catch break :blk;
                    }
                    if (std.posix.getenv("XDG_CACHE_HOME")) |cache_path| {
                        if (cache_path.len > 0) {
                            const path = std.fs.path.join(gpa, &[_][]const u8{ cache_path, "debuginfod_client" }) catch break :blk;
                            defer gpa.free(path);
                            break :dir std.fs.openDirAbsolute(path, .{}) catch break :blk;
                        }
                    }
                    if (std.posix.getenv("HOME")) |home_path| {
                        const path = std.fs.path.join(gpa, &[_][]const u8{ home_path, ".cache", "debuginfod_client" }) catch break :blk;
                        defer gpa.free(path);
                        break :dir std.fs.openDirAbsolute(path, .{}) catch break :blk;
                    }
                    break :blk;
                },
            };
            defer debuginfod_dir.close();

            const filename = std.fmt.allocPrint(gpa, "{x}/debuginfo", .{id}) catch break :blk;
            defer gpa.free(filename);

            const path: Path = .{
                .root_dir = .{ .path = null, .handle = debuginfod_dir },
                .sub_path = filename,
            };

            return loadPath(gpa, path, null, separate_debug_crc, &sections, mapped_mem) catch break :blk;
        }

        const global_debug_directories = [_][]const u8{
            "/usr/lib/debug",
        };

        // <global debug directory>/.build-id/<2-character id prefix>/<id remainder>.debug
        if (build_id) |id| blk: {
            if (id.len < 3) break :blk;

            // Either md5 (16 bytes) or sha1 (20 bytes) are used here in practice
            const extension = ".debug";
            var id_prefix_buf: [2]u8 = undefined;
            var filename_buf: [38 + extension.len]u8 = undefined;

            _ = std.fmt.bufPrint(&id_prefix_buf, "{x}", .{id[0..1]}) catch unreachable;
            const filename = std.fmt.bufPrint(&filename_buf, "{x}" ++ extension, .{id[1..]}) catch break :blk;

            for (global_debug_directories) |global_directory| {
                const path: Path = .{
                    .root_dir = std.Build.Cache.Directory.cwd(),
                    .sub_path = try std.fs.path.join(gpa, &.{
                        global_directory, ".build-id", &id_prefix_buf, filename,
                    }),
                };
                defer gpa.free(path.sub_path);

                return loadPath(gpa, path, null, separate_debug_crc, &sections, mapped_mem) catch continue;
            }
        }

        // use the path from .gnu_debuglink, in the same search order as gdb
        if (separate_debug_filename) |separate_filename| blk: {
            if (elf_filename != null and mem.eql(u8, elf_filename.?, separate_filename))
                return error.MissingDebugInfo;

            exe_dir: {
                var exe_dir_buf: [std.fs.max_path_bytes]u8 = undefined;
                const exe_dir_path = std.fs.selfExeDirPath(&exe_dir_buf) catch break :exe_dir;
                var exe_dir = std.fs.openDirAbsolute(exe_dir_path, .{}) catch break :exe_dir;
                defer exe_dir.close();

                // <exe_dir>/<gnu_debuglink>
                if (loadPath(
                    gpa,
                    .{
                        .root_dir = .{ .path = null, .handle = exe_dir },
                        .sub_path = separate_filename,
                    },
                    null,
                    separate_debug_crc,
                    &sections,
                    mapped_mem,
                )) |debug_info| {
                    return debug_info;
                } else |_| {}

                // <exe_dir>/.debug/<gnu_debuglink>
                const path: Path = .{
                    .root_dir = .{ .path = null, .handle = exe_dir },
                    .sub_path = try std.fs.path.join(gpa, &.{ ".debug", separate_filename }),
                };
                defer gpa.free(path.sub_path);

                if (loadPath(gpa, path, null, separate_debug_crc, &sections, mapped_mem)) |debug_info| return debug_info else |_| {}
            }

            var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
            const cwd_path = std.posix.realpath(".", &cwd_buf) catch break :blk;

            // <global debug directory>/<absolute folder of current binary>/<gnu_debuglink>
            for (global_debug_directories) |global_directory| {
                const path: Path = .{
                    .root_dir = std.Build.Cache.Directory.cwd(),
                    .sub_path = try std.fs.path.join(gpa, &.{ global_directory, cwd_path, separate_filename }),
                };
                defer gpa.free(path.sub_path);
                if (loadPath(gpa, path, null, separate_debug_crc, &sections, mapped_mem)) |debug_info| return debug_info else |_| {}
            }
        }

        return error.MissingDebugInfo;
    }

    var di: Dwarf = .{
        .endian = endian,
        .sections = sections,
        .is_macho = false,
    };

    try Dwarf.open(&di, gpa);

    return .{
        .base_address = 0,
        .dwarf = di,
        .mapped_memory = parent_mapped_mem orelse mapped_mem,
        .external_mapped_memory = if (parent_mapped_mem != null) mapped_mem else null,
    };
}