const std = @import("std"); const Io = std.Io; const fatal = std.process.fatal; const mem = std.mem; const assert = std.debug.assert; const builtin = @import("builtin"); const native_endian = builtin.cpu.arch.endian(); var stdout_buffer: [4000]u8 = undefined; const Options = struct { exports: bool, exports_sort: bool, file_headers: bool, imports: bool, input_path: []const u8, member_filters: []const []const u8 = &.{}, member_headers: bool, elements: std.enums.EnumArray(Element, bool), redact: std.enums.EnumArray(FieldKind, bool), relocs: bool, section_filters: []const []const u8 = &.{}, section_headers: bool, symbol_filters: []const []const u8 = &.{}, strings: bool, symbols: bool, tls: bool, // Coff-specific linker_member: ?std.coff.ArchiveMemberHeader.Kind, }; const FieldKind = enum { va, rva, ord, size, }; const Element = enum { @"file-type", @"header-name", @"member-path", newlines, @"table-header", }; pub fn main(init: std.process.Init) !void { const io = init.io; const args = try init.minimal.args.toSlice(init.arena.allocator()); const arena = init.arena.allocator(); var i: usize = 1; var opt_exports: ?bool = null; var opt_exports_sort: ?bool = null; var opt_file_headers: ?bool = null; var opt_imports: ?bool = null; var opt_input_path: ?[]const u8 = null; var opt_linker_member: ?std.coff.ArchiveMemberHeader.Kind = null; var opt_member_headers: ?bool = null; var any_elements = false; var elements: ?@FieldType(Options, "elements") = null; var redact: @FieldType(Options, "redact") = .initFill(false); var opt_relocs: ?bool = null; var opt_section_headers: ?bool = null; var opt_strings: ?bool = null; var opt_symbols: ?bool = null; var opt_tls: ?bool = null; var section_filters: std.ArrayList([]const u8) = .empty; var symbol_filters: std.ArrayList([]const u8) = .empty; var member_filters: std.ArrayList([]const u8) = .empty; while (i < args.len) : (i += 1) { const arg = args[i]; if (mem.startsWith(u8, arg, "-")) { if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) { return Io.File.stdout().writeStreamingAll(io, usage); } else if (mem.eql(u8, arg, "--all-headers")) { opt_file_headers = true; opt_linker_member = .second_linker; opt_member_headers = true; opt_section_headers = true; opt_symbols = true; opt_relocs = true; } else if (mem.startsWith(u8, arg, "--exports")) { opt_exports = true; opt_linker_member = .second_linker; if (mem.eql(u8, arg["--exports".len..], "=sort")) opt_exports_sort = true; } else if (mem.eql(u8, arg, "--file-headers")) { opt_file_headers = true; } else if (mem.eql(u8, arg, "--imports")) { opt_imports = true; } else if (mem.startsWith(u8, arg, "--linker-member")) { if (mem.eql(u8, arg["--linker-member".len..], "=1")) opt_linker_member = .first_linker else if (mem.eql(u8, arg["--linker-member".len..], "=longnames")) opt_linker_member = .longnames else opt_linker_member = .second_linker; } else if (mem.eql(u8, arg, "--member-headers")) { opt_member_headers = true; } else if (mem.startsWith(u8, arg, "--elements=")) { any_elements = true; var split = std.mem.splitScalar(u8, arg["--elements=".len..], ','); while (split.next()) |element| { const kind, const add = if (element.len > 0 and element[0] == '-') .{ element[1..], false } else .{ element, true }; if (elements == null) elements = .initFill(false); if (std.meta.stringToEnum(Element, kind)) |format_kind| { elements.?.set(format_kind, add); } else if (std.mem.eql(u8, kind, "all")) { elements.? = .initFill(add); } else { fatal("unrecognized element: '{s}'", .{kind}); } } } else if (mem.startsWith(u8, arg, "--only-member=")) { (try member_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-member=".len..]); } else if (mem.startsWith(u8, arg, "--only-section=")) { (try section_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-section=".len..]); } else if (mem.startsWith(u8, arg, "--only-symbol=")) { (try symbol_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-symbol=".len..]); } else if (mem.startsWith(u8, arg, "--redact=")) { const kind = arg["--redact=".len..]; if (std.meta.stringToEnum(FieldKind, kind)) |field_kind| { redact.set(field_kind, true); } else if (std.mem.eql(u8, kind, "all")) { redact = .initFill(true); } else { fatal("unrecognized redaction kind: {s}", .{kind}); } } else if (mem.eql(u8, arg, "--relocs")) { opt_relocs = true; } else if (mem.eql(u8, arg, "--section-headers")) { opt_section_headers = true; } else if (mem.eql(u8, arg, "-s") or mem.eql(u8, arg, "--snapshot")) { elements = .initFill(false); redact = .initFill(true); } else if (mem.eql(u8, arg, "--strings")) { opt_strings = true; } else if (mem.eql(u8, arg, "--symbols")) { opt_symbols = true; } else if (mem.eql(u8, arg, "--tls")) { opt_tls = true; } else { fatal("unrecognized argument: {s}", .{arg}); } } else if (opt_input_path == null) { opt_input_path = arg; } else { fatal("unexpected positional: {s}", .{arg}); } } const opts: Options = .{ .input_path = opt_input_path orelse fatal("missing input file path positional argument", .{}), .exports = opt_exports orelse false, .exports_sort = opt_exports_sort orelse false, .file_headers = opt_file_headers orelse false, .imports = opt_imports orelse false, .linker_member = opt_linker_member, .member_filters = member_filters.items, .member_headers = opt_member_headers orelse false, .elements = elements orelse .initFill(true), .redact = redact, .relocs = opt_relocs orelse false, .section_filters = section_filters.items, .section_headers = opt_section_headers orelse false, .strings = opt_strings orelse false, .symbol_filters = symbol_filters.items, .symbols = opt_symbols orelse false, .tls = opt_tls orelse false, }; var file = std.Io.Dir.cwd().openFile(io, opts.input_path, .{}) catch |err| fatal("failed to open {s}: {t}", .{ opts.input_path, err }); defer file.close(io); var buffer: [4096]u8 = undefined; var file_reader = file.reader(io, &buffer); var stdout_writer = std.Io.File.stdout().writerStreaming(io, &stdout_buffer); const ctx: DumpContext = .{ .gpa = init.gpa, .opts = &opts, .fr = &file_reader, .w = &stdout_writer.interface, }; dump(&ctx) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, error.WriteFailed => return stdout_writer.err.?, error.UnknownFile => fatal("unrecognized file: {s}", .{opts.input_path}), error.ParseFailure => {}, else => |e| return e, }; try stdout_writer.flush(); } fn dump(d: *const DumpContext) !void { const r = &d.fr.interface; try r.fill(4); elf: { if (!mem.eql(u8, r.buffered()[0..4], std.elf.MAGIC)) break :elf; return elf.dump(r, d.w); } macho: { if (mem.readInt(u32, r.buffered()[0..4], .little) != std.macho.MH_MAGIC_64) break :macho; return macho.dump(r, d.w); } wasm: { comptime assert(std.wasm.magic.len == 4); if (!mem.eql(u8, r.buffered()[0..4], &std.wasm.magic)) break :wasm; return wasm.dump(r, d.w); } coff: { const ext = std.fs.path.extension(d.opts.input_path); const basename = std.fs.path.basename(d.opts.input_path); if (std.mem.eql(u8, ext, ".exe") or std.mem.eql(u8, ext, ".dll")) { if (!mem.eql(u8, r.buffered()[0..2], "MZ")) break :coff; try r.discardAll(std.coff.pe_pointer_offset); const sig_offset = try r.takeInt(u32, .little); try d.fr.seekTo(sig_offset); const sig = try r.take(4); if (!std.mem.eql(u8, sig, std.coff.pe_signature)) { try d.w.print("invalid PE signature: {x}", .{sig}); return error.ParseFailure; } if (d.element(.@"file-type")) { try d.w.print("{s}: PE/COFF image\n\n", .{basename}); if (d.element(.newlines)) try d.w.writeByte('\n'); } return coff.dumpObject(d, true, basename); } else if (std.mem.eql(u8, ext, ".lib")) { r.fill(std.coff.archive_signature.len) catch break :coff; if (!mem.eql(u8, r.buffered()[0..std.coff.archive_signature.len], std.coff.archive_signature)) break :coff; if (d.element(.@"file-type")) { try d.w.print("{s}: COFF archive\n", .{basename}); if (d.element(.newlines)) try d.w.writeByte('\n'); } return coff.dumpArchive(d); } else if (std.mem.eql(u8, ext, ".obj")) { if (d.element(.@"file-type")) { try d.w.print("{s}: COFF object\n", .{basename}); if (d.element(.newlines)) try d.w.writeByte('\n'); } return coff.dumpObject(d, false, basename); } } return error.UnknownFile; } const DumpContext = struct { gpa: std.mem.Allocator, opts: *const Options, fr: *Io.File.Reader, w: *Io.Writer, fn element(self: *const DumpContext, e: Element) bool { return self.opts.elements.get(e); } fn redacted(self: *const DumpContext, opt_kind: ?FieldKind) bool { const kind = opt_kind orelse return false; return self.opts.redact.get(kind); } fn failParse( ctx: *const DumpContext, comptime fmt: []const u8, args: anytype, ) noreturn { std.log.err("error parsing '{s}'", .{std.fs.path.basename(ctx.opts.input_path)}); fatal(fmt, args); } }; const elf = struct { fn dump(r: *Io.Reader, w: *Io.Writer) !void { _ = r; try w.writeAll("TODO dump elf file\n"); } }; const macho = struct { fn dump(r: *Io.Reader, w: *Io.Writer) !void { _ = r; try w.writeAll("TODO dump macho file\n"); } }; const wasm = struct { fn dump(r: *Io.Reader, w: *Io.Writer) !void { _ = r; try w.writeAll("TODO dump wasm file\n"); } }; const coff = struct { const DIRECTORY_ENTRY = std.coff.IMAGE.DIRECTORY_ENTRY; const Section = struct { header: std.coff.SectionHeader, name: []const u8, fn rvaFileOffset(section: *const Section, rva: u32) !u32 { if (rva < section.header.virtual_address or rva >= section.header.virtual_address + section.header.size_of_raw_data) return error.OutOfBounds; return section.header.pointer_to_raw_data + (rva - section.header.virtual_address); } }; const ArchiveHeader = struct { name: []const u8, date: u40, user_id: u20, group_id: u20, file_mode: u24, size: u34, pub fn fromRaw(d: *const DumpContext, raw_header: *const std.coff.ArchiveMemberHeader, opt_longnames: ?[]const u8) @This() { const name = raw_header.parseName(opt_longnames) catch |err| switch (err) { error.BadName => d.failParse("malformed member name: '{s}'", .{&raw_header.name}), error.NoLongNames => d.failParse("member uses a long name, but there was no longnames member", .{}), }; return .{ .name = name, .date = raw_header.parseDate() catch |err| d.failParse("unable to parse date '{s}' in member '{s}': {t}", .{ raw_header.date, name, err }), .user_id = raw_header.parseUserId() catch |err| d.failParse("unable to parse user_id '{s}' in member '{s}': {t}", .{ raw_header.user_id, name, err }), .group_id = raw_header.parseGroupId() catch |err| d.failParse("unable to parse group_id '{s}' in member '{s}': {t}", .{ raw_header.group_id, name, err }), .file_mode = raw_header.parseFileMode() catch |err| d.failParse("unable to parse file_mode '{s}' in member '{s}': {t}", .{ raw_header.file_mode, name, err }), .size = raw_header.parseSize() catch |err| d.failParse("unable to parse size '{s}' in member '{s}': {t}", .{ raw_header.size, name, err }), }; } }; fn dumpArchive(d: *const DumpContext) !void { const gpa = d.gpa; const fr = d.fr; const w = d.w; const r = &fr.interface; r.toss(std.coff.archive_signature.len); const Member = struct { offset: u32, order: ?u32, }; var members: std.ArrayList(Member) = .empty; defer members.deinit(gpa); var symbol_member_indices: std.ArrayList(u32) = .empty; defer symbol_member_indices.deinit(gpa); var opt_expected_kind: ?std.coff.ArchiveMemberHeader.Kind = .first_linker; var opt_longnames: ?[]const u8 = null; defer if (opt_longnames) |l| gpa.free(l); var pos = fr.logicalPos(); const size = try fr.getSize(); while (pos < size) : (pos = fr.logicalPos()) { if ((pos & 1) != 0) try r.discardAll(1); const raw_header = try r.takeStruct(std.coff.ArchiveMemberHeader, .little); const header: ArchiveHeader = .fromRaw(d, &raw_header, opt_longnames); if (!std.mem.eql(u8, &raw_header.end_of_header, std.coff.archive_end_of_header)) return d.failParse("malformed end-of-header field in member '{s}': {x}", .{ header.name, raw_header.end_of_header }); const dump_header = (d.opts.member_headers and filterMatches(d.opts.member_filters, header.name)) or (d.opts.linker_member == opt_expected_kind); if (dump_header) try dumpArchiveHeader(d, &header, @intCast(pos)); const member_end = fr.logicalPos() + header.size; if (member_end > size) return d.failParse("out-of-bounds length 0x{x} in member '{s}'", .{ header.size, header.name }); if (opt_expected_kind) |expected_kind| switch (expected_kind) { .first_linker => { if (!std.mem.eql(u8, header.name, "/")) return d.failParse("expected first linker member, found '{s}'", .{header.name}); const num_symbols = try r.takeInt(u32, .big); if (dump_header) try w.print( \\{t: >16} type \\ | {d} symbols \\ , .{ expected_kind, num_symbols }); if (d.opts.linker_member == .first_linker) { if (d.element(.@"table-header")) try w.writeAll( \\ \\Archive symbols: \\& Member Symbol \\ ); const offsets = try r.readAllocAll(gpa, num_symbols * 4); defer gpa.free(offsets); for (0..num_symbols) |symbol_i| { const symbol = r.takeDelimiter(0) catch |err| return d.failParse("unable to read first linker member string table: {t}", .{err}); if (!filterMatches(d.opts.symbol_filters, symbol.?)) continue; const offset = std.mem.readInt(u32, offsets[symbol_i * 4 ..][0..4], .big); try w.print("{f} {s}\n", .{ fmtIntField(d, offset, .{ .kind = .va }), symbol.?, }); } } if (dump_header and d.element(.newlines)) try w.writeByte('\n'); try fr.seekTo(member_end); opt_expected_kind = .second_linker; continue; }, .second_linker => { if (!std.mem.eql(u8, header.name, "/")) return d.failParse("expected second linker member, found '{s}'", .{header.name}); const num_members = try r.takeInt(u32, .little); pos = fr.logicalPos(); if (pos + num_members * @sizeOf(u32) > member_end) return d.failParse("invalid member count 0x{x} in second linker member", .{num_members}); try members.ensureTotalCapacity(gpa, num_members); for (0..num_members) |_| members.addOneAssumeCapacity().* = .{ .offset = try r.takeInt(u32, .little), .order = null, }; const num_symbols = try r.takeInt(u32, .little); pos = fr.logicalPos(); if (pos + num_symbols * @sizeOf(u16) > member_end) return d.failParse("invalid symbol count 0x{x} in second linker member", .{num_symbols}); if (dump_header) try w.print( \\{t: >16} type \\ | {f} symbols \\ | {f} members \\ , .{ expected_kind, fmtIntField(d, num_symbols, .{ .kind = .size, .width = .auto }), fmtIntField(d, num_members, .{ .kind = .size, .width = .auto }), }); try symbol_member_indices.ensureTotalCapacity(gpa, num_symbols); for (0..num_symbols) |order| { const index = (try r.takeInt(u16, .little)) - 1; if (index >= members.items.len) return d.failParse("invalid member index 0x{x} in seconds linker member indices array", .{index}); symbol_member_indices.addOneAssumeCapacity().* = index; if (members.items[index].order == null) members.items[index].order = @intCast(order); } if (d.opts.exports and d.opts.exports_sort) { std.sort.pdq(Member, members.items, {}, struct { fn lessThan(ctx: void, lhs: Member, rhs: Member) bool { _ = ctx; if (lhs.order == null and rhs.order == null) return lhs.offset < rhs.offset else if (lhs.order) |lhs_order| return if (rhs.order) |rhs_order| lhs_order < rhs_order else false else if (rhs.order) |rhs_order| return if (lhs.order) |lhs_order| lhs_order < rhs_order else true else unreachable; } }.lessThan); } if (d.opts.linker_member == .second_linker) { if (d.element(.@"table-header")) try w.writeAll( \\ \\Archive Symbols: \\& Member Symbol \\ ); pos = fr.logicalPos(); var symbol_i: u32 = 0; while (pos < member_end and symbol_i < num_symbols) : ({ pos = fr.logicalPos(); symbol_i += 1; }) { const symbol_name = if (r.takeDelimiter(0) catch |err| switch (err) { error.StreamTooLong => null, else => |e| return e, }) |n| n else return d.failParse("unterminated string found in second linker member", .{}); if (!filterMatches(d.opts.symbol_filters, symbol_name)) continue; try w.print("{f} {s}\n", .{ fmtIntField( d, members.items[symbol_member_indices.items[symbol_i]].offset, .{ .kind = .va }, ), symbol_name, }); } if (symbol_i != num_symbols) return d.failParse( " expected {d} entries in second linker member string table, but found {d}", .{ num_symbols, symbol_i }, ); } if (d.element(.newlines)) try w.writeByte('\n'); try fr.seekTo(member_end); opt_expected_kind = .longnames; continue; }, .longnames => { // This member is optional if (std.mem.eql(u8, header.name, "//")) { opt_longnames = try r.readAllocAll(gpa, header.size); if (dump_header) try w.print("{t: >16} type\n", .{expected_kind}); if (d.opts.linker_member == .longnames) { if (d.element(.@"table-header")) try w.print( \\ \\Longnames (0x{x} bytes): \\ , .{opt_longnames.?.len}); var lr = Io.Reader.fixed(opt_longnames.?); while (try lr.takeDelimiter(0)) |str| { try w.writeAll(str); try w.writeByte('\n'); } } if (d.element(.newlines)) try w.writeByte('\n'); } opt_expected_kind = null; break; }, else => unreachable, }; } if (opt_expected_kind) |expected_kind| switch (expected_kind) { .first_linker => d.failParse("missing first linker member", .{}), .second_linker => d.failParse("missing second linker member", .{}), else => {}, }; for (members.items, 0..) |member, member_i| { fr.seekTo(member.offset) catch |err| d.failParse("unable to read member {d} at offset 0x{x}: {t}", .{ member_i, member.offset, err }); const raw_header = try r.takeStruct(std.coff.ArchiveMemberHeader, .little); const header: ArchiveHeader = .fromRaw(d, &raw_header, opt_longnames); if (!filterMatches(d.opts.member_filters, header.name)) continue; const member_sig = try r.peek(4); const machine: std.coff.IMAGE.FILE.MACHINE = @fromBackingInt(@intCast(std.mem.readInt(u16, member_sig[0..2], .little))); const sig = std.mem.readInt(u16, member_sig[2..4], .little); const is_imp_lib = machine == std.coff.IMAGE.FILE.MACHINE.UNKNOWN and sig == 0xffff; if (d.opts.member_headers) try dumpArchiveHeader(d, &header, member.offset); if (d.opts.member_headers or (d.opts.exports and is_imp_lib)) { if (is_imp_lib) { const imp_header = try r.takeStruct(std.coff.ImportHeader, .little); const sym_name = (try r.takeDelimiter(0)).?; const imp_dll = (try r.takeDelimiter(0)).?; if (!filterMatches(d.opts.symbol_filters, sym_name)) continue; if (d.element(.@"header-name")) try w.writeAll("\nImport header:\n"); try dumpHeader(d, std.coff.ImportHeader, &imp_header, struct { pub fn sig1(_: *const DumpContext, _: *const std.coff.ImportHeader) !void {} pub fn sig2(_: *const DumpContext, _: *const std.coff.ImportHeader) !void {} pub fn types(id: *const DumpContext, h: *const std.coff.ImportHeader) !void { try id.w.print( \\{t: >16} import_type \\{t: >16} name_type \\ , .{ h.types.type, h.types.name_type }); } }); const imp_name = imp_name: switch (imp_header.types.name_type) { .NAME_NOPREFIX, .NAME_UNDECORATE, => |tag| { var imp_name = std.mem.trimStart(u8, sym_name, "?@_"); if (tag == .NAME_UNDECORATE) imp_name = std.mem.sliceTo(imp_name, '@'); break :imp_name imp_name; }, else => sym_name, }; try w.print( \\ symbol name | {s} \\ import name | {s} \\ dll | {s} \\ , .{ sym_name, imp_name, imp_dll, }); } else { try w.writeAll(" COFF object type\n"); } if (d.element(.newlines)) try w.writeByte('\n'); } if (is_imp_lib) continue; if (d.opts.section_headers or d.opts.file_headers or d.opts.relocs or d.opts.strings or d.opts.symbols) { const member_name = if (d.element(.@"member-path")) header.name else std.fs.path.basename(header.name); if (d.element(.@"file-type")) { try w.print("{s}({s}): COFF object\n", .{ std.fs.path.basename(d.opts.input_path), member_name, }); if (d.element(.newlines)) try w.writeByte('\n'); } try dumpObject(d, false, member_name); } } } fn dumpObject( d: *const DumpContext, is_image: bool, obj_name: []const u8, ) !void { const gpa = d.gpa; const fr = d.fr; const w = d.w; const file_location = fr.logicalPos(); const r = &fr.interface; const header = r.takeStruct(std.coff.Header, .little) catch |err| return d.failParse("unable to read COFF header: {t}", .{err}); if (d.opts.file_headers) { if (d.element(.@"header-name")) try w.writeAll("COFF Header:\n"); try dumpHeader(d, std.coff.Header, &header, struct {}); if (d.element(.newlines)) try w.writeByte('\n'); } switch (header.machine) { _ => return d.failParse("unknown machine type: {x}", .{header.machine}), else => {}, } var known_dirs: [DIRECTORY_ENTRY.len]std.coff.ImageDataDirectory = undefined; const needs_data_dirs = d.opts.exports or d.opts.imports or d.opts.tls; const ImageInfo = struct { data_dirs: []const std.coff.ImageDataDirectory, magic: std.coff.OptionalHeader.Magic, image_base: u64, }; const image_info: ?ImageInfo = if (header.size_of_optional_header > 0) image_info: { if (!d.opts.file_headers and !needs_data_dirs) { try fr.seekBy(header.size_of_optional_header); break :image_info null; } if (d.opts.file_headers and d.element(.@"header-name")) try w.writeAll("COFF Optional Header:\n"); const magic: std.coff.OptionalHeader.Magic = @fromBackingInt(@intCast(try r.peekInt(u16, .little))); const num_directory_entries, const image_base = switch (magic) { inline .PE32, .@"PE32+" => |v| num_data_dirs: { const OptionalHeader = if (v == .PE32) std.coff.OptionalHeader.PE32 else std.coff.OptionalHeader.@"PE32+"; const optional_header = r.takeStruct(OptionalHeader, .little) catch |err| return d.failParse("unable to read optional header: {t}", .{err}); if (d.opts.file_headers) { try dumpHeader(d, OptionalHeader, &optional_header, struct { pub fn base_of_code(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void { const base = @as(*const OptionalHeader, @ptrCast(@alignCast(h))).image_base; try dumpRvaField(id, @src().fn_name, h.base_of_code, base); } pub fn address_of_entry_point(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void { const base = @as(*const OptionalHeader, @ptrCast(@alignCast(h))).image_base; try dumpRvaField(id, @src().fn_name, h.base_of_code, base); } pub fn major_linker_version(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void { try dumpVersionField(id.w, "linker_version", h.major_linker_version, h.minor_linker_version); } pub fn minor_linker_version(_: *const DumpContext, _: *const std.coff.OptionalHeader) !void {} pub fn major_operating_system_version(id: *const DumpContext, h: *const OptionalHeader) !void { try dumpVersionField( id.w, "operating_system_version", h.major_operating_system_version, h.minor_operating_system_version, ); } pub fn minor_operating_system_version(_: *const DumpContext, _: *const OptionalHeader) !void {} pub fn major_image_version(id: *const DumpContext, h: *const OptionalHeader) !void { try dumpVersionField(id.w, "image_version", h.major_image_version, h.minor_image_version); } pub fn minor_image_version(_: *const DumpContext, _: *const OptionalHeader) !void {} pub fn major_subsystem_version(id: *const DumpContext, h: *const OptionalHeader) !void { try dumpVersionField(id.w, "subsystem_version", h.major_subsystem_version, h.minor_subsystem_version); } pub fn minor_subsystem_version(_: *const DumpContext, _: *const OptionalHeader) !void {} }); if (d.element(.newlines)) try w.writeByte('\n'); } break :num_data_dirs .{ optional_header.number_of_rva_and_sizes, optional_header.image_base, }; }, else => return d.failParse("invalid optional header magic number: {x}", .{magic}), }; if (d.opts.file_headers and d.element(.@"header-name")) try w.writeAll("Data Directories:\n"); for (0..num_directory_entries) |dir_i| { const dir = r.takeStruct(std.coff.ImageDataDirectory, .little) catch |err| return d.failParse("unable to read data directory {x}: {t}", .{ dir_i, err }); if (dir_i < known_dirs.len) known_dirs[dir_i] = dir; if (d.opts.file_headers) try w.print( "{x: >16} {x: >8} {t}\n", .{ dir.virtual_address, dir.size, @as(DIRECTORY_ENTRY, @fromBackingInt(@intCast(dir_i))) }, ); } if (d.opts.file_headers and d.element(.newlines)) try w.writeByte('\n'); break :image_info .{ .data_dirs = known_dirs[0..@min(known_dirs.len, num_directory_entries)], .magic = magic, .image_base = image_base, }; } else if (is_image) { return d.failParse("image did not contain an optional header", .{}); } else null; // Section names in images don't use the string table, as they must fit inline in the header const load_string_table = (d.opts.strings or !is_image) and header.pointer_to_symbol_table > 0; const string_table = if (load_string_table) string_table: { const pos = fr.logicalPos(); fr.seekTo(file_location + header.pointer_to_symbol_table + header.number_of_symbols * std.coff.Symbol.sizeOf()) catch |err| return d.failParse("unable to seek to string table: {t}", .{err}); const string_table_len = r.peekInt(u32, .little) catch |err| return d.failParse("unable to read string table length: {t}", .{err}); const table = r.readAllocAll(gpa, string_table_len) catch |err| return d.failParse("unable to read string table: {t}", .{err}); try fr.seekTo(pos); break :string_table table; } else &.{}; defer gpa.free(string_table); if (d.opts.strings) { if (string_table.len > 0) { if (d.element(.@"table-header")) try w.print( \\String Table (0x{x} bytes): \\ , .{string_table.len}); var sr = Io.Reader.fixed(string_table[@sizeOf(u32)..]); while (try sr.takeDelimiter(0)) |str| { try w.writeAll(str); try w.writeByte('\n'); } if (d.element(.newlines)) try w.writeByte('\n'); } else try w.writeAll("String table not present\n"); } var sections: std.ArrayList(Section) = .empty; defer sections.deinit(gpa); var sections_with_data: u16 = 0; const load_sections = d.opts.section_headers or d.opts.symbols or d.opts.relocs or needs_data_dirs; if (load_sections) { if (d.opts.section_headers and d.element(.@"table-header")) try w.print( \\Sections in '{s}': \\Num Name RVA Virt Size Data Size & Data & Relocs & Lines # Relocs # Lines Flags \\ , .{obj_name}); try sections.resize(gpa, header.number_of_sections); for (sections.items, 0..) |*section, section_i| { section.header = r.takeStruct(std.coff.SectionHeader, .little) catch |err| return d.failParse("unable to read section header {x}: {t}", .{ section_i, err }); section.name = headerName(§ion.header.name, string_table) catch |err| switch (err) { error.Overflow, error.InvalidCharacter, => return d.failParse("unable to parse section name offset '{s}': {t}", .{ section.name, err, }), error.OutOfBounds => return d.failParse("section name offset '{s}' was out of bounds (>= {x})", .{ section.name, string_table.len, }), }; sections_with_data += @intFromBool(section.header.size_of_raw_data > 0); if (d.opts.section_headers) { if (!filterMatches(d.opts.section_filters, section.name)) continue; const raw_name = std.mem.sliceTo(§ion.header.name, 0); try w.print( "{x: >3} {s: <8} {f} {f} {f} {f} {f} {f} {f} {f} {x:0>8} |", .{ section_i + 1, raw_name, fmtIntField(d, section.header.virtual_address, .{ .kind = .va }), fmtIntField(d, section.header.virtual_size, .{ .kind = .size, .width = .{ .explicit = 9 } }), fmtIntField(d, section.header.size_of_raw_data, .{ .kind = .size, .width = .{ .explicit = 9 } }), fmtIntField(d, section.header.pointer_to_raw_data, .{ .kind = .va }), fmtIntField(d, section.header.pointer_to_relocations, .{ .kind = .va }), fmtIntField(d, section.header.pointer_to_linenumbers, .{ .kind = .va }), fmtIntField(d, section.header.number_of_relocations, .{ .kind = .va }), fmtIntField(d, section.header.number_of_linenumbers, .{ .kind = .va }), @as(u32, @bitCast(section.header.flags)), }, ); try dumpFlags(w, "{s}", std.coff.SectionHeader.Flags, §ion.header.flags, 1); if (section.name.len > 8) try w.print("\n | {s}", .{section.name}); try w.writeByte('\n'); } } if (d.opts.section_headers and d.element(.newlines)) try w.writeByte('\n'); } var symbols: std.ArrayList(struct { name: []const u8, section_number: std.coff.SectionNumber, }) = .empty; defer symbols.deinit(gpa); var name_arena: std.heap.ArenaAllocator = .init(gpa); defer name_arena.deinit(); if (d.opts.relocs) try symbols.ensureUnusedCapacity(gpa, header.number_of_symbols); if (d.opts.symbols or d.opts.relocs) { if (header.pointer_to_symbol_table > 0) { fr.seekTo(file_location + header.pointer_to_symbol_table) catch |err| return d.failParse("unable to seek to symbol table: {t}", .{err}); if (d.opts.symbols and d.element(.@"table-header")) try w.print( \\Symbols in '{s}': \\ Ord Value Sect Type Storage Name \\ , .{obj_name}); const symbol_size = std.coff.Symbol.sizeOf(); var symbol_i: u32 = 0; while (symbol_i < header.number_of_symbols) { var symbol: std.coff.Symbol = undefined; const symbol_bytes = r.take(symbol_size) catch |err| return d.failParse("unable to read symbol {x}: {t}", .{ symbol_i, err }); @memcpy(std.mem.asBytes(&symbol)[0..symbol_size], symbol_bytes); if (native_endian != .little) std.mem.byteSwapAllFields(std.coff.Symbol, &symbol); const aux_symbols = if (symbol.number_of_aux_symbols > 0) try r.take(symbol_size * symbol.number_of_aux_symbols) else &.{}; defer symbol_i += symbol.number_of_aux_symbols + 1; const name = if (std.mem.eql(u8, symbol.name[0..4], "\x00\x00\x00\x00")) name: { const index = std.mem.readInt(u32, symbol.name[4..], .little); if (index >= string_table.len) return d.failParse("invalid name offset for symbol {x} ({x} >= {x})", .{ symbol_i, index, string_table.len, }); break :name std.mem.sliceTo(string_table[index..], 0); } else try name_arena.allocator().dupe(u8, std.mem.sliceTo(&symbol.name, 0)); if (d.opts.relocs) symbols.appendNTimesAssumeCapacity(.{ .name = name, .section_number = symbol.section_number, }, 1 + symbol.number_of_aux_symbols); if (!d.opts.symbols or !filterMatches(d.opts.symbol_filters, name)) continue; try w.print("{f} {x:0>8} ", .{ fmtIntField(d, @as(u16, @intCast(symbol_i)), .{ .kind = .ord }), symbol.value, }); try switch (symbol.section_number) { .UNDEFINED => w.writeAll("UNDEF"), .ABSOLUTE => w.writeAll(" ABS"), .DEBUG => w.writeAll("DEBUG"), else => |v| { const backing = @backingInt(v); const fmt = "{x: >5}"; if (backing >= 0) try w.print(fmt, .{@as(u15, @intCast(backing))}) else try w.print(fmt, .{backing}); }, }; try w.print("{t: >5}", .{symbol.type.base_type}); if (switch (symbol.type.complex_type) { .NULL => " ", .POINTER => "* ", .FUNCTION => "()", .ARRAY => "[]", else => null, }) |suffix| try w.writeAll(suffix) else try w.print("{x}", .{symbol.type.complex_type}); try w.print("{t: >16} | {s}\n", .{ symbol.storage_class, name }); for (0..symbol.number_of_aux_symbols) |aux_i| { _ = aux_i; try w.writeAll(" |"); if (symbol.storage_class == .EXTERNAL and symbol.type == std.coff.SymType{ .complex_type = .FUNCTION, .base_type = .NULL, } and @backingInt(symbol.section_number) > 0) { try w.writeAll("TODO function aux symbol"); } else if (symbol.type == std.coff.SymType{ .complex_type = .FUNCTION, .base_type = .NULL, } and (std.mem.eql(u8, name, ".bf") or std.mem.eql(u8, name, ".ef"))) { try w.writeAll("TODO bf / ef aux symbol"); } else if (symbol.storage_class == .WEAK_EXTERNAL and symbol.section_number == .UNDEFINED) { if (symbol.value != 0) return d.failParse( "invalid value 0x{x} for weak external symbol 0x{x}", .{ symbol.value, symbol_i }, ); var weak_external: std.coff.WeakExternalDefinition = undefined; @memcpy(std.mem.asBytes(&weak_external)[0..symbol_size], aux_symbols[0..symbol_size]); if (native_endian != .little) std.mem.byteSwapAllFields(std.coff.WeakExternalDefinition, &weak_external); if (weak_external.tag_index >= header.number_of_symbols) return d.failParse( "invalid tag_index 0x{x} for weak external symbol 0x{x}", .{ weak_external.tag_index, symbol_i }, ); if (d.redacted(.ord)) try w.print(" Weak External [falls back to relative ordinal {x:0>8} via {t}]", .{ @as(i64, weak_external.tag_index) - symbol_i, weak_external.flag, }) else try w.print(" Weak External [falls back to ordinal {x:0>8} via {t}]", .{ weak_external.tag_index, weak_external.flag, }); } else if (symbol.storage_class == .FILE) { if (!std.mem.eql(u8, name, ".file")) { try w.print(" !! unexpected symbol name '{s}' for file symbol 0x{x}", .{ name, symbol_i }); continue; } const filename = std.mem.sliceTo(aux_symbols, 0); try w.print(" File '{s}'", .{filename}); break; } else if (symbol.storage_class == .STATIC and symbol.type == std.coff.SymType{ .complex_type = .NULL, .base_type = .NULL, } and symbol.value == 0 and switch (symbol.section_number) { .UNDEFINED, .DEBUG, .ABSOLUTE => false, else => |sn| @backingInt(sn) > 0, }) { const section_i: u15 = @intCast(@backingInt(symbol.section_number) - 1); try w.writeAll(" Section "); if (section_i >= sections.items.len) { try w.print(" !! invalid section number: {x}", .{section_i}); continue; } var section_def: std.coff.SectionDefinition = undefined; @memcpy(std.mem.asBytes(§ion_def)[0..symbol_size], aux_symbols[0..symbol_size]); if (native_endian != .little) std.mem.byteSwapAllFields(std.coff.SectionDefinition, §ion_def); const section = §ions.items[section_i]; if (section_def.number_of_relocations != section.header.number_of_relocations) { try w.print( " !! relocation count did not match section header: {d} vs {d}", .{ section_def.number_of_relocations, section.header.number_of_relocations }, ); continue; } if (section_def.number_of_linenumbers != section.header.number_of_linenumbers) { try w.print( " !! line number count did not match section header: {d} vs {d}", .{ section_def.number_of_linenumbers, section.header.number_of_linenumbers }, ); continue; } try w.print(" [size {f} chksum {x:0>8} relocs {x:0>4} lines {x:0>4}]", .{ fmtIntField(d, section_def.length, .{ .kind = .size, .zero_fill = true }), section_def.checksum, section_def.number_of_relocations, section_def.number_of_linenumbers, }); switch (section_def.selection) { .NONE => {}, else => |selection| { try w.print(" COMDAT({t}", .{selection}); if (selection == .ASSOCIATIVE) try w.print("->{x}", .{section_def.number}); try w.writeAll(")"); }, } } try w.writeByte('\n'); } } if (d.opts.symbols and d.element(.newlines)) try w.writeByte('\n'); } else if (d.opts.symbols) { try w.writeAll("No symbol table found\n"); } } if (d.opts.relocs) { const relocation_size = std.coff.Relocation.sizeOf(); for (sections.items, 0..) |section, section_i| { if (section.header.pointer_to_relocations == 0) continue; if (d.element(.@"table-header")) try w.print( \\Relocs for section {x} '{s}' in {s}: \\ Offset Type Symbol -> Sect Name \\ , .{ section_i + 1, section.name, obj_name }); fr.seekTo(file_location + section.header.pointer_to_relocations) catch |err| return d.failParse("unable to seek to section {x} relocation table: {t}", .{ section_i + 1, err }); for (0..section.header.number_of_relocations) |reloc_i| { var reloc: std.coff.Relocation = undefined; @memcpy(std.mem.asBytes(&reloc)[0..relocation_size], try r.take(relocation_size)); if (native_endian != .little) std.mem.byteSwapAllFields(std.coff.Relocation, &reloc); const sym = &symbols.items[reloc.symbol_table_index]; if (!filterMatches(d.opts.symbol_filters, sym.name)) continue; try w.print("{f} ", .{ fmtIntField(d, reloc.virtual_address, .{ .kind = .va, .zero_fill = true }), }); switch (header.machine) { _ => unreachable, inline else => |m| switch (m.RelocationType()) { void => try w.writeAll("(unknown arch)"), else => |RelocationType| try w.print( "{t: <17} ", .{@as(RelocationType, @fromBackingInt(@intCast(reloc.type)))}, ), }, } if (reloc.symbol_table_index >= symbols.items.len) return d.failParse( "reloc {x} in section {x} has out-of-bounds symbol index {x}", .{ reloc_i, section_i + 1, reloc.symbol_table_index }, ); try w.print("{f} {f} | {s}\n", .{ fmtIntField(d, reloc.symbol_table_index, .{ .kind = .ord }), fmtSectionNumber(sym.section_number), sym.name, }); } if (d.element(.newlines)) try w.writeByte('\n'); } } // Sections indices with raw data, sorted by RVA const rva_index = if (needs_data_dirs) rva_index: { const rva_index = try gpa.alloc(u16, sections_with_data); var indices_i: u16 = 0; for (sections.items, 0..) |*section, i| { if (section.header.size_of_raw_data == 0) continue; rva_index[indices_i] = @intCast(i); indices_i += 1; } const Context = struct { indices: []u16, sections: []const Section, pub fn lessThan(ctx: @This(), lhs: usize, rhs: usize) bool { return ctx.sections[ctx.indices[lhs]].header.virtual_address < ctx.sections[ctx.indices[rhs]].header.virtual_address; } pub fn swap(ctx: @This(), lhs: usize, rhs: usize) void { std.mem.swap(u16, &ctx.indices[lhs], &ctx.indices[rhs]); } }; std.sort.pdqContext(0, rva_index.len, Context{ .indices = rva_index, .sections = sections.items, }); break :rva_index rva_index; } else &.{}; defer gpa.free(rva_index); if (d.opts.exports) exports: { if (try seekToDataDirectory( d, rva_index, sections.items, (image_info orelse { try w.writeAll("COFF objects do not contain an export data directory"); break :exports; }).data_dirs, .EXPORT, )) |section_index| { const export_dir = r.takeStruct(std.coff.ExportDirectoryTable, .little) catch |err| return d.failParse("unable to read export directory: {t}", .{err}); try w.print("Export directory:\n", .{}); try dumpHeader(d, std.coff.ExportDirectoryTable, &export_dir, struct { pub fn major_version(id: *const DumpContext, h: *const std.coff.ExportDirectoryTable) !void { try dumpVersionField(id.w, "version", h.major_version, h.minor_version); } pub fn minor_version(_: *const DumpContext, _: *const std.coff.ExportDirectoryTable) !void {} }); const section = sections.items[section_index]; const name_loc = section.rvaFileOffset(export_dir.name_rva) catch return d.failParse( "export name rva 0x{x} was not within the export section", .{export_dir.name_rva}, ); const eat_loc = section.rvaFileOffset(export_dir.export_address_table_rva) catch return d.failParse( "export address table rva 0x{x} was not within the export section", .{export_dir.export_address_table_rva}, ); const name_pointer_loc = section.rvaFileOffset(export_dir.name_pointer_table_rva) catch return d.failParse( "export name pointer table rva 0x{x} was not within the export section", .{export_dir.name_pointer_table_rva}, ); const ord_loc = section.rvaFileOffset(export_dir.ordinal_table_rva) catch return d.failParse( "export ordinal table rva 0x{x} was not within the export section", .{export_dir.ordinal_table_rva}, ); // All the variable length fields should be contained within this directory. // Read it entirely to avoid needing to seek per-name when iterating. const dir = image_info.?.data_dirs[@backingInt(DIRECTORY_ENTRY.EXPORT)]; const dir_end_rva = dir.virtual_address + dir.size; const dir_loc = fr.logicalPos(); const dir_slice = try r.readAllocAll(gpa, dir.size); defer gpa.free(dir_slice); const dll_name = std.mem.sliceTo(dir_slice[name_loc - dir_loc ..], 0); if (d.element(.@"table-header")) try w.print( \\ \\Exports from {s}: \\ Ord Hint RVA Name \\ , .{dll_name}); const name_pointers = dir_slice[name_pointer_loc - dir_loc ..][0 .. export_dir.number_of_names * @sizeOf(u32)]; const ords = dir_slice[ord_loc - dir_loc ..][0 .. export_dir.number_of_names * @sizeOf(u16)]; const addrs = dir_slice[eat_loc - dir_loc ..][0 .. export_dir.number_of_entries * @sizeOf(u32)]; const name_rva_to_offset = dir.virtual_address + @sizeOf(std.coff.ExportDirectoryTable); for (0..export_dir.number_of_names) |name_i| { const name_rva = std.mem.readInt(u32, name_pointers[name_i * @sizeOf(u32) ..][0..@sizeOf(u32)], .little); const name = std.mem.sliceTo(dir_slice[name_rva - name_rva_to_offset ..], 0); if (!filterMatches(d.opts.symbol_filters, name)) continue; const ord = std.mem.readInt(u16, ords[name_i * @sizeOf(u16) ..][0..@sizeOf(u16)], .little); const addr = std.mem.readInt(u32, addrs[@as(u32, ord) * @sizeOf(u32) ..][0..@sizeOf(u32)], .little); try w.print("{f} {f} ", .{ fmtIntField(d, @as(u16, @intCast(export_dir.ordinal_base + ord)), .{ .kind = .ord }), fmtIntField(d, @as(u16, @intCast(name_i)), .{ .kind = .ord }), }); const is_forwarder = addr >= dir.virtual_address and addr < dir_end_rva; if (is_forwarder) { try w.writeAll("forwards"); } else { try w.print("{f}", .{fmtIntField(d, addr, .{ .kind = .rva })}); } try w.print(" | {s}", .{name}); if (is_forwarder) try w.print(" -> {s}", .{std.mem.sliceTo(dir_slice[addr - name_rva_to_offset ..], 0)}); try w.writeByte('\n'); } } } if (d.opts.imports) imports: { if (try seekToDataDirectory( d, rva_index, sections.items, (image_info orelse { try w.writeAll("COFF objects do not contain an import data directory"); break :imports; }).data_dirs, .IMPORT, )) |_| { const Entry = std.coff.ImportDirectoryEntry; var directory_entries: std.ArrayList(Entry) = .empty; defer directory_entries.deinit(gpa); while (true) { const entry = r.takeStruct(Entry, .little) catch |err| return d.failParse( "unable to read import directory entry {x}: {t}", .{ directory_entries.items.len, err }, ); if (std.mem.allEqual(u8, std.mem.asBytes(&entry), 0)) break; (try directory_entries.addOne(gpa)).* = entry; } for (directory_entries.items) |entry| { const name_section = sectionContainingRva( rva_index, sections.items, entry.name_rva, ) orelse return d.failParse( "import directory entry name rva 0x{x} was not found in any section", .{entry.name_rva}, ); const name_loc = sections.items[name_section].rvaFileOffset( entry.name_rva, ) catch unreachable; fr.seekTo(name_loc) catch |err| return d.failParse( "unable to seek to import directory entry name at 0x{x}: {t}", .{ name_loc, err }, ); const dll_name = (try r.takeDelimiter(0)).?; if (d.element(.@"header-name")) try w.print("Import table entry for {s}:\n", .{dll_name}); try dumpHeader(d, Entry, &entry, struct {}); if (d.element(.@"table-header")) try w.print( \\ \\ Ord Hint Name \\ , .{}); const ilt_section = sectionContainingRva( rva_index, sections.items, entry.import_lookup_table_rva, ) orelse return d.failParse( "import directory entry ilt rva 0x{x} was not found in any section", .{entry.import_lookup_table_rva}, ); const ilt_loc = sections.items[ilt_section].rvaFileOffset( entry.import_lookup_table_rva, ) catch unreachable; fr.seekTo(ilt_loc) catch |err| return d.failParse( "unable to seek to import directory ilt at 0x{x}: {t}", .{ ilt_loc, err }, ); switch (image_info.?.magic) { _ => try w.writeAll("(unknown magic)"), inline else => |m| { const TableEntry = std.coff.ImportLookupTableEntry(m); const null_entry: TableEntry = @bitCast(@as(@typeInfo(TableEntry).@"struct".backing_integer.?, 0)); var ilt_entries: std.ArrayList(TableEntry) = .empty; defer ilt_entries.deinit(gpa); while (true) { const table_entry = r.takeStruct(TableEntry, .little) catch |err| return d.failParse( "unable to read ilt entry {s}:{x}: {t}", .{ dll_name, ilt_entries.items.len, err }, ); if (table_entry == null_entry) break; (try ilt_entries.addOne(gpa)).* = table_entry; } for (ilt_entries.items, 0..) |ilt_entry, ilt_entry_i| { if (ilt_entry.is_ordinal) { try w.print("{x: >4}", .{ilt_entry.payload.ordinal.ordinal}); } else { const hint_section = sectionContainingRva( rva_index, sections.items, ilt_entry.payload.hint_name_rva, ) orelse return d.failParse( "import directory ilt entry 0x{x}'s hint rva 0x{x} was not found in any section", .{ ilt_entry_i, ilt_entry.payload.hint_name_rva }, ); const hint_loc = sections.items[hint_section].rvaFileOffset( ilt_entry.payload.hint_name_rva, ) catch unreachable; fr.seekTo(hint_loc) catch |err| return d.failParse( "unable to seek to ilt entry 0x{x}'s hint at 0x{x}: {t}", .{ ilt_entry_i, hint_loc, err }, ); const hint = r.takeInt(u16, .little) catch |err| return d.failParse( "unable to read import directory ilt entry 0x{x}'s hint: {t}", .{ ilt_entry_i, err }, ); const name = r.takeDelimiter(0) catch |err| return d.failParse( "unable to read import directory ilt entry 0x{x}'s name: {t}", .{ ilt_entry_i, err }, ); try w.print(" {x: >4} | {s}\n", .{ hint, name.? }); } } if (d.element(.newlines)) try w.writeByte('\n'); }, } } } } if (d.opts.tls) tls: { if (try seekToDataDirectory( d, rva_index, sections.items, (image_info orelse { try w.writeAll("COFF objects do not contain a TLS data directory"); break :tls; }).data_dirs, .TLS, )) |_| { switch (image_info.?.magic) { _ => try w.writeAll("(unknown magic)"), inline else => |m| { const TlsDirectoryEntry = std.coff.TlsDirectoryEntry(m); const tls_entry = r.takeStruct(TlsDirectoryEntry, .little) catch |err| return d.failParse("unable to read tls directory: {t}", .{err}); try w.writeAll("TLS Directory:\n"); try dumpHeader(d, TlsDirectoryEntry, &tls_entry, struct {}); try w.writeAll(" | "); if (tls_entry.characteristics.alignment == .NONE) { try w.writeAll("Alignment not specified"); } else { try w.print( "Alignment: {d}", .{tls_entry.characteristics.alignment.toByteUnits().?}, ); } try w.writeAll( \\ \\ \\TLS Callbacks: \\ Address \\ ); const callbacks_rva: u32 = @intCast(tls_entry.callbacks_va - image_info.?.image_base); const section_index = sectionContainingRva( rva_index, sections.items, callbacks_rva, ) orelse return d.failParse( "tls callbacks rva 0x{x} was not found in any section", .{callbacks_rva}, ); const callbacks_loc = sections.items[section_index] .rvaFileOffset(callbacks_rva) catch unreachable; fr.seekTo(callbacks_loc) catch |err| return d.failParse( "unable to seek to tls callbacks array at offset 0x{x}: {t}", .{ callbacks_loc, err }, ); while (true) { const callback_va = r.takeInt(@FieldType(TlsDirectoryEntry, "callbacks_va"), .little) catch |err| return d.failParse( "unable to read tls callbacks array: {t}", .{err}, ); try w.print("{f}\n", .{fmtIntField(d, callback_va, .{ .kind = .va })}); if (callback_va == 0) break; } if (d.element(.newlines)) try w.writeByte('\n'); }, } } } } fn seekToDataDirectory( d: *const DumpContext, rva_index: []const u16, sections: []const Section, data_dirs: []const std.coff.ImageDataDirectory, entry: DIRECTORY_ENTRY, ) !?u16 { if (@backingInt(entry) < data_dirs.len) blk: { const rva = data_dirs[@backingInt(entry)].virtual_address; if (rva == 0) break :blk; const section_index = sectionContainingRva(rva_index, sections, rva) orelse return d.failParse( "{t} directory rva 0x{x} was not found in any section", .{ entry, rva }, ); const file_offset = sections[section_index].rvaFileOffset(rva) catch unreachable; d.fr.seekTo(file_offset) catch |err| return d.failParse( "unable to seek to {t} directory at offset 0x{x}: {t}", .{ entry, file_offset, err }, ); return section_index; } try d.w.print("{t} directory was not present in optional header\n", .{entry}); return null; } fn sectionContainingRva( /// Indices into `sections` sorted by rva indices: []const u16, sections: []const Section, rva: u32, ) ?u16 { const Context = struct { rva: u32, sections: []const Section, fn order(ctx: @This(), section_index: u16) std.math.Order { const h = &ctx.sections[section_index].header; if (ctx.rva < h.virtual_address) return .lt; const end = h.virtual_address + h.size_of_raw_data; if (ctx.rva >= end) return .gt; return .eq; } }; const indices_index = std.sort.binarySearch(u16, indices, Context{ .rva = rva, .sections = sections, }, Context.order) orelse return null; return @intCast(indices[indices_index]); } fn headerName(raw: *const [8]u8, string_table: []const u8) ![]const u8 { return if (raw[0] == '/') name: { const name_offset = try std.fmt.parseUnsigned(u24, std.mem.sliceTo(raw[1..], 0), 10); if (name_offset >= string_table.len) return error.OutOfBounds; break :name std.mem.sliceTo(string_table[name_offset..], 0); } else std.mem.sliceTo(raw, 0); } fn fmtSectionNumber(section_number: std.coff.SectionNumber) std.fmt.Alt(std.coff.SectionNumber, sectionNumberString) { return .{ .data = section_number }; } fn sectionNumberString(section_number: std.coff.SectionNumber, w: *std.Io.Writer) std.Io.Writer.Error!void { try switch (section_number) { .UNDEFINED => w.writeAll("UNDEF"), .ABSOLUTE => w.writeAll(" ABS"), .DEBUG => w.writeAll("DEBUG"), else => |v| { const backing = @backingInt(v); const fmt = "{x: >5}"; if (backing >= 0) try w.print(fmt, .{@as(u15, @intCast(backing))}) else try w.print(fmt, .{backing}); }, }; } const FormatIntField = struct { val: ?u64, width: ?usize, zero_fill: bool, }; fn fmtIntField( d: *const DumpContext, val: anytype, params: struct { kind: ?FieldKind = null, width: union(enum) { fit_max, auto, explicit: usize, } = .fit_max, zero_fill: bool = false, }, ) std.fmt.Alt(FormatIntField, intFieldString) { return .{ .data = .{ .val = if (d.redacted(params.kind)) null else val, .width = switch (params.width) { .fit_max => @typeInfo(@TypeOf(val)).int.bits / 4, .auto => null, .explicit => |w| w, }, .zero_fill = params.zero_fill, }, }; } fn intFieldString(field: FormatIntField, w: *std.Io.Writer) std.Io.Writer.Error!void { if (field.val) |val| { try w.printInt(val, 16, .lower, .{ .width = field.width, .alignment = .right, .fill = if (field.zero_fill) '0' else ' ', }); } else try w.splatByteAll('x', field.width orelse 1); } fn dumpFlags(w: *Io.Writer, comptime fmt: []const u8, comptime T: type, flags: *const T, cols: u32) !void { const s = @typeInfo(T).@"struct"; inline for (s.field_names, s.field_types) |field_name, field_type| { if (field_type == bool and @field(flags, field_name)) { try w.splatByteAll(' ', cols); try w.print(fmt, .{field_name}); } } } fn dumpArchiveHeader(d: *const DumpContext, header: *const ArchiveHeader, pos: u32) !void { if (d.element(.@"header-name")) try d.w.print("Archive member at offset 0x{x}: '{s}'\n", .{ pos, header.name }); try dumpHeader(d, ArchiveHeader, header, struct { pub fn name(_: *const DumpContext, _: *const ArchiveHeader) !void {} pub fn file_mode(id: *const DumpContext, h: *const ArchiveHeader) !void { try id.w.print("{o: >16} file_mode\n", .{h.file_mode}); } }); } fn fieldKind(name: []const u8) ?FieldKind { if (std.mem.endsWith(u8, name, "_rva")) return .rva; if (std.mem.endsWith(u8, name, "_va") or std.mem.endsWith(u8, name, "_address") or std.mem.startsWith(u8, name, "pointer_")) return .va; if (std.mem.startsWith(u8, name, "number_") or std.mem.startsWith(u8, name, "size")) return .size; if (std.mem.startsWith(u8, name, "hint")) return .ord; return null; } fn dumpHeader( d: *const DumpContext, comptime T: type, header: *const T, Custom: type, ) !void { const s = @typeInfo(T).@"struct"; inline for (s.field_names, s.field_types) |field_name, field_type| { const val = &@field(header, field_name); if (@hasDecl(Custom, field_name)) { try @field(Custom, field_name)(d, header); } else { switch (@typeInfo(field_type)) { .int => try d.w.print("{f} {s}\n", .{ fmtIntField(d, val.*, .{ .kind = comptime fieldKind(field_name), .width = .{ .explicit = 16 }, }), field_name }), .@"enum" => try d.w.print("{x: >16} {s} ({t})\n", .{ val.*, field_name, val.* }), .@"struct" => |s_field| { switch (s_field.layout) { .auto, .@"extern", => try dumpHeader(d, field_type, val, Custom), .@"packed" => { try d.w.print("{x: >16} {s}\n", .{ @as(s_field.backing_integer.?, @bitCast(val.*)), field_name }); try dumpFlags(d.w, "| {s}\n", field_type, val, 15); }, } }, else => unreachable, } } } } fn dumpVersionField(w: *Io.Writer, name: []const u8, major: anytype, minor: anytype) !void { try w.print("{d: >13}.{x:0<2} {s}\n", .{ major, minor, name }); } fn dumpRvaField(d: *const DumpContext, name: []const u8, rva: u64, base: u64) !void { try d.w.print("{f} {s} ({f})\n", .{ fmtIntField(d, rva, .{ .kind = .rva }), name, fmtIntField(d, base + rva, .{ .kind = .va }), }); } }; fn filterMatches(filters: []const []const u8, val: []const u8) bool { return for (filters) |filter| { if (std.mem.containsAtLeast(u8, val, 1, filter)) break true; } else filters.len == 0; } const usage = \\Usage: zig objdump [options] file \\ \\Options: \\ -h, --help Print this help and exit \\ --all-headers Alias for --file-headers --linker-member=2 --member-headers --section-headers --relocs --symbols \\ --exports[=sort] Display exported symbols. \\ In the case of COFF import libraries, displays the symbol list and import headers. \\ Specify =sort to optionally sort the import headers by symbol name. \\ --file-headers Display file-format specific headers \\ --imports Display imported symbols \\ --linker-member[=1|2|longnames] (Coff) Display contents of the specified archive linker member (default 2) \\ --member-headers Display archive member headers \\ --elements=[e1],[e2],-[e3],... Select which formatting elements are displayed. Intended for snapshot testing. \\ file-type File type summary \\ header-name Name that precedes a header block \\ member-path Display full member paths. If removed, only basenames will be used. \\ newlines Newlines between output sections \\ table-header Table headers with column names \\ all (default) All of the above \\ --only-member=[name] Only consider archive members names that contain [name]. Can be specified multiple times. \\ --only-section=[name] Only consider section names that contain [name]. Can be specified multiple times. \\ --only-symbol=[name] Only consider symbol names that contain [name]. Can be specified multiple times. \\ --redact=[kind] Redact the specified field kind. Intended for snapshot testing. \\ rva Relative virtual addresses \\ va Virtual addresses and file offsets \\ ord Symbol ordinals / hints \\ size Sizes and lengths \\ all All of the above \\ --relocs Display relocations \\ -s, --snapshot Alias for --redact=all --elements=-all \\ --section-headers Display section headers \\ --strings Display string tables \\ --symbols Display symbol tables \\ --tls Display TLS information \\ ;