draugr.git


draugr

~ single ELF file analysis tool
git clone https://github.com/djnnvx/draugr.git
log | files | refs | archive | README

commit 25b82aa44cb52f9b7989ef94583d3ca573f0f1bc
Author: djnn <[email protected]>
Date: Sat, 25 Jul 2026 15:29:15 +0900

initial commit

Diffstat:
A.gitignore | 3+++
A.tool-versions | 1+
AREADME.md | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aanalysis/analysis_test.odin | 865+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aanalysis/disasm.odin | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aanalysis/entropy.odin | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aanalysis/hexdump.odin | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aanalysis/mapper.odin | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aanalysis/output.odin | 23+++++++++++++++++++++++
Aelf/elf.odin | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelf/elf_test.odin | 1423+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelf/loader.odin | 270+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelf/printer.odin | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain.odin | 204+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

14 files changed, 3643 insertions(+), 0 deletions(-)

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
+}