diff options
| author | Gabriel Schneider <[email protected]> | 2026-06-08 22:36:22 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-06-08 23:05:25 -0300 |
| commit | 34de4c7fa3a740eed14bd58b778728942bf0b005 (patch) | |
| tree | c9bc67c7e3361fa25f94fdc567506fc70cf7545b | |
| parent | d578c82c7fb3a2ac9557926ae6690b4f13534c22 (diff) | |
| download | codenomicon-34de4c7fa3a740eed14bd58b778728942bf0b005.tar.gz codenomicon-34de4c7fa3a740eed14bd58b778728942bf0b005.zip | |
added tests
| -rw-r--r-- | build.zig | 21 | ||||
| -rw-r--r-- | build.zig.zon | 6 | ||||
| -rw-r--r-- | mise.toml | 3 | ||||
| -rw-r--r-- | src/elfo.zig | 140 | ||||
| -rw-r--r-- | tests/test_symbols.zig | 119 |
5 files changed, 254 insertions, 35 deletions
@@ -101,6 +101,22 @@ pub fn build(b: *std.Build) !void { const objdump_step = b.step("objdump", "Compare elfo output against objdump per symbol"); objdump_step.dependOn(&compare_runner.step); + + // Symbol resolution test + const test_symbols_mod = b.createModule(.{ + .root_source_file = b.path("tests/test_symbols.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + test_symbols_mod.addImport("elfo", elfo.root_module); + const test_symbols = b.addExecutable(.{ + .name = "test_symbols", + .root_module = test_symbols_mod, + }); + const run_test_symbols = b.addRunArtifact(test_symbols); + run_test_symbols.addArtifactArg(c_test_exe); + run_test_symbols.expectExitCode(0); // --- const zls_check = b.step("check", "Check if the program compiles"); @@ -108,8 +124,11 @@ pub fn build(b: *std.Build) !void { zls_check.dependOn(&shock.step); zls_check.dependOn(&unit_tests.step); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_test_symbols.step); + const run_steps = [_]struct { *std.Build.Step, *std.Build.Step.Compile }{ - .{ b.step("test", "Run unit tests"), unit_tests }, + .{ test_step, unit_tests }, .{ b.step("elfo", "See the pretty elfo!"), elfo }, .{ b.step("gloves", "See the pretty gloves!"), gloves }, .{ b.step("shock", "SHOCK something!"), shock }, diff --git a/build.zig.zon b/build.zig.zon index d47ea8a..bb04c17 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = ._01_codenomicom, - .version = "0.15.2", + .version = "0.16.0", .dependencies = .{ .capstone = .{ .url = "git+https://github.com/capstone-engine/capstone?ref=6.0.0-Alpha6#484857da5dc67f7d0e0a01c36b0ebc37a349e0fd", @@ -11,8 +11,8 @@ .hash = "N-V-__8AAFeo1AEV9-c5BwB6KQi5gLoDP-sj6d2duyERG2u-", }, .vaxis = .{ - .url = "git+https://github.com/unexge/libvaxis.git#42a9d80d7129d4ecda160acb23a868acfd87fb38", - .hash = "vaxis-0.5.1-BWNV_KU-CQA3Cp59zzqkaJ376rYQK4PIwLlzNZkpwLef", + .url = "git+https://github.com/rockorager/libvaxis.git#a367b89da09bfe5e1b628501940de5b4f858f5f3", + .hash = "vaxis-0.6.0-BWNV_HHwCQB451KS7A8SMykALblPmGwHnzSfiJHjN3_9", }, }, .paths = .{""}, @@ -1,2 +1,3 @@ [tools] -zig = "master" +zig = "0.16" +zls = "0.16" diff --git a/src/elfo.zig b/src/elfo.zig index 73b666e..c52dc8e 100644 --- a/src/elfo.zig +++ b/src/elfo.zig @@ -80,21 +80,22 @@ pub fn printElf( } else null; defer if (shstrtab_data) |d| gpa.free(d); - // Find .strtab by name (requires shstrtab_data) - const strtab = if (shstrtab_data) |data| - findStrtab(sections.all.items, data) - else - null; - - // Load symbol string table - const strtab_data: ?[]u8 = if (strtab) |s| blk: { + // Load .strtab (for .symtab symbols, resolved via sh_link) + const strtab_data: ?[]u8 = if (sections.strtab) |s| blk: { try reader.seekTo(s.sh_offset); break :blk try reader.interface.readAlloc(gpa, s.sh_size); } else null; defer if (strtab_data) |d| gpa.free(d); - // Collect symbols from symtab and dynsym - var symbols = try collectSymbols(gpa, header, &reader, strtab_data, sections); + // Load .dynstr (for .dynsym symbols, resolved via sh_link) + const dynstr_data: ?[]u8 = if (sections.dynstr) |s| blk: { + try reader.seekTo(s.sh_offset); + break :blk try reader.interface.readAlloc(gpa, s.sh_size); + } else null; + defer if (dynstr_data) |d| gpa.free(d); + + // Collect symbols from symtab and dynsym with their respective string tables + var symbols = try collectSymbols(gpa, header, &reader, strtab_data, dynstr_data, shstrtab_data, sections); defer { for (symbols.items) |sym| gpa.free(sym.name); symbols.deinit(gpa); @@ -216,14 +217,19 @@ fn compareWithObjdump( } else null; defer if (shstrtab_data) |d| gpa.free(d); - const strtab = if (shstrtab_data) |data| findStrtab(sections.all.items, data) else null; - const strtab_data: ?[]u8 = if (strtab) |s| blk: { + const strtab_data: ?[]u8 = if (sections.strtab) |s| blk: { try reader.seekTo(s.sh_offset); break :blk try reader.interface.readAlloc(gpa, s.sh_size); } else null; defer if (strtab_data) |d| gpa.free(d); - var symbols = try collectSymbols(gpa, header, &reader, strtab_data, sections); + const dynstr_data: ?[]u8 = if (sections.dynstr) |s| blk: { + try reader.seekTo(s.sh_offset); + break :blk try reader.interface.readAlloc(gpa, s.sh_size); + } else null; + defer if (dynstr_data) |d| gpa.free(d); + + var symbols = try collectSymbols(gpa, header, &reader, strtab_data, dynstr_data, shstrtab_data, sections); defer { for (symbols.items) |sym| gpa.free(sym.name); symbols.deinit(gpa); @@ -318,18 +324,22 @@ fn compareWithObjdump( // --- Section collection --- -const SectionInfo = struct { +pub const SectionInfo = struct { all: std.ArrayList(std.elf.Elf64_Shdr), shstrtab: ?std.elf.Elf64_Shdr = null, symtab: ?std.elf.Elf64_Shdr = null, dynsym: ?std.elf.Elf64_Shdr = null, + /// String table for .symtab (resolved via sh_link) + strtab: ?std.elf.Elf64_Shdr = null, + /// String table for .dynsym (resolved via sh_link, typically .dynstr) + dynstr: ?std.elf.Elf64_Shdr = null, - fn deinit(self: *SectionInfo, gpa: std.mem.Allocator) void { + pub fn deinit(self: *SectionInfo, gpa: std.mem.Allocator) void { self.all.deinit(gpa); } }; -fn collectSections( +pub fn collectSections( header: std.elf.Header, reader: *std.Io.File.Reader, gpa: std.mem.Allocator, @@ -350,6 +360,16 @@ fn collectSections( else => {}, } } + // Resolve linked string tables via sh_link before sorting changes indices + if (info.symtab) |st| + if (st.sh_link < info.all.items.len) { + info.strtab = info.all.items[st.sh_link]; + }; + if (info.dynsym) |ds| + if (ds.sh_link < info.all.items.len) { + info.dynstr = info.all.items[ds.sh_link]; + }; + std.mem.sort(std.elf.Elf64_Shdr, info.all.items, {}, struct { fn inner(_: void, x: std.elf.Elf64_Shdr, y: std.elf.Elf64_Shdr) bool { return x.sh_addr < y.sh_addr; @@ -358,32 +378,25 @@ fn collectSections( return info; } -fn findStrtab(sections: []const std.elf.Elf64_Shdr, shstrtab_data: []const u8) ?std.elf.Elf64_Shdr { - for (sections) |s| { - if (s.sh_type == std.elf.SHT_STRTAB and - std.mem.eql(u8, ".strtab", std.mem.sliceTo(shstrtab_data[s.sh_name..], 0))) - return s; - } - return null; -} - // --- Symbol collection --- -fn collectSymbols( +pub fn collectSymbols( gpa: std.mem.Allocator, header: std.elf.Header, reader: *std.Io.File.Reader, strtab_data: ?[]const u8, + dynstr_data: ?[]const u8, + shstrtab_data: ?[]const u8, sections: SectionInfo, ) !std.ArrayList(SymbolRange) { var syms: std.ArrayList(SymbolRange) = try .initCapacity(gpa, 8); - if (strtab_data) |data| { + if (strtab_data) |data| if (sections.symtab) |st| try collectSymbolsFrom(gpa, header, reader, st, data, &syms); - // TODO: dynsym should technically use .dynstr, not .strtab + if (dynstr_data) |data| if (sections.dynsym) |ds| try collectSymbolsFrom(gpa, header, reader, ds, data, &syms); - } + try collectPltSymbols(gpa, reader, sections.all.items, shstrtab_data, dynstr_data, header.is_64, header.endian, &syms); std.mem.sort(SymbolRange, syms.items, {}, struct { fn inner(_: void, x: SymbolRange, y: SymbolRange) bool { return x.start < y.start; @@ -392,6 +405,73 @@ fn collectSymbols( return syms; } +/// Create synthetic symbols for PLT entries by parsing .rela.plt relocations. +/// Each .rela.plt entry maps a GOT slot to a dynsym index; the corresponding +/// PLT entry is at plt_base + (1 + i) * plt_entry_size (skipping PLT0). +fn collectPltSymbols( + gpa: std.mem.Allocator, + reader: *std.Io.File.Reader, + sections: []const std.elf.Elf64_Shdr, + shstrtab_data: ?[]const u8, + dynstr_data: ?[]const u8, + is_64: bool, + endian: std.builtin.Endian, + syms: *std.ArrayList(SymbolRange), +) !void { + const strtab = shstrtab_data orelse return; + const dstr = dynstr_data orelse return; + + // Find .plt and .rela.plt by name + var plt_section: ?std.elf.Elf64_Shdr = null; + var rela_plt: ?std.elf.Elf64_Shdr = null; + var dynsym_section: ?std.elf.Elf64_Shdr = null; + for (sections) |s| { + const name = std.mem.sliceTo(strtab[s.sh_name..], 0); + if (std.mem.eql(u8, name, ".plt")) plt_section = s; + if (std.mem.eql(u8, name, ".rela.plt")) rela_plt = s; + if (s.sh_type == std.elf.SHT_DYNSYM) dynsym_section = s; + } + + const plt = plt_section orelse return; + const rela = rela_plt orelse return; + const dsym = dynsym_section orelse return; + + const entry_size: u64 = if (plt.sh_entsize > 0) plt.sh_entsize else 16; + const rela_entry_size: u64 = if (rela.sh_entsize > 0) rela.sh_entsize else @sizeOf(std.elf.Elf64_Rela); + const num_entries = rela.sh_size / rela_entry_size; + const sym_entry_size: u64 = if (is_64) @sizeOf(std.elf.Elf64_Sym) else @sizeOf(std.elf.Elf64_Sym); + + var i: u64 = 0; + while (i < num_entries) : (i += 1) { + // Read rela entry + try reader.seekTo(rela.sh_offset + i * rela_entry_size); + const rela_entry = try reader.interface.takeStruct(std.elf.Elf64_Rela, endian); + + // Extract symbol index from r_info (upper 32 bits on 64-bit ELF) + const sym_idx = rela_entry.r_info >> 32; + if (sym_idx == 0) continue; + + // Read the dynamic symbol to get its name + const sym_offset = dsym.sh_offset + sym_idx * sym_entry_size; + if (sym_offset >= dsym.sh_offset + dsym.sh_size) continue; + try reader.seekTo(sym_offset); + const sym = try reader.interface.takeStruct(std.elf.Elf64_Sym, endian); + + const base_name = std.mem.sliceTo(dstr[sym.st_name..], 0); + if (base_name.len == 0) continue; + + // PLT entry address: skip PLT0, then entry_size per relocation + const plt_addr = plt.sh_addr + (1 + i) * entry_size; + const name = try std.fmt.allocPrint(gpa, "{s}@plt", .{base_name}); + try syms.append(gpa, .{ + .start = plt_addr, + .end = plt_addr + entry_size, + .name = name, + .kind = std.elf.STT_FUNC, + }); + } +} + fn collectSymbolsFrom( gpa: std.mem.Allocator, header: std.elf.Header, @@ -558,7 +638,7 @@ fn allocComment( // --- Types --- -const SymbolRange = struct { +pub const SymbolRange = struct { start: u64, end: u64, name: []u8, diff --git a/tests/test_symbols.zig b/tests/test_symbols.zig new file mode 100644 index 0000000..55028e4 --- /dev/null +++ b/tests/test_symbols.zig @@ -0,0 +1,119 @@ +const std = @import("std"); +const elfo = @import("elfo"); + +/// Build-system test that parses a controlled C binary and asserts correct +/// symbol resolution — static symbols, dynamic symbols, and PLT entries. +pub fn main(init: std.process.Init.Minimal) !void { + var debug_alloc: std.heap.DebugAllocator(.{}) = .init; + const gpa = debug_alloc.allocator(); + + var threaded: std.Io.Threaded = .init(gpa, .{ + .argv0 = .init(init.args), + .environ = init.environ, + }); + defer threaded.deinit(); + const io = threaded.io(); + + var args = init.args.iterate(); + _ = args.skip(); // argv[0] + const path = args.next() orelse return error.MissingArg; + + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.of(u64), 1024 * 100); + defer gpa.free(buffer); + + const f = try std.Io.Dir.cwd().openFile(io, path, .{ .mode = .read_only }); + var reader = f.reader(io, buffer); + const header = try std.elf.Header.read(&reader.interface); + + var sections = try elfo.collectSections(header, &reader, gpa); + defer sections.deinit(gpa); + + const shstrtab_data: ?[]u8 = if (sections.shstrtab) |s| blk: { + try reader.seekTo(s.sh_offset); + break :blk try reader.interface.readAlloc(gpa, s.sh_size); + } else null; + defer if (shstrtab_data) |d| gpa.free(d); + + const strtab_data: ?[]u8 = if (sections.strtab) |s| blk: { + try reader.seekTo(s.sh_offset); + break :blk try reader.interface.readAlloc(gpa, s.sh_size); + } else null; + defer if (strtab_data) |d| gpa.free(d); + + const dynstr_data: ?[]u8 = if (sections.dynstr) |s| blk: { + try reader.seekTo(s.sh_offset); + break :blk try reader.interface.readAlloc(gpa, s.sh_size); + } else null; + defer if (dynstr_data) |d| gpa.free(d); + + var symbols = try elfo.collectSymbols(gpa, header, &reader, strtab_data, dynstr_data, shstrtab_data, sections); + defer { + for (symbols.items) |sym| gpa.free(sym.name); + symbols.deinit(gpa); + } + + // --- Assertions --- + + // 1. Key sections must exist + try assert(sections.symtab != null, ".symtab must exist"); + try assert(sections.dynsym != null, ".dynsym must exist"); + try assert(sections.strtab != null, ".strtab must exist (via sh_link)"); + try assert(sections.dynstr != null, ".dynstr must exist (via sh_link)"); + + // 2. Static function symbols from .symtab + try assertSymbol(symbols.items, "main", .{ .is_func = true, .nonzero_addr = true }); + try assertSymbol(symbols.items, "a", .{ .is_func = true, .nonzero_addr = true }); + try assertSymbol(symbols.items, "bcd", .{ .is_func = true, .nonzero_addr = true }); + + // 3. Dynamic symbols from .dynsym (via .dynstr - clean names without version) + try assertSymbol(symbols.items, "printf", .{}); + try assertSymbol(symbols.items, "malloc", .{}); + try assertSymbol(symbols.items, "free", .{}); + + // 4. PLT symbols (synthesized from .rela.plt) + try assertSymbol(symbols.items, "printf@plt", .{ .is_func = true, .nonzero_addr = true }); + try assertSymbol(symbols.items, "malloc@plt", .{ .is_func = true, .nonzero_addr = true }); + try assertSymbol(symbols.items, "free@plt", .{ .is_func = true, .nonzero_addr = true }); + + // 5. strtab and dynstr must be different sections (sh_link resolved correctly) + if (sections.strtab) |st| { + if (sections.dynstr) |ds| { + try assert(st.sh_offset != ds.sh_offset, ".strtab and .dynstr must be different sections"); + } + } + + // success — no output means no warnings from the build system +} + +const AssertOpts = struct { + is_func: bool = false, + nonzero_addr: bool = false, +}; + +fn assertSymbol(symbols: []const elfo.SymbolRange, name: []const u8, opts: AssertOpts) !void { + for (symbols) |sym| { + if (std.mem.eql(u8, sym.name, name)) { + if (opts.is_func and sym.kind != std.elf.STT_FUNC) { + std.debug.print("FAIL: '{s}' expected STT_FUNC, got {d}\n", .{ name, sym.kind }); + return error.TestFailed; + } + if (opts.nonzero_addr and sym.start == 0) { + std.debug.print("FAIL: '{s}' expected nonzero address\n", .{name}); + return error.TestFailed; + } + return; // found + } + } + std.debug.print("FAIL: symbol '{s}' not found. available symbols:\n", .{name}); + for (symbols) |sym| { + std.debug.print(" {x}-{x} kind={d} '{s}'\n", .{ sym.start, sym.end, sym.kind, sym.name }); + } + return error.SymbolNotFound; +} + +fn assert(condition: bool, msg: []const u8) !void { + if (!condition) { + std.debug.print("FAIL: {s}\n", .{msg}); + return error.TestFailed; + } +} |
