├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src ├── Context.zig ├── Context └── Elf.zig ├── ObjDump.zig └── main.zig /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jakub Konka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-objdump 2 | 3 | `objdump` utility but in Zig. 4 | 5 | ## Why? 6 | 7 | We need a set of good disassemblers and encoders for Zig's self-hosted native backends, and 8 | what better way than to test them than by disassembling actual relocatables and binaries. 9 | 10 | ## Building 11 | 12 | This project using the experimental Zig's package manager and so you need the latest Zig 13 | nightly to build it. 14 | 15 | ``` 16 | $ zig build 17 | ``` 18 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build.Builder) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const mode = b.standardOptimizeOption(.{}); 6 | 7 | const zig_dis_x86_64 = b.dependency("zig-dis-x86_64", .{ 8 | .target = target, 9 | .optimize = mode, 10 | }); 11 | 12 | const exe = b.addExecutable(.{ 13 | .name = "zig-objdump", 14 | .root_source_file = .{ .path = "src/main.zig" }, 15 | .target = target, 16 | .optimize = mode, 17 | }); 18 | exe.addModule("dis_x86_64", zig_dis_x86_64.module("dis_x86_64")); 19 | b.installArtifact(exe); 20 | 21 | const run_cmd = b.addRunArtifact(exe); 22 | run_cmd.step.dependOn(b.getInstallStep()); 23 | if (b.args) |args| { 24 | run_cmd.addArgs(args); 25 | } 26 | 27 | const run_step = b.step("run", "Run the app"); 28 | run_step.dependOn(&run_cmd.step); 29 | } 30 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "zig-objdump", 3 | .version = "0.0.1", 4 | .dependencies = .{ 5 | .@"zig-dis-x86_64" = .{ 6 | .url = "https://github.com/kubkon/zig-dis-x86_64/archive/920e46978b031d889350aa2d2d5cdd7a66475f05.tar.gz", 7 | .hash = "12207f95722e22e094e5e3ceb671d6c50dacb7054c7d964475be14a30e3d7fcbd2e6", 8 | }, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Context.zig: -------------------------------------------------------------------------------- 1 | const Context = @This(); 2 | 3 | const std = @import("std"); 4 | 5 | const Allocator = std.mem.Allocator; 6 | 7 | pub const Elf = @import("Context/Elf.zig"); 8 | 9 | tag: Tag, 10 | data: []const u8, 11 | 12 | pub const Tag = enum { 13 | elf, 14 | }; 15 | 16 | pub fn asMut(base: *Context, comptime T: type) ?*T { 17 | if (base.tag != T.base_tag) 18 | return null; 19 | 20 | return @fieldParentPtr(T, "base", base); 21 | } 22 | 23 | pub fn asConst(base: *const Context, comptime T: type) ?*const T { 24 | if (base.tag != T.base_tag) 25 | return null; 26 | 27 | return @fieldParentPtr(T, "base", base); 28 | } 29 | 30 | pub fn deinit(base: *Context, gpa: Allocator) void { 31 | gpa.free(base.data); 32 | } 33 | 34 | pub fn destroy(base: *Context, gpa: Allocator) void { 35 | base.deinit(gpa); 36 | switch (base.tag) { 37 | .elf => { 38 | const parent = @fieldParentPtr(Elf, "base", base); 39 | parent.deinit(gpa); 40 | gpa.destroy(parent); 41 | }, 42 | } 43 | } 44 | 45 | pub fn parse(gpa: Allocator, data: []const u8) !*Context { 46 | if (Elf.isElfFile(data)) { 47 | return &(try Elf.parse(gpa, data)).base; 48 | } 49 | return error.UnknownFileFormat; 50 | } 51 | -------------------------------------------------------------------------------- /src/Context/Elf.zig: -------------------------------------------------------------------------------- 1 | const Elf = @This(); 2 | 3 | const std = @import("std"); 4 | const assert = std.debug.assert; 5 | 6 | const Allocator = std.mem.Allocator; 7 | const Context = @import("../Context.zig"); 8 | 9 | pub const base_tag: Context.Tag = .elf; 10 | 11 | base: Context, 12 | header: std.elf.Elf64_Ehdr, 13 | symtab_shndx: ?u16 = null, 14 | symtab: std.ArrayListUnmanaged(std.elf.Elf64_Sym) = .{}, 15 | 16 | pub fn isElfFile(data: []const u8) bool { 17 | // TODO: 32bit ELF files 18 | const header = @ptrCast(*const std.elf.Elf64_Ehdr, @alignCast(@alignOf(std.elf.Elf64_Ehdr), data.ptr)).*; 19 | return std.mem.eql(u8, "\x7fELF", header.e_ident[0..4]); 20 | } 21 | 22 | pub fn deinit(elf: *Elf, gpa: Allocator) void { 23 | elf.symtab.deinit(gpa); 24 | } 25 | 26 | pub fn parse(gpa: Allocator, data: []const u8) !*Elf { 27 | const elf = try gpa.create(Elf); 28 | errdefer gpa.destroy(elf); 29 | 30 | elf.* = .{ 31 | .base = .{ 32 | .tag = .elf, 33 | .data = data, 34 | }, 35 | .header = undefined, 36 | }; 37 | elf.header = @ptrCast(*const std.elf.Elf64_Ehdr, @alignCast(@alignOf(std.elf.Elf64_Ehdr), data.ptr)).*; 38 | 39 | const shdrs = elf.getShdrs(); 40 | const symtab_shndx = for (shdrs, 0..) |shdr, i| switch (shdr.sh_type) { 41 | std.elf.SHT_SYMTAB => break @intCast(u16, i), 42 | else => {}, 43 | } else null; 44 | 45 | if (symtab_shndx) |shndx| { 46 | const symtab_shdr = elf.getShdr(shndx); 47 | const symtab_data = elf.getShdrData(symtab_shdr); 48 | const nsyms = @divExact(symtab_data.len, @sizeOf(std.elf.Elf64_Sym)); 49 | try elf.symtab.appendSlice( 50 | gpa, 51 | @ptrCast([*]const std.elf.Elf64_Sym, @alignCast(@alignOf(std.elf.Elf64_Sym), symtab_data))[0..nsyms], 52 | ); 53 | } 54 | elf.symtab_shndx = symtab_shndx; 55 | 56 | return elf; 57 | } 58 | 59 | pub fn getShdrIndexByName(elf: *const Elf, name: []const u8) ?u32 { 60 | const shdrs = elf.getShdrs(); 61 | for (shdrs, 0..) |shdr, i| { 62 | const shdr_name = elf.getShString(shdr.sh_name); 63 | if (std.mem.eql(u8, shdr_name, name)) return @intCast(u32, i); 64 | } 65 | return null; 66 | } 67 | 68 | pub fn getShdrByName(elf: *const Elf, name: []const u8) ?std.elf.Elf64_Shdr { 69 | const index = elf.getShdrIndexByName(name) orelse return null; 70 | return elf.getShdr(index); 71 | } 72 | 73 | pub fn getShdrs(elf: *const Elf) []const std.elf.Elf64_Shdr { 74 | const shdrs = @ptrCast( 75 | [*]const std.elf.Elf64_Shdr, 76 | @alignCast(@alignOf(std.elf.Elf64_Shdr), elf.base.data.ptr + elf.header.e_shoff), 77 | )[0..elf.header.e_shnum]; 78 | return shdrs; 79 | } 80 | 81 | pub fn getShdr(elf: *const Elf, shndx: u32) std.elf.Elf64_Shdr { 82 | return elf.getShdrs()[shndx]; 83 | } 84 | 85 | pub fn getShdrData(elf: *const Elf, shdr: std.elf.Elf64_Shdr) []const u8 { 86 | return elf.base.data[shdr.sh_offset..][0..shdr.sh_size]; 87 | } 88 | 89 | pub fn getShString(elf: *const Elf, off: u32) []const u8 { 90 | const shdr = elf.getShdrs()[elf.header.e_shstrndx]; 91 | const shstrtab = elf.getShdrData(shdr); 92 | assert(off < shstrtab.len); 93 | return std.mem.sliceTo(@ptrCast([*:0]const u8, shstrtab.ptr + off), 0); 94 | } 95 | 96 | pub fn getStrtab(elf: *const Elf) []const u8 { 97 | const symtab_shndx = elf.symtab_shndx orelse return &[0]u8{}; 98 | const symtab_shdr = elf.getShdr(symtab_shndx); 99 | const strtab_shdr = elf.getShdr(symtab_shdr.sh_link); 100 | return elf.getShdrData(strtab_shdr); 101 | } 102 | 103 | pub fn getString(elf: *const Elf, off: u32) []const u8 { 104 | const strtab = elf.getStrtab(); 105 | assert(off < strtab.len); 106 | return std.mem.sliceTo(@ptrCast([*:0]const u8, strtab.ptr + off), 0); 107 | } 108 | -------------------------------------------------------------------------------- /src/ObjDump.zig: -------------------------------------------------------------------------------- 1 | const ObjDump = @This(); 2 | 3 | const std = @import("std"); 4 | 5 | const Allocator = std.mem.Allocator; 6 | const Context = @import("Context.zig"); 7 | const Disassembler = @import("dis_x86_64").Disassembler; 8 | 9 | gpa: Allocator, 10 | ctx: *Context, 11 | 12 | pub fn parse(gpa: Allocator, file: std.fs.File) !ObjDump { 13 | const file_size = try file.getEndPos(); 14 | const data = try file.readToEndAlloc(gpa, file_size); 15 | errdefer gpa.free(data); 16 | 17 | var obj = ObjDump{ 18 | .gpa = gpa, 19 | .ctx = undefined, 20 | }; 21 | 22 | obj.ctx = try Context.parse(gpa, data); 23 | 24 | return obj; 25 | } 26 | 27 | pub fn deinit(obj: *ObjDump) void { 28 | obj.ctx.deinit(obj.gpa); 29 | } 30 | 31 | pub fn dump(obj: *const ObjDump, writer: anytype) !void { 32 | if (obj.ctx.asConst(Context.Elf)) |elf| { 33 | // TODO get all machine code sections, not only .text 34 | const shdr_index = elf.getShdrIndexByName(".text") orelse { 35 | return writer.writeAll("No .text section found.\n"); 36 | }; 37 | const shdr = elf.getShdr(shdr_index); 38 | const data = elf.getShdrData(shdr); 39 | 40 | var symbols = std.ArrayList(std.elf.Elf64_Sym).init(obj.gpa); 41 | defer symbols.deinit(); 42 | 43 | const SymSort = struct { 44 | pub fn lessThan(ctx: void, lhs: std.elf.Elf64_Sym, rhs: std.elf.Elf64_Sym) bool { 45 | _ = ctx; 46 | if (lhs.st_value == rhs.st_value) { 47 | const lhs_bind = lhs.st_info >> 4; 48 | const rhs_bind = rhs.st_info >> 4; 49 | switch (lhs_bind) { 50 | std.elf.STB_GLOBAL => switch (rhs_bind) { 51 | std.elf.STB_GLOBAL => unreachable, 52 | std.elf.STB_WEAK, std.elf.STB_LOCAL => return true, 53 | else => unreachable, 54 | }, 55 | std.elf.STB_WEAK => switch (rhs_bind) { 56 | std.elf.STB_GLOBAL, std.elf.STB_WEAK => return false, 57 | std.elf.STB_LOCAL => return true, 58 | else => unreachable, 59 | }, 60 | std.elf.STB_LOCAL => switch (rhs_bind) { 61 | std.elf.STB_GLOBAL, std.elf.STB_WEAK, std.elf.STB_LOCAL => return false, 62 | else => unreachable, 63 | }, 64 | else => unreachable, 65 | } 66 | } 67 | return lhs.st_value < rhs.st_value; 68 | } 69 | }; 70 | 71 | for (elf.symtab.items) |sym| { 72 | const shndx = sym.st_shndx; 73 | switch (sym.st_shndx) { 74 | std.elf.SHN_UNDEF, std.elf.SHN_LIVEPATCH, std.elf.SHN_ABS => continue, 75 | std.elf.SHN_COMMON => return error.TODOCommonSymbol, 76 | else => {}, 77 | } 78 | 79 | if (shdr_index != shndx) continue; 80 | try symbols.append(sym); 81 | } 82 | 83 | std.mem.sort(std.elf.Elf64_Sym, symbols.items, {}, SymSort.lessThan); 84 | 85 | var current_offset: ?u64 = null; 86 | 87 | for (symbols.items, 0..) |sym, i| { 88 | const offset = sym.st_value - shdr.sh_addr; 89 | if (current_offset) |off| { 90 | if (off == offset) continue; 91 | } 92 | const size = sym.st_size; 93 | const code = data[offset..][0..size]; 94 | 95 | switch (elf.header.e_machine.toTargetCpuArch().?) { 96 | .x86_64 => try obj.disassembleX8664(code, sym, writer), 97 | else => |arch| { 98 | return writer.print("TODO add disassembler for {s}\n", .{@tagName(arch)}); 99 | }, 100 | } 101 | 102 | if (i + 1 < symbols.items.len) { 103 | try writer.writeAll("...\n\n"); 104 | } 105 | 106 | current_offset = offset; 107 | } 108 | } 109 | } 110 | 111 | fn disassembleX8664(obj: *const ObjDump, data: []const u8, sym: std.elf.Elf64_Sym, writer: anytype) !void { 112 | const elf = obj.ctx.asConst(Context.Elf).?; 113 | var disassembler = Disassembler.init(data); 114 | var pos: usize = 0; 115 | 116 | const padding = [_]u8{' '} ** 8; 117 | const max_inst_length = 8; 118 | 119 | const name = elf.getString(sym.st_name); 120 | try writer.print("{x:0>16} <{s}>:\n", .{ sym.st_value, name }); 121 | 122 | while (try disassembler.next()) |inst| { 123 | try writer.print("{x:0>16}:", .{sym.st_value + pos}); 124 | try writer.writeAll(&padding); 125 | 126 | const slice = data[pos..disassembler.pos]; 127 | var i: usize = 0; 128 | while (i < max_inst_length) : (i += 1) { 129 | if (i < slice.len) { 130 | try writer.print("{x:0>2} ", .{slice[i]}); 131 | } else { 132 | try writer.writeAll(" "); 133 | } 134 | } 135 | 136 | try writer.print(" {}\n", .{inst}); 137 | 138 | pos = disassembler.pos; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const ObjDump = @import("ObjDump.zig"); 4 | 5 | var gpa_alloc = std.heap.GeneralPurposeAllocator(.{}){}; 6 | const gpa = gpa_alloc.allocator(); 7 | 8 | const usage = 9 | \\zig-objdump 10 | \\ 11 | \\General options: 12 | \\-h, --help Print this help and exit 13 | ; 14 | 15 | pub fn main() !void { 16 | var args_arena = std.heap.ArenaAllocator.init(gpa); 17 | defer args_arena.deinit(); 18 | const arena = args_arena.allocator(); 19 | 20 | const stdout = std.io.getStdOut().writer(); 21 | const stderr = std.io.getStdErr().writer(); 22 | 23 | const all_args = try std.process.argsAlloc(arena); 24 | const args = all_args[1..]; 25 | if (args.len == 0) { 26 | try stderr.writeAll("fatal: missing required positional argument FILE\n"); 27 | return; 28 | } 29 | 30 | const Iterator = struct { 31 | args: []const []const u8, 32 | i: usize = 0, 33 | fn next(it: *@This()) ?[]const u8 { 34 | if (it.i >= it.args.len) { 35 | return null; 36 | } 37 | defer it.i += 1; 38 | return it.args[it.i]; 39 | } 40 | }; 41 | var args_iter = Iterator{ .args = args }; 42 | 43 | var positionals = std.ArrayList([]const u8).init(arena); 44 | while (args_iter.next()) |arg| { 45 | if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { 46 | try stdout.writeAll(usage); 47 | return; 48 | } else { 49 | try positionals.append(arg); 50 | } 51 | } 52 | 53 | const filename = positionals.items[0]; 54 | const file = try std.fs.cwd().openFile(filename, .{}); 55 | defer file.close(); 56 | 57 | var obj = try ObjDump.parse(gpa, file); 58 | defer obj.deinit(); 59 | try obj.dump(stdout); 60 | } 61 | --------------------------------------------------------------------------------