diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+/draugr
+*.bin
+*.o
diff --git a/.tool-versions b/.tool-versions
@@ -0,0 +1 @@
+odin dev-2026-06
diff --git a/README.md b/README.md
@@ -0,0 +1,55 @@
+# draugr
+
+ELF mapping and analysis tool. Parses ELF32/ELF64, little and big endian, and reports
+sections, segments, symbols, entropy and memory layout.
+
+Written in Odin with no external dependencies. Odin ships no ELF package, so the parser
+is hand-rolled; byte reads go through `core:encoding/endian`.
+
+## Build
+
+```sh
+asdf install
+odin build . -out:draugr
+```
+
+## Usage
+
+```
+draugr <elf-path> [flags]
+```
+
+| Flag | Description |
+| --- | --- |
+| `--info` | ELF summary only (class, type, machine, entry, PIE, dynamic, counts) |
+| `--sections` | Section header table |
+| `--segments` | Program header table, objdump -p style |
+| `--symbols` | Symbol table, nm style, static and dynamic |
+| `--map` | Visual memory layout |
+| `--entropy` | Shannon entropy, 256-byte blocks, bits per byte |
+| `--hexdump` | Hex dump the whole file |
+| `--disasm` | List executable sections with a byte preview |
+| `--remap <file>` | Remap against a prototype file (stub) |
+| `--json` | JSON output (applies to `--info`) |
+| `--verbose` | Verbose output |
+
+`--hexdump`, `--entropy` and `--disasm` each have an `--<name>-out <file>` variant that
+writes to a file instead of stdout. Flags combine, so `--sections --symbols` prints both.
+
+Both `--flag value` and `--flag=value` work, and a single dash is accepted.
+
+### Output streams
+
+Data goes to **stdout**. Headers, separators and the `arch=` summary go to **stderr**.
+So the tool stays readable on a terminal and pipeable everywhere else.
+
+```sh
+draugr --info --json /bin/ls | jq .entry
+draugr --sections /bin/ls | awk '$3 == "PROGBITS"'
+draugr --symbols /bin/ls | grep ' U ' # undefined imports
+```
+
+Exit code is 0 on success, 1 on a read, parse or output-write failure. A failing
+`--<name>-out` does not prevent the other requested analyses from running.
+
+
diff --git a/analysis/analysis_test.odin b/analysis/analysis_test.odin
@@ -0,0 +1,865 @@
+// analysis/analysis_test.odin - Unit tests for analysis functions
+package analysis
+
+import "core:fmt"
+import "core:os"
+import "core:strings"
+import "core:testing"
+
+import "../elf"
+
+//------------------------------------------------------------------------------
+// Layout constants - built by doubling so the widths are provable, not counted
+//------------------------------------------------------------------------------
+
+SP1 :: " "
+SP2 :: SP1 + SP1
+SP3 :: SP2 + SP1
+SP4 :: SP2 + SP2
+SP5 :: SP4 + SP1
+SP6 :: SP4 + SP2
+SP7 :: SP4 + SP3
+SP8 :: SP4 + SP4
+SP9 :: SP8 + SP1
+SP10 :: SP8 + SP2
+SP12 :: SP8 + SP4
+SP13 :: SP8 + SP5
+SP15 :: SP8 + SP7
+SP16 :: SP8 + SP8
+SP17 :: SP16 + SP1
+SP32 :: SP16 + SP16
+SP38 :: SP32 + SP6
+SP47 :: SP32 + SP15
+
+DASH1 :: "-"
+DASH2 :: DASH1 + DASH1
+DASH4 :: DASH2 + DASH2
+DASH8 :: DASH4 + DASH4
+DASH16 :: DASH8 + DASH8
+DASH25 :: DASH16 + DASH8 + DASH1
+DASH32 :: DASH16 + DASH16
+DASH40 :: DASH32 + DASH8
+DASH42 :: DASH40 + DASH2
+DASH49 :: DASH32 + DASH16 + DASH1
+DASH60 :: DASH32 + DASH16 + DASH8 + DASH4
+DASH69 :: DASH60 + DASH8 + DASH1
+
+HASH1 :: "#"
+HASH2 :: HASH1 + HASH1
+HASH4 :: HASH2 + HASH2
+HASH8 :: HASH4 + HASH4
+HASH16 :: HASH8 + HASH8
+HASH25 :: HASH16 + HASH8 + HASH1
+HASH50 :: HASH25 + HASH25
+
+TOLERANCE :: 1e-9
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+tmp_path :: proc(name: string) -> string {
+ dir, err := os.temp_directory(context.allocator)
+ if err != nil {
+ return fmt.aprintf("draugr_test_%s.out", name)
+ }
+ defer delete(dir)
+ return fmt.aprintf("%s/draugr_test_%s.out", dir, name)
+}
+
+drop :: proc(path: string) {
+ os.remove(path)
+ delete(path)
+}
+
+read_all :: proc(t: ^testing.T, path: string) -> string {
+ data, err := os.read_entire_file_from_path(path, context.allocator)
+ testing.expectf(t, err == nil, "cannot read back %s: %v", path, err)
+ return string(data)
+}
+
+expect_near :: proc(t: ^testing.T, got, want: f64, loc := #caller_location) {
+ testing.expectf(t, abs(got - want) < TOLERANCE, "expected %.12f, got %.12f", want, got, loc = loc)
+}
+
+new_info :: proc() -> elf.ELF_Info {
+ info: elf.ELF_Info
+ info.program_hdrs = make([dynamic]elf.Program_Header)
+ info.section_hdrs = make([dynamic]elf.Section_Header)
+ info.symbols = make([dynamic]elf.Symbol)
+ info.dyn_symbols = make([dynamic]elf.Symbol)
+ return info
+}
+
+// offset 1 -> ".text", offset 7 -> ".data"
+SHSTRTAB := [?]u8{0, '.', 't', 'e', 'x', 't', 0, '.', 'd', 'a', 't', 'a', 0}
+
+//------------------------------------------------------------------------------
+// Tests - open_out / close_out
+//------------------------------------------------------------------------------
+
+@(test)
+test_open_out_empty_path_returns_stdout :: proc(t: ^testing.T) {
+ f, err := open_out("")
+
+ testing.expect_value(t, err, "")
+ testing.expect(t, f == os.stdout, "empty out_path must return os.stdout")
+}
+
+@(test)
+test_close_out_leaves_stdout_open :: proc(t: ^testing.T) {
+ f, err := open_out("")
+ testing.expect_value(t, err, "")
+
+ close_out(f, "")
+
+ fi, ferr := os.fstat(os.stdout, context.allocator)
+ testing.expectf(t, ferr == nil, "close_out closed stdout: %v", ferr)
+ if ferr == nil {
+ os.file_info_delete(fi, context.allocator)
+ }
+}
+
+@(test)
+test_open_out_unwritable_path_errors :: proc(t: ^testing.T) {
+ f, err := open_out("/draugr_no_such_dir_4c1f/out.txt")
+
+ testing.expect(t, f == nil, "failed open must return a nil handle")
+ testing.expect(t, err != "", "failed open must return an error string")
+ testing.expect(t, strings.contains(err, "cannot create"), "error must describe the failure")
+}
+
+@(test)
+test_open_out_creates_file_and_close_out_persists_it :: proc(t: ^testing.T) {
+ path := tmp_path("open_close")
+ defer drop(path)
+
+ f, err := open_out(path)
+ testing.expect_value(t, err, "")
+ testing.expect(t, f != nil, "real out_path must return a handle")
+ testing.expect(t, f != os.stdout, "real out_path must not return stdout")
+
+ fmt.fprintf(f, "draugr")
+ close_out(f, path)
+
+ testing.expect(t, os.exists(path), "close_out must leave the file on disk")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got, "draugr")
+}
+
+//------------------------------------------------------------------------------
+// Tests - hexdump
+//------------------------------------------------------------------------------
+
+@(test)
+test_hexdump_empty_data :: proc(t: ^testing.T) {
+ path := tmp_path("hexdump_empty")
+ defer drop(path)
+
+ testing.expect_value(t, hexdump([]u8{}, 0, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got, "")
+}
+
+@(test)
+test_hexdump_full_line :: proc(t: ^testing.T) {
+ path := tmp_path("hexdump_full_line")
+ defer drop(path)
+
+ data := []u8{
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+ }
+ testing.expect_value(t, hexdump(data, 0, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got,
+ "00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|\n")
+}
+
+@(test)
+test_hexdump_printable_ascii_column :: proc(t: ^testing.T) {
+ path := tmp_path("hexdump_printable")
+ defer drop(path)
+
+ msg := "Hello, World!!!!"
+ testing.expect_value(t, hexdump(transmute([]u8)msg, 0, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got,
+ "00000000 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 21 21 21 |Hello, World!!!!|\n")
+}
+
+@(test)
+test_hexdump_short_trailing_line_is_padded :: proc(t: ^testing.T) {
+ path := tmp_path("hexdump_short")
+ defer drop(path)
+
+ data := []u8{'A', 'B', 'C', 'D'}
+ testing.expect_value(t, hexdump(data, 0, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got, "00000000 41 42 43 44" + SP38 + "|ABCD" + SP12 + "|\n")
+}
+
+@(test)
+test_hexdump_non_zero_offset :: proc(t: ^testing.T) {
+ path := tmp_path("hexdump_offset")
+ defer drop(path)
+
+ data := []u8{0xff}
+ testing.expect_value(t, hexdump(data, 0x1000, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got, "00001000 ff" + SP47 + "|." + SP15 + "|\n")
+}
+
+@(test)
+test_hexdump_offset_advances_per_line :: proc(t: ^testing.T) {
+ path := tmp_path("hexdump_multiline")
+ defer drop(path)
+
+ data := make([]u8, 20)
+ defer delete(data)
+ for i in 0 ..< 16 {
+ data[i] = 'A'
+ }
+ for i in 16 ..< 20 {
+ data[i] = 'B'
+ }
+
+ testing.expect_value(t, hexdump(data, 0x20, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got,
+ "00000020 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|\n" +
+ "00000030 42 42 42 42" + SP38 + "|BBBB" + SP12 + "|\n")
+}
+
+@(test)
+test_hexdump_non_printable_bytes_are_dots :: proc(t: ^testing.T) {
+ path := tmp_path("hexdump_nonprintable")
+ defer drop(path)
+
+ data := []u8{0x00, 0x0a, 0x1f, 0x7f, 0x80, 0xff}
+ testing.expect_value(t, hexdump(data, 0, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got, "00000000 00 0a 1f 7f 80 ff" + SP32 + "|......" + SP10 + "|\n")
+}
+
+@(test)
+test_hexdump_printable_boundaries_are_inclusive :: proc(t: ^testing.T) {
+ path := tmp_path("hexdump_boundaries")
+ defer drop(path)
+
+ data := []u8{31, 32, 126, 127}
+ testing.expect_value(t, hexdump(data, 0, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got, "00000000 1f 20 7e 7f" + SP38 + "|. ~." + SP12 + "|\n")
+}
+
+//------------------------------------------------------------------------------
+// Tests - calculate_entropy
+//------------------------------------------------------------------------------
+
+@(test)
+test_calculate_entropy_empty_is_zero :: proc(t: ^testing.T) {
+ expect_near(t, calculate_entropy([]u8{}), 0.0)
+}
+
+@(test)
+test_calculate_entropy_single_value_is_zero :: proc(t: ^testing.T) {
+ expect_near(t, calculate_entropy([]u8{0x41, 0x41, 0x41, 0x41, 0x41}), 0.0)
+}
+
+@(test)
+test_calculate_entropy_two_values_is_one_bit :: proc(t: ^testing.T) {
+ expect_near(t, calculate_entropy([]u8{0x00, 0xff}), 1.0)
+ expect_near(t, calculate_entropy([]u8{0x00, 0x00, 0xff, 0xff}), 1.0)
+}
+
+@(test)
+test_calculate_entropy_four_values_is_two_bits :: proc(t: ^testing.T) {
+ data := []u8{0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03}
+ expect_near(t, calculate_entropy(data), 2.0)
+}
+
+// Pins the logarithm base: with the natural log this would be 5.545.
+@(test)
+test_calculate_entropy_all_256_values_is_eight_bits :: proc(t: ^testing.T) {
+ data := make([]u8, 256)
+ defer delete(data)
+ for i in 0 ..< 256 {
+ data[i] = u8(i)
+ }
+
+ expect_near(t, calculate_entropy(data), 8.0)
+}
+
+@(test)
+test_calculate_entropy_lopsided_is_between_zero_and_one :: proc(t: ^testing.T) {
+ data := []u8{0, 0, 0, 0, 0, 0, 0, 1}
+
+ entropy := calculate_entropy(data)
+ testing.expectf(t, entropy > 0.0, "7:1 split should be positive, got %.12f", entropy)
+ testing.expectf(t, entropy < 1.0, "7:1 split should be under 1 bit, got %.12f", entropy)
+ expect_near(t, entropy, 0.543564443)
+}
+
+//------------------------------------------------------------------------------
+// Tests - entropy_output
+//------------------------------------------------------------------------------
+
+ENTROPY_HEAD :: "Entropy Analysis (block size: 256 bytes)\n" +
+ "Offset" + SP7 + "Size" + SP3 + "Entropy\n" +
+ DASH40 + "\n"
+
+@(test)
+test_entropy_output_empty_data :: proc(t: ^testing.T) {
+ path := tmp_path("entropy_empty")
+ defer drop(path)
+
+ testing.expect_value(t, entropy_output([]u8{}, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got,
+ ENTROPY_HEAD +
+ "\nOverall entropy: 0.0000 bits/byte\n" +
+ "Classification: Low entropy (text/code)\n")
+}
+
+@(test)
+test_entropy_output_block_table_and_low_class :: proc(t: ^testing.T) {
+ path := tmp_path("entropy_blocks")
+ defer drop(path)
+
+ data := make([]u8, 600)
+ defer delete(data)
+
+ testing.expect_value(t, entropy_output(data, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got,
+ ENTROPY_HEAD +
+ "0x00000000" + SP3 + "256" + SP4 + "0.0000\n" +
+ "0x00000100" + SP3 + "256" + SP4 + "0.0000\n" +
+ "0x00000200" + SP3 + "88" + SP5 + "0.0000\n" +
+ "\nOverall entropy: 0.0000 bits/byte\n" +
+ "Classification: Low entropy (text/code)\n")
+}
+
+@(test)
+test_entropy_output_medium_class :: proc(t: ^testing.T) {
+ path := tmp_path("entropy_medium")
+ defer drop(path)
+
+ data := make([]u8, 32)
+ defer delete(data)
+ for i in 0 ..< 32 {
+ data[i] = u8(i)
+ }
+
+ testing.expect_value(t, entropy_output(data, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got,
+ ENTROPY_HEAD +
+ "0x00000000" + SP3 + "32" + SP5 + "5.0000\n" +
+ "\nOverall entropy: 5.0000 bits/byte\n" +
+ "Classification: Medium entropy (mixed)\n")
+}
+
+@(test)
+test_entropy_output_high_class :: proc(t: ^testing.T) {
+ path := tmp_path("entropy_high")
+ defer drop(path)
+
+ data := make([]u8, 256)
+ defer delete(data)
+ for i in 0 ..< 256 {
+ data[i] = u8(i)
+ }
+
+ testing.expect_value(t, entropy_output(data, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got,
+ ENTROPY_HEAD +
+ "0x00000000" + SP3 + "256" + SP4 + "8.0000\n" +
+ "\nOverall entropy: 8.0000 bits/byte\n" +
+ "Classification: High entropy (compressed/encrypted)\n")
+}
+
+// Brackets the 4.0 threshold from below without sitting on it.
+@(test)
+test_entropy_output_three_bits_is_low :: proc(t: ^testing.T) {
+ path := tmp_path("entropy_three_bits")
+ defer drop(path)
+
+ data := make([]u8, 8)
+ defer delete(data)
+ for i in 0 ..< 8 {
+ data[i] = u8(i)
+ }
+
+ testing.expect_value(t, entropy_output(data, path), "")
+ expect_near(t, calculate_entropy(data), 3.0)
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got, "Classification: Low entropy (text/code)\n"),
+ "3.0 bits must classify as low")
+}
+
+// Brackets the 7.0 threshold from below without sitting on it.
+@(test)
+test_entropy_output_six_bits_is_medium :: proc(t: ^testing.T) {
+ path := tmp_path("entropy_six_bits")
+ defer drop(path)
+
+ data := make([]u8, 64)
+ defer delete(data)
+ for i in 0 ..< 64 {
+ data[i] = u8(i)
+ }
+
+ testing.expect_value(t, entropy_output(data, path), "")
+ expect_near(t, calculate_entropy(data), 6.0)
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got, "Classification: Medium entropy (mixed)\n"),
+ "6.0 bits must classify as medium")
+}
+
+@(test)
+test_entropy_output_reports_open_failure :: proc(t: ^testing.T) {
+ err := entropy_output([]u8{1, 2, 3}, "/draugr_no_such_dir_4c1f/entropy.txt")
+
+ testing.expect(t, err != "", "unwritable out_path must surface an error")
+}
+
+//------------------------------------------------------------------------------
+// Tests - disasm_output
+//------------------------------------------------------------------------------
+
+DISASM_HEAD :: "Disassembly Analysis\n" +
+ "====================\n\n" +
+ "Executable Sections:\n" +
+ "Name" + SP17 + "Offset" + SP7 + "Size" + SP9 + "VAddr\n" +
+ DASH49 + "\n"
+
+DISASM_FOOT :: "\nNote: Full disassembly requires external disassembler\n" +
+ "Consider integrating zydis or capstone for x86/x64\n"
+
+@(test)
+test_disasm_output_no_sections :: proc(t: ^testing.T) {
+ path := tmp_path("disasm_empty")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+
+ testing.expect_value(t, disasm_output([]u8{}, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got, DISASM_HEAD + DISASM_FOOT)
+}
+
+@(test)
+test_disasm_output_lists_only_executable_sections :: proc(t: ^testing.T) {
+ path := tmp_path("disasm_exec")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+ info.shstrtab = SHSTRTAB[:]
+
+ append(&info.section_hdrs, elf.Section_Header{type = elf.SHT_NULL})
+ append(&info.section_hdrs, elf.Section_Header{
+ name = 1,
+ type = elf.SHT_PROGBITS,
+ flags = elf.SHF_EXECINSTR,
+ addr = 0x401000,
+ offset = 0,
+ size = 4,
+ })
+ append(&info.section_hdrs, elf.Section_Header{
+ name = 7,
+ type = elf.SHT_PROGBITS,
+ flags = 0,
+ addr = 0x402000,
+ offset = 4,
+ size = 4,
+ })
+
+ data := []u8{0x90, 0x90, 0xc3, 0x00, 0xde, 0xad, 0xbe, 0xef}
+ testing.expect_value(t, disasm_output(data, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got,
+ DISASM_HEAD +
+ ".text" + SP16 + "0x00000000 0x00000004 0x0000000000401000\n" +
+ " First 64 bytes (hex): 90 90 c3 00 \n" +
+ DISASM_FOOT)
+ testing.expect(t, !strings.contains(got, ".data"), "non-executable sections must be skipped")
+}
+
+@(test)
+test_disasm_output_caps_hex_preview_at_64_bytes :: proc(t: ^testing.T) {
+ path := tmp_path("disasm_cap")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+ info.shstrtab = SHSTRTAB[:]
+
+ append(&info.section_hdrs, elf.Section_Header{
+ name = 1,
+ type = elf.SHT_PROGBITS,
+ flags = elf.SHF_EXECINSTR,
+ size = 200,
+ })
+
+ data := make([]u8, 200)
+ defer delete(data)
+
+ testing.expect_value(t, disasm_output(data, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+
+ PREFIX :: " First 64 bytes (hex): "
+ start := strings.index(got, PREFIX)
+ testing.expect(t, start >= 0, "hex preview line must be present")
+ if start < 0 {
+ return
+ }
+
+ rest := got[start + len(PREFIX):]
+ end := strings.index_byte(rest, '\n')
+ testing.expect(t, end >= 0, "hex preview line must be terminated")
+ if end < 0 {
+ return
+ }
+
+ fields := strings.fields(rest[:end], context.allocator)
+ defer delete(fields)
+ testing.expect_value(t, len(fields), 64)
+}
+
+@(test)
+test_disasm_output_clamps_section_running_past_eof :: proc(t: ^testing.T) {
+ path := tmp_path("disasm_clamp")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+ info.shstrtab = SHSTRTAB[:]
+
+ append(&info.section_hdrs, elf.Section_Header{
+ name = 1,
+ type = elf.SHT_PROGBITS,
+ flags = elf.SHF_EXECINSTR,
+ offset = 4,
+ size = 100,
+ })
+
+ data := []u8{0, 0, 0, 0, 0xaa, 0xbb}
+ testing.expect_value(t, disasm_output(data, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got, " First 64 bytes (hex): aa bb \n"),
+ "preview must stop at end of file")
+}
+
+@(test)
+test_disasm_output_skips_preview_for_section_beyond_eof :: proc(t: ^testing.T) {
+ path := tmp_path("disasm_beyond")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+ info.shstrtab = SHSTRTAB[:]
+
+ append(&info.section_hdrs, elf.Section_Header{
+ name = 1,
+ type = elf.SHT_PROGBITS,
+ flags = elf.SHF_EXECINSTR,
+ offset = 1000,
+ size = 4,
+ })
+
+ data := []u8{1, 2, 3, 4}
+ testing.expect_value(t, disasm_output(data, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got, ".text"), "section row must still be listed")
+ testing.expect(t, !strings.contains(got, "First 64 bytes"),
+ "out-of-file section must not print a preview")
+}
+
+//------------------------------------------------------------------------------
+// Tests - map_output
+//------------------------------------------------------------------------------
+
+MAP_HEAD :: "ELF Memory Map\n" +
+ "==============\n\n" +
+ "File Layout:\n" +
+ DASH42 + "\n" +
+ "0x00000000: ELF Header (64 bytes)\n"
+
+MAP_SEG_HEAD :: "Segments:\n" +
+ "Type" + SP10 + "Offset" + SP5 + "VirtAddr" + SP3 +
+ "FileSize" + SP3 + "MemSize" + SP4 + "Flags\n" +
+ DASH69 + "\n"
+
+MAP_SEC_HEAD :: "Sections:\n" +
+ "Name" + SP13 + "Type" + SP7 + "Addr" + SP7 + "Offset" + SP5 + "Size\n" +
+ DASH60 + "\n"
+
+@(test)
+test_map_output_no_segments_no_sections :: proc(t: ^testing.T) {
+ path := tmp_path("map_empty")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+
+ data := make([]u8, 32)
+ defer delete(data)
+
+ testing.expect_value(t, map_output(data, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got,
+ MAP_HEAD +
+ "0x00000020: EOF (file size: 32 bytes)\n\n" +
+ MAP_SEG_HEAD + "\n" +
+ MAP_SEC_HEAD)
+}
+
+@(test)
+test_map_output_empty_data_no_segments :: proc(t: ^testing.T) {
+ path := tmp_path("map_empty_data")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+
+ testing.expect_value(t, map_output([]u8{}, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got, "0x00000000: EOF (file size: 0 bytes)\n"),
+ "empty file must still report an EOF marker")
+}
+
+@(test)
+test_map_output_header_table_offsets :: proc(t: ^testing.T) {
+ path := tmp_path("map_headers")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+ info.header.phoff = 64
+ info.header.phnum = 2
+ info.header.phentsize = 56
+ info.header.shoff = 1000
+ info.header.shnum = 3
+ info.header.shentsize = 64
+ for _ in 0 ..< 2 { append(&info.program_hdrs, elf.Program_Header{}) }
+ for _ in 0 ..< 3 { append(&info.section_hdrs, elf.Section_Header{}) }
+
+ data := make([]u8, 2048)
+ defer delete(data)
+
+ testing.expect_value(t, map_output(data, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got,
+ "0x00000040: Program Headers\n" +
+ "0x000000b0: Program Headers End\n" +
+ "0x000003e8: Section Headers\n" +
+ "0x000004a8: Section Headers End\n"),
+ "header table extents must be derived from phoff/phnum/phentsize and shoff/shnum/shentsize")
+}
+
+@(test)
+test_map_output_segment_row_and_bar :: proc(t: ^testing.T) {
+ path := tmp_path("map_segments")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+ append(&info.program_hdrs, elf.Program_Header{
+ type = elf.PT_LOAD,
+ flags = elf.PF_R | elf.PF_X,
+ offset = 0,
+ vaddr = 0x400000,
+ filesz = 50,
+ memsz = 50,
+ })
+ append(&info.program_hdrs, elf.Program_Header{
+ type = elf.PT_LOAD,
+ flags = elf.PF_R | elf.PF_W,
+ offset = 50,
+ vaddr = 0x600000,
+ filesz = 50,
+ memsz = 50,
+ })
+
+ data := make([]u8, 100)
+ defer delete(data)
+
+ testing.expect_value(t, map_output(data, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got,
+ "LOAD" + SP10 + "0x00000000 0x00400000 0x00000032 0x00000032 R X\n" +
+ " [|" + HASH25 + DASH25 + "|]\n" +
+ "LOAD" + SP10 + "0x00000032 0x00600000 0x00000032 0x00000032 RW \n" +
+ " [|" + DASH25 + HASH25 + "|]\n"),
+ "segment rows and coverage bars must match the file layout")
+}
+
+@(test)
+test_map_output_bar_saturates_on_tiny_file :: proc(t: ^testing.T) {
+ path := tmp_path("map_tiny")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+ append(&info.program_hdrs, elf.Program_Header{type = elf.PT_LOAD, filesz = 1, memsz = 1})
+
+ data := []u8{0x7f}
+ testing.expect_value(t, map_output(data, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got, " [|" + HASH50 + "|]\n"),
+ "a segment covering the whole file must fill the bar")
+}
+
+@(test)
+test_map_output_skips_bar_for_pt_null :: proc(t: ^testing.T) {
+ path := tmp_path("map_pt_null")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+ append(&info.program_hdrs, elf.Program_Header{type = elf.PT_NULL, filesz = 8, memsz = 8})
+
+ data := make([]u8, 16)
+ defer delete(data)
+
+ testing.expect_value(t, map_output(data, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got, "NULL"), "PT_NULL row must still be listed")
+ testing.expect(t, !strings.contains(got, " [|"), "PT_NULL must not draw a bar")
+}
+
+// BUG: print_bar computes offset*50/u64(total) with total = len(data).
+// Empty data divides by zero and kills the process with SIGILL, so this test
+// cannot clean up after itself. It writes to /dev/null to leave nothing behind.
+@(test)
+test_map_output_empty_data_with_segment_divides_by_zero :: proc(t: ^testing.T) {
+ info := new_info()
+ defer elf.destroy(&info)
+ append(&info.program_hdrs, elf.Program_Header{type = elf.PT_LOAD, filesz = 1, memsz = 1})
+
+ testing.expect_value(t, map_output([]u8{}, &info, "/dev/null"), "")
+}
+
+@(test)
+test_map_output_section_rows :: proc(t: ^testing.T) {
+ path := tmp_path("map_sections")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+ info.shstrtab = SHSTRTAB[:]
+ append(&info.section_hdrs, elf.Section_Header{
+ name = 1,
+ type = elf.SHT_PROGBITS,
+ addr = 0x401000,
+ offset = 0,
+ size = 4,
+ })
+
+ data := make([]u8, 8)
+ defer delete(data)
+
+ testing.expect_value(t, map_output(data, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got,
+ ".text" + SP12 + "PROGBITS" + SP3 + "0x00401000 0x00000000 0x4\n"),
+ "section row must render name, type, addr, offset and size")
+}
+
+@(test)
+test_map_output_reports_open_failure :: proc(t: ^testing.T) {
+ info := new_info()
+ defer elf.destroy(&info)
+
+ err := map_output([]u8{}, &info, "/draugr_no_such_dir_4c1f/map.txt")
+
+ testing.expect(t, err != "", "unwritable out_path must surface an error")
+}
+
+//------------------------------------------------------------------------------
+// Tests - remap_output
+//------------------------------------------------------------------------------
+
+@(test)
+test_remap_output_stub_reaches_out_file :: proc(t: ^testing.T) {
+ path := tmp_path("remap_stub")
+ defer drop(path)
+
+ info := new_info()
+ defer elf.destroy(&info)
+
+ testing.expect_value(t, remap_output([]u8{}, &info, "/proto/path", path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect_value(t, got, "ELF Remapping (stub - prototype: /proto/path)\n")
+}
+
+@(test)
+test_remap_output_reports_open_failure :: proc(t: ^testing.T) {
+ info := new_info()
+ defer elf.destroy(&info)
+
+ err := remap_output([]u8{}, &info, "/proto/path", "/draugr_no_such_dir_4c1f/remap.txt")
+
+ testing.expect(t, err != "", "unwritable out_path must surface an error")
+}
diff --git a/analysis/disasm.odin b/analysis/disasm.odin
@@ -0,0 +1,58 @@
+// analysis/disasm.odin - Disassembly (stub/simple)
+package analysis
+
+import "core:fmt"
+import "core:os"
+
+import "../elf"
+
+disasm_output :: proc(data: []u8, info: ^elf.ELF_Info, out_path: string) -> string {
+ f, err := open_out(out_path)
+ if err != "" {
+ return err
+ }
+ defer close_out(f, out_path)
+
+ fmt.fprintf(f, "Disassembly Analysis\n")
+ fmt.fprintf(f, "====================\n\n")
+
+ fmt.fprintf(f, "Executable Sections:\n")
+ fmt.fprintf(f, "%-20s %-12s %-12s %s\n", "Name", "Offset", "Size", "VAddr")
+ fmt.fprintf(f, "%s\n", "-------------------------------------------------")
+
+ for i := 0; i < len(info.section_hdrs); i += 1 {
+ shdr := info.section_hdrs[i]
+ if shdr.type == elf.SHT_NULL {
+ continue
+ }
+
+ name := elf.get_section_name(info, shdr)
+
+ if shdr.flags & elf.SHF_EXECINSTR != 0 {
+ fmt.fprintf(f, "%-20s 0x%08x 0x%08x 0x%016x\n",
+ name, shdr.offset, shdr.size, shdr.addr)
+
+ if int(shdr.offset) < len(data) {
+ end := int(shdr.offset + shdr.size)
+ if end > len(data) {
+ end = len(data)
+ }
+ section_data := data[int(shdr.offset):end]
+
+ fmt.fprintf(f, " First 64 bytes (hex): ")
+ for j := 0; j < len(section_data); j += 1 {
+ if j >= 64 {
+ break
+ }
+ fmt.fprintf(f, "%02x ", section_data[j])
+ }
+ fmt.fprintf(f, "\n")
+ }
+ }
+ }
+
+ fmt.fprintf(f, "\nNote: Full disassembly requires external disassembler\n")
+ fmt.fprintf(f, "Consider integrating zydis or capstone for x86/x64\n")
+
+ return ""
+}
+\ No newline at end of file
diff --git a/analysis/entropy.odin b/analysis/entropy.odin
@@ -0,0 +1,79 @@
+// analysis/entropy.odin - Shannon entropy analysis
+package analysis
+
+import "core:fmt"
+import "core:math"
+import "core:os"
+
+//------------------------------------------------------------------------------
+// Public API
+//------------------------------------------------------------------------------
+
+// calculate_entropy calculates Shannon entropy for data (0.0 to 8.0)
+calculate_entropy :: proc(data: []u8) -> f64 {
+ if len(data) == 0 {
+ return 0.0
+ }
+
+ // Count byte frequencies
+ freq: [256]f64
+ for b in data {
+ freq[b] += 1.0
+ }
+
+ // Calculate entropy
+ total := f64(len(data))
+ entropy := 0.0
+ for i := 0; i < len(freq); i += 1 {
+ f := freq[i]
+ if f > 0 {
+ p := f / total
+ entropy -= p * math.log_f64(p, 2)
+ }
+ }
+
+ return entropy
+}
+
+// entropy_output prints entropy analysis for the entire file
+// Uses 256-byte blocks for granular analysis
+entropy_output :: proc(data: []u8, out_path: string) -> string {
+ f, err := open_out(out_path)
+ if err != "" {
+ return err
+ }
+ defer close_out(f, out_path)
+
+ block_size := 256
+ fmt.fprintf(f, "Entropy Analysis (block size: %d bytes)\n", block_size)
+ fmt.fprintf(f, "%-12s %-6s %s\n", "Offset", "Size", "Entropy")
+ fmt.fprintf(f, "%s\n", "----------------------------------------")
+
+ for i := 0; i < len(data); i += block_size {
+ end := i + block_size
+ if end > len(data) {
+ end = len(data)
+ }
+
+ block := data[i:end]
+ entropy := calculate_entropy(block)
+
+ // Odin's fmt zero-fills numeric widths, so pad via a string for alignment.
+ fmt.fprintf(f, "0x%08x %-6s %.4f\n", i, fmt.tprintf("%d", len(block)), entropy)
+ }
+
+ // Overall entropy
+ overall := calculate_entropy(data)
+ fmt.fprintf(f, "\nOverall entropy: %.4f bits/byte\n", overall)
+
+ // Classification
+ if overall < 4.0 {
+ fmt.fprintf(f, "Classification: Low entropy (text/code)\n")
+ } else if overall < 7.0 {
+ fmt.fprintf(f, "Classification: Medium entropy (mixed)\n")
+ } else {
+ fmt.fprintf(f, "Classification: High entropy (compressed/encrypted)\n")
+ }
+
+ return ""
+}
diff --git a/analysis/hexdump.odin b/analysis/hexdump.odin
@@ -0,0 +1,75 @@
+// analysis/hexdump.odin - Hex dump functionality
+package analysis
+
+import "core:fmt"
+import "core:os"
+
+//------------------------------------------------------------------------------
+// Public API
+//------------------------------------------------------------------------------
+
+HexdumpConfig :: struct {
+ width : int,
+ show_offset : bool,
+ show_ascii : bool,
+}
+
+default_config :: proc() -> HexdumpConfig {
+ return HexdumpConfig{
+ width = 16,
+ show_offset = true,
+ show_ascii = true,
+ }
+}
+
+// hexdump performs a hex dump of data to stdout or file
+// Returns empty string on success, error message on failure
+hexdump :: proc(data: []u8, offset: u64, out_path: string) -> string {
+ cfg := default_config()
+
+ f, err := open_out(out_path)
+ if err != "" {
+ return err
+ }
+ defer close_out(f, out_path)
+
+
+ for i := 0; i < len(data); i += cfg.width {
+ if cfg.show_offset {
+ fmt.fprintf(f, "%08x ", offset + u64(i))
+ }
+
+ for j := 0; j < cfg.width; j += 1 {
+ if i + j < len(data) {
+ fmt.fprintf(f, "%02x ", data[i + j])
+ } else {
+ fmt.fprintf(f, " ")
+ }
+ }
+
+ if cfg.show_ascii {
+ fmt.fprintf(f, " |")
+ for j := 0; j < cfg.width; j += 1 {
+ if i + j < len(data) {
+ c := data[i + j]
+ if c >= 32 {
+ if c <= 126 {
+ fmt.fprintf(f, "%c", c)
+ } else {
+ fmt.fprintf(f, ".")
+ }
+ } else {
+ fmt.fprintf(f, ".")
+ }
+ } else {
+ fmt.fprintf(f, " ")
+ }
+ }
+ fmt.fprintf(f, "|")
+ }
+
+ fmt.fprintf(f, "\n")
+ }
+
+ return ""
+}
+\ No newline at end of file
diff --git a/analysis/mapper.odin b/analysis/mapper.odin
@@ -0,0 +1,122 @@
+// analysis/mapper.odin - ELF memory mapping visualization
+package analysis
+
+import "core:fmt"
+import "core:os"
+
+import "../elf"
+
+//------------------------------------------------------------------------------
+// Public API
+//------------------------------------------------------------------------------
+
+map_output :: proc(data: []u8, info: ^elf.ELF_Info, out_path: string) -> string {
+ f, err := open_out(out_path)
+ if err != "" {
+ return err
+ }
+ defer close_out(f, out_path)
+
+ fmt.fprintf(f, "ELF Memory Map\n")
+ fmt.fprintf(f, "==============\n\n")
+
+ fmt.fprintf(f, "File Layout:\n")
+ fmt.fprintf(f, "%s\n", "------------------------------------------")
+ fmt.fprintf(f, "0x%08x: ELF Header (64 bytes)\n", 0)
+
+ if info.header.phoff > 0 {
+ ph_end := info.header.phoff + u64(len(info.program_hdrs)) * u64(info.header.phentsize)
+ fmt.fprintf(f, "0x%08x: Program Headers\n", info.header.phoff)
+ fmt.fprintf(f, "0x%08x: Program Headers End\n", ph_end)
+ }
+
+ if info.header.shoff > 0 {
+ sh_end := info.header.shoff + u64(len(info.section_hdrs)) * u64(info.header.shentsize)
+ fmt.fprintf(f, "0x%08x: Section Headers\n", info.header.shoff)
+ fmt.fprintf(f, "0x%08x: Section Headers End\n", sh_end)
+ }
+
+ fmt.fprintf(f, "0x%08x: EOF (file size: %d bytes)\n\n", len(data), len(data))
+
+ fmt.fprintf(f, "Segments:\n")
+ fmt.fprintf(f, "%-13s %-10s %-10s %-10s %-10s %s\n", "Type", "Offset", "VirtAddr", "FileSize", "MemSize", "Flags")
+ fmt.fprintf(f, "%s\n", "---------------------------------------------------------------------")
+
+ for i := 0; i < len(info.program_hdrs); i += 1 {
+ ph := info.program_hdrs[i]
+ type_str := elf.segment_type_str(ph.type)
+ flags_str := elf.flags_to_str(ph.flags)
+
+ fmt.fprintf(f, "%-13s 0x%08x 0x%08x 0x%08x 0x%08x %s\n",
+ type_str, ph.offset, ph.vaddr, ph.filesz, ph.memsz, flags_str)
+
+ if ph.filesz > 0 {
+ if ph.type != elf.PT_NULL {
+ print_bar(f, ph.offset, ph.filesz, len(data))
+ }
+ }
+ }
+
+ fmt.fprintf(f, "\n")
+
+ fmt.fprintf(f, "Sections:\n")
+ fmt.fprintf(f, "%-16s %-10s %-10s %-10s %s\n", "Name", "Type", "Addr", "Offset", "Size")
+ fmt.fprintf(f, "%s\n", "------------------------------------------------------------")
+
+ for i := 0; i < len(info.section_hdrs); i += 1 {
+ shdr := info.section_hdrs[i]
+ name := elf.get_section_name(info, shdr)
+ if name == "" {
+ name = fmt.tprintf("[%d]", i)
+ }
+
+ type_str := elf.section_type_str(shdr.type)
+
+ fmt.fprintf(f, "%-16s %-10s 0x%08x 0x%08x 0x%x\n",
+ name, type_str, shdr.addr, shdr.offset, shdr.size)
+ }
+
+ return ""
+}
+
+remap_output :: proc(data: []u8, info: ^elf.ELF_Info, proto_path: string, out_path: string) -> string {
+ f, err := open_out(out_path)
+ if err != "" {
+ return err
+ }
+ defer close_out(f, out_path)
+
+ fmt.fprintf(f, "ELF Remapping (stub - prototype: %s)\n", proto_path)
+ return ""
+}
+
+//------------------------------------------------------------------------------
+// Internal Helpers
+//------------------------------------------------------------------------------
+
+print_bar :: proc(f: ^os.File, offset: u64, size: u64, total: int) {
+ if total <= 0 {
+ return
+ }
+ bar_width := 50
+ start := int(offset * u64(bar_width) / u64(total))
+ end := int((offset + size) * u64(bar_width) / u64(total))
+ if end > bar_width {
+ end = bar_width
+ }
+
+ fmt.fprintf(f, " [|")
+ for i := 0; i < bar_width; i += 1 {
+ if i >= start {
+ if i < end {
+ fmt.fprintf(f, "#")
+ } else {
+ fmt.fprintf(f, "-")
+ }
+ } else {
+ fmt.fprintf(f, "-")
+ }
+ }
+ fmt.fprintf(f, "|]\n")
+}
+
diff --git a/analysis/output.odin b/analysis/output.odin
@@ -0,0 +1,23 @@
+// analysis/output.odin - stdout-or-file output handles
+package analysis
+
+import "core:fmt"
+import "core:os"
+
+// An empty out_path means stdout, which must not be closed.
+open_out :: proc(out_path: string) -> (^os.File, string) {
+ if out_path == "" {
+ return os.stdout, ""
+ }
+ f, err := os.create(out_path)
+ if err != nil {
+ return nil, fmt.tprintf("cannot create %s: %v", out_path, err)
+ }
+ return f, ""
+}
+
+close_out :: proc(f: ^os.File, out_path: string) {
+ if out_path != "" {
+ os.close(f)
+ }
+}
diff --git a/elf/elf.odin b/elf/elf.odin
@@ -0,0 +1,251 @@
+// elf/elf.odin - Core ELF Structures
+package elf
+
+import "core:encoding/endian"
+import "core:fmt"
+
+//------------------------------------------------------------------------------
+// Public API - ELF Magic and Constants
+//------------------------------------------------------------------------------
+
+ELF_MAGIC :: [4]u8{0x7f, 'E', 'L', 'F'}
+
+ELF_CLASS_32 :: 1
+ELF_CLASS_64 :: 2
+
+ELF_DATA_LSB :: 1
+ELF_DATA_MSB :: 2
+
+ET_REL :: 1
+ET_EXEC :: 2
+ET_DYN :: 3
+ET_CORE :: 4
+
+EM_386 :: 3
+EM_X86_64 :: 62
+EM_ARM :: 40
+EM_AARCH64 :: 183
+EM_RISCV :: 243
+
+PT_NULL :: 0
+PT_LOAD :: 1
+PT_DYNAMIC :: 2
+PT_INTERP :: 3
+PT_NOTE :: 4
+PT_SHLIB :: 5
+PT_PHDR :: 6
+PT_TLS :: 7
+PT_GNU_EH_FRAME :: 0x6474e550
+PT_GNU_STACK :: 0x6474e551
+PT_GNU_RELRO :: 0x6474e552
+
+PF_X :: 1
+PF_W :: 2
+PF_R :: 4
+
+SHF_EXECINSTR :: 4
+
+SHT_NULL :: 0
+SHT_PROGBITS :: 1
+SHT_SYMTAB :: 2
+SHT_STRTAB :: 3
+SHT_RELA :: 4
+SHT_HASH :: 5
+SHT_DYNAMIC :: 6
+SHT_NOTE :: 7
+SHT_NOBITS :: 8
+SHT_REL :: 9
+SHT_SHLIB :: 10
+SHT_DYNSYM :: 11
+SHT_INIT_ARRAY :: 14
+SHT_FINI_ARRAY :: 15
+SHT_PREINIT_ARRAY :: 16
+SHT_GROUP :: 17
+SHT_SYMTAB_SHNDX :: 18
+SHT_GNU_ATTRIBUTES :: 0x6ffffff5
+SHT_GNU_HASH :: 0x6ffffff6
+SHT_GNU_VERDEF :: 0x6ffffffd
+SHT_GNU_VERNEED :: 0x6ffffffe
+SHT_GNU_VERSYM :: 0x6fffffff
+
+SHN_UNDEF :: 0
+SHN_XINDEX :: 0xFFFF
+PN_XNUM :: 0xFFFF
+
+STB_LOCAL :: 0
+STB_GLOBAL :: 1
+STB_WEAK :: 2
+
+STT_NOTYPE :: 0
+STT_OBJECT :: 1
+STT_FUNC :: 2
+STT_SECTION :: 3
+STT_FILE :: 4
+
+//------------------------------------------------------------------------------
+// Public API - ELF Structures
+//------------------------------------------------------------------------------
+
+ELF_Header :: struct {
+ class : u8,
+ data : u8,
+ osabi : u8,
+ type : u16,
+ machine : u16,
+ entry : u64,
+ phoff : u64,
+ shoff : u64,
+ phentsize : u16,
+ phnum : u16,
+ shentsize : u16,
+ shnum : u16,
+ shstrndx : u16,
+ flags : u32,
+}
+
+Program_Header :: struct {
+ type : u32,
+ flags : u64,
+ offset : u64,
+ vaddr : u64,
+ paddr : u64,
+ filesz : u64,
+ memsz : u64,
+ align : u64,
+}
+
+Section_Header :: struct {
+ name : u32,
+ type : u32,
+ flags : u64,
+ addr : u64,
+ offset : u64,
+ size : u64,
+ link : u32,
+ info : u32,
+ addralign : u64,
+ entsize : u64,
+}
+
+Symbol :: struct {
+ name : string,
+ value : u64,
+ size : u64,
+ bind : u8,
+ type : u8,
+ shndx : u16,
+}
+
+ELF_Info :: struct {
+ header : ELF_Header,
+ program_hdrs : [dynamic]Program_Header,
+ section_hdrs : [dynamic]Section_Header,
+ symbols : [dynamic]Symbol,
+ dyn_symbols : [dynamic]Symbol,
+ is_64bit : bool,
+ is_pie : bool,
+ is_dynamic : bool,
+ interp_offset : u64,
+ shstrtab : []u8,
+ strtab : []u8,
+ dynstr : []u8,
+}
+
+//------------------------------------------------------------------------------
+// Public API - Helper Functions
+//------------------------------------------------------------------------------
+
+// Overflow-safe: no addition that can wrap.
+in_bounds :: proc(data: []u8, offset: u64, size: u64) -> bool {
+ return offset <= u64(len(data)) && size <= u64(len(data)) - offset
+}
+
+Byte_Order :: endian.Byte_Order
+
+byte_order :: proc(header: ELF_Header) -> Byte_Order {
+ return .Little if header.data == ELF_DATA_LSB else .Big
+}
+
+// Out-of-range reads yield 0 rather than faulting; ELF fields are untrusted.
+read_u16 :: proc(data: []u8, offset: int, order: Byte_Order) -> u16 {
+ if offset < 0 || offset + 2 > len(data) { return 0 }
+ v, _ := endian.get_u16(data[offset:offset + 2], order)
+ return v
+}
+
+read_u32 :: proc(data: []u8, offset: int, order: Byte_Order) -> u32 {
+ if offset < 0 || offset + 4 > len(data) { return 0 }
+ v, _ := endian.get_u32(data[offset:offset + 4], order)
+ return v
+}
+
+read_u64 :: proc(data: []u8, offset: int, order: Byte_Order) -> u64 {
+ if offset < 0 || offset + 8 > len(data) { return 0 }
+ v, _ := endian.get_u64(data[offset:offset + 8], order)
+ return v
+}
+
+//------------------------------------------------------------------------------
+// Public API - Parsing Functions
+//------------------------------------------------------------------------------
+
+probe :: proc(data: []u8) -> bool {
+ if len(data) < 16 { return false }
+ if data[0] != ELF_MAGIC[0] { return false }
+ if data[1] != ELF_MAGIC[1] { return false }
+ if data[2] != ELF_MAGIC[2] { return false }
+ if data[3] != ELF_MAGIC[3] { return false }
+ return true
+}
+
+parse_header :: proc(data: []u8) -> (ELF_Header, string) {
+ header: ELF_Header
+
+ if len(data) < 16 {
+ return header, "Data too small"
+ }
+
+ if !probe(data) {
+ return header, "Invalid ELF magic"
+ }
+
+ header.class = data[4]
+ header.data = data[5]
+ header.osabi = data[7]
+
+ is_64bit := header.class == ELF_CLASS_64
+ order := byte_order(header)
+
+ if !is_64bit {
+ if header.class != ELF_CLASS_32 {
+ return header, fmt.tprintf("Unknown ELF class: %d", header.class)
+ }
+ }
+
+ header.type = read_u16(data, 16, order)
+ header.machine = read_u16(data, 18, order)
+
+ if is_64bit {
+ header.entry = read_u64(data, 24, order)
+ header.phoff = read_u64(data, 32, order)
+ header.shoff = read_u64(data, 40, order)
+ header.flags = read_u32(data, 48, order)
+ header.phentsize = read_u16(data, 54, order)
+ header.phnum = read_u16(data, 56, order)
+ header.shentsize = read_u16(data, 58, order)
+ header.shnum = read_u16(data, 60, order)
+ header.shstrndx = read_u16(data, 62, order)
+ } else {
+ header.entry = u64(read_u32(data, 24, order))
+ header.phoff = u64(read_u32(data, 28, order))
+ header.shoff = u64(read_u32(data, 32, order))
+ header.flags = read_u32(data, 36, order)
+ header.phentsize = read_u16(data, 42, order)
+ header.phnum = read_u16(data, 44, order)
+ header.shentsize = read_u16(data, 46, order)
+ header.shnum = read_u16(data, 48, order)
+ header.shstrndx = read_u16(data, 50, order)
+ }
+
+ return header, ""
+}
+\ No newline at end of file
diff --git a/elf/elf_test.odin b/elf/elf_test.odin
@@ -0,0 +1,1422 @@
+// elf/elf_test.odin - Unit tests for ELF parsing
+package elf
+
+import "core:mem"
+import "core:testing"
+
+make_elf64_header :: proc(data: []u8, etype: u16, machine: u16) {
+ data[0] = 0x7f
+ data[1] = 0x45
+ data[2] = 0x4c
+ data[3] = 0x46
+ data[4] = ELF_CLASS_64
+ data[5] = ELF_DATA_LSB
+ data[6] = 1
+ data[7] = 0
+
+ data[16] = u8(etype)
+ data[17] = u8(etype >> 8)
+ data[18] = u8(machine)
+ data[19] = u8(machine >> 8)
+ data[24] = 0x00
+ data[25] = 0x00
+ data[26] = 0x40
+ data[27] = 0x00
+ data[32] = 64
+ data[33] = 0
+ data[52] = 64
+ data[53] = 0
+ data[54] = 56
+ data[55] = 0
+ data[58] = 64
+ data[59] = 0
+}
+
+@(test)
+test_probe_elf64 :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_EXEC, EM_X86_64)
+
+ result := probe(data)
+ testing.expect(t, result == true, "should recognize ELF64 header")
+}
+
+@(test)
+test_probe_elf32 :: proc(t: ^testing.T) {
+ data := make([]u8, 52)
+ defer delete(data)
+
+ data[0] = 0x7f
+ data[1] = 0x45
+ data[2] = 0x4c
+ data[3] = 0x46
+ data[4] = ELF_CLASS_32
+ data[5] = ELF_DATA_LSB
+
+ result := probe(data)
+ testing.expect(t, result == true, "should recognize ELF32 header")
+}
+
+@(test)
+test_probe_invalid_magic :: proc(t: ^testing.T) {
+ data := []u8{0x00, 0x01, 0x02, 0x03}
+
+ result := probe(data)
+ testing.expect(t, result == false, "should reject invalid magic")
+}
+
+@(test)
+test_probe_too_small :: proc(t: ^testing.T) {
+ data := []u8{0x7f, 0x45, 0x4c}
+
+ result := probe(data)
+ testing.expect(t, result == false, "should reject data smaller than 16 bytes")
+}
+
+@(test)
+test_parse_header_class_64 :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_EXEC, EM_X86_64)
+
+ header, err := parse_header(data)
+ testing.expect(t, err == "", "parse should succeed")
+ testing.expect(t, header.class == ELF_CLASS_64, "class should be 64")
+}
+
+@(test)
+test_parse_header_class_32 :: proc(t: ^testing.T) {
+ data := make([]u8, 52)
+ defer delete(data)
+
+ data[0] = 0x7f
+ data[1] = 0x45
+ data[2] = 0x4c
+ data[3] = 0x46
+ data[4] = ELF_CLASS_32
+ data[5] = ELF_DATA_LSB
+ data[16] = u8(ET_EXEC)
+ data[17] = 0
+ data[18] = u8(EM_386)
+ data[19] = 0
+
+ header, err := parse_header(data)
+ testing.expect(t, err == "", "parse should succeed")
+ testing.expect(t, header.class == ELF_CLASS_32, "class should be 32")
+}
+
+@(test)
+test_parse_header_type_exec :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_EXEC, EM_X86_64)
+
+ header, err := parse_header(data)
+
+ testing.expect(t, err == "")
+ testing.expect(t, header.type == ET_EXEC, "type should be ET_EXEC")
+}
+
+@(test)
+test_parse_header_type_dyn :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_DYN, EM_X86_64)
+
+ header, err := parse_header(data)
+
+ testing.expect(t, err == "")
+ testing.expect(t, header.type == ET_DYN, "type should be ET_DYN")
+}
+
+@(test)
+test_parse_header_type_rel :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_REL, EM_X86_64)
+
+ header, err := parse_header(data)
+
+ testing.expect(t, err == "")
+ testing.expect(t, header.type == ET_REL, "type should be ET_REL")
+}
+
+@(test)
+test_parse_header_machine_x86_64 :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_EXEC, EM_X86_64)
+
+ header, err := parse_header(data)
+
+ testing.expect(t, err == "")
+ testing.expect(t, header.machine == EM_X86_64, "machine should be x86-64")
+}
+
+@(test)
+test_parse_header_machine_arm :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_EXEC, EM_ARM)
+
+ header, err := parse_header(data)
+
+ testing.expect(t, err == "")
+ testing.expect(t, header.machine == EM_ARM, "machine should be ARM")
+}
+
+@(test)
+test_parse_header_machine_aarch64 :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_EXEC, EM_AARCH64)
+
+ header, err := parse_header(data)
+
+ testing.expect(t, err == "")
+ testing.expect(t, header.machine == EM_AARCH64, "machine should be AArch64")
+}
+
+@(test)
+test_parse_header_machine_riscv :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_EXEC, EM_RISCV)
+
+ header, err := parse_header(data)
+
+ testing.expect(t, err == "")
+ testing.expect(t, header.machine == EM_RISCV, "machine should be RISC-V")
+}
+
+@(test)
+test_read_u16_le_basic :: proc(t: ^testing.T) {
+ data := []u8{0x01, 0x02}
+
+ val := read_u16(data, 0, .Little)
+ testing.expect_value(t, val, u16(0x0201))
+}
+
+@(test)
+test_read_u16_le_offset :: proc(t: ^testing.T) {
+ data := []u8{0x00, 0x00, 0x01, 0x02}
+
+ val := read_u16(data, 2, .Little)
+ testing.expect_value(t, val, u16(0x0201))
+}
+
+@(test)
+test_read_u32_le_basic :: proc(t: ^testing.T) {
+ data := []u8{0x01, 0x02, 0x03, 0x04}
+
+ val := read_u32(data, 0, .Little)
+ testing.expect_value(t, val, u32(0x04030201))
+}
+
+@(test)
+test_read_u64_le_basic :: proc(t: ^testing.T) {
+ data := []u8{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
+
+ val := read_u64(data, 0, .Little)
+ testing.expect_value(t, val, u64(0x0807060504030201))
+}
+
+@(test)
+test_read_u16_be_basic :: proc(t: ^testing.T) {
+ data := []u8{0x01, 0x02}
+
+ val := read_u16(data, 0, .Big)
+ testing.expect_value(t, val, u16(0x0102))
+}
+
+@(test)
+test_read_u32_be_basic :: proc(t: ^testing.T) {
+ data := []u8{0x01, 0x02, 0x03, 0x04}
+
+ val := read_u32(data, 0, .Big)
+ testing.expect_value(t, val, u32(0x01020304))
+}
+
+@(test)
+test_read_u64_be_basic :: proc(t: ^testing.T) {
+ data := []u8{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
+
+ val := read_u64(data, 0, .Big)
+ testing.expect_value(t, val, u64(0x0102030405060708))
+}
+
+@(test)
+test_load_elf64_basic :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_EXEC, EM_X86_64)
+
+ info, err := load(data)
+ defer destroy(&info)
+ testing.expect(t, err == "", "load should succeed")
+ testing.expect(t, info.is_64bit == true, "should be 64-bit")
+ testing.expect(t, info.is_pie == false, "ET_EXEC should not be PIE")
+}
+
+@(test)
+test_load_elf64_pie :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_DYN, EM_X86_64)
+
+ info, err := load(data)
+ defer destroy(&info)
+ testing.expect(t, err == "", "load should succeed")
+ testing.expect(t, info.is_pie == true, "ET_DYN should be PIE")
+}
+
+@(test)
+test_load_elf32_basic :: proc(t: ^testing.T) {
+ data := make([]u8, 52)
+ defer delete(data)
+
+ data[0] = 0x7f
+ data[1] = 0x45
+ data[2] = 0x4c
+ data[3] = 0x46
+ data[4] = ELF_CLASS_32
+ data[5] = ELF_DATA_LSB
+ data[16] = u8(ET_EXEC)
+ data[17] = 0
+ data[18] = u8(EM_386)
+ data[19] = 0
+
+ info, err := load(data)
+ defer destroy(&info)
+ testing.expect(t, err == "", "load should succeed")
+ testing.expect(t, info.is_64bit == false, "should be 32-bit")
+}
+
+//------------------------------------------------------------------------------
+// In-memory ELF image builder, parameterised by class and endianness
+//------------------------------------------------------------------------------
+
+TEXT_BLOB :: "\x55\x89\xe5\x83\xec\x08\xc9\xc3"
+INTERP_BLOB :: "/lib/ld-linux.so.2\x00"
+SHSTRTAB_BLOB :: "\x00.text\x00.interp\x00.symtab\x00.strtab\x00.dynsym\x00.dynstr\x00.shstrtab\x00"
+STRTAB_BLOB :: "\x00main\x00counter\x00printf\x00"
+DYNSTR_BLOB :: "\x00puts\x00"
+
+NAME_TEXT :: 1
+NAME_INTERP :: 7
+NAME_SYMTAB :: 15
+NAME_STRTAB :: 23
+NAME_DYNSYM :: 31
+NAME_DYNSTR :: 39
+NAME_SHSTRTAB :: 47
+
+STR_MAIN :: 1
+STR_COUNTER :: 6
+STR_PRINTF :: 14
+DYN_PUTS :: 1
+
+SEC_NULL :: 0
+SEC_TEXT :: 1
+SEC_INTERP :: 2
+SEC_SYMTAB :: 3
+SEC_STRTAB :: 4
+SEC_DYNSYM :: 5
+SEC_DYNSTR :: 6
+SEC_SHSTRTAB :: 7
+
+Raw_Sym :: struct {
+ name : u32,
+ info : u8,
+ other : u8,
+ shndx : u16,
+ value : u64,
+ size : u64,
+}
+
+Fixture :: struct {
+ data : []u8,
+ class : u8,
+ endian : u8,
+ type : u16,
+ machine : u16,
+ entry : u64,
+ flags : u32,
+ phoff : u64,
+ shoff : u64,
+ phentsize : u16,
+ shentsize : u16,
+ phnum : u16,
+ shnum : u16,
+ shstrndx : u16,
+ symsize : int,
+ interp_off: u64,
+ segments : [4]Program_Header,
+ sections : [8]Section_Header,
+ symbols : [4]Raw_Sym,
+ dynsyms : [2]Raw_Sym,
+}
+
+put_u16 :: proc(data: []u8, off: int, v: u16, little: bool) {
+ if little {
+ data[off+0] = u8(v)
+ data[off+1] = u8(v >> 8)
+ } else {
+ data[off+0] = u8(v >> 8)
+ data[off+1] = u8(v)
+ }
+}
+
+put_u32 :: proc(data: []u8, off: int, v: u32, little: bool) {
+ for i in 0 ..< 4 {
+ shift := uint(8 * i) if little else uint(8 * (3 - i))
+ data[off+i] = u8(v >> shift)
+ }
+}
+
+put_u64 :: proc(data: []u8, off: int, v: u64, little: bool) {
+ for i in 0 ..< 8 {
+ shift := uint(8 * i) if little else uint(8 * (7 - i))
+ data[off+i] = u8(v >> shift)
+ }
+}
+
+align_up :: proc(v: int, a: int) -> int {
+ return (v + a - 1) / a * a
+}
+
+write_phdr :: proc(data: []u8, off: int, is64: bool, little: bool, p: Program_Header) {
+ if is64 {
+ put_u32(data, off+0, p.type, little)
+ put_u32(data, off+4, u32(p.flags), little)
+ put_u64(data, off+8, p.offset, little)
+ put_u64(data, off+16, p.vaddr, little)
+ put_u64(data, off+24, p.paddr, little)
+ put_u64(data, off+32, p.filesz, little)
+ put_u64(data, off+40, p.memsz, little)
+ put_u64(data, off+48, p.align, little)
+ } else {
+ put_u32(data, off+0, p.type, little)
+ put_u32(data, off+4, u32(p.offset), little)
+ put_u32(data, off+8, u32(p.vaddr), little)
+ put_u32(data, off+12, u32(p.paddr), little)
+ put_u32(data, off+16, u32(p.filesz), little)
+ put_u32(data, off+20, u32(p.memsz), little)
+ put_u32(data, off+24, u32(p.flags), little)
+ put_u32(data, off+28, u32(p.align), little)
+ }
+}
+
+write_shdr :: proc(data: []u8, off: int, is64: bool, little: bool, s: Section_Header) {
+ if is64 {
+ put_u32(data, off+0, s.name, little)
+ put_u32(data, off+4, s.type, little)
+ put_u64(data, off+8, s.flags, little)
+ put_u64(data, off+16, s.addr, little)
+ put_u64(data, off+24, s.offset, little)
+ put_u64(data, off+32, s.size, little)
+ put_u32(data, off+40, s.link, little)
+ put_u32(data, off+44, s.info, little)
+ put_u64(data, off+48, s.addralign, little)
+ put_u64(data, off+56, s.entsize, little)
+ } else {
+ put_u32(data, off+0, s.name, little)
+ put_u32(data, off+4, s.type, little)
+ put_u32(data, off+8, u32(s.flags), little)
+ put_u32(data, off+12, u32(s.addr), little)
+ put_u32(data, off+16, u32(s.offset), little)
+ put_u32(data, off+20, u32(s.size), little)
+ put_u32(data, off+24, s.link, little)
+ put_u32(data, off+28, s.info, little)
+ put_u32(data, off+32, u32(s.addralign), little)
+ put_u32(data, off+36, u32(s.entsize), little)
+ }
+}
+
+write_sym :: proc(data: []u8, off: int, is64: bool, little: bool, s: Raw_Sym) {
+ if is64 {
+ put_u32(data, off+0, s.name, little)
+ data[off+4] = s.info
+ data[off+5] = s.other
+ put_u16(data, off+6, s.shndx, little)
+ put_u64(data, off+8, s.value, little)
+ put_u64(data, off+16, s.size, little)
+ } else {
+ put_u32(data, off+0, s.name, little)
+ put_u32(data, off+4, u32(s.value), little)
+ put_u32(data, off+8, u32(s.size), little)
+ data[off+12] = s.info
+ data[off+13] = s.other
+ put_u16(data, off+14, s.shndx, little)
+ }
+}
+
+build_elf :: proc(class: u8, endian: u8) -> Fixture {
+ f: Fixture
+ f.class = class
+ f.endian = endian
+
+ is64 := class == ELF_CLASS_64
+ little := endian == ELF_DATA_LSB
+
+ ehsize := 64 if is64 else 52
+ f.phentsize = 56 if is64 else 32
+ f.shentsize = 64 if is64 else 40
+ f.symsize = 24 if is64 else 16
+
+ f.phnum = 4
+ f.shnum = 8
+ f.shstrndx = SEC_SHSTRTAB
+ f.type = ET_EXEC
+ f.machine = EM_X86_64 if is64 else EM_386
+ f.entry = 0x0000000000401120 if is64 else 0x08049000
+ f.flags = 0x05000200
+
+ off_ph := ehsize
+ off_interp := off_ph + int(f.phnum) * int(f.phentsize)
+ off_text := off_interp + len(INTERP_BLOB)
+ off_shstr := off_text + len(TEXT_BLOB)
+ off_strtab := off_shstr + len(SHSTRTAB_BLOB)
+ off_dynstr := off_strtab + len(STRTAB_BLOB)
+ off_symtab := align_up(off_dynstr + len(DYNSTR_BLOB), 8)
+ off_dynsym := off_symtab + 4 * f.symsize
+ off_sh := align_up(off_dynsym + 2 * f.symsize, 8)
+ total := off_sh + int(f.shnum) * int(f.shentsize)
+
+ f.phoff = u64(off_ph)
+ f.shoff = u64(off_sh)
+ f.interp_off = u64(off_interp)
+
+ f.segments = [4]Program_Header{
+ {
+ type = PT_LOAD, flags = PF_R | PF_X,
+ offset = 0, vaddr = 0x08048000, paddr = 0x08048000,
+ filesz = 0x0640, memsz = 0x0640, align = 0x1000,
+ },
+ {
+ type = PT_LOAD, flags = PF_R | PF_W,
+ offset = 0x0f00, vaddr = 0x0804af00, paddr = 0x0804af10,
+ filesz = 0x0118, memsz = 0x0220, align = 0x1000,
+ },
+ {
+ type = PT_INTERP, flags = PF_R,
+ offset = u64(off_interp), vaddr = 0x08048154, paddr = 0x08048154,
+ filesz = u64(len(INTERP_BLOB)), memsz = u64(len(INTERP_BLOB)), align = 1,
+ },
+ {
+ type = PT_DYNAMIC, flags = PF_R | PF_W,
+ offset = 0x0f14, vaddr = 0x0804af14, paddr = 0x0804af18,
+ filesz = 0x00e8, memsz = 0x00e8, align = 4,
+ },
+ }
+
+ f.sections = [8]Section_Header{
+ {},
+ {
+ name = NAME_TEXT, type = SHT_PROGBITS, flags = 0x6, addr = 0x08049000,
+ offset = u64(off_text), size = u64(len(TEXT_BLOB)), addralign = 16,
+ },
+ {
+ name = NAME_INTERP, type = SHT_PROGBITS, flags = 0x2, addr = 0x08048154,
+ offset = u64(off_interp), size = u64(len(INTERP_BLOB)), addralign = 1,
+ },
+ {
+ name = NAME_SYMTAB, type = SHT_SYMTAB,
+ offset = u64(off_symtab), size = u64(4 * f.symsize),
+ link = SEC_STRTAB, info = 2, addralign = 8, entsize = u64(f.symsize),
+ },
+ {
+ name = NAME_STRTAB, type = SHT_STRTAB,
+ offset = u64(off_strtab), size = u64(len(STRTAB_BLOB)), addralign = 1,
+ },
+ {
+ name = NAME_DYNSYM, type = SHT_DYNSYM, flags = 0x2, addr = 0x080481b0,
+ offset = u64(off_dynsym), size = u64(2 * f.symsize),
+ link = SEC_DYNSTR, info = 1, addralign = 4, entsize = u64(f.symsize),
+ },
+ {
+ name = NAME_DYNSTR, type = SHT_STRTAB, flags = 0x2, addr = 0x080481f0,
+ offset = u64(off_dynstr), size = u64(len(DYNSTR_BLOB)), addralign = 1,
+ },
+ {
+ name = NAME_SHSTRTAB, type = SHT_STRTAB,
+ offset = u64(off_shstr), size = u64(len(SHSTRTAB_BLOB)), addralign = 1,
+ },
+ }
+
+ f.symbols = [4]Raw_Sym{
+ {},
+ {name = STR_COUNTER, info = (STB_LOCAL << 4) | STT_OBJECT, value = 0x0804af00, size = 4, shndx = SEC_TEXT},
+ {name = STR_MAIN, info = (STB_GLOBAL << 4) | STT_FUNC, value = 0x08049000, size = 0x40, shndx = SEC_TEXT},
+ {name = STR_PRINTF, info = (STB_GLOBAL << 4) | STT_FUNC, value = 0, size = 0, shndx = 0},
+ }
+
+ f.dynsyms = [2]Raw_Sym{
+ {},
+ {name = DYN_PUTS, info = (STB_GLOBAL << 4) | STT_FUNC, value = 0, size = 0, shndx = 0},
+ }
+
+ data := make([]u8, total)
+ f.data = data
+
+ data[0] = 0x7f
+ data[1] = 'E'
+ data[2] = 'L'
+ data[3] = 'F'
+ data[4] = class
+ data[5] = endian
+ data[6] = 1
+ data[7] = 0
+
+ put_u16(data, 16, f.type, little)
+ put_u16(data, 18, f.machine, little)
+ put_u32(data, 20, 1, little)
+
+ if is64 {
+ put_u64(data, 24, f.entry, little)
+ put_u64(data, 32, f.phoff, little)
+ put_u64(data, 40, f.shoff, little)
+ put_u32(data, 48, f.flags, little)
+ put_u16(data, 52, u16(ehsize), little)
+ put_u16(data, 54, f.phentsize, little)
+ put_u16(data, 56, f.phnum, little)
+ put_u16(data, 58, f.shentsize, little)
+ put_u16(data, 60, f.shnum, little)
+ put_u16(data, 62, f.shstrndx, little)
+ } else {
+ put_u32(data, 24, u32(f.entry), little)
+ put_u32(data, 28, u32(f.phoff), little)
+ put_u32(data, 32, u32(f.shoff), little)
+ put_u32(data, 36, f.flags, little)
+ put_u16(data, 40, u16(ehsize), little)
+ put_u16(data, 42, f.phentsize, little)
+ put_u16(data, 44, f.phnum, little)
+ put_u16(data, 46, f.shentsize, little)
+ put_u16(data, 48, f.shnum, little)
+ put_u16(data, 50, f.shstrndx, little)
+ }
+
+ for seg, i in f.segments {
+ write_phdr(data, off_ph + i * int(f.phentsize), is64, little, seg)
+ }
+
+ copy(data[off_interp:], INTERP_BLOB)
+ copy(data[off_text:], TEXT_BLOB)
+ copy(data[off_shstr:], SHSTRTAB_BLOB)
+ copy(data[off_strtab:], STRTAB_BLOB)
+ copy(data[off_dynstr:], DYNSTR_BLOB)
+
+ for sym, i in f.symbols {
+ write_sym(data, off_symtab + i * f.symsize, is64, little, sym)
+ }
+ for sym, i in f.dynsyms {
+ write_sym(data, off_dynsym + i * f.symsize, is64, little, sym)
+ }
+ for sec, i in f.sections {
+ write_shdr(data, off_sh + i * int(f.shentsize), is64, little, sec)
+ }
+
+ return f
+}
+
+//------------------------------------------------------------------------------
+// Full image round-trip across every class/endianness combination
+//------------------------------------------------------------------------------
+
+SECTION_NAMES :: [8]string{"", ".text", ".interp", ".symtab", ".strtab", ".dynsym", ".dynstr", ".shstrtab"}
+SYMBOL_NAMES :: [4]string{"", "counter", "main", "printf"}
+DYNSYM_NAMES :: [2]string{"", "puts"}
+
+check_full_image :: proc(t: ^testing.T, class: u8, endian: u8) {
+ f := build_elf(class, endian)
+ defer delete(f.data)
+
+ info, err := load(f.data)
+ testing.expect(t, err == "", "load should succeed")
+ defer destroy(&info)
+
+ testing.expect_value(t, info.header.class, class)
+ testing.expect_value(t, info.header.data, endian)
+ testing.expect_value(t, info.is_64bit, class == ELF_CLASS_64)
+ testing.expect_value(t, info.is_pie, false)
+
+ testing.expect_value(t, info.header.type, f.type)
+ testing.expect_value(t, info.header.machine, f.machine)
+ testing.expect_value(t, info.header.entry, f.entry)
+ testing.expect_value(t, info.header.phoff, f.phoff)
+ testing.expect_value(t, info.header.shoff, f.shoff)
+ testing.expect_value(t, info.header.flags, f.flags)
+ testing.expect_value(t, info.header.phentsize, f.phentsize)
+ testing.expect_value(t, info.header.shentsize, f.shentsize)
+ testing.expect_value(t, info.header.phnum, f.phnum)
+ testing.expect_value(t, info.header.shnum, f.shnum)
+ testing.expect_value(t, info.header.shstrndx, f.shstrndx)
+
+ testing.expect_value(t, len(info.program_hdrs), int(f.phnum))
+ if len(info.program_hdrs) == int(f.phnum) {
+ for want, i in f.segments {
+ testing.expect_value(t, info.program_hdrs[i], want)
+ }
+ }
+
+ testing.expect_value(t, info.is_dynamic, true)
+ testing.expect_value(t, info.interp_offset, f.interp_off)
+
+ testing.expect_value(t, len(info.section_hdrs), int(f.shnum))
+ if len(info.section_hdrs) == int(f.shnum) {
+ names := SECTION_NAMES
+ for want, i in f.sections {
+ testing.expect_value(t, info.section_hdrs[i], want)
+ testing.expect_value(t, get_section_name(&info, info.section_hdrs[i]), names[i])
+ }
+ }
+
+ testing.expect_value(t, string(info.shstrtab), SHSTRTAB_BLOB)
+ testing.expect_value(t, string(info.strtab), STRTAB_BLOB)
+ testing.expect_value(t, string(info.dynstr), DYNSTR_BLOB)
+
+ testing.expect_value(t, len(info.symbols), len(f.symbols))
+ if len(info.symbols) == len(f.symbols) {
+ names := SYMBOL_NAMES
+ for want, i in f.symbols {
+ got := info.symbols[i]
+ testing.expect_value(t, got.name, names[i])
+ testing.expect_value(t, got.value, want.value)
+ testing.expect_value(t, got.size, want.size)
+ testing.expect_value(t, got.bind, want.info >> 4)
+ testing.expect_value(t, got.type, want.info & 0xf)
+ testing.expect_value(t, got.shndx, want.shndx)
+ }
+ testing.expect_value(t, info.symbols[1].bind, u8(STB_LOCAL))
+ testing.expect_value(t, info.symbols[1].type, u8(STT_OBJECT))
+ testing.expect_value(t, info.symbols[2].bind, u8(STB_GLOBAL))
+ testing.expect_value(t, info.symbols[2].type, u8(STT_FUNC))
+ testing.expect_value(t, info.symbols[3].shndx, u16(0))
+ }
+
+ testing.expect_value(t, len(info.dyn_symbols), len(f.dynsyms))
+ if len(info.dyn_symbols) == len(f.dynsyms) {
+ names := DYNSYM_NAMES
+ for want, i in f.dynsyms {
+ got := info.dyn_symbols[i]
+ testing.expect_value(t, got.name, names[i])
+ testing.expect_value(t, got.bind, want.info >> 4)
+ testing.expect_value(t, got.type, want.info & 0xf)
+ testing.expect_value(t, got.shndx, want.shndx)
+ }
+ }
+}
+
+@(test)
+test_full_image_elf32_le :: proc(t: ^testing.T) {
+ check_full_image(t, ELF_CLASS_32, ELF_DATA_LSB)
+}
+
+@(test)
+test_full_image_elf32_be :: proc(t: ^testing.T) {
+ check_full_image(t, ELF_CLASS_32, ELF_DATA_MSB)
+}
+
+@(test)
+test_full_image_elf64_le :: proc(t: ^testing.T) {
+ check_full_image(t, ELF_CLASS_64, ELF_DATA_LSB)
+}
+
+@(test)
+test_full_image_elf64_be :: proc(t: ^testing.T) {
+ check_full_image(t, ELF_CLASS_64, ELF_DATA_MSB)
+}
+
+//------------------------------------------------------------------------------
+// Regression: ELF32 header fields must use the 32-bit layout
+//------------------------------------------------------------------------------
+
+build_elf32_header_only :: proc(little: bool, entry: u32, phoff: u32, shoff: u32, flags: u32) -> []u8 {
+ data := make([]u8, 52)
+ data[0] = 0x7f
+ data[1] = 'E'
+ data[2] = 'L'
+ data[3] = 'F'
+ data[4] = ELF_CLASS_32
+ data[5] = ELF_DATA_LSB if little else ELF_DATA_MSB
+ data[6] = 1
+
+ put_u16(data, 16, ET_EXEC, little)
+ put_u16(data, 18, EM_386, little)
+ put_u32(data, 20, 1, little)
+ put_u32(data, 24, entry, little)
+ put_u32(data, 28, phoff, little)
+ put_u32(data, 32, shoff, little)
+ put_u32(data, 36, flags, little)
+ put_u16(data, 40, 52, little)
+ put_u16(data, 42, 32, little)
+ put_u16(data, 44, 9, little)
+ put_u16(data, 46, 40, little)
+ put_u16(data, 48, 30, little)
+ put_u16(data, 50, 29, little)
+ return data
+}
+
+@(test)
+test_regression_elf32_header_uses_32bit_layout_le :: proc(t: ^testing.T) {
+ data := build_elf32_header_only(true, 0x08049000, 52, 12620, 0)
+ defer delete(data)
+
+ h, err := parse_header(data)
+ testing.expect(t, err == "", "parse should succeed")
+
+ testing.expect_value(t, h.entry, u64(0x08049000))
+ testing.expect_value(t, h.phoff, u64(52))
+ testing.expect_value(t, h.shoff, u64(12620))
+ testing.expect_value(t, h.flags, u32(0))
+ testing.expect_value(t, h.phentsize, u16(32))
+ testing.expect_value(t, h.phnum, u16(9))
+ testing.expect_value(t, h.shentsize, u16(40))
+ testing.expect_value(t, h.shnum, u16(30))
+ testing.expect_value(t, h.shstrndx, u16(29))
+}
+
+@(test)
+test_regression_elf32_header_uses_32bit_layout_be :: proc(t: ^testing.T) {
+ data := build_elf32_header_only(false, 0x08049000, 52, 12620, 0)
+ defer delete(data)
+
+ h, err := parse_header(data)
+ testing.expect(t, err == "", "parse should succeed")
+
+ testing.expect_value(t, h.entry, u64(0x08049000))
+ testing.expect_value(t, h.phoff, u64(52))
+ testing.expect_value(t, h.shoff, u64(12620))
+ testing.expect_value(t, h.flags, u32(0))
+ testing.expect_value(t, h.phentsize, u16(32))
+ testing.expect_value(t, h.phnum, u16(9))
+ testing.expect_value(t, h.shentsize, u16(40))
+ testing.expect_value(t, h.shnum, u16(30))
+ testing.expect_value(t, h.shstrndx, u16(29))
+}
+
+@(test)
+test_regression_elf32_flags_not_contaminated :: proc(t: ^testing.T) {
+ for little in ([2]bool{true, false}) {
+ data := build_elf32_header_only(little, 0xdeadbeef, 0x11223344, 0x55667788, 0x05000402)
+ defer delete(data)
+
+ h, err := parse_header(data)
+ testing.expect(t, err == "", "parse should succeed")
+
+ testing.expect_value(t, h.entry, u64(0xdeadbeef))
+ testing.expect_value(t, h.phoff, u64(0x11223344))
+ testing.expect_value(t, h.shoff, u64(0x55667788))
+ testing.expect_value(t, h.flags, u32(0x05000402))
+ }
+}
+
+@(test)
+test_regression_elf32_program_headers_parse :: proc(t: ^testing.T) {
+ f := build_elf(ELF_CLASS_32, ELF_DATA_LSB)
+ defer delete(f.data)
+
+ info, err := load(f.data)
+ testing.expect(t, err == "", "load should succeed")
+ defer destroy(&info)
+
+ testing.expect_value(t, info.header.phoff, u64(52))
+ testing.expect_value(t, len(info.program_hdrs), 4)
+ if len(info.program_hdrs) == 4 {
+ testing.expect_value(t, info.program_hdrs[0].type, u32(PT_LOAD))
+ testing.expect_value(t, info.program_hdrs[2].type, u32(PT_INTERP))
+ testing.expect_value(t, info.program_hdrs[3].type, u32(PT_DYNAMIC))
+ }
+}
+
+//------------------------------------------------------------------------------
+// Bounds helpers
+//------------------------------------------------------------------------------
+
+@(test)
+test_in_bounds :: proc(t: ^testing.T) {
+ data := make([]u8, 16)
+ defer delete(data)
+
+ testing.expect_value(t, in_bounds(data, 0, 16), true)
+ testing.expect_value(t, in_bounds(data, 16, 0), true)
+ testing.expect_value(t, in_bounds(data, 8, 8), true)
+ testing.expect_value(t, in_bounds(data, 15, 1), true)
+
+ testing.expect_value(t, in_bounds(data, 17, 0), false)
+ testing.expect_value(t, in_bounds(data, 16, 1), false)
+ testing.expect_value(t, in_bounds(data, 8, 9), false)
+ testing.expect_value(t, in_bounds(data, 0, 17), false)
+ testing.expect_value(t, in_bounds(data, 0, 0xFFFFFFFFFFFFFFFF), false)
+ testing.expect_value(t, in_bounds(data, 0xFFFFFFFFFFFFFFFF, 0), false)
+ testing.expect_value(t, in_bounds(data, 0xFFFFFFFFFFFFFFFF, 1), false)
+ testing.expect_value(t, in_bounds(data, 0xFFFFFFFFFFFFFFF8, 8), false)
+ testing.expect_value(t, in_bounds(data, 0xFF00000000000000, 0), false)
+ testing.expect_value(t, in_bounds(data, 0x8000000000000000, 0), false)
+}
+
+@(test)
+test_in_bounds_empty_slice :: proc(t: ^testing.T) {
+ empty: []u8
+ testing.expect_value(t, in_bounds(empty, 0, 0), true)
+ testing.expect_value(t, in_bounds(empty, 0, 1), false)
+ testing.expect_value(t, in_bounds(empty, 1, 0), false)
+}
+
+@(test)
+test_getters_reject_negative_offsets :: proc(t: ^testing.T) {
+ data := []u8{1, 2, 3, 4, 5, 6, 7, 8}
+
+ for off in ([3]int{-1, -8, -1024}) {
+ testing.expect_value(t, read_u16(data, off, .Little), u16(0))
+ testing.expect_value(t, read_u32(data, off, .Little), u32(0))
+ testing.expect_value(t, read_u64(data, off, .Little), u64(0))
+ testing.expect_value(t, read_u16(data, off, .Big), u16(0))
+ testing.expect_value(t, read_u32(data, off, .Big), u32(0))
+ testing.expect_value(t, read_u64(data, off, .Big), u64(0))
+ }
+}
+
+@(test)
+test_getters_reject_past_end :: proc(t: ^testing.T) {
+ data := []u8{1, 2, 3, 4, 5, 6, 7, 8}
+
+ testing.expect_value(t, read_u16(data, 7, .Little), u16(0))
+ testing.expect_value(t, read_u16(data, 8, .Little), u16(0))
+ testing.expect_value(t, read_u16(data, 999, .Little), u16(0))
+ testing.expect_value(t, read_u16(data, 7, .Big), u16(0))
+ testing.expect_value(t, read_u16(data, 999, .Big), u16(0))
+
+ testing.expect_value(t, read_u32(data, 5, .Little), u32(0))
+ testing.expect_value(t, read_u32(data, 8, .Little), u32(0))
+ testing.expect_value(t, read_u32(data, 5, .Big), u32(0))
+ testing.expect_value(t, read_u32(data, 999, .Big), u32(0))
+
+ testing.expect_value(t, read_u64(data, 1, .Little), u64(0))
+ testing.expect_value(t, read_u64(data, 8, .Little), u64(0))
+ testing.expect_value(t, read_u64(data, 1, .Big), u64(0))
+ testing.expect_value(t, read_u64(data, 999, .Big), u64(0))
+}
+
+@(test)
+test_getters_exact_fit :: proc(t: ^testing.T) {
+ data := []u8{1, 2, 3, 4, 5, 6, 7, 8}
+
+ testing.expect_value(t, read_u16(data, 6, .Little), u16(0x0807))
+ testing.expect_value(t, read_u16(data, 6, .Big), u16(0x0708))
+ testing.expect_value(t, read_u32(data, 4, .Little), u32(0x08070605))
+ testing.expect_value(t, read_u32(data, 4, .Big), u32(0x05060708))
+ testing.expect_value(t, read_u64(data, 0, .Little), u64(0x0807060504030201))
+ testing.expect_value(t, read_u64(data, 0, .Big), u64(0x0102030405060708))
+}
+
+@(test)
+test_getters_on_empty_slice :: proc(t: ^testing.T) {
+ empty: []u8
+ testing.expect_value(t, read_u16(empty, 0, .Little), u16(0))
+ testing.expect_value(t, read_u32(empty, 0, .Little), u32(0))
+ testing.expect_value(t, read_u64(empty, 0, .Little), u64(0))
+ testing.expect_value(t, read_u16(empty, 0, .Big), u16(0))
+ testing.expect_value(t, read_u32(empty, 0, .Big), u32(0))
+ testing.expect_value(t, read_u64(empty, 0, .Big), u64(0))
+}
+
+//------------------------------------------------------------------------------
+// is_dynamic detection
+//------------------------------------------------------------------------------
+
+build_elf64_with_segments :: proc(segs: []Program_Header) -> []u8 {
+ ehsize := 64
+ total := ehsize + len(segs) * 56
+ data := make([]u8, total)
+
+ data[0] = 0x7f
+ data[1] = 'E'
+ data[2] = 'L'
+ data[3] = 'F'
+ data[4] = ELF_CLASS_64
+ data[5] = ELF_DATA_LSB
+ data[6] = 1
+
+ put_u16(data, 16, ET_DYN, true)
+ put_u16(data, 18, EM_X86_64, true)
+ put_u64(data, 32, u64(ehsize), true)
+ put_u16(data, 52, u16(ehsize), true)
+ put_u16(data, 54, 56, true)
+ put_u16(data, 56, u16(len(segs)), true)
+ put_u16(data, 58, 64, true)
+
+ for seg, i in segs {
+ write_phdr(data, ehsize + i * 56, true, true, seg)
+ }
+ return data
+}
+
+@(test)
+test_is_dynamic_with_pt_dynamic_and_no_interp :: proc(t: ^testing.T) {
+ segs := [2]Program_Header{
+ {type = PT_LOAD, offset = 0, filesz = 0x100, memsz = 0x100},
+ {type = PT_DYNAMIC, offset = 0x2000, filesz = 0xe0, memsz = 0xe0},
+ }
+ data := build_elf64_with_segments(segs[:])
+ defer delete(data)
+
+ info, err := load(data)
+ testing.expect(t, err == "", "load should succeed")
+ defer destroy(&info)
+
+ testing.expect_value(t, info.is_dynamic, true)
+ testing.expect_value(t, info.interp_offset, u64(0))
+ testing.expect_value(t, info.is_pie, true)
+}
+
+@(test)
+test_is_dynamic_with_interp_only :: proc(t: ^testing.T) {
+ segs := [2]Program_Header{
+ {type = PT_LOAD, offset = 0, filesz = 0x100, memsz = 0x100},
+ {type = PT_INTERP, offset = 0x318, filesz = 0x1c, memsz = 0x1c},
+ }
+ data := build_elf64_with_segments(segs[:])
+ defer delete(data)
+
+ info, err := load(data)
+ testing.expect(t, err == "", "load should succeed")
+ defer destroy(&info)
+
+ testing.expect_value(t, info.is_dynamic, true)
+ testing.expect_value(t, info.interp_offset, u64(0x318))
+}
+
+@(test)
+test_is_dynamic_false_for_static :: proc(t: ^testing.T) {
+ segs := [2]Program_Header{
+ {type = PT_LOAD, offset = 0, filesz = 0x100, memsz = 0x100},
+ {type = PT_GNU_STACK, offset = 0, filesz = 0, memsz = 0},
+ }
+ data := build_elf64_with_segments(segs[:])
+ defer delete(data)
+
+ info, err := load(data)
+ testing.expect(t, err == "", "load should succeed")
+ defer destroy(&info)
+
+ testing.expect_value(t, info.is_dynamic, false)
+ testing.expect_value(t, info.interp_offset, u64(0))
+}
+
+//------------------------------------------------------------------------------
+// Allocation hygiene
+//------------------------------------------------------------------------------
+
+@(test)
+test_load_destroy_leaks_nothing :: proc(t: ^testing.T) {
+ track: mem.Tracking_Allocator
+ mem.tracking_allocator_init(&track, context.allocator)
+ defer mem.tracking_allocator_destroy(&track)
+
+ {
+ context.allocator = mem.tracking_allocator(&track)
+
+ classes := [2]u8{ELF_CLASS_32, ELF_CLASS_64}
+ endians := [2]u8{ELF_DATA_LSB, ELF_DATA_MSB}
+ for class in classes {
+ for endian in endians {
+ f := build_elf(class, endian)
+ info, err := load(f.data)
+ testing.expect(t, err == "", "load should succeed")
+ testing.expect(t, len(info.symbols) > 0, "fixture should carry symbols")
+ destroy(&info)
+ delete(f.data)
+ }
+ }
+ }
+
+ testing.expect_value(t, len(track.allocation_map), 0)
+ testing.expect_value(t, len(track.bad_free_array), 0)
+}
+
+//------------------------------------------------------------------------------
+// Malformed input robustness
+//------------------------------------------------------------------------------
+
+POISON :: [7]u64{
+ 0xFF00000000000000,
+ 0x8000000000000000,
+ 0xFFFFFFFFFFFFFFFF,
+ 0x7FFFFFFFFFFFFFFF,
+ 0x00000000FFFFFFFF,
+ 0x0000000080000000,
+ 0xFFFFFFFFFFFFFFF0,
+}
+
+// Every string the parser hands back must alias the caller's buffer.
+inside :: proc(ptr: rawptr, size: int, data: []u8) -> bool {
+ if size == 0 { return true }
+ if size < 0 { return false }
+ lo := uintptr(raw_data(data))
+ p := uintptr(ptr)
+ return p >= lo && p - lo <= uintptr(len(data)) && uintptr(len(data)) - (p - lo) >= uintptr(size)
+}
+
+expect_sane_load :: proc(t: ^testing.T, data: []u8, label: string) {
+ info, err := load(data)
+ if err != "" { return }
+ defer destroy(&info)
+
+ phentsize := 56 if info.is_64bit else 32
+ shentsize := 64 if info.is_64bit else 40
+
+ // e_phnum == PN_XNUM and e_shnum == 0 are escape hatches; the real counts
+ // come from section[0], so the header field is not an upper bound there.
+ if info.header.phnum != PN_XNUM {
+ testing.expect(t, len(info.program_hdrs) <= int(info.header.phnum), label)
+ }
+ if info.header.shnum != 0 {
+ testing.expect(t, len(info.section_hdrs) <= int(info.header.shnum), label)
+ }
+ testing.expect(t, len(info.program_hdrs) * phentsize <= len(data), label)
+ testing.expect(t, len(info.section_hdrs) * shentsize <= len(data), label)
+
+ testing.expect(t, inside(raw_data(info.shstrtab), len(info.shstrtab), data), label)
+ testing.expect(t, inside(raw_data(info.strtab), len(info.strtab), data), label)
+ testing.expect(t, inside(raw_data(info.dynstr), len(info.dynstr), data), label)
+
+ for shdr in info.section_hdrs {
+ name := get_section_name(&info, shdr)
+ if name == "<unknown>" { continue }
+ testing.expect(t, inside(raw_data(name), len(name), data), label)
+ }
+ for sym in info.symbols {
+ testing.expect(t, inside(raw_data(sym.name), len(sym.name), data), label)
+ }
+ for sym in info.dyn_symbols {
+ testing.expect(t, inside(raw_data(sym.name), len(sym.name), data), label)
+ }
+}
+
+poison_header_offsets :: proc(f: Fixture, v: u64) {
+ little := f.endian == ELF_DATA_LSB
+ if f.class == ELF_CLASS_64 {
+ put_u64(f.data, 32, v, little)
+ put_u64(f.data, 40, v, little)
+ } else {
+ put_u32(f.data, 28, u32(v), little)
+ put_u32(f.data, 32, u32(v), little)
+ }
+}
+
+@(test)
+test_malformed_poisoned_phoff_shoff :: proc(t: ^testing.T) {
+ classes := [2]u8{ELF_CLASS_32, ELF_CLASS_64}
+ endians := [2]u8{ELF_DATA_LSB, ELF_DATA_MSB}
+ poisons := POISON
+
+ for class in classes {
+ for endian in endians {
+ for p in poisons {
+ f := build_elf(class, endian)
+ poison_header_offsets(f, p)
+ expect_sane_load(t, f.data, "poisoned phoff/shoff")
+ delete(f.data)
+ }
+
+ f := build_elf(class, endian)
+ poison_header_offsets(f, u64(len(f.data)) + 1)
+ expect_sane_load(t, f.data, "phoff/shoff just past end")
+ delete(f.data)
+
+ g := build_elf(class, endian)
+ poison_header_offsets(g, u64(len(g.data)))
+ expect_sane_load(t, g.data, "phoff/shoff exactly at end")
+ delete(g.data)
+ }
+ }
+}
+
+@(test)
+test_malformed_saturated_counts :: proc(t: ^testing.T) {
+ classes := [2]u8{ELF_CLASS_32, ELF_CLASS_64}
+ endians := [2]u8{ELF_DATA_LSB, ELF_DATA_MSB}
+
+ for class in classes {
+ for endian in endians {
+ f := build_elf(class, endian)
+ little := endian == ELF_DATA_LSB
+ if class == ELF_CLASS_64 {
+ put_u16(f.data, 56, 0xFFFF, little)
+ put_u16(f.data, 60, 0xFFFF, little)
+ put_u16(f.data, 62, 0xFFFF, little)
+ } else {
+ put_u16(f.data, 44, 0xFFFF, little)
+ put_u16(f.data, 48, 0xFFFF, little)
+ put_u16(f.data, 50, 0xFFFF, little)
+ }
+ expect_sane_load(t, f.data, "saturated phnum/shnum/shstrndx")
+ delete(f.data)
+ }
+ }
+}
+
+@(test)
+test_malformed_saturated_entsizes :: proc(t: ^testing.T) {
+ classes := [2]u8{ELF_CLASS_32, ELF_CLASS_64}
+
+ for class in classes {
+ f := build_elf(class, ELF_DATA_LSB)
+ if class == ELF_CLASS_64 {
+ put_u16(f.data, 54, 0xFFFF, true)
+ put_u16(f.data, 58, 0xFFFF, true)
+ } else {
+ put_u16(f.data, 42, 0xFFFF, true)
+ put_u16(f.data, 46, 0xFFFF, true)
+ }
+ expect_sane_load(t, f.data, "saturated phentsize/shentsize")
+ delete(f.data)
+ }
+}
+
+@(test)
+test_malformed_poisoned_section_offsets :: proc(t: ^testing.T) {
+ classes := [2]u8{ELF_CLASS_32, ELF_CLASS_64}
+ endians := [2]u8{ELF_DATA_LSB, ELF_DATA_MSB}
+ poisons := POISON
+
+ for class in classes {
+ for endian in endians {
+ for p in poisons {
+ f := build_elf(class, endian)
+ little := endian == ELF_DATA_LSB
+ for i in 0 ..< int(f.shnum) {
+ base := int(f.shoff) + i * int(f.shentsize)
+ if class == ELF_CLASS_64 {
+ put_u64(f.data, base+24, p, little)
+ put_u64(f.data, base+32, p, little)
+ } else {
+ put_u32(f.data, base+16, u32(p), little)
+ put_u32(f.data, base+20, u32(p), little)
+ }
+ }
+ expect_sane_load(t, f.data, "poisoned sh_offset/sh_size")
+ delete(f.data)
+ }
+ }
+ }
+}
+
+// offset + size wraps past 2^64, which a naive `offset + size <= len` check accepts.
+@(test)
+test_malformed_section_offset_size_wraps :: proc(t: ^testing.T) {
+ pairs := [5][2]u64{
+ {0xFFFFFFFFFFFFFFF0, 0x10},
+ {0xFFFFFFFFFFFFFF00, 0x100},
+ {0xFFFFFFFFFFFFFFFF, 1},
+ {0x8000000000000000, 0x8000000000000000},
+ {8, 0xFFFFFFFFFFFFFFF8},
+ }
+ targets := [4]int{SEC_SYMTAB, SEC_STRTAB, SEC_DYNSYM, SEC_SHSTRTAB}
+
+ for pair in pairs {
+ for target in targets {
+ f := build_elf(ELF_CLASS_64, ELF_DATA_LSB)
+ base := int(f.shoff) + target * int(f.shentsize)
+ put_u64(f.data, base+24, pair[0], true)
+ put_u64(f.data, base+32, pair[1], true)
+ expect_sane_load(t, f.data, "sh_offset + sh_size wraps around u64")
+ delete(f.data)
+ }
+ }
+}
+
+@(test)
+test_malformed_poisoned_section_names :: proc(t: ^testing.T) {
+ classes := [2]u8{ELF_CLASS_32, ELF_CLASS_64}
+ names := [4]u32{0xFFFFFFFF, 0x80000000, u32(len(SHSTRTAB_BLOB)), u32(len(SHSTRTAB_BLOB)) - 1}
+
+ for class in classes {
+ for n in names {
+ f := build_elf(class, ELF_DATA_LSB)
+ for i in 0 ..< int(f.shnum) {
+ put_u32(f.data, int(f.shoff) + i * int(f.shentsize), n, true)
+ }
+ expect_sane_load(t, f.data, "poisoned sh_name")
+ delete(f.data)
+ }
+ }
+}
+
+@(test)
+test_malformed_symtab_link_out_of_range :: proc(t: ^testing.T) {
+ classes := [2]u8{ELF_CLASS_32, ELF_CLASS_64}
+ links := [4]u32{0xFFFFFFFF, 0x7FFFFFFF, 8, 0}
+
+ for class in classes {
+ for link in links {
+ f := build_elf(class, ELF_DATA_LSB)
+ link_off := 40 if class == ELF_CLASS_64 else 24
+ base := int(f.shoff) + SEC_SYMTAB * int(f.shentsize)
+ put_u32(f.data, base + link_off, link, true)
+
+ info, err := load(f.data)
+ testing.expect(t, err == "", "load should succeed")
+
+ if link >= u32(f.shnum) {
+ testing.expect_value(t, len(info.symbols), 0)
+ }
+ for sym in info.symbols {
+ testing.expect(t, len(sym.name) <= len(info.strtab), "symbol name must stay inside strtab")
+ }
+ destroy(&info)
+ delete(f.data)
+ }
+ }
+}
+
+@(test)
+test_malformed_truncated_images :: proc(t: ^testing.T) {
+ classes := [2]u8{ELF_CLASS_32, ELF_CLASS_64}
+ endians := [2]u8{ELF_DATA_LSB, ELF_DATA_MSB}
+
+ for class in classes {
+ for endian in endians {
+ f := build_elf(class, endian)
+ for n in 0 ..= len(f.data) {
+ expect_sane_load(t, f.data[:n], "truncated image")
+ }
+ delete(f.data)
+ }
+ }
+}
+
+@(test)
+test_malformed_header_byte_sweep :: proc(t: ^testing.T) {
+ patterns := [4]u8{0x00, 0x01, 0x7f, 0xff}
+
+ for pat in patterns {
+ for i in 0 ..< 64 {
+ f := build_elf(ELF_CLASS_64, ELF_DATA_LSB)
+ f.data[i] = pat
+ expect_sane_load(t, f.data, "single byte flip in ELF64 header")
+ delete(f.data)
+
+ g := build_elf(ELF_CLASS_32, ELF_DATA_MSB)
+ if i < len(g.data) {
+ g.data[i] = pat
+ }
+ expect_sane_load(t, g.data, "single byte flip in ELF32 header")
+ delete(g.data)
+ }
+ }
+}
+
+@(test)
+test_parse_header_unknown_class :: proc(t: ^testing.T) {
+ data := make([]u8, 64)
+ defer delete(data)
+ make_elf64_header(data, ET_EXEC, EM_X86_64)
+ data[4] = 7
+
+ _, err := parse_header(data)
+ testing.expect(t, err != "", "unknown class must be rejected")
+
+ info, load_err := load(data)
+ testing.expect(t, load_err != "", "load must propagate the class error")
+ testing.expect_value(t, len(info.program_hdrs), 0)
+}
+
+@(test)
+test_load_rejects_non_elf :: proc(t: ^testing.T) {
+ data := []u8{'M', 'Z', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
+ _, err := load(data)
+ testing.expect(t, err != "", "non-ELF data must be rejected")
+}
+
+@(test)
+test_get_section_name_without_shstrtab :: proc(t: ^testing.T) {
+ info: ELF_Info
+ shdr := Section_Header{name = 4}
+ testing.expect_value(t, get_section_name(&info, shdr), "<unknown>")
+
+ empty: []u8
+ info.shstrtab = empty
+ testing.expect_value(t, get_section_name(&info, shdr), "<unknown>")
+}
+
+@(test)
+test_get_symbol_name_bounds :: proc(t: ^testing.T) {
+ strtab := transmute([]u8)string(STRTAB_BLOB)
+
+ testing.expect_value(t, get_symbol_name(strtab, STR_MAIN), "main")
+ testing.expect_value(t, get_symbol_name(strtab, STR_COUNTER), "counter")
+ testing.expect_value(t, get_symbol_name(strtab, STR_PRINTF), "printf")
+ testing.expect_value(t, get_symbol_name(strtab, 0), "")
+ testing.expect_value(t, get_symbol_name(strtab, u32(len(STRTAB_BLOB))), "")
+ testing.expect_value(t, get_symbol_name(strtab, 0xFFFFFFFF), "")
+
+ unterminated := []u8{'a', 'b', 'c'}
+ testing.expect_value(t, get_symbol_name(unterminated, 0), "")
+
+ empty: []u8
+ testing.expect_value(t, get_symbol_name(empty, 0), "")
+ testing.expect_value(t, get_symbol_name(nil, 5), "")
+}
+
+//------------------------------------------------------------------------------
+// Printer
+//------------------------------------------------------------------------------
+
+@(test)
+test_printer_strings :: proc(t: ^testing.T) {
+ defer free_all(context.temp_allocator)
+
+ testing.expect_value(t, type_to_str(ET_REL), "REL (Relocatable)")
+ testing.expect_value(t, type_to_str(ET_EXEC), "EXEC (Executable)")
+ testing.expect_value(t, type_to_str(ET_DYN), "DYN (Shared Object/PIE)")
+ testing.expect_value(t, type_to_str(ET_CORE), "CORE")
+ testing.expect_value(t, type_to_str(999), "UNKNOWN (999)")
+
+ testing.expect_value(t, machine_to_str(EM_386), "x86 (i386)")
+ testing.expect_value(t, machine_to_str(EM_X86_64), "x86-64")
+ testing.expect_value(t, machine_to_str(EM_ARM), "ARM")
+ testing.expect_value(t, machine_to_str(EM_AARCH64), "AArch64")
+ testing.expect_value(t, machine_to_str(EM_RISCV), "RISC-V")
+ testing.expect_value(t, machine_to_str(1234), "Unknown (1234)")
+
+ testing.expect_value(t, section_type_str(SHT_NULL), "NULL")
+ testing.expect_value(t, section_type_str(SHT_PROGBITS), "PROGBITS")
+ testing.expect_value(t, section_type_str(SHT_SYMTAB), "SYMTAB")
+ testing.expect_value(t, section_type_str(SHT_STRTAB), "STRTAB")
+ testing.expect_value(t, section_type_str(SHT_DYNSYM), "DYNSYM")
+ testing.expect_value(t, section_type_str(0x6ffffff6), "GNU_HASH")
+ testing.expect_value(t, section_type_str(0x1234), "0x1234")
+
+ testing.expect_value(t, symbol_type_str(STT_NOTYPE), "NOTYPE")
+ testing.expect_value(t, symbol_type_str(STT_OBJECT), "OBJECT")
+ testing.expect_value(t, symbol_type_str(STT_FUNC), "FUNC")
+ testing.expect_value(t, symbol_type_str(STT_SECTION), "SECTION")
+ testing.expect_value(t, symbol_type_str(STT_FILE), "FILE")
+ testing.expect_value(t, symbol_type_str(13), "0xd")
+
+ testing.expect_value(t, symbol_bind_str(STB_LOCAL), "LOCAL")
+ testing.expect_value(t, symbol_bind_str(STB_GLOBAL), "GLOBAL")
+ testing.expect_value(t, symbol_bind_str(STB_WEAK), "WEAK")
+ testing.expect_value(t, symbol_bind_str(10), "0xa")
+
+ testing.expect_value(t, segment_type_str(PT_NULL), "NULL")
+ testing.expect_value(t, segment_type_str(PT_LOAD), "LOAD")
+ testing.expect_value(t, segment_type_str(PT_DYNAMIC), "DYNAMIC")
+ testing.expect_value(t, segment_type_str(PT_INTERP), "INTERP")
+ testing.expect_value(t, segment_type_str(PT_GNU_RELRO), "GNU_RELRO")
+ testing.expect_value(t, segment_type_str(0xabc), "0xabc")
+}
+
+@(test)
+test_flags_to_str :: proc(t: ^testing.T) {
+ testing.expect_value(t, flags_to_str(0), " ")
+ testing.expect_value(t, flags_to_str(PF_X), " X")
+ testing.expect_value(t, flags_to_str(PF_W), " W ")
+ testing.expect_value(t, flags_to_str(PF_R), "R ")
+ testing.expect_value(t, flags_to_str(PF_R | PF_X), "R X")
+ testing.expect_value(t, flags_to_str(PF_R | PF_W), "RW ")
+ testing.expect_value(t, flags_to_str(PF_R | PF_W | PF_X), "RWX")
+ testing.expect_value(t, flags_to_str(0xFFFFFFFFFFFFFFFF), "RWX")
+}
+\ No newline at end of file
diff --git a/elf/loader.odin b/elf/loader.odin
@@ -0,0 +1,269 @@
+// elf/loader.odin - Full ELF parsing (segments, sections, symbols)
+package elf
+
+load :: proc(data: []u8) -> (ELF_Info, string) {
+ info: ELF_Info
+
+ header, err := parse_header(data)
+ if err != "" {
+ return info, err
+ }
+ info.header = header
+ info.is_64bit = header.class == ELF_CLASS_64
+ info.is_pie = header.type == ET_DYN
+ info.is_dynamic = false
+
+ info.program_hdrs = make([dynamic]Program_Header)
+ info.section_hdrs = make([dynamic]Section_Header)
+ info.symbols = make([dynamic]Symbol)
+ info.dyn_symbols = make([dynamic]Symbol)
+
+ // Sections first: PN_XNUM resolves e_phnum through section[0].
+ parse_section_headers(data, &info)
+ parse_program_headers(data, &info)
+ parse_symbols(data, &info)
+
+ return info, ""
+}
+
+destroy :: proc(info: ^ELF_Info) {
+ delete(info.program_hdrs)
+ delete(info.section_hdrs)
+ delete(info.symbols)
+ delete(info.dyn_symbols)
+}
+
+get_section_name :: proc(info: ^ELF_Info, shdr: Section_Header) -> string {
+ if info.shstrtab == nil {
+ return "<unknown>"
+ }
+ if len(info.shstrtab) == 0 {
+ return "<unknown>"
+ }
+ start := int(shdr.name)
+ for i := start; i < len(info.shstrtab); i += 1 {
+ if info.shstrtab[i] == 0 {
+ return string(info.shstrtab[start:i])
+ }
+ }
+ return ""
+}
+
+get_symbol_name :: proc(strtab: []u8, name_offset: u32) -> string {
+ if strtab == nil {
+ return ""
+ }
+ if len(strtab) == 0 {
+ return ""
+ }
+ start := int(name_offset)
+ if start >= len(strtab) {
+ return ""
+ }
+ for i := start; i < len(strtab); i += 1 {
+ if strtab[i] == 0 {
+ return string(strtab[start:i])
+ }
+ }
+ return ""
+}
+
+PHDR_SIZE_64 :: 56
+PHDR_SIZE_32 :: 32
+SHDR_SIZE_64 :: 64
+SHDR_SIZE_32 :: 40
+SYM_SIZE_64 :: 24
+SYM_SIZE_32 :: 16
+
+read_program_header :: proc(data: []u8, offset: int, is_64bit: bool, order: Byte_Order) -> Program_Header {
+ ph: Program_Header
+ if is_64bit {
+ ph.type = read_u32(data, offset, order)
+ ph.flags = u64(read_u32(data, offset + 4, order))
+ ph.offset = read_u64(data, offset + 8, order)
+ ph.vaddr = read_u64(data, offset + 16, order)
+ ph.paddr = read_u64(data, offset + 24, order)
+ ph.filesz = read_u64(data, offset + 32, order)
+ ph.memsz = read_u64(data, offset + 40, order)
+ ph.align = read_u64(data, offset + 48, order)
+ } else {
+ ph.type = read_u32(data, offset, order)
+ ph.offset = u64(read_u32(data, offset + 4, order))
+ ph.vaddr = u64(read_u32(data, offset + 8, order))
+ ph.paddr = u64(read_u32(data, offset + 12, order))
+ ph.filesz = u64(read_u32(data, offset + 16, order))
+ ph.memsz = u64(read_u32(data, offset + 20, order))
+ ph.flags = u64(read_u32(data, offset + 24, order))
+ ph.align = u64(read_u32(data, offset + 28, order))
+ }
+ return ph
+}
+
+parse_program_headers :: proc(data: []u8, info: ^ELF_Info) {
+ header := info.header
+ is_64bit := info.is_64bit
+ order := byte_order(info.header)
+
+ if !in_bounds(data, header.phoff, 0) { return }
+
+ min_size := PHDR_SIZE_64 if is_64bit else PHDR_SIZE_32
+ entsize := int(header.phentsize)
+ if entsize < min_size { entsize = min_size }
+
+ // PN_XNUM: the real segment count lives in section[0].sh_info.
+ phnum := u64(header.phnum)
+ if phnum == PN_XNUM && len(info.section_hdrs) > 0 {
+ phnum = u64(info.section_hdrs[0].info)
+ }
+
+ offset := int(header.phoff)
+ for i := u64(0); i < phnum; i += 1 {
+ if offset + min_size > len(data) { break }
+
+ ph := read_program_header(data, offset, is_64bit, order)
+ offset += entsize
+ append(&info.program_hdrs, ph)
+
+ if ph.type == PT_INTERP {
+ info.interp_offset = ph.offset
+ info.is_dynamic = true
+ }
+ if ph.type == PT_DYNAMIC {
+ info.is_dynamic = true
+ }
+ }
+}
+
+read_section_header :: proc(data: []u8, offset: int, is_64bit: bool, order: Byte_Order) -> Section_Header {
+ sh: Section_Header
+ if is_64bit {
+ sh.name = read_u32(data, offset, order)
+ sh.type = read_u32(data, offset + 4, order)
+ sh.flags = read_u64(data, offset + 8, order)
+ sh.addr = read_u64(data, offset + 16, order)
+ sh.offset = read_u64(data, offset + 24, order)
+ sh.size = read_u64(data, offset + 32, order)
+ sh.link = read_u32(data, offset + 40, order)
+ sh.info = read_u32(data, offset + 44, order)
+ sh.addralign = read_u64(data, offset + 48, order)
+ sh.entsize = read_u64(data, offset + 56, order)
+ } else {
+ sh.name = read_u32(data, offset, order)
+ sh.type = read_u32(data, offset + 4, order)
+ sh.flags = u64(read_u32(data, offset + 8, order))
+ sh.addr = u64(read_u32(data, offset + 12, order))
+ sh.offset = u64(read_u32(data, offset + 16, order))
+ sh.size = u64(read_u32(data, offset + 20, order))
+ sh.link = read_u32(data, offset + 24, order)
+ sh.info = read_u32(data, offset + 28, order)
+ sh.addralign = u64(read_u32(data, offset + 32, order))
+ sh.entsize = u64(read_u32(data, offset + 36, order))
+ }
+ return sh
+}
+
+parse_section_headers :: proc(data: []u8, info: ^ELF_Info) {
+ header := info.header
+ is_64bit := info.is_64bit
+ order := byte_order(info.header)
+
+ if !in_bounds(data, header.shoff, 0) { return }
+
+ min_size := SHDR_SIZE_64 if is_64bit else SHDR_SIZE_32
+ entsize := int(header.shentsize)
+ if entsize < min_size { entsize = min_size }
+
+ start := int(header.shoff)
+ shnum := u64(header.shnum)
+ shstrndx := u64(header.shstrndx)
+
+ // e_shnum == 0 and e_shstrndx == SHN_XINDEX are escape hatches: the real
+ // values live in section[0].sh_size and section[0].sh_link.
+ if shnum == 0 || shstrndx == SHN_XINDEX {
+ if start + min_size > len(data) { return }
+ sh0 := read_section_header(data, start, is_64bit, order)
+ if shnum == 0 { shnum = sh0.size }
+ if shstrndx == SHN_XINDEX { shstrndx = u64(sh0.link) }
+ }
+
+ offset := start
+ for i := u64(0); i < shnum; i += 1 {
+ if offset + min_size > len(data) { break }
+ append(&info.section_hdrs, read_section_header(data, offset, is_64bit, order))
+ offset += entsize
+ }
+
+ if shstrndx < u64(len(info.section_hdrs)) {
+ shstrtab_hdr := info.section_hdrs[shstrndx]
+ if shstrtab_hdr.type != SHT_NULL && in_bounds(data, shstrtab_hdr.offset, shstrtab_hdr.size) {
+ s := int(shstrtab_hdr.offset)
+ info.shstrtab = data[s : s + int(shstrtab_hdr.size)]
+ }
+ }
+}
+
+parse_symbols :: proc(data: []u8, info: ^ELF_Info) {
+ for shdr in info.section_hdrs {
+ if shdr.type != SHT_SYMTAB && shdr.type != SHT_DYNSYM {
+ continue
+ }
+ strtab_idx := int(shdr.link)
+ if strtab_idx >= len(info.section_hdrs) { continue }
+
+ strtab_hdr := info.section_hdrs[strtab_idx]
+ if !in_bounds(data, strtab_hdr.offset, strtab_hdr.size) { continue }
+ if !in_bounds(data, shdr.offset, shdr.size) { continue }
+
+ strtab_start := int(strtab_hdr.offset)
+ strtab := data[strtab_start : strtab_start + int(strtab_hdr.size)]
+
+ if shdr.type == SHT_DYNSYM {
+ info.dynstr = strtab
+ } else {
+ info.strtab = strtab
+ }
+
+ is_64bit := info.is_64bit
+ order := byte_order(info.header)
+ offset := int(shdr.offset)
+
+ min_size := u64(SYM_SIZE_64) if is_64bit else u64(SYM_SIZE_32)
+ entsize := shdr.entsize
+ if entsize < min_size { entsize = min_size }
+
+ num_symbols := shdr.size / entsize
+ for j := u64(0); j < num_symbols; j += 1 {
+ sym: Symbol
+ name_off: u32
+ sym_info: u8
+ next := offset + int(entsize)
+
+ if is_64bit {
+ if offset + SYM_SIZE_64 > len(data) { break }
+ name_off = read_u32(data, offset, order)
+ sym_info = data[offset + 4]
+ sym.shndx = read_u16(data, offset + 6, order)
+ sym.value = read_u64(data, offset + 8, order)
+ sym.size = read_u64(data, offset + 16, order)
+ } else {
+ if offset + SYM_SIZE_32 > len(data) { break }
+ name_off = read_u32(data, offset, order)
+ sym.value = u64(read_u32(data, offset + 4, order))
+ sym.size = u64(read_u32(data, offset + 8, order))
+ sym_info = data[offset + 12]
+ sym.shndx = read_u16(data, offset + 14, order)
+ }
+ offset = next
+
+ sym.name = get_symbol_name(strtab, name_off)
+ sym.bind = sym_info >> 4
+ sym.type = sym_info & 0xf
+
+ if shdr.type == SHT_DYNSYM {
+ append(&info.dyn_symbols, sym)
+ } else {
+ append(&info.symbols, sym)
+ }
+ }
+ }
+}
+\ No newline at end of file
diff --git a/elf/printer.odin b/elf/printer.odin
@@ -0,0 +1,210 @@
+// elf/printer.odin - ELF info printing
+package elf
+
+import "core:fmt"
+import "core:os"
+
+print_info :: proc(info: ^ELF_Info, verbose: bool, json: bool) {
+ f := os.stdout
+
+ if json {
+ print_info_json(info)
+ return
+ }
+
+ fmt.fprintf(f, "ELF File Information\n")
+ fmt.fprintf(f, "====================\n\n")
+
+ class_bits := 32
+ if info.is_64bit {
+ class_bits = 64
+ }
+ fmt.fprintf(f, "Class: ELF%d\n", class_bits)
+ fmt.fprintf(f, "Type: %s\n", type_to_str(info.header.type))
+ fmt.fprintf(f, "Machine: %s\n", machine_to_str(info.header.machine))
+ fmt.fprintf(f, "Entry: 0x%016x\n", info.header.entry)
+ fmt.fprintf(f, "Flags: 0x%08x\n", info.header.flags)
+
+ pie_str := "no"
+ if info.is_pie {
+ pie_str = "yes"
+ }
+ dyn_str := "no"
+ if info.is_dynamic {
+ dyn_str = "yes"
+ }
+ fmt.fprintf(f, "PIE: %s\n", pie_str)
+ fmt.fprintf(f, "Dynamic: %s\n", dyn_str)
+ if info.interp_offset != 0 {
+ fmt.fprintf(f, "Interpreter offset: 0x%x\n", info.interp_offset)
+ }
+
+ fmt.fprintf(f, "\n")
+ fmt.fprintf(f, "Program Headers: %d\n", len(info.program_hdrs))
+ fmt.fprintf(f, "Section Headers: %d\n", len(info.section_hdrs))
+ fmt.fprintf(f, "Symbols (static): %d\n", len(info.symbols))
+ fmt.fprintf(f, "Symbols (dynamic): %d\n", len(info.dyn_symbols))
+
+ if verbose {
+ fmt.fprintf(f, "\n")
+ fmt.fprintf(f, "Detailed Sections:\n")
+ for i := 0; i < len(info.section_hdrs); i += 1 {
+ shdr := info.section_hdrs[i]
+ name := get_section_name(info, shdr)
+ if name == "" {
+ name = fmt.tprintf("[%d]", i)
+ }
+ fmt.fprintf(f, " [%2d] %-20s %-12s 0x%08x 0x%06x 0x%x\n",
+ i, name, section_type_str(shdr.type), shdr.addr, shdr.offset, shdr.size)
+ }
+
+ if len(info.symbols) > 0 {
+ fmt.fprintf(f, "\nStatic Symbols (first 20):\n")
+ count := 20
+ if len(info.symbols) < count {
+ count = len(info.symbols)
+ }
+ for i := 0; i < count; i += 1 {
+ sym := info.symbols[i]
+ sym_type := symbol_type_str(sym.type)
+ sym_bind := symbol_bind_str(sym.bind)
+ fmt.fprintf(f, " %-30s %s %-8s 0x%016x (size: %d)\n",
+ sym.name, sym_bind, sym_type, sym.value, sym.size)
+ }
+ }
+
+ if len(info.dyn_symbols) > 0 {
+ fmt.fprintf(f, "\nDynamic Symbols (first 20):\n")
+ count := 20
+ if len(info.dyn_symbols) < count {
+ count = len(info.dyn_symbols)
+ }
+ for i := 0; i < count; i += 1 {
+ sym := info.dyn_symbols[i]
+ sym_type := symbol_type_str(sym.type)
+ sym_bind := symbol_bind_str(sym.bind)
+ fmt.fprintf(f, " %-30s %s %-8s 0x%016x (size: %d)\n",
+ sym.name, sym_bind, sym_type, sym.value, sym.size)
+ }
+ }
+ }
+}
+
+print_info_json :: proc(info: ^ELF_Info) {
+ class_bits := 32
+ if info.is_64bit {
+ class_bits = 64
+ }
+ fmt.println("{")
+ fmt.printf(" \"class\": %d,\n", class_bits)
+ fmt.printf(" \"type\": \"%s\",\n", type_to_str(info.header.type))
+ fmt.printf(" \"machine\": \"%s\",\n", machine_to_str(info.header.machine))
+ fmt.printf(" \"entry\": \"0x%016x\",\n", info.header.entry)
+ pie_str := "false"
+ if info.is_pie {
+ pie_str = "true"
+ }
+ fmt.printf(" \"pie\": %s,\n", pie_str)
+ dyn_str := "false"
+ if info.is_dynamic {
+ dyn_str = "true"
+ }
+ fmt.printf(" \"dynamic\": %s,\n", dyn_str)
+ fmt.printf(" \"program_headers\": %d,\n", len(info.program_hdrs))
+ fmt.printf(" \"section_headers\": %d,\n", len(info.section_hdrs))
+ fmt.printf(" \"symbols_static\": %d,\n", len(info.symbols))
+ fmt.printf(" \"symbols_dynamic\": %d\n", len(info.dyn_symbols))
+ fmt.printf("}\n")
+}
+
+type_to_str :: proc(t: u16) -> string {
+ switch t {
+ case ET_REL: return "REL (Relocatable)"
+ case ET_EXEC: return "EXEC (Executable)"
+ case ET_DYN: return "DYN (Shared Object/PIE)"
+ case ET_CORE: return "CORE"
+ }
+ return fmt.tprintf("UNKNOWN (%d)", t)
+}
+
+machine_to_str :: proc(m: u16) -> string {
+ switch m {
+ case EM_386: return "x86 (i386)"
+ case EM_X86_64: return "x86-64"
+ case EM_ARM: return "ARM"
+ case EM_AARCH64: return "AArch64"
+ case EM_RISCV: return "RISC-V"
+ }
+ return fmt.tprintf("Unknown (%d)", m)
+}
+
+section_type_str :: proc(t: u32) -> string {
+ switch t {
+ case SHT_NULL: return "NULL"
+ case SHT_PROGBITS: return "PROGBITS"
+ case SHT_SYMTAB: return "SYMTAB"
+ case SHT_STRTAB: return "STRTAB"
+ case SHT_RELA: return "RELA"
+ case SHT_HASH: return "HASH"
+ case SHT_DYNAMIC: return "DYNAMIC"
+ case SHT_NOTE: return "NOTE"
+ case SHT_NOBITS: return "NOBITS"
+ case SHT_REL: return "REL"
+ case SHT_DYNSYM: return "DYNSYM"
+ case SHT_SHLIB: return "SHLIB"
+ case SHT_INIT_ARRAY: return "INIT_ARRAY"
+ case SHT_FINI_ARRAY: return "FINI_ARRAY"
+ case SHT_PREINIT_ARRAY: return "PREINIT_ARRAY"
+ case SHT_GROUP: return "GROUP"
+ case SHT_SYMTAB_SHNDX: return "SYMTAB SECTION INDICES"
+ case SHT_GNU_ATTRIBUTES: return "GNU_ATTRIBUTES"
+ case SHT_GNU_HASH: return "GNU_HASH"
+ case SHT_GNU_VERDEF: return "VERDEF"
+ case SHT_GNU_VERNEED: return "VERNEED"
+ case SHT_GNU_VERSYM: return "VERSYM"
+ }
+ return fmt.tprintf("0x%x", t)
+}
+
+symbol_type_str :: proc(t: u8) -> string {
+ switch t {
+ case STT_NOTYPE: return "NOTYPE"
+ case STT_OBJECT: return "OBJECT"
+ case STT_FUNC: return "FUNC"
+ case STT_SECTION: return "SECTION"
+ case STT_FILE: return "FILE"
+ }
+ return fmt.tprintf("0x%x", t)
+}
+
+symbol_bind_str :: proc(b: u8) -> string {
+ switch b {
+ case STB_LOCAL: return "LOCAL"
+ case STB_GLOBAL: return "GLOBAL"
+ case STB_WEAK: return "WEAK"
+ }
+ return fmt.tprintf("0x%x", b)
+}
+
+segment_type_str :: proc(t: u32) -> string {
+ switch t {
+ case PT_NULL: return "NULL"
+ case PT_LOAD: return "LOAD"
+ case PT_DYNAMIC: return "DYNAMIC"
+ case PT_INTERP: return "INTERP"
+ case PT_NOTE: return "NOTE"
+ case PT_SHLIB: return "SHLIB"
+ case PT_PHDR: return "PHDR"
+ case PT_TLS: return "TLS"
+ case PT_GNU_EH_FRAME: return "GNU_EH_FRAME"
+ case PT_GNU_STACK: return "GNU_STACK"
+ case PT_GNU_RELRO: return "GNU_RELRO"
+ }
+ return fmt.tprintf("0x%x", t)
+}
+
+// bit 0 = X, bit 1 = W, bit 2 = R
+flags_to_str :: proc(flags: u64) -> string {
+ @static table := [8]string{" ", " X", " W ", " WX", "R ", "R X", "RW ", "RWX"}
+ return table[flags & 7]
+}
+\ No newline at end of file
diff --git a/main.odin b/main.odin
@@ -0,0 +1,204 @@
+// main.odin - draugr: ELF mapping and analysis tool
+package main
+
+import "core:flags"
+import "core:fmt"
+import "core:os"
+
+import "./elf"
+import "./analysis"
+
+// Author: djnn
+
+Options :: struct {
+ elf_path : string `args:"pos=0,required" usage:"ELF binary to analyse."`,
+ hexdump : bool `usage:"Hex dump entire file to stdout."`,
+ hexdump_out : string `usage:"Hex dump to file."`,
+ disasm : bool `usage:"Disassemble code sections."`,
+ disasm_out : string `usage:"Disassemble to file."`,
+ entropy : bool `usage:"Shannon entropy report."`,
+ entropy_out : string `usage:"Entropy report to file."`,
+ symbols : bool `usage:"Print symbol table (nm-like)."`,
+ sections : bool `usage:"Print section headers."`,
+ segments : bool `usage:"Print program headers (objdump -p style)."`,
+ show_map : bool `args:"name=map" usage:"Visual memory layout map."`,
+ remap : string `usage:"Remap using this prototype file."`,
+ info : bool `usage:"Print ELF summary only."`,
+ json : bool `usage:"Output in JSON format."`,
+ verbose : bool `usage:"Verbose output."`,
+}
+
+main :: proc() {
+ opts: Options
+ flags.parse_or_exit(&opts, os.args, .Unix)
+
+ elf_data, read_err := os.read_entire_file_from_path(opts.elf_path, context.allocator)
+ if read_err != nil {
+ fmt.fprintf(os.stderr, "Failed to read file: %s\n", opts.elf_path)
+ os.exit(1)
+ }
+
+ if !elf.probe(elf_data) {
+ fmt.fprintf(os.stderr, "Not a valid ELF file: %s\n", opts.elf_path)
+ os.exit(1)
+ }
+
+ info, parse_err := elf.load(elf_data)
+ if parse_err != "" {
+ fmt.fprintf(os.stderr, "ELF parse error: %s\n", parse_err)
+ os.exit(1)
+ }
+ defer elf.destroy(&info)
+
+ bits := 64 if info.is_64bit else 32
+ elf_type := "PIE" if info.is_pie else "ET_EXEC"
+ dyn_type := "dynamic" if info.is_dynamic else "static"
+
+ fmt.fprintln(os.stderr, "----------------------------------------")
+ fmt.fprintf(os.stderr, "arch=%-12s class=ELF%d\t%s\t%s\n",
+ elf.machine_to_str(info.header.machine), bits, elf_type, dyn_type)
+ fmt.fprintln(os.stderr, "")
+
+ if opts.info {
+ elf.print_info(&info, opts.verbose, opts.json)
+ return
+ }
+
+ failed := false
+
+ if opts.hexdump || opts.hexdump_out != "" {
+ section_header("Hexdump", opts.hexdump_out)
+ report("Hexdump", analysis.hexdump(elf_data, 0, opts.hexdump_out), &failed)
+ fmt.println("")
+ }
+
+ if opts.entropy || opts.entropy_out != "" {
+ section_header("Entropy Analysis", opts.entropy_out)
+ report("Entropy", analysis.entropy_output(elf_data, opts.entropy_out), &failed)
+ fmt.println("")
+ }
+
+ if opts.disasm || opts.disasm_out != "" {
+ section_header("Disassembly", opts.disasm_out)
+ report("Disasm", analysis.disasm_output(elf_data, &info, opts.disasm_out), &failed)
+ fmt.println("")
+ }
+
+ if opts.symbols {
+ section_header("Symbol Table (nm-style)", "")
+ print_symbols(&info)
+ fmt.println("")
+ }
+
+ if opts.sections {
+ section_header("Section Headers", "")
+ print_sections(&info)
+ fmt.println("")
+ }
+
+ if opts.segments {
+ section_header("Program Headers (objdump-style)", "")
+ print_segments(&info)
+ fmt.println("")
+ }
+
+ if opts.show_map {
+ section_header("Memory Map", "")
+ report("Map", analysis.map_output(elf_data, &info, ""), &failed)
+ fmt.println("")
+ }
+
+ if opts.remap != "" {
+ section_header("Remap", "")
+ report("Remap", analysis.remap_output(elf_data, &info, opts.remap, ""), &failed)
+ fmt.println("")
+ }
+
+ if failed {
+ os.exit(1)
+ }
+}
+
+report :: proc(label: string, err: string, failed: ^bool) {
+ if err == "" {
+ return
+ }
+ fmt.fprintf(os.stderr, "%s error: %s\n", label, err)
+ failed^ = true
+}
+
+section_header :: proc(title: string, out_path: string) {
+ fmt.fprintln(os.stderr, "----------------------------------------")
+ if out_path == "" {
+ fmt.fprintf(os.stderr, "%s:\n", title)
+ } else {
+ fmt.fprintf(os.stderr, "%s -> %s\n", title, out_path)
+ }
+}
+
+
+print_symbols :: proc(info: ^elf.ELF_Info) {
+ print_symbol_table(info.symbols[:])
+ print_symbol_table(info.dyn_symbols[:])
+}
+
+print_symbol_table :: proc(syms: []elf.Symbol) {
+ for sym in syms {
+ if sym.name == "" {
+ continue
+ }
+ if sym.shndx == elf.SHN_UNDEF {
+ fmt.printf("%18s %c %s\n", "", sym_to_nm_type(sym), sym.name)
+ } else {
+ fmt.printf("0x%016x %c %s\n", sym.value, sym_to_nm_type(sym), sym.name)
+ }
+ }
+}
+
+// Odin's fmt zero-fills numeric widths, so decimal columns pad via a string.
+print_sections :: proc(info: ^elf.ELF_Info) {
+ fmt.printf("%-4s %-20s %-10s %-10s %-10s %s\n",
+ "Idx", "Name", "Type", "Addr", "Offset", "Size")
+
+ for shdr, i in info.section_hdrs {
+ name := elf.get_section_name(info, shdr)
+ if name == "" {
+ name = fmt.tprintf("[%d]", i)
+ }
+ fmt.printf("%-4s %-20s %-10s 0x%08x 0x%08x 0x%x\n",
+ fmt.tprintf("%d", i), name, elf.section_type_str(shdr.type),
+ shdr.addr, shdr.offset, shdr.size)
+ }
+}
+
+print_segments :: proc(info: ^elf.ELF_Info) {
+ fmt.printf("%-13s %-5s %-10s %-10s %-10s %s\n",
+ "Type", "Flags", "Offset", "VirtAddr", "FileSize", "MemSize")
+
+ for ph in info.program_hdrs {
+ fmt.printf("%-13s %-5s 0x%08x 0x%08x 0x%08x 0x%08x\n",
+ elf.segment_type_str(ph.type),
+ elf.flags_to_str(ph.flags),
+ ph.offset, ph.vaddr, ph.filesz, ph.memsz)
+ }
+}
+
+sym_to_nm_type :: proc(sym: elf.Symbol) -> u8 {
+ if sym.shndx == elf.SHN_UNDEF {
+ return 'U'
+ }
+
+ letter: u8 = '?'
+ switch sym.type {
+ case elf.STT_NOTYPE: letter = ' '
+ case elf.STT_OBJECT: letter = 'O'
+ case elf.STT_FUNC: letter = 'T'
+ case elf.STT_SECTION: letter = 'S'
+ case elf.STT_FILE: letter = 'F'
+ }
+
+ if sym.bind == elf.STB_LOCAL && letter >= 'A' && letter <= 'Z' {
+ letter += 'a' - 'A'
+ }
+ return letter
+}
| | | | | | | | | | | | | |