object-lang 0.2.0

Writes and reads object files and executables: ELF64 for x86-64 and AArch64 from a format-neutral model, with no linker or assembler involved.
Documentation
# object-lang v0.2.0 — Object Files and Executables, With No Linker in the Way

**The foundation.** v0.2.0 turns the scaffold into a working object-file layer: a
format-neutral model of sections, symbols, and relocations, and an ELF64 writer and
reader for x86-64 and AArch64. It writes relocatable objects any ELF linker accepts and
static executables the Linux kernel runs directly — a hand-assembled `exit(42)` is one
call — with no system linker, no assembler, no C toolchain, and no third-party crate.
The reader turns ELF bytes back into the same model, strictly and within budgets, and
never panics on any input.

## What is object-lang?

The object-file layer of the `-lang` family's native compiler backend. Code generators
hand it machine code, symbols, and relocations; it produces the files an operating
system loads. It is what will replace the private byte format `aot-lang` 1.x emits, and
what `linker-lang` 2.0 will read and write. It is a CODE-tier crate with no
dependencies.

## What's new in 0.2.0

### A format-neutral model

An `Object` is a file kind (relocatable or executable), an architecture, sections,
symbols, and an entry point. Section kinds (`Text`, `Data`, `ReadOnlyData`, `Bss`,
`Note`, `InitArray`, `FiniArray`, `PreinitArray`, `Other`) say what a section holds and
`SectionFlags` how it is used; symbols carry a kind, a binding (local, global, weak), a
visibility, and where they are defined (a section, undefined, absolute, or common).
Relocation kinds are named for their computation — `X86_64Reloc::Plt32`,
`Aarch64Reloc::Call26` — not for ELF's numbering, and target a symbol or the start of a
section with an explicit addend, so the COFF/PE and Mach-O writers planned for v0.5 can
take the same objects.

`Object::validate` checks every rule in one pass and reports the first violation as a
`ModelError` naming the section, symbol, or relocation and the rule broken. Writers
validate before writing and readers validate what they decode.

```rust
use object_lang::{
    Architecture, Object, Relocation, RelocationTarget, Section, SectionKind, Symbol,
    X86_64Reloc,
};

let mut obj = Object::relocatable(Architecture::X86_64);
let text = obj.add_section(
    Section::new(".text", SectionKind::Text).with_data(vec![0xe8, 0, 0, 0, 0, 0xc3]),
);
obj.add_symbol(Symbol::function("main", text, 0, 6));
let helper = obj.add_symbol(Symbol::undefined("helper"));
obj.add_relocation(
    text,
    Relocation::new(1, X86_64Reloc::Plt32, RelocationTarget::Symbol(helper), -4),
)?;
let bytes = object_lang::elf::write(&obj)?;
assert_eq!(object_lang::elf::read(&bytes)?, obj);
# Ok::<(), Box<dyn std::error::Error>>(())
```

### The ELF writer

- **Relocatable objects** (`ET_REL`): content sections first, one `.rela` section per
  patched section, symbol tables with every local before every global and `sh_info`
  set, `STT_SECTION` symbols for section targets, string tables that share the bytes of
  suffixes, `.note.GNU-stack`, and ELF's extended numbering past 65,279 sections.
- **Executables** (`ET_EXEC`, static, no interpreter): the writer takes section
  addresses from the model, groups loadable sections into `PT_LOAD` segments by
  permission, aligns segments to the page size (4 KiB x86-64, 64 KiB AArch64) with file
  offsets congruent to addresses, maps the ELF and program headers read-only below the
  code so `AT_PHDR` is valid, and adds `PT_GNU_STACK`. Permission changes that would
  share a page are refused, not silently widened.
- Relocations: x86-64 `64`, `32`, `32S`, `PC32`, `PC64`, `PLT32`, `GOTPCREL`,
  `GOTPCRELX`, `REX_GOTPCRELX`; AArch64 `ABS64`, `ABS32`, `PREL64`, `PREL32`, `CALL26`,
  `JUMP26`, `CONDBR19`, `TSTBR14`, `ADR_PREL_LO21`, `ADR_PREL_PG_HI21`,
  `ADD_ABS_LO12_NC`, `LDST8/16/32/64/128_ABS_LO12_NC`, `ADR_GOT_PAGE`,
  `LD64_GOT_LO12_NC`.
- Output is deterministic, and `write_into` appends to a reusable buffer.

```rust
use object_lang::Architecture;

let code = [
    0xbf, 0x2a, 0x00, 0x00, 0x00, // mov edi, 42
    0xb8, 0x3c, 0x00, 0x00, 0x00, // mov eax, 60 (exit)
    0x0f, 0x05, //                   syscall
];
let program = object_lang::elf::executable(Architecture::X86_64, &code)?;
assert_eq!(&program[..4], b"\x7fELF");
# Ok::<(), object_lang::WriteError>(())
```

