1const std = @import("std");
2const Io = std.Io;
3const fatal = std.process.fatal;
4const mem = std.mem;
5const assert = std.debug.assert;
6
7const builtin = @import("builtin");
8const native_endian = builtin.cpu.arch.endian();
9
10var stdout_buffer: [4000]u8 = undefined;
11
12const Options = struct {
13 exports: bool,
14 exports_sort: bool,
15 file_headers: bool,
16 imports: bool,
17 input_path: []const u8,
18 member_filters: []const []const u8 = &.{},
19 member_headers: bool,
20 elements: std.enums.EnumArray(Element, bool),
21 redact: std.enums.EnumArray(FieldKind, bool),
22 relocs: bool,
23 section_filters: []const []const u8 = &.{},
24 section_headers: bool,
25 symbol_filters: []const []const u8 = &.{},
26 strings: bool,
27 symbols: bool,
28 tls: bool,
29
30 // Coff-specific
31 linker_member: ?std.coff.ArchiveMemberHeader.Kind,
32};
33
34const FieldKind = enum {
35 va,
36 rva,
37 ord,
38 size,
39};
40
41const Element = enum {
42 @"file-type",
43 @"header-name",
44 @"member-path",
45 newlines,
46 @"table-header",
47};
48
49pub fn main(init: std.process.Init) !void {
50 const io = init.io;
51 const args = try init.minimal.args.toSlice(init.arena.allocator());
52 const arena = init.arena.allocator();
53
54 var i: usize = 1;
55
56 var opt_exports: ?bool = null;
57 var opt_exports_sort: ?bool = null;
58 var opt_file_headers: ?bool = null;
59 var opt_imports: ?bool = null;
60 var opt_input_path: ?[]const u8 = null;
61 var opt_linker_member: ?std.coff.ArchiveMemberHeader.Kind = null;
62 var opt_member_headers: ?bool = null;
63 var any_elements = false;
64 var elements: ?@FieldType(Options, "elements") = null;
65 var redact: @FieldType(Options, "redact") = .initFill(false);
66 var opt_relocs: ?bool = null;
67 var opt_section_headers: ?bool = null;
68 var opt_strings: ?bool = null;
69 var opt_symbols: ?bool = null;
70 var opt_tls: ?bool = null;
71 var section_filters: std.ArrayList([]const u8) = .empty;
72 var symbol_filters: std.ArrayList([]const u8) = .empty;
73 var member_filters: std.ArrayList([]const u8) = .empty;
74 while (i < args.len) : (i += 1) {
75 const arg = args[i];
76 if (mem.startsWith(u8, arg, "-")) {
77 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
78 return Io.File.stdout().writeStreamingAll(io, usage);
79 } else if (mem.eql(u8, arg, "--all-headers")) {
80 opt_file_headers = true;
81 opt_linker_member = .second_linker;
82 opt_member_headers = true;
83 opt_section_headers = true;
84 opt_symbols = true;
85 opt_relocs = true;
86 } else if (mem.startsWith(u8, arg, "--exports")) {
87 opt_exports = true;
88 opt_linker_member = .second_linker;
89 if (mem.eql(u8, arg["--exports".len..], "=sort"))
90 opt_exports_sort = true;
91 } else if (mem.eql(u8, arg, "--file-headers")) {
92 opt_file_headers = true;
93 } else if (mem.eql(u8, arg, "--imports")) {
94 opt_imports = true;
95 } else if (mem.startsWith(u8, arg, "--linker-member")) {
96 if (mem.eql(u8, arg["--linker-member".len..], "=1"))
97 opt_linker_member = .first_linker
98 else if (mem.eql(u8, arg["--linker-member".len..], "=longnames"))
99 opt_linker_member = .longnames
100 else
101 opt_linker_member = .second_linker;
102 } else if (mem.eql(u8, arg, "--member-headers")) {
103 opt_member_headers = true;
104 } else if (mem.startsWith(u8, arg, "--elements=")) {
105 any_elements = true;
106 var split = std.mem.splitScalar(u8, arg["--elements=".len..], ',');
107 while (split.next()) |element| {
108 const kind, const add = if (element.len > 0 and element[0] == '-')
109 .{ element[1..], false }
110 else
111 .{ element, true };
112
113 if (elements == null) elements = .initFill(false);
114 if (std.meta.stringToEnum(Element, kind)) |format_kind| {
115 elements.?.set(format_kind, add);
116 } else if (std.mem.eql(u8, kind, "all")) {
117 elements.? = .initFill(add);
118 } else {
119 fatal("unrecognized element: '{s}'", .{kind});
120 }
121 }
122 } else if (mem.startsWith(u8, arg, "--only-member=")) {
123 (try member_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-member=".len..]);
124 } else if (mem.startsWith(u8, arg, "--only-section=")) {
125 (try section_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-section=".len..]);
126 } else if (mem.startsWith(u8, arg, "--only-symbol=")) {
127 (try symbol_filters.addOne(arena)).* = try arena.dupe(u8, arg["--only-symbol=".len..]);
128 } else if (mem.startsWith(u8, arg, "--redact=")) {
129 const kind = arg["--redact=".len..];
130 if (std.meta.stringToEnum(FieldKind, kind)) |field_kind| {
131 redact.set(field_kind, true);
132 } else if (std.mem.eql(u8, kind, "all")) {
133 redact = .initFill(true);
134 } else {
135 fatal("unrecognized redaction kind: {s}", .{kind});
136 }
137 } else if (mem.eql(u8, arg, "--relocs")) {
138 opt_relocs = true;
139 } else if (mem.eql(u8, arg, "--section-headers")) {
140 opt_section_headers = true;
141 } else if (mem.eql(u8, arg, "-s") or mem.eql(u8, arg, "--snapshot")) {
142 elements = .initFill(false);
143 redact = .initFill(true);
144 } else if (mem.eql(u8, arg, "--strings")) {
145 opt_strings = true;
146 } else if (mem.eql(u8, arg, "--symbols")) {
147 opt_symbols = true;
148 } else if (mem.eql(u8, arg, "--tls")) {
149 opt_tls = true;
150 } else {
151 fatal("unrecognized argument: {s}", .{arg});
152 }
153 } else if (opt_input_path == null) {
154 opt_input_path = arg;
155 } else {
156 fatal("unexpected positional: {s}", .{arg});
157 }
158 }
159
160 const opts: Options = .{
161 .input_path = opt_input_path orelse fatal("missing input file path positional argument", .{}),
162 .exports = opt_exports orelse false,
163 .exports_sort = opt_exports_sort orelse false,
164 .file_headers = opt_file_headers orelse false,
165 .imports = opt_imports orelse false,
166 .linker_member = opt_linker_member,
167 .member_filters = member_filters.items,
168 .member_headers = opt_member_headers orelse false,
169 .elements = elements orelse .initFill(true),
170 .redact = redact,
171 .relocs = opt_relocs orelse false,
172 .section_filters = section_filters.items,
173 .section_headers = opt_section_headers orelse false,
174 .strings = opt_strings orelse false,
175 .symbol_filters = symbol_filters.items,
176 .symbols = opt_symbols orelse false,
177 .tls = opt_tls orelse false,
178 };
179
180 var file = std.Io.Dir.cwd().openFile(io, opts.input_path, .{}) catch |err|
181 fatal("failed to open {s}: {t}", .{ opts.input_path, err });
182 defer file.close(io);
183
184 var buffer: [4096]u8 = undefined;
185 var file_reader = file.reader(io, &buffer);
186 var stdout_writer = std.Io.File.stdout().writerStreaming(io, &stdout_buffer);
187
188 const ctx: DumpContext = .{
189 .gpa = init.gpa,
190 .opts = &opts,
191 .fr = &file_reader,
192 .w = &stdout_writer.interface,
193 };
194
195 dump(&ctx) catch |err| switch (err) {
196 error.ReadFailed => return file_reader.err.?,
197 error.WriteFailed => return stdout_writer.err.?,
198 error.UnknownFile => fatal("unrecognized file: {s}", .{opts.input_path}),
199 error.ParseFailure => {},
200 else => |e| return e,
201 };
202 try stdout_writer.flush();
203}
204
205fn dump(d: *const DumpContext) !void {
206 const r = &d.fr.interface;
207 try r.fill(4);
208 elf: {
209 if (!mem.eql(u8, r.buffered()[0..4], std.elf.MAGIC)) break :elf;
210 return elf.dump(r, d.w);
211 }
212 macho: {
213 if (mem.readInt(u32, r.buffered()[0..4], .little) != std.macho.MH_MAGIC_64) break :macho;
214 return macho.dump(r, d.w);
215 }
216 wasm: {
217 comptime assert(std.wasm.magic.len == 4);
218 if (!mem.eql(u8, r.buffered()[0..4], &std.wasm.magic)) break :wasm;
219 return wasm.dump(r, d.w);
220 }
221 coff: {
222 const ext = std.fs.path.extension(d.opts.input_path);
223 const basename = std.fs.path.basename(d.opts.input_path);
224 if (std.mem.eql(u8, ext, ".exe") or std.mem.eql(u8, ext, ".dll")) {
225 if (!mem.eql(u8, r.buffered()[0..2], "MZ")) break :coff;
226 try r.discardAll(std.coff.pe_pointer_offset);
227 const sig_offset = try r.takeInt(u32, .little);
228 try d.fr.seekTo(sig_offset);
229 const sig = try r.take(4);
230
231 if (!std.mem.eql(u8, sig, std.coff.pe_signature)) {
232 try d.w.print("invalid PE signature: {x}", .{sig});
233 return error.ParseFailure;
234 }
235
236 if (d.element(.@"file-type")) {
237 try d.w.print("{s}: PE/COFF image\n\n", .{basename});
238 if (d.element(.newlines)) try d.w.writeByte('\n');
239 }
240
241 return coff.dumpObject(d, true, basename);
242 } else if (std.mem.eql(u8, ext, ".lib")) {
243 r.fill(std.coff.archive_signature.len) catch break :coff;
244 if (!mem.eql(u8, r.buffered()[0..std.coff.archive_signature.len], std.coff.archive_signature)) break :coff;
245 if (d.element(.@"file-type")) {
246 try d.w.print("{s}: COFF archive\n", .{basename});
247 if (d.element(.newlines)) try d.w.writeByte('\n');
248 }
249
250 return coff.dumpArchive(d);
251 } else if (std.mem.eql(u8, ext, ".obj")) {
252 if (d.element(.@"file-type")) {
253 try d.w.print("{s}: COFF object\n", .{basename});
254 if (d.element(.newlines)) try d.w.writeByte('\n');
255 }
256
257 return coff.dumpObject(d, false, basename);
258 }
259 }
260 return error.UnknownFile;
261}
262
263const DumpContext = struct {
264 gpa: std.mem.Allocator,
265 opts: *const Options,
266 fr: *Io.File.Reader,
267 w: *Io.Writer,
268
269 fn element(self: *const DumpContext, e: Element) bool {
270 return self.opts.elements.get(e);
271 }
272
273 fn redacted(self: *const DumpContext, opt_kind: ?FieldKind) bool {
274 const kind = opt_kind orelse return false;
275 return self.opts.redact.get(kind);
276 }
277
278 fn failParse(
279 ctx: *const DumpContext,
280 comptime fmt: []const u8,
281 args: anytype,
282 ) noreturn {
283 std.log.err("error parsing '{s}'", .{std.fs.path.basename(ctx.opts.input_path)});
284 fatal(fmt, args);
285 }
286};
287
288const elf = struct {
289 fn dump(r: *Io.Reader, w: *Io.Writer) !void {
290 _ = r;
291 try w.writeAll("TODO dump elf file\n");
292 }
293};
294
295const macho = struct {
296 fn dump(r: *Io.Reader, w: *Io.Writer) !void {
297 _ = r;
298 try w.writeAll("TODO dump macho file\n");
299 }
300};
301
302const wasm = struct {
303 fn dump(r: *Io.Reader, w: *Io.Writer) !void {
304 _ = r;
305 try w.writeAll("TODO dump wasm file\n");
306 }
307};
308
309const coff = struct {
310 const DIRECTORY_ENTRY = std.coff.IMAGE.DIRECTORY_ENTRY;
311
312 const Section = struct {
313 header: std.coff.SectionHeader,
314 name: []const u8,
315
316 fn rvaFileOffset(section: *const Section, rva: u32) !u32 {
317 if (rva < section.header.virtual_address or
318 rva >= section.header.virtual_address + section.header.size_of_raw_data)
319 return error.OutOfBounds;
320
321 return section.header.pointer_to_raw_data + (rva - section.header.virtual_address);
322 }
323 };
324
325 const ArchiveHeader = struct {
326 name: []const u8,
327 date: u40,
328 user_id: u20,
329 group_id: u20,
330 file_mode: u24,
331 size: u34,
332
333 pub fn fromRaw(d: *const DumpContext, raw_header: *const std.coff.ArchiveMemberHeader, opt_longnames: ?[]const u8) @This() {
334 const name = raw_header.parseName(opt_longnames) catch |err| switch (err) {
335 error.BadName => d.failParse("malformed member name: '{s}'", .{&raw_header.name}),
336 error.NoLongNames => d.failParse("member uses a long name, but there was no longnames member", .{}),
337 };
338
339 return .{
340 .name = name,
341 .date = raw_header.parseDate() catch |err|
342 d.failParse("unable to parse date '{s}' in member '{s}': {t}", .{ raw_header.date, name, err }),
343 .user_id = raw_header.parseUserId() catch |err|
344 d.failParse("unable to parse user_id '{s}' in member '{s}': {t}", .{ raw_header.user_id, name, err }),
345 .group_id = raw_header.parseGroupId() catch |err|
346 d.failParse("unable to parse group_id '{s}' in member '{s}': {t}", .{ raw_header.group_id, name, err }),
347 .file_mode = raw_header.parseFileMode() catch |err|
348 d.failParse("unable to parse file_mode '{s}' in member '{s}': {t}", .{ raw_header.file_mode, name, err }),
349 .size = raw_header.parseSize() catch |err|
350 d.failParse("unable to parse size '{s}' in member '{s}': {t}", .{ raw_header.size, name, err }),
351 };
352 }
353 };
354
355 fn dumpArchive(d: *const DumpContext) !void {
356 const gpa = d.gpa;
357 const fr = d.fr;
358 const w = d.w;
359
360 const r = &fr.interface;
361 r.toss(std.coff.archive_signature.len);
362
363 const Member = struct {
364 offset: u32,
365 order: ?u32,
366 };
367
368 var members: std.ArrayList(Member) = .empty;
369 defer members.deinit(gpa);
370 var symbol_member_indices: std.ArrayList(u32) = .empty;
371 defer symbol_member_indices.deinit(gpa);
372
373 var opt_expected_kind: ?std.coff.ArchiveMemberHeader.Kind = .first_linker;
374 var opt_longnames: ?[]const u8 = null;
375 defer if (opt_longnames) |l| gpa.free(l);
376
377 var pos = fr.logicalPos();
378 const size = try fr.getSize();
379 while (pos < size) : (pos = fr.logicalPos()) {
380 if ((pos & 1) != 0) try r.discardAll(1);
381 const raw_header = try r.takeStruct(std.coff.ArchiveMemberHeader, .little);
382 const header: ArchiveHeader = .fromRaw(d, &raw_header, opt_longnames);
383
384 if (!std.mem.eql(u8, &raw_header.end_of_header, std.coff.archive_end_of_header))
385 return d.failParse("malformed end-of-header field in member '{s}': {x}", .{ header.name, raw_header.end_of_header });
386
387 const dump_header =
388 (d.opts.member_headers and filterMatches(d.opts.member_filters, header.name)) or
389 (d.opts.linker_member == opt_expected_kind);
390
391 if (dump_header)
392 try dumpArchiveHeader(d, &header, @intCast(pos));
393
394 const member_end = fr.logicalPos() + header.size;
395 if (member_end > size)
396 return d.failParse("out-of-bounds length 0x{x} in member '{s}'", .{ header.size, header.name });
397
398 if (opt_expected_kind) |expected_kind| switch (expected_kind) {
399 .first_linker => {
400 if (!std.mem.eql(u8, header.name, "/"))
401 return d.failParse("expected first linker member, found '{s}'", .{header.name});
402
403 const num_symbols = try r.takeInt(u32, .big);
404 if (dump_header)
405 try w.print(
406 \\{t: >16} type
407 \\ | {d} symbols
408 \\
409 , .{ expected_kind, num_symbols });
410
411 if (d.opts.linker_member == .first_linker) {
412 if (d.element(.@"table-header"))
413 try w.writeAll(
414 \\
415 \\Archive symbols:
416 \\& Member Symbol
417 \\
418 );
419
420 const offsets = try r.readAlloc(gpa, num_symbols * 4);
421 defer gpa.free(offsets);
422
423 for (0..num_symbols) |symbol_i| {
424 const symbol = r.takeDelimiter(0) catch |err|
425 return d.failParse("unable to read first linker member string table: {t}", .{err});
426
427 if (!filterMatches(d.opts.symbol_filters, symbol.?))
428 continue;
429
430 const offset = std.mem.readInt(u32, offsets[symbol_i * 4 ..][0..4], .big);
431 try w.print("{f} {s}\n", .{
432 fmtIntField(d, offset, .{ .kind = .va }),
433 symbol.?,
434 });
435 }
436 }
437 if (dump_header and d.element(.newlines)) try w.writeByte('\n');
438
439 try fr.seekTo(member_end);
440 opt_expected_kind = .second_linker;
441 continue;
442 },
443 .second_linker => {
444 if (!std.mem.eql(u8, header.name, "/"))
445 return d.failParse("expected second linker member, found '{s}'", .{header.name});
446
447 const num_members = try r.takeInt(u32, .little);
448 pos = fr.logicalPos();
449 if (pos + num_members * @sizeOf(u32) > member_end)
450 return d.failParse("invalid member count 0x{x} in second linker member", .{num_members});
451
452 try members.ensureTotalCapacity(gpa, num_members);
453 for (0..num_members) |_|
454 members.addOneAssumeCapacity().* = .{
455 .offset = try r.takeInt(u32, .little),
456 .order = null,
457 };
458
459 const num_symbols = try r.takeInt(u32, .little);
460 pos = fr.logicalPos();
461 if (pos + num_symbols * @sizeOf(u16) > member_end)
462 return d.failParse("invalid symbol count 0x{x} in second linker member", .{num_symbols});
463
464 if (dump_header)
465 try w.print(
466 \\{t: >16} type
467 \\ | {f} symbols
468 \\ | {f} members
469 \\
470 , .{
471 expected_kind,
472 fmtIntField(d, num_symbols, .{ .kind = .size, .width = .auto }),
473 fmtIntField(d, num_members, .{ .kind = .size, .width = .auto }),
474 });
475
476 try symbol_member_indices.ensureTotalCapacity(gpa, num_symbols);
477 for (0..num_symbols) |order| {
478 const index = (try r.takeInt(u16, .little)) - 1;
479 if (index >= members.items.len)
480 return d.failParse("invalid member index 0x{x} in seconds linker member indices array", .{index});
481
482 symbol_member_indices.addOneAssumeCapacity().* = index;
483
484 if (members.items[index].order == null)
485 members.items[index].order = @intCast(order);
486 }
487
488 if (d.opts.exports and d.opts.exports_sort) {
489 std.sort.pdq(Member, members.items, {}, struct {
490 fn lessThan(ctx: void, lhs: Member, rhs: Member) bool {
491 _ = ctx;
492 if (lhs.order == null and rhs.order == null)
493 return lhs.offset < rhs.offset
494 else if (lhs.order) |lhs_order|
495 return if (rhs.order) |rhs_order| lhs_order < rhs_order else false
496 else if (rhs.order) |rhs_order|
497 return if (lhs.order) |lhs_order| lhs_order < rhs_order else true
498 else
499 unreachable;
500 }
501 }.lessThan);
502 }
503
504 if (d.opts.linker_member == .second_linker) {
505 if (d.element(.@"table-header"))
506 try w.writeAll(
507 \\
508 \\Archive Symbols:
509 \\& Member Symbol
510 \\
511 );
512
513 pos = fr.logicalPos();
514 var symbol_i: u32 = 0;
515 while (pos < member_end and symbol_i < num_symbols) : ({
516 pos = fr.logicalPos();
517 symbol_i += 1;
518 }) {
519 const symbol_name = if (r.takeDelimiter(0) catch |err| switch (err) {
520 error.StreamTooLong => null,
521 else => |e| return e,
522 }) |n| n else return d.failParse("unterminated string found in second linker member", .{});
523
524 if (!filterMatches(d.opts.symbol_filters, symbol_name))
525 continue;
526
527 try w.print("{f} {s}\n", .{
528 fmtIntField(
529 d,
530 members.items[symbol_member_indices.items[symbol_i]].offset,
531 .{ .kind = .va },
532 ),
533 symbol_name,
534 });
535 }
536
537 if (symbol_i != num_symbols)
538 return d.failParse(
539 " expected {d} entries in second linker member string table, but found {d}",
540 .{ num_symbols, symbol_i },
541 );
542 }
543
544 if (d.element(.newlines)) try w.writeByte('\n');
545 try fr.seekTo(member_end);
546 opt_expected_kind = .longnames;
547 continue;
548 },
549 .longnames => {
550 // This member is optional
551 if (std.mem.eql(u8, header.name, "//")) {
552 opt_longnames = try r.readAlloc(gpa, header.size);
553 if (dump_header)
554 try w.print("{t: >16} type\n", .{expected_kind});
555
556 if (d.opts.linker_member == .longnames) {
557 if (d.element(.@"table-header"))
558 try w.print(
559 \\
560 \\Longnames (0x{x} bytes):
561 \\
562 , .{opt_longnames.?.len});
563
564 var lr = Io.Reader.fixed(opt_longnames.?);
565 while (try lr.takeDelimiter(0)) |str| {
566 try w.writeAll(str);
567 try w.writeByte('\n');
568 }
569 }
570
571 if (d.element(.newlines)) try w.writeByte('\n');
572 }
573
574 opt_expected_kind = null;
575 break;
576 },
577 else => unreachable,
578 };
579 }
580
581 if (opt_expected_kind) |expected_kind| switch (expected_kind) {
582 .first_linker => d.failParse("missing first linker member", .{}),
583 .second_linker => d.failParse("missing second linker member", .{}),
584 else => {},
585 };
586
587 for (members.items, 0..) |member, member_i| {
588 fr.seekTo(member.offset) catch |err|
589 d.failParse("unable to read member {d} at offset 0x{x}: {t}", .{ member_i, member.offset, err });
590
591 const raw_header = try r.takeStruct(std.coff.ArchiveMemberHeader, .little);
592 const header: ArchiveHeader = .fromRaw(d, &raw_header, opt_longnames);
593 if (!filterMatches(d.opts.member_filters, header.name)) continue;
594
595 const member_sig = try r.peek(4);
596 const machine: std.coff.IMAGE.FILE.MACHINE =
597 @fromBackingInt(@intCast(std.mem.readInt(u16, member_sig[0..2], .little)));
598 const sig = std.mem.readInt(u16, member_sig[2..4], .little);
599
600 const is_imp_lib = machine == std.coff.IMAGE.FILE.MACHINE.UNKNOWN and sig == 0xffff;
601 if (d.opts.member_headers)
602 try dumpArchiveHeader(d, &header, member.offset);
603
604 if (d.opts.member_headers or (d.opts.exports and is_imp_lib)) {
605 if (is_imp_lib) {
606 const imp_header = try r.takeStruct(std.coff.ImportHeader, .little);
607 const sym_name = (try r.takeDelimiter(0)).?;
608 const imp_dll = (try r.takeDelimiter(0)).?;
609
610 if (!filterMatches(d.opts.symbol_filters, sym_name))
611 continue;
612
613 if (d.element(.@"header-name"))
614 try w.writeAll("\nImport header:\n");
615
616 try dumpHeader(d, std.coff.ImportHeader, &imp_header, struct {
617 pub fn sig1(_: *const DumpContext, _: *const std.coff.ImportHeader) !void {}
618 pub fn sig2(_: *const DumpContext, _: *const std.coff.ImportHeader) !void {}
619 pub fn types(id: *const DumpContext, h: *const std.coff.ImportHeader) !void {
620 try id.w.print(
621 \\{t: >16} import_type
622 \\{t: >16} name_type
623 \\
624 , .{ h.types.type, h.types.name_type });
625 }
626 });
627
628 const imp_name = imp_name: switch (imp_header.types.name_type) {
629 .NAME_NOPREFIX,
630 .NAME_UNDECORATE,
631 => |tag| {
632 var imp_name = std.mem.trimStart(u8, sym_name, "?@_");
633 if (tag == .NAME_UNDECORATE)
634 imp_name = std.mem.sliceTo(imp_name, '@');
635 break :imp_name imp_name;
636 },
637 else => sym_name,
638 };
639
640 try w.print(
641 \\ symbol name | {s}
642 \\ import name | {s}
643 \\ dll | {s}
644 \\
645 , .{
646 sym_name,
647 imp_name,
648 imp_dll,
649 });
650 } else {
651 try w.writeAll(" COFF object type\n");
652 }
653 if (d.element(.newlines)) try w.writeByte('\n');
654 }
655
656 if (is_imp_lib) continue;
657 if (d.opts.section_headers or
658 d.opts.file_headers or
659 d.opts.relocs or
660 d.opts.strings or
661 d.opts.symbols)
662 {
663 const member_name = if (d.element(.@"member-path"))
664 header.name
665 else
666 std.fs.path.basename(header.name);
667
668 if (d.element(.@"file-type")) {
669 try w.print("{s}({s}): COFF object\n", .{
670 std.fs.path.basename(d.opts.input_path),
671 member_name,
672 });
673 if (d.element(.newlines)) try w.writeByte('\n');
674 }
675 try dumpObject(d, false, member_name);
676 }
677 }
678 }
679
680 fn dumpObject(
681 d: *const DumpContext,
682 is_image: bool,
683 obj_name: []const u8,
684 ) !void {
685 const gpa = d.gpa;
686 const fr = d.fr;
687 const w = d.w;
688
689 const file_location = fr.logicalPos();
690 const r = &fr.interface;
691 const header = r.takeStruct(std.coff.Header, .little) catch |err|
692 return d.failParse("unable to read COFF header: {t}", .{err});
693
694 if (d.opts.file_headers) {
695 if (d.element(.@"header-name")) try w.writeAll("COFF Header:\n");
696 try dumpHeader(d, std.coff.Header, &header, struct {});
697 if (d.element(.newlines)) try w.writeByte('\n');
698 }
699
700 switch (header.machine) {
701 _ => return d.failParse("unknown machine type: {x}", .{header.machine}),
702 else => {},
703 }
704
705 var known_dirs: [DIRECTORY_ENTRY.len]std.coff.ImageDataDirectory = undefined;
706 const needs_data_dirs =
707 d.opts.exports or
708 d.opts.imports or
709 d.opts.tls;
710
711 const ImageInfo = struct {
712 data_dirs: []const std.coff.ImageDataDirectory,
713 magic: std.coff.OptionalHeader.Magic,
714 image_base: u64,
715 };
716
717 const image_info: ?ImageInfo = if (header.size_of_optional_header > 0) image_info: {
718 if (!d.opts.file_headers and !needs_data_dirs) {
719 try fr.seekBy(header.size_of_optional_header);
720 break :image_info null;
721 }
722
723 if (d.opts.file_headers and d.element(.@"header-name"))
724 try w.writeAll("COFF Optional Header:\n");
725
726 const magic: std.coff.OptionalHeader.Magic = @fromBackingInt(@intCast(try r.peekInt(u16, .little)));
727 const num_directory_entries, const image_base = switch (magic) {
728 inline .PE32, .@"PE32+" => |v| num_data_dirs: {
729 const OptionalHeader = if (v == .PE32)
730 std.coff.OptionalHeader.PE32
731 else
732 std.coff.OptionalHeader.@"PE32+";
733
734 const optional_header = r.takeStruct(OptionalHeader, .little) catch |err|
735 return d.failParse("unable to read optional header: {t}", .{err});
736
737 if (d.opts.file_headers) {
738 try dumpHeader(d, OptionalHeader, &optional_header, struct {
739 pub fn base_of_code(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void {
740 const base = @as(*const OptionalHeader, @ptrCast(@alignCast(h))).image_base;
741 try dumpRvaField(id, @src().fn_name, h.base_of_code, base);
742 }
743
744 pub fn address_of_entry_point(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void {
745 const base = @as(*const OptionalHeader, @ptrCast(@alignCast(h))).image_base;
746 try dumpRvaField(id, @src().fn_name, h.base_of_code, base);
747 }
748
749 pub fn major_linker_version(id: *const DumpContext, h: *const std.coff.OptionalHeader) !void {
750 try dumpVersionField(id.w, "linker_version", h.major_linker_version, h.minor_linker_version);
751 }
752 pub fn minor_linker_version(_: *const DumpContext, _: *const std.coff.OptionalHeader) !void {}
753
754 pub fn major_operating_system_version(id: *const DumpContext, h: *const OptionalHeader) !void {
755 try dumpVersionField(
756 id.w,
757 "operating_system_version",
758 h.major_operating_system_version,
759 h.minor_operating_system_version,
760 );
761 }
762 pub fn minor_operating_system_version(_: *const DumpContext, _: *const OptionalHeader) !void {}
763
764 pub fn major_image_version(id: *const DumpContext, h: *const OptionalHeader) !void {
765 try dumpVersionField(id.w, "image_version", h.major_image_version, h.minor_image_version);
766 }
767 pub fn minor_image_version(_: *const DumpContext, _: *const OptionalHeader) !void {}
768
769 pub fn major_subsystem_version(id: *const DumpContext, h: *const OptionalHeader) !void {
770 try dumpVersionField(id.w, "subsystem_version", h.major_subsystem_version, h.minor_subsystem_version);
771 }
772 pub fn minor_subsystem_version(_: *const DumpContext, _: *const OptionalHeader) !void {}
773 });
774 if (d.element(.newlines)) try w.writeByte('\n');
775 }
776
777 break :num_data_dirs .{
778 optional_header.number_of_rva_and_sizes,
779 optional_header.image_base,
780 };
781 },
782 else => return d.failParse("invalid optional header magic number: {x}", .{magic}),
783 };
784
785 if (d.opts.file_headers and d.element(.@"header-name"))
786 try w.writeAll("Data Directories:\n");
787
788 for (0..num_directory_entries) |dir_i| {
789 const dir = r.takeStruct(std.coff.ImageDataDirectory, .little) catch |err|
790 return d.failParse("unable to read data directory {x}: {t}", .{ dir_i, err });
791
792 if (dir_i < known_dirs.len)
793 known_dirs[dir_i] = dir;
794
795 if (d.opts.file_headers)
796 try w.print(
797 "{x: >16} {x: >8} {t}\n",
798 .{ dir.virtual_address, dir.size, @as(DIRECTORY_ENTRY, @fromBackingInt(@intCast(dir_i))) },
799 );
800 }
801 if (d.opts.file_headers and d.element(.newlines)) try w.writeByte('\n');
802
803 break :image_info .{
804 .data_dirs = known_dirs[0..@min(known_dirs.len, num_directory_entries)],
805 .magic = magic,
806 .image_base = image_base,
807 };
808 } else if (is_image) {
809 return d.failParse("image did not contain an optional header", .{});
810 } else null;
811
812 // Section names in images don't use the string table, as they must fit inline in the header
813 const load_string_table = (d.opts.strings or !is_image) and header.pointer_to_symbol_table > 0;
814 const string_table = if (load_string_table) string_table: {
815 const pos = fr.logicalPos();
816 fr.seekTo(file_location + header.pointer_to_symbol_table + header.number_of_symbols * std.coff.Symbol.sizeOf()) catch |err|
817 return d.failParse("unable to seek to string table: {t}", .{err});
818
819 const string_table_len = r.peekInt(u32, .little) catch |err|
820 return d.failParse("unable to read string table length: {t}", .{err});
821
822 const table = r.readAlloc(gpa, string_table_len) catch |err|
823 return d.failParse("unable to read string table: {t}", .{err});
824
825 try fr.seekTo(pos);
826 break :string_table table;
827 } else &.{};
828 defer gpa.free(string_table);
829
830 if (d.opts.strings) {
831 if (d.element(.@"table-header"))
832 try w.print(
833 \\String Table (0x{x} bytes):
834 \\
835 , .{string_table.len});
836
837 var sr = Io.Reader.fixed(string_table[@sizeOf(u32)..]);
838 while (try sr.takeDelimiter(0)) |str| {
839 try w.writeAll(str);
840 try w.writeByte('\n');
841 }
842
843 if (d.element(.newlines)) try w.writeByte('\n');
844 }
845
846 var sections: std.ArrayList(Section) = .empty;
847 defer sections.deinit(gpa);
848 var sections_with_data: u16 = 0;
849
850 const load_sections =
851 d.opts.section_headers or
852 d.opts.symbols or
853 d.opts.relocs or
854 needs_data_dirs;
855
856 if (load_sections) {
857 if (d.opts.section_headers and d.element(.@"table-header"))
858 try w.print(
859 \\Sections in '{s}':
860 \\Num Name RVA Virt Size Data Size & Data & Relocs & Lines # Relocs # Lines Flags
861 \\
862 , .{obj_name});
863
864 try sections.resize(gpa, header.number_of_sections);
865 for (sections.items, 0..) |*section, section_i| {
866 section.header = r.takeStruct(std.coff.SectionHeader, .little) catch |err|
867 return d.failParse("unable to read section header {x}: {t}", .{ section_i, err });
868 section.name = headerName(&section.header.name, string_table) catch |err| switch (err) {
869 error.Overflow,
870 error.InvalidCharacter,
871 => return d.failParse("unable to parse section name offset '{s}': {t}", .{
872 section.name,
873 err,
874 }),
875 error.OutOfBounds => return d.failParse("section name offset '{s}' was out of bounds (>= {x})", .{
876 section.name,
877 string_table.len,
878 }),
879 };
880
881 sections_with_data += @intFromBool(section.header.size_of_raw_data > 0);
882 if (d.opts.section_headers) {
883 if (!filterMatches(d.opts.section_filters, section.name)) continue;
884 const raw_name = std.mem.sliceTo(&section.header.name, 0);
885 try w.print(
886 "{x: >3} {s: <8} {f} {f} {f} {f} {f} {f} {f} {f} {x:0>8} |",
887 .{
888 section_i + 1,
889 raw_name,
890 fmtIntField(d, section.header.virtual_address, .{ .kind = .va }),
891 fmtIntField(d, section.header.virtual_size, .{ .kind = .size, .width = .{ .explicit = 9 } }),
892 fmtIntField(d, section.header.size_of_raw_data, .{ .kind = .size, .width = .{ .explicit = 9 } }),
893 fmtIntField(d, section.header.pointer_to_raw_data, .{ .kind = .va }),
894 fmtIntField(d, section.header.pointer_to_relocations, .{ .kind = .va }),
895 fmtIntField(d, section.header.pointer_to_linenumbers, .{ .kind = .va }),
896 fmtIntField(d, section.header.number_of_relocations, .{ .kind = .va }),
897 fmtIntField(d, section.header.number_of_linenumbers, .{ .kind = .va }),
898 @as(u32, @bitCast(section.header.flags)),
899 },
900 );
901
902 try dumpFlags(w, "{s}", std.coff.SectionHeader.Flags, &section.header.flags, 1);
903 if (section.name.len > 8)
904 try w.print("\n | {s}", .{section.name});
905
906 try w.writeByte('\n');
907 }
908 }
909
910 if (d.opts.section_headers and d.element(.newlines)) try w.writeByte('\n');
911 }
912
913 var symbols: std.ArrayList(struct {
914 name: []const u8,
915 section_number: std.coff.SectionNumber,
916 }) = .empty;
917 defer symbols.deinit(gpa);
918
919 var name_arena: std.heap.ArenaAllocator = .init(gpa);
920 defer name_arena.deinit();
921
922 if (d.opts.relocs)
923 try symbols.ensureUnusedCapacity(gpa, header.number_of_symbols);
924
925 if (d.opts.symbols or d.opts.relocs) {
926 if (header.pointer_to_symbol_table > 0) {
927 fr.seekTo(file_location + header.pointer_to_symbol_table) catch |err|
928 return d.failParse("unable to seek to symbol table: {t}", .{err});
929
930 if (d.opts.symbols and d.element(.@"table-header"))
931 try w.print(
932 \\Symbols in '{s}':
933 \\ Ord Value Sect Type Storage Name
934 \\
935 , .{obj_name});
936
937 const symbol_size = std.coff.Symbol.sizeOf();
938 var symbol_i: u32 = 0;
939 while (symbol_i < header.number_of_symbols) {
940 var symbol: std.coff.Symbol = undefined;
941 const symbol_bytes = r.take(symbol_size) catch |err|
942 return d.failParse("unable to read symbol {x}: {t}", .{ symbol_i, err });
943
944 @memcpy(std.mem.asBytes(&symbol)[0..symbol_size], symbol_bytes);
945 if (native_endian != .little)
946 std.mem.byteSwapAllFields(std.coff.Symbol, &symbol);
947
948 const aux_symbols = if (symbol.number_of_aux_symbols > 0)
949 try r.take(symbol_size * symbol.number_of_aux_symbols)
950 else
951 &.{};
952 defer symbol_i += symbol.number_of_aux_symbols + 1;
953
954 const name = if (std.mem.eql(u8, symbol.name[0..4], "\x00\x00\x00\x00")) name: {
955 const index = std.mem.readInt(u32, symbol.name[4..], .little);
956 if (index >= string_table.len)
957 return d.failParse("invalid name offset for symbol {x} ({x} >= {x})", .{
958 symbol_i,
959 index,
960 string_table.len,
961 });
962 break :name std.mem.sliceTo(string_table[index..], 0);
963 } else try name_arena.allocator().dupe(u8, std.mem.sliceTo(&symbol.name, 0));
964
965 if (d.opts.relocs)
966 symbols.appendNTimesAssumeCapacity(.{
967 .name = name,
968 .section_number = symbol.section_number,
969 }, 1 + symbol.number_of_aux_symbols);
970
971 if (!d.opts.symbols or !filterMatches(d.opts.symbol_filters, name))
972 continue;
973
974 try w.print("{f} {x:0>8} ", .{
975 fmtIntField(d, @as(u16, @intCast(symbol_i)), .{ .kind = .ord }),
976 symbol.value,
977 });
978 try switch (symbol.section_number) {
979 .UNDEFINED => w.writeAll("UNDEF"),
980 .ABSOLUTE => w.writeAll(" ABS"),
981 .DEBUG => w.writeAll("DEBUG"),
982 else => |v| {
983 const backing = @backingInt(v);
984 const fmt = "{x: >5}";
985 if (backing >= 0)
986 try w.print(fmt, .{@as(u15, @intCast(backing))})
987 else
988 try w.print(fmt, .{backing});
989 },
990 };
991
992 try w.print("{t: >5}", .{symbol.type.base_type});
993 if (switch (symbol.type.complex_type) {
994 .NULL => " ",
995 .POINTER => "* ",
996 .FUNCTION => "()",
997 .ARRAY => "[]",
998 else => null,
999 }) |suffix| try w.writeAll(suffix) else try w.print("{x}", .{symbol.type.complex_type});
1000
1001 try w.print("{t: >16} | {s}\n", .{ symbol.storage_class, name });
1002
1003 for (0..symbol.number_of_aux_symbols) |aux_i| {
1004 _ = aux_i;
1005 try w.writeAll(" |");
1006
1007 if (symbol.storage_class == .EXTERNAL and
1008 symbol.type == std.coff.SymType{
1009 .complex_type = .FUNCTION,
1010 .base_type = .NULL,
1011 } and
1012 @backingInt(symbol.section_number) > 0)
1013 {
1014 try w.writeAll("TODO function aux symbol");
1015 } else if (symbol.type == std.coff.SymType{
1016 .complex_type = .FUNCTION,
1017 .base_type = .NULL,
1018 } and
1019 (std.mem.eql(u8, name, ".bf") or std.mem.eql(u8, name, ".ef")))
1020 {
1021 try w.writeAll("TODO bf / ef aux symbol");
1022 } else if (symbol.storage_class == .WEAK_EXTERNAL and symbol.section_number == .UNDEFINED) {
1023 if (symbol.value != 0)
1024 return d.failParse(
1025 "invalid value 0x{x} for weak external symbol 0x{x}",
1026 .{ symbol.value, symbol_i },
1027 );
1028
1029 var weak_external: std.coff.WeakExternalDefinition = undefined;
1030 @memcpy(std.mem.asBytes(&weak_external)[0..symbol_size], aux_symbols[0..symbol_size]);
1031 if (native_endian != .little)
1032 std.mem.byteSwapAllFields(std.coff.WeakExternalDefinition, &weak_external);
1033
1034 if (weak_external.tag_index >= header.number_of_symbols)
1035 return d.failParse(
1036 "invalid tag_index 0x{x} for weak external symbol 0x{x}",
1037 .{ weak_external.tag_index, symbol_i },
1038 );
1039
1040 if (d.redacted(.ord))
1041 try w.print(" Weak External [falls back to relative ordinal {x:0>8} via {t}]", .{
1042 @as(i64, weak_external.tag_index) - symbol_i,
1043 weak_external.flag,
1044 })
1045 else
1046 try w.print(" Weak External [falls back to ordinal {x:0>8} via {t}]", .{
1047 weak_external.tag_index,
1048 weak_external.flag,
1049 });
1050 } else if (symbol.storage_class == .FILE) {
1051 if (!std.mem.eql(u8, name, ".file")) {
1052 try w.print(" !! unexpected symbol name '{s}' for file symbol 0x{x}", .{ name, symbol_i });
1053 continue;
1054 }
1055
1056 const filename = std.mem.sliceTo(aux_symbols, 0);
1057 try w.print(" File '{s}'", .{filename});
1058 break;
1059 } else if (symbol.storage_class == .STATIC and
1060 symbol.type == std.coff.SymType{
1061 .complex_type = .NULL,
1062 .base_type = .NULL,
1063 } and
1064 symbol.value == 0 and
1065 switch (symbol.section_number) {
1066 .UNDEFINED, .DEBUG, .ABSOLUTE => false,
1067 else => |sn| @backingInt(sn) > 0,
1068 })
1069 {
1070 const section_i: u15 = @intCast(@backingInt(symbol.section_number) - 1);
1071 try w.writeAll(" Section ");
1072
1073 if (section_i >= sections.items.len) {
1074 try w.print(" !! invalid section number: {x}", .{section_i});
1075 continue;
1076 }
1077
1078 var section_def: std.coff.SectionDefinition = undefined;
1079 @memcpy(std.mem.asBytes(&section_def)[0..symbol_size], aux_symbols[0..symbol_size]);
1080 if (native_endian != .little)
1081 std.mem.byteSwapAllFields(std.coff.SectionDefinition, &section_def);
1082
1083 const section = &sections.items[section_i];
1084 if (section_def.number_of_relocations != section.header.number_of_relocations) {
1085 try w.print(
1086 " !! relocation count did not match section header: {d} vs {d}",
1087 .{ section_def.number_of_relocations, section.header.number_of_relocations },
1088 );
1089 continue;
1090 }
1091
1092 if (section_def.number_of_linenumbers != section.header.number_of_linenumbers) {
1093 try w.print(
1094 " !! line number count did not match section header: {d} vs {d}",
1095 .{ section_def.number_of_linenumbers, section.header.number_of_linenumbers },
1096 );
1097 continue;
1098 }
1099
1100 try w.print(" [size {f} chksum {x:0>8} relocs {x:0>4} lines {x:0>4}]", .{
1101 fmtIntField(d, section_def.length, .{ .kind = .size, .zero_fill = true }),
1102 section_def.checksum,
1103 section_def.number_of_relocations,
1104 section_def.number_of_linenumbers,
1105 });
1106
1107 switch (section_def.selection) {
1108 .NONE => {},
1109 else => |selection| {
1110 try w.print(" COMDAT({t}", .{selection});
1111 if (selection == .ASSOCIATIVE)
1112 try w.print("->{x}", .{section_def.number});
1113 try w.writeAll(")");
1114 },
1115 }
1116 }
1117
1118 try w.writeByte('\n');
1119 }
1120 }
1121
1122 if (d.opts.symbols and d.element(.newlines)) try w.writeByte('\n');
1123 } else if (d.opts.symbols) {
1124 try w.writeAll("No symbol table found\n");
1125 }
1126 }
1127
1128 if (d.opts.relocs) {
1129 const relocation_size = std.coff.Relocation.sizeOf();
1130
1131 for (sections.items, 0..) |section, section_i| {
1132 if (section.header.pointer_to_relocations == 0) continue;
1133
1134 if (d.element(.@"table-header"))
1135 try w.print(
1136 \\Relocs for section {x} '{s}' in {s}:
1137 \\ Offset Type Symbol -> Sect Name
1138 \\
1139 , .{ section_i + 1, section.name, obj_name });
1140
1141 fr.seekTo(file_location + section.header.pointer_to_relocations) catch |err|
1142 return d.failParse("unable to seek to section {x} relocation table: {t}", .{ section_i + 1, err });
1143
1144 for (0..section.header.number_of_relocations) |reloc_i| {
1145 var reloc: std.coff.Relocation = undefined;
1146 @memcpy(std.mem.asBytes(&reloc)[0..relocation_size], try r.take(relocation_size));
1147 if (native_endian != .little)
1148 std.mem.byteSwapAllFields(std.coff.Relocation, &reloc);
1149
1150 const sym = &symbols.items[reloc.symbol_table_index];
1151 if (!filterMatches(d.opts.symbol_filters, sym.name))
1152 continue;
1153
1154 try w.print("{f} ", .{
1155 fmtIntField(d, reloc.virtual_address, .{ .kind = .va, .zero_fill = true }),
1156 });
1157 switch (header.machine) {
1158 _ => unreachable,
1159 inline else => |m| switch (m.RelocationType()) {
1160 void => try w.writeAll("(unknown arch)"),
1161 else => |RelocationType| try w.print(
1162 "{t: <17} ",
1163 .{@as(RelocationType, @fromBackingInt(@intCast(reloc.type)))},
1164 ),
1165 },
1166 }
1167
1168 if (reloc.symbol_table_index >= symbols.items.len)
1169 return d.failParse(
1170 "reloc {x} in section {x} has out-of-bounds symbol index {x}",
1171 .{ reloc_i, section_i + 1, reloc.symbol_table_index },
1172 );
1173
1174 try w.print("{f} {f} | {s}\n", .{
1175 fmtIntField(d, reloc.symbol_table_index, .{ .kind = .ord }),
1176 fmtSectionNumber(sym.section_number),
1177 sym.name,
1178 });
1179 }
1180 if (d.element(.newlines)) try w.writeByte('\n');
1181 }
1182 }
1183
1184 // Sections indices with raw data, sorted by RVA
1185 const rva_index = if (needs_data_dirs) rva_index: {
1186 const rva_index = try gpa.alloc(u16, sections_with_data);
1187 var indices_i: u16 = 0;
1188 for (sections.items, 0..) |*section, i| {
1189 if (section.header.size_of_raw_data == 0) continue;
1190 rva_index[indices_i] = @intCast(i);
1191 indices_i += 1;
1192 }
1193
1194 const Context = struct {
1195 indices: []u16,
1196 sections: []const Section,
1197
1198 pub fn lessThan(ctx: @This(), lhs: usize, rhs: usize) bool {
1199 return ctx.sections[ctx.indices[lhs]].header.virtual_address <
1200 ctx.sections[ctx.indices[rhs]].header.virtual_address;
1201 }
1202
1203 pub fn swap(ctx: @This(), lhs: usize, rhs: usize) void {
1204 std.mem.swap(u16, &ctx.indices[lhs], &ctx.indices[rhs]);
1205 }
1206 };
1207
1208 std.sort.pdqContext(0, rva_index.len, Context{
1209 .indices = rva_index,
1210 .sections = sections.items,
1211 });
1212
1213 break :rva_index rva_index;
1214 } else &.{};
1215 defer gpa.free(rva_index);
1216
1217 if (d.opts.exports) exports: {
1218 if (try seekToDataDirectory(
1219 d,
1220 rva_index,
1221 sections.items,
1222 (image_info orelse {
1223 try w.writeAll("COFF objects do not contain an export data directory");
1224 break :exports;
1225 }).data_dirs,
1226 .EXPORT,
1227 )) |section_index| {
1228 const export_dir = r.takeStruct(std.coff.ExportDirectoryTable, .little) catch |err|
1229 return d.failParse("unable to read export directory: {t}", .{err});
1230
1231 try w.print("Export directory:\n", .{});
1232 try dumpHeader(d, std.coff.ExportDirectoryTable, &export_dir, struct {
1233 pub fn major_version(id: *const DumpContext, h: *const std.coff.ExportDirectoryTable) !void {
1234 try dumpVersionField(id.w, "version", h.major_version, h.minor_version);
1235 }
1236 pub fn minor_version(_: *const DumpContext, _: *const std.coff.ExportDirectoryTable) !void {}
1237 });
1238
1239 const section = sections.items[section_index];
1240 const name_loc = section.rvaFileOffset(export_dir.name_rva) catch
1241 return d.failParse(
1242 "export name rva 0x{x} was not within the export section",
1243 .{export_dir.name_rva},
1244 );
1245
1246 const eat_loc = section.rvaFileOffset(export_dir.export_address_table_rva) catch
1247 return d.failParse(
1248 "export address table rva 0x{x} was not within the export section",
1249 .{export_dir.export_address_table_rva},
1250 );
1251
1252 const name_pointer_loc = section.rvaFileOffset(export_dir.name_pointer_table_rva) catch
1253 return d.failParse(
1254 "export name pointer table rva 0x{x} was not within the export section",
1255 .{export_dir.name_pointer_table_rva},
1256 );
1257
1258 const ord_loc = section.rvaFileOffset(export_dir.ordinal_table_rva) catch
1259 return d.failParse(
1260 "export ordinal table rva 0x{x} was not within the export section",
1261 .{export_dir.ordinal_table_rva},
1262 );
1263
1264 // All the variable length fields should be contained within this directory.
1265 // Read it entirely to avoid needing to seek per-name when iterating.
1266 const dir = image_info.?.data_dirs[@backingInt(DIRECTORY_ENTRY.EXPORT)];
1267 const dir_end_rva = dir.virtual_address + dir.size;
1268 const dir_loc = fr.logicalPos();
1269 const dir_slice = try r.readAlloc(gpa, dir.size);
1270 defer gpa.free(dir_slice);
1271
1272 const dll_name = std.mem.sliceTo(dir_slice[name_loc - dir_loc ..], 0);
1273 if (d.element(.@"table-header"))
1274 try w.print(
1275 \\
1276 \\Exports from {s}:
1277 \\ Ord Hint RVA Name
1278 \\
1279 , .{dll_name});
1280
1281 const name_pointers = dir_slice[name_pointer_loc - dir_loc ..][0 .. export_dir.number_of_names * @sizeOf(u32)];
1282 const ords = dir_slice[ord_loc - dir_loc ..][0 .. export_dir.number_of_names * @sizeOf(u16)];
1283 const addrs = dir_slice[eat_loc - dir_loc ..][0 .. export_dir.number_of_entries * @sizeOf(u32)];
1284 const name_rva_to_offset = dir.virtual_address + @sizeOf(std.coff.ExportDirectoryTable);
1285 for (0..export_dir.number_of_names) |name_i| {
1286 const name_rva = std.mem.readInt(u32, name_pointers[name_i * @sizeOf(u32) ..][0..@sizeOf(u32)], .little);
1287 const name = std.mem.sliceTo(dir_slice[name_rva - name_rva_to_offset ..], 0);
1288 if (!filterMatches(d.opts.symbol_filters, name))
1289 continue;
1290
1291 const ord = std.mem.readInt(u16, ords[name_i * @sizeOf(u16) ..][0..@sizeOf(u16)], .little);
1292 const addr = std.mem.readInt(u32, addrs[@as(u32, ord) * @sizeOf(u32) ..][0..@sizeOf(u32)], .little);
1293
1294 try w.print("{f} {f} ", .{
1295 fmtIntField(d, @as(u16, @intCast(export_dir.ordinal_base + ord)), .{ .kind = .ord }),
1296 fmtIntField(d, @as(u16, @intCast(name_i)), .{ .kind = .ord }),
1297 });
1298 const is_forwarder = addr >= dir.virtual_address and addr < dir_end_rva;
1299 if (is_forwarder) {
1300 try w.writeAll("forwards");
1301 } else {
1302 try w.print("{f}", .{fmtIntField(d, addr, .{ .kind = .rva })});
1303 }
1304
1305 try w.print(" | {s}", .{name});
1306 if (is_forwarder)
1307 try w.print(" -> {s}", .{std.mem.sliceTo(dir_slice[addr - name_rva_to_offset ..], 0)});
1308 try w.writeByte('\n');
1309 }
1310 }
1311 }
1312
1313 if (d.opts.imports) imports: {
1314 if (try seekToDataDirectory(
1315 d,
1316 rva_index,
1317 sections.items,
1318 (image_info orelse {
1319 try w.writeAll("COFF objects do not contain an import data directory");
1320 break :imports;
1321 }).data_dirs,
1322 .IMPORT,
1323 )) |_| {
1324 const Entry = std.coff.ImportDirectoryEntry;
1325 var directory_entries: std.ArrayList(Entry) = .empty;
1326 defer directory_entries.deinit(gpa);
1327 while (true) {
1328 const entry = r.takeStruct(Entry, .little) catch |err|
1329 return d.failParse(
1330 "unable to read import directory entry {x}: {t}",
1331 .{ directory_entries.items.len, err },
1332 );
1333
1334 if (std.mem.allEqual(u8, std.mem.asBytes(&entry), 0)) break;
1335 (try directory_entries.addOne(gpa)).* = entry;
1336 }
1337
1338 for (directory_entries.items) |entry| {
1339 const name_section = sectionContainingRva(
1340 rva_index,
1341 sections.items,
1342 entry.name_rva,
1343 ) orelse
1344 return d.failParse(
1345 "import directory entry name rva 0x{x} was not found in any section",
1346 .{entry.name_rva},
1347 );
1348
1349 const name_loc = sections.items[name_section].rvaFileOffset(
1350 entry.name_rva,
1351 ) catch unreachable;
1352 fr.seekTo(name_loc) catch |err|
1353 return d.failParse(
1354 "unable to seek to import directory entry name at 0x{x}: {t}",
1355 .{ name_loc, err },
1356 );
1357
1358 const dll_name = (try r.takeDelimiter(0)).?;
1359
1360 if (d.element(.@"header-name"))
1361 try w.print("Import table entry for {s}:\n", .{dll_name});
1362 try dumpHeader(d, Entry, &entry, struct {});
1363
1364 if (d.element(.@"table-header"))
1365 try w.print(
1366 \\
1367 \\ Ord Hint Name
1368 \\
1369 , .{});
1370
1371 const ilt_section = sectionContainingRva(
1372 rva_index,
1373 sections.items,
1374 entry.import_lookup_table_rva,
1375 ) orelse
1376 return d.failParse(
1377 "import directory entry ilt rva 0x{x} was not found in any section",
1378 .{entry.import_lookup_table_rva},
1379 );
1380
1381 const ilt_loc = sections.items[ilt_section].rvaFileOffset(
1382 entry.import_lookup_table_rva,
1383 ) catch unreachable;
1384 fr.seekTo(ilt_loc) catch |err|
1385 return d.failParse(
1386 "unable to seek to import directory ilt at 0x{x}: {t}",
1387 .{ ilt_loc, err },
1388 );
1389
1390 switch (image_info.?.magic) {
1391 _ => try w.writeAll("(unknown magic)"),
1392 inline else => |m| {
1393 const TableEntry = std.coff.ImportLookupTableEntry(m);
1394 const null_entry: TableEntry = @bitCast(@as(@typeInfo(TableEntry).@"struct".backing_integer.?, 0));
1395
1396 var ilt_entries: std.ArrayList(TableEntry) = .empty;
1397 defer ilt_entries.deinit(gpa);
1398 while (true) {
1399 const table_entry = r.takeStruct(TableEntry, .little) catch |err|
1400 return d.failParse(
1401 "unable to read ilt entry {s}:{x}: {t}",
1402 .{ dll_name, ilt_entries.items.len, err },
1403 );
1404 if (table_entry == null_entry) break;
1405 (try ilt_entries.addOne(gpa)).* = table_entry;
1406 }
1407
1408 for (ilt_entries.items, 0..) |ilt_entry, ilt_entry_i| {
1409 if (ilt_entry.is_ordinal) {
1410 try w.print("{x: >4}", .{ilt_entry.payload.ordinal.ordinal});
1411 } else {
1412 const hint_section = sectionContainingRva(
1413 rva_index,
1414 sections.items,
1415 ilt_entry.payload.hint_name_rva,
1416 ) orelse
1417 return d.failParse(
1418 "import directory ilt entry 0x{x}'s hint rva 0x{x} was not found in any section",
1419 .{ ilt_entry_i, ilt_entry.payload.hint_name_rva },
1420 );
1421
1422 const hint_loc = sections.items[hint_section].rvaFileOffset(
1423 ilt_entry.payload.hint_name_rva,
1424 ) catch unreachable;
1425 fr.seekTo(hint_loc) catch |err|
1426 return d.failParse(
1427 "unable to seek to ilt entry 0x{x}'s hint at 0x{x}: {t}",
1428 .{ ilt_entry_i, hint_loc, err },
1429 );
1430
1431 const hint = r.takeInt(u16, .little) catch |err|
1432 return d.failParse(
1433 "unable to read import directory ilt entry 0x{x}'s hint: {t}",
1434 .{ ilt_entry_i, err },
1435 );
1436
1437 const name = r.takeDelimiter(0) catch |err|
1438 return d.failParse(
1439 "unable to read import directory ilt entry 0x{x}'s name: {t}",
1440 .{ ilt_entry_i, err },
1441 );
1442
1443 try w.print(" {x: >4} | {s}\n", .{ hint, name.? });
1444 }
1445 }
1446 if (d.element(.newlines)) try w.writeByte('\n');
1447 },
1448 }
1449 }
1450 }
1451 }
1452
1453 if (d.opts.tls) tls: {
1454 if (try seekToDataDirectory(
1455 d,
1456 rva_index,
1457 sections.items,
1458 (image_info orelse {
1459 try w.writeAll("COFF objects do not contain a TLS data directory");
1460 break :tls;
1461 }).data_dirs,
1462 .TLS,
1463 )) |_| {
1464 switch (image_info.?.magic) {
1465 _ => try w.writeAll("(unknown magic)"),
1466 inline else => |m| {
1467 const TlsDirectoryEntry = std.coff.TlsDirectoryEntry(m);
1468 const tls_entry = r.takeStruct(TlsDirectoryEntry, .little) catch |err|
1469 return d.failParse("unable to read tls directory: {t}", .{err});
1470
1471 try w.writeAll("TLS Directory:\n");
1472 try dumpHeader(d, TlsDirectoryEntry, &tls_entry, struct {});
1473
1474 try w.writeAll(" | ");
1475 if (tls_entry.characteristics.alignment == .NONE) {
1476 try w.writeAll("Alignment not specified");
1477 } else {
1478 try w.print(
1479 "Alignment: {d}",
1480 .{tls_entry.characteristics.alignment.toByteUnits().?},
1481 );
1482 }
1483
1484 try w.writeAll(
1485 \\
1486 \\
1487 \\TLS Callbacks:
1488 \\ Address
1489 \\
1490 );
1491
1492 const callbacks_rva: u32 = @intCast(tls_entry.callbacks_va - image_info.?.image_base);
1493 const section_index = sectionContainingRva(
1494 rva_index,
1495 sections.items,
1496 callbacks_rva,
1497 ) orelse
1498 return d.failParse(
1499 "tls callbacks rva 0x{x} was not found in any section",
1500 .{callbacks_rva},
1501 );
1502
1503 const callbacks_loc = sections.items[section_index]
1504 .rvaFileOffset(callbacks_rva) catch unreachable;
1505
1506 fr.seekTo(callbacks_loc) catch |err|
1507 return d.failParse(
1508 "unable to seek to tls callbacks array at offset 0x{x}: {t}",
1509 .{ callbacks_loc, err },
1510 );
1511
1512 while (true) {
1513 const callback_va = r.takeInt(@FieldType(TlsDirectoryEntry, "callbacks_va"), .little) catch |err|
1514 return d.failParse(
1515 "unable to read tls callbacks array: {t}",
1516 .{err},
1517 );
1518
1519 try w.print("{f}\n", .{fmtIntField(d, callback_va, .{ .kind = .va })});
1520 if (callback_va == 0) break;
1521 }
1522 if (d.element(.newlines)) try w.writeByte('\n');
1523 },
1524 }
1525 }
1526 }
1527 }
1528
1529 fn seekToDataDirectory(
1530 d: *const DumpContext,
1531 rva_index: []const u16,
1532 sections: []const Section,
1533 data_dirs: []const std.coff.ImageDataDirectory,
1534 entry: DIRECTORY_ENTRY,
1535 ) !?u16 {
1536 if (@backingInt(entry) < data_dirs.len) blk: {
1537 const rva = data_dirs[@backingInt(entry)].virtual_address;
1538 if (rva == 0) break :blk;
1539
1540 const section_index = sectionContainingRva(rva_index, sections, rva) orelse
1541 return d.failParse(
1542 "{t} directory rva 0x{x} was not found in any section",
1543 .{ entry, rva },
1544 );
1545
1546 const file_offset = sections[section_index].rvaFileOffset(rva) catch unreachable;
1547 d.fr.seekTo(file_offset) catch |err|
1548 return d.failParse(
1549 "unable to seek to {t} directory at offset 0x{x}: {t}",
1550 .{ entry, file_offset, err },
1551 );
1552
1553 return section_index;
1554 }
1555
1556 try d.w.print("{t} directory was not present in optional header\n", .{entry});
1557 return null;
1558 }
1559
1560 fn sectionContainingRva(
1561 /// Indices into `sections` sorted by rva
1562 indices: []const u16,
1563 sections: []const Section,
1564 rva: u32,
1565 ) ?u16 {
1566 const Context = struct {
1567 rva: u32,
1568 sections: []const Section,
1569
1570 fn order(ctx: @This(), section_index: u16) std.math.Order {
1571 const h = &ctx.sections[section_index].header;
1572 if (ctx.rva < h.virtual_address) return .lt;
1573 const end = h.virtual_address + h.size_of_raw_data;
1574 if (ctx.rva >= end) return .gt;
1575 return .eq;
1576 }
1577 };
1578
1579 const indices_index = std.sort.binarySearch(u16, indices, Context{
1580 .rva = rva,
1581 .sections = sections,
1582 }, Context.order) orelse return null;
1583 return @intCast(indices[indices_index]);
1584 }
1585
1586 fn headerName(raw: *const [8]u8, string_table: []const u8) ![]const u8 {
1587 return if (raw[0] == '/') name: {
1588 const name_offset = try std.fmt.parseUnsigned(u24, std.mem.sliceTo(raw[1..], 0), 10);
1589 if (name_offset >= string_table.len)
1590 return error.OutOfBounds;
1591
1592 break :name std.mem.sliceTo(string_table[name_offset..], 0);
1593 } else std.mem.sliceTo(raw, 0);
1594 }
1595
1596 fn fmtSectionNumber(section_number: std.coff.SectionNumber) std.fmt.Alt(std.coff.SectionNumber, sectionNumberString) {
1597 return .{ .data = section_number };
1598 }
1599
1600 fn sectionNumberString(section_number: std.coff.SectionNumber, w: *std.Io.Writer) std.Io.Writer.Error!void {
1601 try switch (section_number) {
1602 .UNDEFINED => w.writeAll("UNDEF"),
1603 .ABSOLUTE => w.writeAll(" ABS"),
1604 .DEBUG => w.writeAll("DEBUG"),
1605 else => |v| {
1606 const backing = @backingInt(v);
1607 const fmt = "{x: >5}";
1608 if (backing >= 0)
1609 try w.print(fmt, .{@as(u15, @intCast(backing))})
1610 else
1611 try w.print(fmt, .{backing});
1612 },
1613 };
1614 }
1615
1616 const FormatIntField = struct {
1617 val: ?u64,
1618 width: ?usize,
1619 zero_fill: bool,
1620 };
1621
1622 fn fmtIntField(
1623 d: *const DumpContext,
1624 val: anytype,
1625 params: struct {
1626 kind: ?FieldKind = null,
1627 width: union(enum) {
1628 fit_max,
1629 auto,
1630 explicit: usize,
1631 } = .fit_max,
1632 zero_fill: bool = false,
1633 },
1634 ) std.fmt.Alt(FormatIntField, intFieldString) {
1635 return .{
1636 .data = .{
1637 .val = if (d.redacted(params.kind)) null else val,
1638 .width = switch (params.width) {
1639 .fit_max => @typeInfo(@TypeOf(val)).int.bits / 4,
1640 .auto => null,
1641 .explicit => |w| w,
1642 },
1643 .zero_fill = params.zero_fill,
1644 },
1645 };
1646 }
1647
1648 fn intFieldString(field: FormatIntField, w: *std.Io.Writer) std.Io.Writer.Error!void {
1649 if (field.val) |val| {
1650 try w.printInt(val, 16, .lower, .{
1651 .width = field.width,
1652 .alignment = .right,
1653 .fill = if (field.zero_fill) '0' else ' ',
1654 });
1655 } else try w.splatByteAll('x', field.width orelse 1);
1656 }
1657
1658 fn dumpFlags(w: *Io.Writer, comptime fmt: []const u8, comptime T: type, flags: *const T, cols: u32) !void {
1659 const s = @typeInfo(T).@"struct";
1660 inline for (s.field_names, s.field_types) |field_name, field_type| {
1661 if (field_type == bool and @field(flags, field_name)) {
1662 try w.splatByteAll(' ', cols);
1663 try w.print(fmt, .{field_name});
1664 }
1665 }
1666 }
1667
1668 fn dumpArchiveHeader(d: *const DumpContext, header: *const ArchiveHeader, pos: u32) !void {
1669 if (d.element(.@"header-name"))
1670 try d.w.print("Archive member at offset 0x{x}: '{s}'\n", .{ pos, header.name });
1671 try dumpHeader(d, ArchiveHeader, header, struct {
1672 pub fn name(_: *const DumpContext, _: *const ArchiveHeader) !void {}
1673 pub fn file_mode(id: *const DumpContext, h: *const ArchiveHeader) !void {
1674 try id.w.print("{o: >16} file_mode\n", .{h.file_mode});
1675 }
1676 });
1677 }
1678
1679 fn fieldKind(name: []const u8) ?FieldKind {
1680 if (std.mem.endsWith(u8, name, "_rva"))
1681 return .rva;
1682 if (std.mem.endsWith(u8, name, "_va") or
1683 std.mem.endsWith(u8, name, "_address") or
1684 std.mem.startsWith(u8, name, "pointer_"))
1685 return .va;
1686 if (std.mem.startsWith(u8, name, "number_") or
1687 std.mem.startsWith(u8, name, "size"))
1688 return .size;
1689 if (std.mem.startsWith(u8, name, "hint"))
1690 return .ord;
1691 return null;
1692 }
1693
1694 fn dumpHeader(
1695 d: *const DumpContext,
1696 comptime T: type,
1697 header: *const T,
1698 Custom: type,
1699 ) !void {
1700 const s = @typeInfo(T).@"struct";
1701 inline for (s.field_names, s.field_types) |field_name, field_type| {
1702 const val = &@field(header, field_name);
1703 if (@hasDecl(Custom, field_name)) {
1704 try @field(Custom, field_name)(d, header);
1705 } else {
1706 switch (@typeInfo(field_type)) {
1707 .int => try d.w.print("{f} {s}\n", .{ fmtIntField(d, val.*, .{
1708 .kind = comptime fieldKind(field_name),
1709 .width = .{ .explicit = 16 },
1710 }), field_name }),
1711 .@"enum" => try d.w.print("{x: >16} {s} ({t})\n", .{ val.*, field_name, val.* }),
1712 .@"struct" => |s_field| {
1713 switch (s_field.layout) {
1714 .auto,
1715 .@"extern",
1716 => try dumpHeader(d, field_type, val, Custom),
1717 .@"packed" => {
1718 try d.w.print("{x: >16} {s}\n", .{ @as(s_field.backing_integer.?, @bitCast(val.*)), field_name });
1719 try dumpFlags(d.w, "| {s}\n", field_type, val, 15);
1720 },
1721 }
1722 },
1723 else => unreachable,
1724 }
1725 }
1726 }
1727 }
1728
1729 fn dumpVersionField(w: *Io.Writer, name: []const u8, major: anytype, minor: anytype) !void {
1730 try w.print("{d: >13}.{x:0<2} {s}\n", .{ major, minor, name });
1731 }
1732
1733 fn dumpRvaField(d: *const DumpContext, name: []const u8, rva: u64, base: u64) !void {
1734 try d.w.print("{f} {s} ({f})\n", .{
1735 fmtIntField(d, rva, .{ .kind = .rva }),
1736 name,
1737 fmtIntField(d, base + rva, .{ .kind = .va }),
1738 });
1739 }
1740};
1741
1742fn filterMatches(filters: []const []const u8, val: []const u8) bool {
1743 return for (filters) |filter| {
1744 if (std.mem.containsAtLeast(u8, val, 1, filter)) break true;
1745 } else filters.len == 0;
1746}
1747
1748const usage =
1749 \\Usage: zig objdump [options] file
1750 \\
1751 \\Options:
1752 \\ -h, --help Print this help and exit
1753 \\ --all-headers Alias for --file-headers --linker-member=2 --member-headers --section-headers --relocs --symbols
1754 \\ --exports[=sort] Display exported symbols.
1755 \\ In the case of COFF import libraries, displays the symbol list and import headers.
1756 \\ Specify =sort to optionally sort the import headers by symbol name.
1757 \\ --file-headers Display file-format specific headers
1758 \\ --imports Display imported symbols
1759 \\ --linker-member[=1|2|longnames] (Coff) Display contents of the specified archive linker member (default 2)
1760 \\ --member-headers Display archive member headers
1761 \\ --elements=[e1],[e2],-[e3],... Select which formatting elements are displayed. Intended for snapshot testing.
1762 \\ file-type File type summary
1763 \\ header-name Name that precedes a header block
1764 \\ member-path Display full member paths. If removed, only basenames will be used.
1765 \\ newlines Newlines between output sections
1766 \\ table-header Table headers with column names
1767 \\ all (default) All of the above
1768 \\ --only-member=[name] Only consider archive members names that contain [name]. Can be specified multiple times.
1769 \\ --only-section=[name] Only consider section names that contain [name]. Can be specified multiple times.
1770 \\ --only-symbol=[name] Only consider symbol names that contain [name]. Can be specified multiple times.
1771 \\ --redact=[kind] Redact the specified field kind. Intended for snapshot testing.
1772 \\ rva Relative virtual addresses
1773 \\ va Virtual addresses and file offsets
1774 \\ ord Symbol ordinals / hints
1775 \\ size Sizes and lengths
1776 \\ all All of the above
1777 \\ --relocs Display relocations
1778 \\ -s, --snapshot Alias for --redact=all --elements=-all
1779 \\ --section-headers Display section headers
1780 \\ --strings Display string tables
1781 \\ --symbols Display symbol tables
1782 \\ --tls Display TLS information
1783;