Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
9.0 kB · 244 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244//! Minimal ELF64 executable writer for the `.rgx` native backend.//!//! Emits a GNU/Linux ET_EXEC (x86_64 or AArch64) that makes only raw kernel//! syscalls (no libc). The caller supplies encoded machine-code bytes for//! `.text` (beginning with `_start`) plus optional read-only data (string//! literals / format tables). Everything lives in a single `RWX` PT_LOAD//! segment so the image is self-contained and directly executable with no//! relocations.//!//! The writer is restricted to the very narrow x86_64 / AArch64 Linux ABI//! targets: it bails out with `error.UnsupportedArch` anywhere else. Machine//! selection follows `builtin.cpu.arch` (the build host).//!//! This module owns only file-format assembly (headers + layout). All//! instruction encoding lives in the value `.rgx` lowering.const std = @import("std");const builtin = @import("builtin");
pub const Error = error{ OutOfMemory, UnsupportedArch };
pub const ELF_HEADER_SIZE = 64;pub const PHDR_SIZE = 56;pub const PHDR_COUNT = 1; // a single RWX PT_LOAD; a second degenerate (empty) LOAD// crashes the aarch64 kernel's elf_map, so it was dropped.
/// Where `.text` is placed in the address space (page-aligned; both the/// x86-64 SysV PIE origin and a plain AArch64 static origin work here).pub const LOAD_BASE: u64 = 0x400000;
/// ELF `e_machine` for the build-host architecture.pub fn machineType() u16 { return switch (builtin.cpu.arch) { .x86_64 => 0x3e, // EM_X86_64 .aarch64 => 183, // EM_AARCH64 else => unreachable, // assemble() already rejected other arches };}
/// Describes the pieces of one emitted image.pub const Image = struct { /// Encoded instructions starting at the `_start` entry. text: []const u8, /// Read-only data appended (16-aligned) after the code; RIP-relative /// references from `text` are patched by the caller using `rodata_addr`. rodata: []const u8 = &.{},
/// Filled by `assemble`: absolute virtual address of `.text`. text_addr: u64 = 0, /// Filled by `assemble`: absolute virtual address of `.rodata`. rodata_addr: u64 = 0,};
const PT_LOAD = 1;const PF_X: u32 = 1;const PF_W: u32 = 2;const PF_R: u32 = 4;
/// Result of `layout`: where each piece lands in the final image.pub const Layout = struct { text_off: usize, rodata_off: usize, total: usize,};
/// Computes the file offsets and absolute virtual addresses for `img` without/// producing bytes. The `.rgx` lowering uses this to patch RIP-relative/// displacements (which depend on the final rodata address) before assembling.////// Fills `img.text_addr` and `img.rodata_addr`.pub fn layout(img: *Image) Layout { const text_off = ELF_HEADER_SIZE + PHDR_COUNT * PHDR_SIZE; const rodata_off = alignA(text_off + img.text.len, 16); img.text_addr = LOAD_BASE + @as(u64, text_off); img.rodata_addr = LOAD_BASE + @as(u64, rodata_off); return .{ .text_off = text_off, .rodata_off = rodata_off, .total = rodata_off + img.rodata.len, };}
/// Builds the complete ELF byte image. Caller owns the returned block.pub fn assemble(alloc: std.mem.Allocator, img: *Image) Error![]u8 { if ((builtin.cpu.arch != .x86_64 and builtin.cpu.arch != .aarch64) or builtin.os.tag != .linux) { return error.UnsupportedArch; }
const lay = layout(img); const out = try alloc.alloc(u8, lay.total); @memset(out, 0);
writeEhdr(out, img.text_addr); writePhdr(out, 0, .{ .p_offset = 0, .p_vaddr = LOAD_BASE, .p_filesz = @as(u64, lay.total), .p_memsz = @as(u64, lay.total), .p_flags = PF_R | PF_W | PF_X, });
@memcpy(out[lay.text_off .. lay.text_off + img.text.len], img.text); @memcpy(out[lay.rodata_off .. lay.rodata_off + img.rodata.len], img.rodata);
return out;}
fn putU16(out: []u8, off: usize, val: u16) void { const b = std.mem.toBytes(val); @memcpy(out[off .. off + 2], &b);}
fn putU32(out: []u8, off: usize, val: u32) void { const b = std.mem.toBytes(val); @memcpy(out[off .. off + 4], &b);}
fn putU64(out: []u8, off: usize, val: u64) void { const b = std.mem.toBytes(val); @memcpy(out[off .. off + 8], &b);}
fn getU16(in: []const u8, off: usize) u16 { var b: [2]u8 = undefined; @memcpy(&b, in[off .. off + 2]); return std.mem.readInt(u16, &b, .little);}
fn getU32(in: []const u8, off: usize) u32 { var b: [4]u8 = undefined; @memcpy(&b, in[off .. off + 4]); return std.mem.readInt(u32, &b, .little);}
fn getU64(in: []const u8, off: usize) u64 { var b: [8]u8 = undefined; @memcpy(&b, in[off .. off + 8]); return std.mem.readInt(u64, &b, .little);}
fn alignA(v: usize, a: usize) usize { return (v + a - 1) & ~(a - 1);}
fn writeEhdr(out: []u8, entry: u64) void { out[0] = 0x7f; out[1] = 'E'; out[2] = 'L'; out[3] = 'F'; out[4] = 2; // 64-bit out[5] = 1; // little-endian out[6] = 1; // ELF version putU16(out, 16, 2); // ET_EXEC putU16(out, 18, machineType()); // e_machine (host arch) putU32(out, 20, 1); // e_version putU64(out, 24, entry); // e_entry putU64(out, 32, ELF_HEADER_SIZE); // e_phoff putU64(out, 40, 0); // e_shoff putU32(out, 48, 0); // e_flags putU16(out, 52, ELF_HEADER_SIZE); // e_ehsize putU16(out, 54, PHDR_SIZE); // e_phentsize putU16(out, 56, PHDR_COUNT); // e_phnum putU16(out, 58, 0); // e_shentsize putU16(out, 60, 0); // e_shnum putU16(out, 62, 0); // e_shstrndx}
fn writePhdr(out: []u8, i: usize, p: struct { p_offset: u64, p_vaddr: u64, p_filesz: u64, p_memsz: u64, p_flags: u32,}) void { const h = ELF_HEADER_SIZE + i * PHDR_SIZE; putU32(out, h, PT_LOAD); putU32(out, h + 4, p.p_flags); putU64(out, h + 8, p.p_offset); putU64(out, h + 16, p.p_vaddr); putU64(out, h + 24, p.p_vaddr); // p_paddr putU64(out, h + 32, p.p_filesz); putU64(out, h + 40, p.p_memsz); putU64(out, h + 48, 0x1000); // p_align}
const testing = std.testing;
test "elf: headers and single load segment are structurally sound" { // A trivial `_start` that immediately exits(42), for the host arch. const text = switch (builtin.cpu.arch) { .x86_64 => [_]u8{ 0x48, 0xc7, 0xc0, 0x3c, 0x00, 0x00, 0x00, 0x48, 0xc7, 0xc7, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05 }, .aarch64 => [_]u8{ 0x40, 0x05, 0x80, 0xd2, 0xa8, 0x0b, 0x80, 0xd2, 0x01, 0x00, 0x00, 0xd4 }, else => return error.SkipZigTest, }; var img = Image{ .text = &text }; const blob = try assemble(testing.allocator, &img); defer testing.allocator.free(blob);
try testing.expect(std.mem.eql(u8, blob[0..4], &.{ 0x7f, 'E', 'L', 'F' })); try testing.expectEqual(@as(u16, 2), getU16(blob, 16)); // ET_EXEC try testing.expectEqual(machineType(), getU16(blob, 18)); // host arch machine type try testing.expectEqual(img.text_addr, getU64(blob, 24)); // e_entry const phoff = getU64(blob, 32); try testing.expectEqual(@as(u16, 1), getU16(blob, 56)); // e_phnum try testing.expectEqual(@as(u32, PT_LOAD), getU32(blob, phoff)); // Code bytes are present in the image after the ELF+phdr headers. const text_off = ELF_HEADER_SIZE + PHDR_COUNT * PHDR_SIZE; try testing.expect(std.mem.eql(u8, blob[text_off .. text_off + text.len], &text));}
test "elf: emitted image is a runnable no-libc executable (exit 42)" { if ((builtin.cpu.arch != .x86_64 and builtin.cpu.arch != .aarch64) or builtin.os.tag != .linux) return error.SkipZigTest;
// `_start` exits(42) with raw syscalls for the host arch. const text = switch (builtin.cpu.arch) { // mov rax,60; mov rdi,42; syscall .x86_64 => [_]u8{ 0x48, 0xc7, 0xc0, 0x3c, 0x00, 0x00, 0x00, 0x48, 0xc7, 0xc7, 0x2a, 0x00, 0x00, 0x00, 0x0f, 0x05 }, // movz x0,#42; movz x8,#93; svc #0 .aarch64 => [_]u8{ 0x40, 0x05, 0x80, 0xd2, 0xa8, 0x0b, 0x80, 0xd2, 0x01, 0x00, 0x00, 0xd4 }, else => return error.SkipZigTest, }; var img = Image{ .text = &text }; const blob = try assemble(testing.allocator, &img); defer testing.allocator.free(blob);
const path = "/tmp/rg_elf_exit42_rinnescript"; const io = std.testing.io; const cwd = std.Io.Dir.cwd(); cwd.writeFile(io, .{ .sub_path = path, .data = blob }) catch return error.TestUnexpectedResult; defer { cwd.deleteFile(io, path) catch {}; }
const path_z = try testing.allocator.dupeZ(u8, path); defer testing.allocator.free(path_z); try testing.expect(std.os.linux.chmod(path_z, 0o755) == 0);
var child = std.process.spawn(io, .{ .argv = &.{ path }, .stdout = .ignore, .stderr = .ignore }) catch return error.TestUnexpectedResult; const term = child.wait(io) catch return error.TestUnexpectedResult; switch (term) { .exited => |code| try testing.expectEqual(@as(u8, 42), code), else => return error.TestUnexpectedResult, }}