From 1963a0a6d7398a426d0701f90a5bc796e32899ae Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 02:55:10 +0200 Subject: feat(rsboot): initial commit — UEFI bootloader extracted from RNG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++ AGENTS.md | 82 +++++++++++++++++++ Makefile | 48 +++++++++++ fix_pe_simple.py | 64 +++++++++++++++ main.c | 114 ++++++++++++++++++++++++++ pad_for_optiplex.py | 69 ++++++++++++++++ uefi.h | 230 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 612 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Makefile create mode 100644 fix_pe_simple.py create mode 100644 main.c create mode 100644 pad_for_optiplex.py create mode 100644 uefi.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e828353 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.o +*.elf +/build/ +build/ +.pi/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a39c192 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,82 @@ +# AGENTS.md — rsboot (RaiserSoft Bootloader) + +## Identity + +rsboot is a custom **UEFI** bootloader (PE32+, x64) that loads a Linux kernel + +initramfs from an ext2 partition using UEFI file system protocols, then +launches the kernel via `LoadImage`/`StartImage`. Single-stage design — +no MBR, no real mode, no stage1/stage2 split. + +This is a standalone submodule of the [RNG](https://git.2137697.xyz/RaiserSoft/rng) project. + +## Code + +- **Language:** C (freestanding, `-m64`, `-ffreestanding`, `-fPIE`, `-mabi=ms`). +- **Assembler:** none currently (no `.asm` files needed). +- **Linker:** `ld` with `-Ttext=0x1000 -e efi_main`, producing an ELF64 + intermediate (`main.elf`). +- **PE conversion:** `objcopy -O pei-x86-64 --subsystem=10` converts the ELF + to a PE32+ image, then Python scripts fix up headers and pad for + firmware compatibility. +- **Dependencies:** none. No libc, no CRT — just raw UEFI protocol calls. +- **Debug:** UEFI console output (`con_out->OutputString()` via serial + + framebuffer as configured by the firmware). +- **UEFI types:** `uefi.h` provides minimal stand-alone type definitions, + GUIDs, and protocol structs (system table, boot services, file system + protocol, loaded image protocol, etc.). + +## Source Layout + +``` +main.c — EFI entry point, loads kernel + initrd, starts image +uefi.h — UEFI types, GUIDs, protocol structs +Makefile — builds BOOTX64.EFI +fix_pe_simple.py — PE header fixup (ImageBase, DllChar, strip relocs) +pad_for_optiplex.py — PE padding for Optiplex 7010 firmware +``` + +## Build + +```sh +make # produces ../build/bootloader/BOOTX64.EFI +``` + +The build pipeline: + +1. **main.o** — compiled with `-fPIE -fshort-wchar -mno-red-zone -mabi=ms` +2. **main.elf** — linked as position-independent ELF64 starting at text 0x1000 +3. **BOOTX64.EFI** — objcopy to `pei-x86-64`, subsystem=10 (EFI application) +4. **fix_pe_simple.py** — sets ImageBase, marks RELOCS_STRIPPED (PIE code + uses only PC-relative addressing, so no fixups needed), sets NX | + DYNAMIC_BASE | HIGH_ENTROPY_VA, clears .reloc data directory and + checksum +5. **pad_for_optiplex.py** — if the PE is smaller than ~160KB, pad to match + the size systemd-boot uses (some AMI Aptio firmware versions on + Optiplex 7010 reject smaller PE files) + +## Kernel Loading Flow + +1. Get our own `LoadedImage` protocol via `handle_protocol` +2. Get `SimpleFileSystem` from the device handle +3. `OpenVolume` → root directory +4. Open `\bzImage`, read into allocated pages +5. Open `\initramfs.cpio.gz`, read into allocated pages +6. `LoadImage` (from memory, not device path) → kernel handle +7. Set kernel's `load_options` (command line) on its `LoadedImage` protocol +8. `StartImage` → kernel takes over + +## Key Conventions + +- **GUIDs:** EFI_LOADED_IMAGE_GUID, EFI_SIMPLE_FILE_SYSTEM_GUID (defined + in `uefi.h` matching the UEFI 2.10 spec). +- **Boot device:** The device we were loaded from (our `LoadedImage.device_handle`). +- **File paths:** `\bzImage`, `\initramfs.cpio.gz` (backslash prefix for + UEFI device paths). +- **Cmdline:** `console=ttyS0,115200 console=tty0 earlyprintk=serial,ttyS0,115200 loglevel=3 rng_tty=ttyS0 initrd=\initramfs.cpio.gz` +- **Memory allocation:** UEFI boot services `AllocatePages` (type 6 = + EfiLoaderData) for kernel/initrd buffers; `AllocatePool` for cmdline copy. +- **Compilation quirks:** `-mabi=ms` is required so UEFI calling convention + (Microsoft x64 ABI) is used; `-fshort-wchar` makes `wchar_t` 16-bit to + match UEFI's CHAR16; `-mno-red-zone` because the red-zone can be clobbered + by UEFI firmware handlers. +- **Commits:** `feat(rsboot):` prefix. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bd21c45 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +# ============================================================================= +# Makefile — rsbl (UEFI bootloader for RNG) +# +# Produces build/bootloader/BOOTX64.EFI (PE32+ for UEFI) +# +# Uses -fPIE for position-independent code so the image works at any +# load address. The Optiplex 7010 firmware ignores ImageBase + relocs, +# so RELOCS_STRIPPED + padding to ~160KB is required. +# ============================================================================= + +BUILD_DIR = ../build/bootloader +CC = gcc +LD = ld +OBJCOPY = objcopy + +CFLAGS = -m64 -ffreestanding -nostdinc -nostartfiles -nodefaultlibs \ + -fno-builtin -fno-stack-protector -fPIE -fPIC \ + -fshort-wchar -mno-red-zone -mabi=ms -Qn \ + -fno-asynchronous-unwind-tables -fno-unwind-tables \ + -fno-exceptions \ + -I. -Wall -Wextra -Werror + +LDFLAGS = -m elf_x86_64 -nostdlib -z norelro --no-dynamic-linker \ + -Ttext=0x1000 -e efi_main + +.PHONY: all clean + +all: $(BUILD_DIR)/BOOTX64.EFI + +$(BUILD_DIR)/BOOTX64.EFI: main.elf + @mkdir -p $(BUILD_DIR) + $(OBJCOPY) -O pei-x86-64 --subsystem=10:1.1 \ + --heap 0x100000,0x1000 \ + --stack 0x100000,0x1000 \ + $< $@ + python3 fix_pe_simple.py $@ + python3 pad_for_optiplex.py $@ + @echo " -> $@" + @ls -lh $@ + +main.elf: main.o + $(LD) $(LDFLAGS) -o "$@" "$^" + +main.o: main.c uefi.h + $(CC) $(CFLAGS) -c -o "$@" "$<" + +clean: + rm -f *.o *.elf $(BUILD_DIR)/BOOTX64.EFI diff --git a/fix_pe_simple.py b/fix_pe_simple.py new file mode 100644 index 0000000..77b1366 --- /dev/null +++ b/fix_pe_simple.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""fix_pe_simple.py — PE header fixup for PIE-built UEFI bootloader + +The -fPIE-built bootloader uses only PC-relative addressing, so it needs +zero absolute fixups. This script just sets proper PE flags: + + - ImageBase (env IMAGE_BASE, default 0x10000000) + - Characteristics = 0x022e (RELOCS_STRIPPED) + - DllChar = 0x0160 (NX | DYNAMIC_BASE | HIGH_ENTROPY_VA) + - Clears .reloc DD (no relocations needed) + - Clears checksum (not signed) +""" + +import struct, os, sys + +def fix_pe(path): + try: + with open(path, 'rb') as f: + d = bytearray(f.read()) + except IOError as e: + print(f" ERROR: cannot read {path}: {e}") + return False + + po = struct.unpack('") + sys.exit(1) + if not fix_pe(sys.argv[1]): + sys.exit(1) diff --git a/main.c b/main.c new file mode 100644 index 0000000..d28a337 --- /dev/null +++ b/main.c @@ -0,0 +1,114 @@ +/* ============================================================================= + * main.c — RNG UEFI bootloader (rsbl) — minimal smoke-test version + * + * Just prints to screen then loads kernel + initrd via LoadImage/StartImage. + * ============================================================================= */ + +#include "uefi.h" + +static const char16 kpath[] = u"\\bzImage"; +static const char16 ipath[] = u"\\initramfs.cpio.gz"; +/* same cmdline systemd-boot used */ +static const char16 base_opt[] = u"console=ttyS0,115200 console=tty0 earlyprintk=serial,ttyS0,115200 loglevel=3 rng_tty=ttyS0 initrd=\\initramfs.cpio.gz"; + +static efi_guid lg = EFI_LOADED_IMAGE_GUID; +static efi_guid sg = EFI_SIMPLE_FILE_SYSTEM_GUID; + +static void pr(efi_simple_text_output *c, const char16 *s) { c->output_string(c, s); } +static void ph(efi_simple_text_output *c, u64 v) { + char16 b[19] = u"0x0000000000000000"; + for (int i = 17; i >= 2; i--) { + u8 n = (u8)(v & 0xF); + b[i] = n < 10 ? u'0' + n : u'a' + n - 10; + v >>= 4; + } + c->output_string(c, b); +} + +static u64 allocate_pages(efi_boot_services *b, u64 mt, u64 np, u64 *a) { + typedef u64 (*F)(u64, u64, u64, u64 *); + return ((F)b->allocate_pages)(0, mt, np, a); +} +static u8 *rf(efi_boot_services *b, efi_file_protocol *f, u64 *sz, u64 mx) { + u64 np = (mx + 4095) / 4096, a; + if (allocate_pages(b, 6, np, &a)) return 0; + u8 *d = (u8 *)a; + u64 r = mx; + if (f->read(f, &r, d) && r != mx) return 0; + *sz = r; return d; +} + +u64 __attribute__((aligned(16))) efi_main(handle ih, efi_system_table *st) { + efi_simple_text_output *c = st->con_out; + efi_boot_services *b = st->boot_services; + u64 s; + + pr(c, u"\r\n"); + pr(c, u"rsbl v0.1 -- RNG UEFI Bootloader\r\n"); + pr(c, u"\r\n"); + + efi_loaded_image *our_li; + s = b->handle_protocol(ih, &lg, (void **)&our_li); + if (s) { pr(c, u" handle_protocol LI failed\r\n"); goto h; } + + efi_simple_file_system *fs; + s = b->handle_protocol(our_li->device_handle, &sg, (void **)&fs); + if (s) { pr(c, u" SFSP not on device handle\r\n"); goto h; } + + efi_file_protocol *rt; + s = fs->open_volume(fs, (void **)&rt); + if (s) { pr(c, u" open_volume failed\r\n"); goto h; } + + pr(c, u" [ loading kernel\r"); + efi_file_protocol *f; + s = rt->open(rt, &f, (char16 *)kpath, 1, 0); + if (s) { pr(c, u" [FAIL] loading kernel\r\n"); goto h; } + u64 ks; u8 *kd = rf(b, f, &ks, 64*1024*1024); + f->close(f); + if (!kd) { pr(c, u" [FAIL] loading kernel\r\n"); goto h; } + pr(c, u" [ ok] loading kernel "); ph(c, ks); pr(c, u" bytes\r\n"); + + pr(c, u" [ loading initrd\r"); + s = rt->open(rt, &f, (char16 *)ipath, 1, 0); + if (s) { pr(c, u" [FAIL] loading initrd\r\n"); goto h; } + u64 is; u8 *id = rf(b, f, &is, 32*1024*1024); + f->close(f); + if (!id) { pr(c, u" [FAIL] loading initrd\r\n"); goto h; } + pr(c, u" [ ok] loading initrd "); ph(c, is); pr(c, u" bytes\r\n"); + + pr(c, u" [ starting image\r"); + handle kh = 0; + s = b->load_image(0, ih, 0, kd, ks, &kh); + if (s) { pr(c, u" [FAIL] LoadImage\r\n"); goto h; } + + efi_loaded_image *kli; + s = b->handle_protocol(kh, &lg, (void **)&kli); + if (s) { pr(c, u" [FAIL] kernel image info\r\n"); goto h; } + kli->device_handle = our_li->device_handle; + + u16 osz = 0; + while (base_opt[osz]) osz++; + osz = (osz + 1) * 2; + u16 *opts; + { + typedef u64 (*ap_t)(u8, u64, void **); + s = ((ap_t)b->allocate_pool)(0, osz, (void **)&opts); + } + if (s) { pr(c, u" [FAIL] cmdline alloc\r\n"); goto h; } + for (u16 i = 0; i < osz / 2; i++) opts[i] = base_opt[i]; + kli->load_options = opts; + kli->load_options_size = osz; + + pr(c, u" [ ok] starting image\r\n"); + pr(c, u"\r\n"); + pr(c, u" Launching RNG/Linux ...\r\n"); + pr(c, u"\r\n"); + + s = b->start_image(kh, 0, 0); + pr(c, u" kernel returned "); ph(c, s); pr(c, u"\r\n"); + +h: + pr(c, u" Halt\r\n"); + while (1) asm volatile("hlt"); + return s; +} diff --git a/pad_for_optiplex.py b/pad_for_optiplex.py new file mode 100644 index 0000000..2d48552 --- /dev/null +++ b/pad_for_optiplex.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""pad_for_optiplex.py — pad a UEFI bootloader PE for Optiplex 7010 + +The Optiplex 7010 AMI Aptio firmware rejects PEs smaller than ~160KB +("no bootable devices"). This pads the PE to match systemd-boot's size +and sets SizeOfImage accordingly. +""" + +import struct, sys, os + +def pad(path): + try: + with open(path, 'rb') as f: + d = bytearray(f.read()) + except IOError as e: + print(f" ERROR: cannot read {path}: {e}") + return False + + po = struct.unpack('= target: + print(f" Already >= {target} bytes ({len(d)}), nothing to do") + return True + + # Compute SizeOfImage: round current SizeOfImage to match file growth + cur_si = struct.unpack('") + sys.exit(1) + if not pad(sys.argv[1]): + sys.exit(1) diff --git a/uefi.h b/uefi.h new file mode 100644 index 0000000..6d46f16 --- /dev/null +++ b/uefi.h @@ -0,0 +1,230 @@ +/* ============================================================================= + * uefi.h — minimal UEFI types and protocols for the RNG bootloader + * + * Standalone, freestanding, no external headers. + * ============================================================================= */ + +#ifndef UEFI_H +#define UEFI_H + +/* ---- basic types ---- */ +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef unsigned long long u64; +/* UEFI CHAR16 — wchar_t must be 16-bit (use -fshort-wchar). + * GCC by default makes wchar_t = int (32-bit). -fshort-wchar makes + * it unsigned short (16-bit) to match UEFI's CHAR16. + * We don't include (freestanding), so define our own. */ +typedef __CHAR16_TYPE__ char16; +typedef signed short int16; +typedef long long int64; +typedef void *handle; + +/* ---- EFI status codes ---- */ +#define EFI_SUCCESS 0 +#define EFI_LOAD_ERROR ((u64)1) +#define EFI_INVALID_PARAMETER ((u64)2) +#define EFI_UNSUPPORTED ((u64)3) +#define EFI_NOT_FOUND ((u64)14) +#define EFI_ALREADY_STARTED ((u64)25) +#define EFI_OUT_OF_RESOURCES ((u64)9) + +/* ---- GUIDs (128-bit) ---- */ +typedef struct { u32 a; u16 b; u16 c; u8 d[8]; } efi_guid; + +/* ---- EFI time (unused, part of file info struct) ---- */ +typedef struct { + u16 year; u8 month; u8 day; + u8 hour; u8 minute; u8 second; u8 pad1; + u32 ns; int16 tz; u8 daylight; +} efi_time; + +/* ---- table headers ---- */ +typedef struct { + u64 signature; + u32 revision; + u32 header_size; + u32 crc32; + u32 reserved; +} efi_table_header; + +/* ---- system table ---- */ +typedef struct efi_config_table efi_config_table; +typedef struct efi_simple_text_output efi_simple_text_output; +typedef struct efi_simple_text_input efi_simple_text_input; +typedef struct efi_boot_services efi_boot_services; +typedef struct efi_runtime_services efi_runtime_services; + +typedef struct { + efi_table_header hdr; + char16 *firmware_vendor; + u32 firmware_revision; + handle console_in_handle; + efi_simple_text_input *con_in; + handle console_out_handle; + efi_simple_text_output *con_out; + handle stderr_handle; + efi_simple_text_output *std_err; + efi_runtime_services *runtime_services; + efi_boot_services *boot_services; + u64 number_of_table_entries; + efi_config_table *configuration_table; +} efi_system_table; + +/* ---- EFI input key ---- */ +typedef struct { + u16 scan_code; + char16 unicode_char; +} efi_input_key; + +/* ---- simple text output (console) ---- */ +typedef u64 (*efi_text_reset)(efi_simple_text_output *, u8); +typedef u64 (*efi_text_output)(efi_simple_text_output *, const char16 *); + +struct efi_simple_text_output { + void *unused; // Index 0: Reset + efi_text_output output_string; // Index 1: OutputString + void *unused2[8]; // Index 2-9: TestString..Mode +}; + +/* ---- simple text input (keyboard) ---- */ +typedef u64 (*efi_input_reset)(efi_simple_text_input *, u8); +typedef u64 (*efi_input_read_key)(efi_simple_text_input *, efi_input_key *); + +struct efi_simple_text_input { + efi_input_reset reset; /* Index 0: Reset */ + efi_input_read_key read_key_stroke; /* Index 1: ReadKeyStroke */ + handle wait_for_key; /* Index 2: WaitForKey event */ +}; + +/* ---- boot services ---- */ +typedef u64 (*efi_exit_boot_services)(handle, u64); +typedef u64 (*efi_get_memory_map)(void*, void*, void*, void*, void*); +typedef u64 (*efi_locate_protocol)(const efi_guid *, void *, void **); +typedef u64 (*efi_load_image)(u8, handle, void *, void *, u64, handle *); +typedef u64 (*efi_start_image)(handle, void *, u64 *); +typedef u64 (*efi_handle_protocol)(handle, const efi_guid *, void **); +typedef u64 (*efi_open_volume)(void *, void **); +typedef u64 (*efi_open_file)(void *, void **, char16 *, u64, u64); +typedef u64 (*efi_read_file)(void *, u64 *, void *); +typedef u64 (*efi_allocate_pool)(u8, u64, void **); + +/* Boot Services table matching UEFI spec v2.10 function order. + Each entry is one function pointer (8 bytes on x64). */ +struct efi_boot_services { + efi_table_header hdr; /* 0 */ + void *raise_tpl; /* 24 index 0 */ + void *restore_tpl; /* 32 index 1 */ + void *allocate_pages; /* 40 index 2 */ + void *free_pages; /* 48 index 3 */ + efi_get_memory_map get_memory_map; /* 56 index 4 */ + efi_allocate_pool allocate_pool; /* 64 index 5 */ + void *free_pool; /* 72 index 6 */ + void *create_event; /* 80 index 7 */ + void *set_timer; /* 88 index 8 */ + void *wait_for_event; /* 96 index 9 */ + void *signal_event; /*104 index 10 */ + void *close_event; /*112 index 11 */ + void *check_event; /*120 index 12 */ + void *install_protocol_interface; /*128 index 13 */ + void *reinstall_protocol_interface;/*136 index 14 */ + void *uninstall_protocol_interface;/*144 index 15 */ + efi_handle_protocol handle_protocol; /*152 index 16 */ + void *reserved; /*160 index 17 */ + void *register_protocol_notify; /*168 index 18 */ + void *locate_handle; /*176 index 19 */ + void *locate_device_path; /*184 index 20 */ + void *install_config_table; /*192 index 21 */ + efi_load_image load_image; /*200 index 22 */ + efi_start_image start_image; /*208 index 23 */ + void *exit; /*216 index 24 */ + void *unload_image; /*224 index 25 */ + efi_exit_boot_services exit_boot_services; /*232 index 26 */ + void *get_next_monotonic_count; /*240 index 27 */ + void *stall; /*248 index 28 */ + void *set_watchdog_timer; /*256 index 29 */ + void *connect_controller; /*264 index 30 */ + void *disconnect_controller; /*272 index 31 */ + void *open_protocol; /*280 index 32 */ + void *close_protocol; /*288 index 33 */ + void *open_protocol_information; /*296 index 34 */ + void *protocols_per_handle; /*304 index 35 */ + void *locate_handle_buffer; /*312 index 36 */ + efi_locate_protocol locate_protocol; /*320 index 37 */ + void *install_multiple_protocols; /*328 index 38 */ + void *uninstall_multiple_protocols;/*336 index 39 */ + void *calculate_crc32; /*344 index 40 */ + void *copy_mem; /*352 index 41 */ + void *set_mem; /*360 index 42 */ + void *create_event_ex; /*368 index 43 */ +}; + +/* ---- GUID for Simple File System Protocol ---- */ +#define EFI_SIMPLE_FILE_SYSTEM_GUID \ + {0x0964e5b22, 0x6459, 0x11d2, {0x8e,0x39,0x00,0xa0,0xc9,0x69,0x72,0x3b}} + +/* ---- file protocol ---- */ +typedef struct { + u64 revision; + efi_open_volume open_volume; +} efi_simple_file_system; + +typedef struct efi_file_protocol efi_file_protocol; + +typedef u64 (*efi_file_open)(efi_file_protocol *, efi_file_protocol **, char16 *, u64, u64); +typedef u64 (*efi_file_read)(efi_file_protocol *, u64 *, void *); +typedef u64 (*efi_file_close)(efi_file_protocol *); + +#define EFI_FILE_MODE_READ 1 + +struct efi_file_protocol { + u64 revision; + efi_file_open open; + efi_file_close close; + void *unused; + efi_file_read read; + void *unused2[5]; +}; + +/* ---- file info (for getting file size) ---- */ +typedef struct { + u64 size; + u64 file_size; + u64 physical_size; + efi_time create_time; + efi_time last_access; + efi_time modification_time; + u64 attribute; + char16 file_name[1]; +} efi_file_info; + +/* ---- loaded image protocol ---- */ +typedef struct { + u32 revision; + handle parent_handle; + handle system_table; + handle device_handle; + void *file_path; + void *reserved; + u32 load_options_size; + void *load_options; + void *image_base; + u64 image_size; + u64 image_code_type; + u64 image_data_type; + void *unused; +} efi_loaded_image; + +/* ---- loaded image protocol GUID ---- */ +#define EFI_LOADED_IMAGE_GUID \ + {0x5b1b31a1, 0x9562, 0x11d2, {0x8e,0x3f,0x00,0xa0,0xc9,0x69,0x72,0x3b}} + +/* ---- device path protocol ---- */ +typedef struct { + u8 type; + u8 sub_type; + u16 length; +} efi_device_path; + +#endif /* UEFI_H */ -- cgit v1.2.3