### The ELF reader

Strict: anything the model cannot represent exactly — COMDAT groups, `SHT_REL`, dynamic
sections, thread-local relocations, unknown section, symbol, or relocation types — is a
`ReadError` naming it. Budgeted: section, symbol, and relocation counts, name length, and
total name bytes are bounded by `Limits`, every count is checked against the input
length before allocation, offsets are added with overflow checks, and section data is
never copied beyond the input size however the headers overlap. Executables are checked
against their program headers: every loadable section must be loaded where its header
says, with permissions that cover its flags. It reads clang's objects and ld.lld's
static executables.

### Tests and verification

- **Round trip.** Property tests over random relocatable objects and executables, every
  section kind, flag combination, symbol shape, and relocation kind: what the writer
  accepts reads back unchanged, and rewriting gives identical bytes. One-off runs of
  400,000 cases per property found no failure.
- **Against independent references.** A decoder written in the tests from the ELF
  specification checks every header, section, symbol, and relocation; a page-level
  simulation of the Linux loader checks that every section is mapped at its address with
  its permissions and that BSS reads as zeros. Known-answer tests pin header fields and
  whole-file layouts byte for byte.
- **Against LLVM.** Objects using every relocation kind, for both architectures, are
  linked by `ld.lld` and the patched bytes are checked against each kind's formula;
  `llvm-readobj` parses our files without a warning. Objects compiled by clang 18 are
  read symbol by symbol and relocation by relocation and rewritten without change. The
  oracle tests are `#[ignore]`d by default (they need LLVM) and run in a CI job.
- **Running.** On Linux the suite executes what it writes: `exit(42)` on x86-64 and
  AArch64, and a four-segment program that prints to stdout and exits with a status
  computed from `.data` and a written `.bss`.
- **Hostile input.** Arbitrary bytes, mutated and truncated files, every single-byte
  corruption of a sample object, and dozens of targeted corruptions: no panic, the
  specific error each time, and whatever is accepted writes back and reads back
  unchanged.

### Dependency wiring

No dependencies. No first-party crate is wired: `linker-lang` and `aot-lang` will depend
on object-lang, not the other way round; machine code comes from callers; and nothing
here needs spans, diagnostics, or interning. The decision is recorded in
`dev/ROADMAP.md`.

## Breaking changes

**None.** v0.1.0 had no public API.

## Verification

Run on Windows x86_64, on Rust stable (1.95 and 1.99) and the 1.85 MSRV:

```bash
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --all-targets --no-default-features -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo +1.85 clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo test --no-default-features
cargo +1.85 test --all-features
cargo test --test llvm_oracle -- --ignored   # with LLVM 18 installed
cargo bench --no-run
```

Linux (where the executables are run) and macOS are covered by the CI matrix.

Counts at this tag: 5 unit tests, 49 integration tests (3 compiler-output, 17 reader,
19 writer, 5 executable, 5 property) plus 3 LLVM oracle tests, and 143 doctests
(including every `rust` example in `README.md` and `docs/API.md`).

Benchmarks (`cargo bench --bench bench`, release profile, Windows x86_64):

| Benchmark | Windows |
|---|---:|
| Write a relocatable object: 1,000 sections, 100,000 symbols, 200,000 relocations (12.5 MB) | ~24 ms |
| Read it back | ~26 ms |
| Write an executable: 3,000 sections, 100,000 symbols | ~8.3 ms |
| `elf::executable` on 64 KiB of code | ~6 µs |

## What's next

- **0.5.0 — Implementation.** COFF/PE32+ and Mach-O 64 writers and readers on the same
  model; import and export tables; thread-local storage (relocations and `PT_TLS`);
  COMDAT groups; unwind and DWARF line tables.

## Installation

```toml
[dependencies]
object-lang = "0.2"

# no_std (alloc only)
object-lang = { version = "0.2", default-features = false }
```

MSRV: Rust 1.85.

## Documentation

- [README]https://github.com/jamesgober/object-lang/blob/main/README.md
- [API Reference]https://github.com/jamesgober/object-lang/blob/main/docs/API.md
- [Roadmap]https://github.com/jamesgober/object-lang/blob/main/dev/ROADMAP.md
- [CHANGELOG]https://github.com/jamesgober/object-lang/blob/main/CHANGELOG.md

---

**Full diff:** [`v0.1.0...v0.2.0`](https://github.com/jamesgober/object-lang/compare/v0.1.0...v0.2.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/object-lang/blob/main/CHANGELOG.md#020---2026-10-08).