summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig21
-rw-r--r--build.zig.zon6
-rw-r--r--mise.toml3
-rw-r--r--src/elfo.zig140
-rw-r--r--tests/test_symbols.zig119
5 files changed, 254 insertions, 35 deletions
diff --git a/build.zig b/build.zig
index bdc140e..d147c28 100644
--- a/build.zig
+++ b/build.zig
@@ -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 = .{""},
diff --git a/mise.toml b/mise.toml
index 3b9ae66..cc3880d 100644
--- a/mise.toml
+++ b/mise.toml
@@ -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;
+ }
+}