417 lines
14 KiB
C
417 lines
14 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
*/
|
||
|
||
/**
|
||
* elf_loader.c - ELF64 kernel loader for StarKernel
|
||
*
|
||
* Loads the StarKernel ELF binary from ESP, parses segments,
|
||
* applies relocations, and jumps to entry point.
|
||
*/
|
||
|
||
#include "uefi.h"
|
||
#include "elf64.h"
|
||
#include "arch.h"
|
||
|
||
/**
|
||
* @brief Fill a memory region with a byte value (freestanding memset).
|
||
*
|
||
* Writes @p n copies of the low byte of @p c to the memory starting at
|
||
* @p s. Used by @c elf_load_segments() to zero-fill BSS regions
|
||
* (@c p_memsz > @c p_filesz) without a libc dependency. The byte loop
|
||
* is correct for all sizes; a future optimisation could use SIMD or
|
||
* word-width stores for large BSS regions.
|
||
*
|
||
* @param s Pointer to the start of the destination region.
|
||
* @param c Value to fill (only the low 8 bits are used).
|
||
* @param n Number of bytes to write.
|
||
* @return @p s (matches standard @c memset signature).
|
||
*/
|
||
static void *elf_memset(void *s, int c, uint64_t n)
|
||
{
|
||
unsigned char *p = (unsigned char *)s;
|
||
while (n--) *p++ = (unsigned char)c;
|
||
return s;
|
||
}
|
||
|
||
/**
|
||
* @brief Copy a non-overlapping memory region (freestanding memcpy).
|
||
*
|
||
* Copies @p n bytes from @p src to @p dest using a byte-at-a-time loop.
|
||
* Behaviour is undefined if the source and destination ranges overlap.
|
||
* Used by @c elf_load_segments() to copy each PT_LOAD segment's
|
||
* @c p_filesz bytes from the ELF image in memory to its load address.
|
||
*
|
||
* @param dest Destination buffer; must not overlap with @p src.
|
||
* @param src Source buffer.
|
||
* @param n Number of bytes to copy.
|
||
* @return @p dest (matches standard @c memcpy signature).
|
||
*/
|
||
static void *elf_memcpy(void *dest, const void *src, uint64_t n)
|
||
{
|
||
unsigned char *d = (unsigned char *)dest;
|
||
const unsigned char *s = (const unsigned char *)src;
|
||
while (n--) *d++ = *s++;
|
||
return dest;
|
||
}
|
||
|
||
/**
|
||
* @brief Validate an ELF64 header for the current target architecture.
|
||
*
|
||
* Checks the following fields in the ELF identification block and header:
|
||
* - Magic bytes @c e_ident[0..3]: must be @c 0x7F 'E' 'L' 'F'.
|
||
* - Class @c e_ident[EI_CLASS]: must be @c ELFCLASS64 (64-bit).
|
||
* - Data encoding @c e_ident[EI_DATA]: must be @c ELFDATA2LSB
|
||
* (little-endian, the only byte order used by all three supported ISAs).
|
||
* - Version @c e_ident[EI_VERSION]: must be @c EV_CURRENT (1).
|
||
* - Type @c e_type: must be @c ET_EXEC (statically linked) or
|
||
* @c ET_DYN (position-independent executable).
|
||
* - Machine @c e_machine: must match the build target —
|
||
* @c EM_X86_64 for @c ARCH_AMD64,
|
||
* @c EM_AARCH64 for @c ARCH_AARCH64,
|
||
* @c EM_RISCV for @c ARCH_RISCV64.
|
||
*
|
||
* @param ehdr Pointer to the ELF64 header at the start of the image.
|
||
* @return 1 if the header is valid for this architecture, 0 otherwise.
|
||
*/
|
||
static int elf_validate_header(const Elf64_Ehdr *ehdr)
|
||
{
|
||
/* Check magic number */
|
||
if (ehdr->e_ident[EI_MAG0] != ELFMAG0 ||
|
||
ehdr->e_ident[EI_MAG1] != ELFMAG1 ||
|
||
ehdr->e_ident[EI_MAG2] != ELFMAG2 ||
|
||
ehdr->e_ident[EI_MAG3] != ELFMAG3)
|
||
{
|
||
return 0; /* Not an ELF file */
|
||
}
|
||
|
||
/* Check class (64-bit) */
|
||
if (ehdr->e_ident[EI_CLASS] != ELFCLASS64) {
|
||
return 0;
|
||
}
|
||
|
||
/* Check data encoding (little-endian) */
|
||
if (ehdr->e_ident[EI_DATA] != ELFDATA2LSB) {
|
||
return 0;
|
||
}
|
||
|
||
/* Check version */
|
||
if (ehdr->e_ident[EI_VERSION] != EV_CURRENT) {
|
||
return 0;
|
||
}
|
||
|
||
/* Check type (ET_EXEC or ET_DYN) */
|
||
if (ehdr->e_type != ET_EXEC && ehdr->e_type != ET_DYN) {
|
||
return 0;
|
||
}
|
||
|
||
/* Check machine type matches architecture */
|
||
#if defined(ARCH_AMD64)
|
||
if (ehdr->e_machine != EM_X86_64) {
|
||
return 0;
|
||
}
|
||
#elif defined(ARCH_AARCH64)
|
||
if (ehdr->e_machine != EM_AARCH64) {
|
||
return 0;
|
||
}
|
||
#elif defined(ARCH_RISCV64)
|
||
if (ehdr->e_machine != EM_RISCV) {
|
||
return 0;
|
||
}
|
||
#else
|
||
#error "Unknown architecture"
|
||
#endif
|
||
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* @brief Map all PT_LOAD segments from an ELF64 image into memory.
|
||
*
|
||
* Iterates the program header table (@c e_phnum entries starting at
|
||
* offset @c e_phoff) and processes every entry with @c p_type == @c PT_LOAD:
|
||
*
|
||
* 1. Computes the destination address as @p load_base + @c p_vaddr.
|
||
* 2. Copies @c p_filesz bytes from @p elf_data + @c p_offset using
|
||
* @c elf_memcpy().
|
||
* 3. If @c p_memsz > @c p_filesz, zero-fills the remaining
|
||
* (@c p_memsz - @c p_filesz) bytes with @c elf_memset() to
|
||
* initialise the BSS region within the segment.
|
||
*
|
||
* The function does not check for address-range overlaps or that
|
||
* destination memory is physically available; the UEFI loader is
|
||
* responsible for allocating pages before calling @c elf_load_kernel().
|
||
*
|
||
* @param elf_data Pointer to the start of the ELF64 file in memory.
|
||
* @param load_base Base address at which to load the ELF image
|
||
* (0 for @c ET_EXEC, 0x400000 for @c ET_DYN).
|
||
* @return 1 always (errors are silently ignored at this milestone; a
|
||
* future revision should return 0 on a failed memory access).
|
||
*/
|
||
static int elf_load_segments(const uint8_t *elf_data, Elf64_Addr load_base)
|
||
{
|
||
const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)elf_data;
|
||
const Elf64_Phdr *phdr = (const Elf64_Phdr *)(elf_data + ehdr->e_phoff);
|
||
|
||
/* Iterate through program headers */
|
||
for (uint16_t i = 0; i < ehdr->e_phnum; i++) {
|
||
if (phdr[i].p_type == PT_LOAD) {
|
||
/* Calculate load address */
|
||
uint8_t *dest = (uint8_t *)(load_base + phdr[i].p_vaddr);
|
||
const uint8_t *src = elf_data + phdr[i].p_offset;
|
||
|
||
/* Copy file contents to memory */
|
||
elf_memcpy(dest, src, phdr[i].p_filesz);
|
||
|
||
/* Zero out BSS (p_memsz > p_filesz) */
|
||
if (phdr[i].p_memsz > phdr[i].p_filesz) {
|
||
elf_memset(dest + phdr[i].p_filesz, 0,
|
||
phdr[i].p_memsz - phdr[i].p_filesz);
|
||
}
|
||
}
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* @brief Apply all RELA relocation sections from an ELF64 image.
|
||
*
|
||
* Scans the section header table for sections of type @c SHT_RELA. For
|
||
* each such section, resolves the associated symbol table (via
|
||
* @c sh_link) and applies every relocation entry to the loaded image.
|
||
*
|
||
* Supported relocation types are ISA-specific and compile-time selected:
|
||
*
|
||
* - **amd64 (@c ARCH_AMD64)**:
|
||
* - @c R_X86_64_NONE — no-op.
|
||
* - @c R_X86_64_RELATIVE — absolute address = @c load_base + @c r_addend.
|
||
* - @c R_X86_64_64 — @c load_base + symbol value + @c r_addend (64-bit).
|
||
* - @c R_X86_64_32 / @c R_X86_64_32S — same, truncated to 32 bits.
|
||
*
|
||
* - **aarch64 (@c ARCH_AARCH64)**:
|
||
* - @c R_AARCH64_NONE — no-op.
|
||
* - @c R_AARCH64_RELATIVE — @c load_base + @c r_addend.
|
||
* - @c R_AARCH64_ABS64 — @c load_base + symbol + @c r_addend.
|
||
*
|
||
* - **riscv64 (@c ARCH_RISCV64)**:
|
||
* - @c R_RISCV_NONE — no-op.
|
||
* - @c R_RISCV_RELATIVE — @c load_base + @c r_addend.
|
||
* - @c R_RISCV_64 — @c load_base + symbol + @c r_addend.
|
||
*
|
||
* Returns 0 (failure) if a symbol index in a relocation entry is out
|
||
* of bounds, if the symbol table entry size is zero, or if an unknown
|
||
* relocation type is encountered. Returns 1 on success.
|
||
*
|
||
* @param elf_data Pointer to the ELF64 file image.
|
||
* @param load_base Load base address applied during @c elf_load_segments().
|
||
* @return 1 on success, 0 on any relocation error.
|
||
*/
|
||
static int elf_apply_relocations(const uint8_t *elf_data, Elf64_Addr load_base)
|
||
{
|
||
const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)elf_data;
|
||
const Elf64_Shdr *shdr = (const Elf64_Shdr *)(elf_data + ehdr->e_shoff);
|
||
|
||
for (uint16_t i = 0; i < ehdr->e_shnum; i++) {
|
||
const Elf64_Shdr *rela_sh = &shdr[i];
|
||
if (rela_sh->sh_type != SHT_RELA) {
|
||
continue;
|
||
}
|
||
if (rela_sh->sh_link >= ehdr->e_shnum) {
|
||
return 0;
|
||
}
|
||
|
||
const Elf64_Shdr *symtab_sh = &shdr[rela_sh->sh_link];
|
||
if (symtab_sh->sh_entsize == 0) {
|
||
return 0;
|
||
}
|
||
|
||
const Elf64_Rela *rela = (const Elf64_Rela *)(elf_data + rela_sh->sh_offset);
|
||
uint64_t num_rela = rela_sh->sh_size / sizeof(Elf64_Rela);
|
||
const Elf64_Sym *symtab = (const Elf64_Sym *)(elf_data + symtab_sh->sh_offset);
|
||
uint64_t sym_count = symtab_sh->sh_size / symtab_sh->sh_entsize;
|
||
|
||
for (uint64_t j = 0; j < num_rela; j++) {
|
||
uint32_t type = ELF64_R_TYPE(rela[j].r_info);
|
||
uint32_t sym_index = ELF64_R_SYM(rela[j].r_info);
|
||
uint64_t reloc_addr = load_base + rela[j].r_offset;
|
||
uint64_t sym_value = 0;
|
||
if (sym_index < sym_count) {
|
||
sym_value = symtab[sym_index].st_value;
|
||
} else if (sym_index != 0) {
|
||
return 0;
|
||
}
|
||
|
||
#if defined(ARCH_AMD64)
|
||
switch (type) {
|
||
case R_X86_64_NONE:
|
||
break;
|
||
case R_X86_64_RELATIVE: {
|
||
uint64_t *target = (uint64_t *)reloc_addr;
|
||
*target = load_base + rela[j].r_addend;
|
||
break;
|
||
}
|
||
case R_X86_64_64: {
|
||
uint64_t *target = (uint64_t *)reloc_addr;
|
||
*target = load_base + sym_value + rela[j].r_addend;
|
||
break;
|
||
}
|
||
case R_X86_64_32:
|
||
case R_X86_64_32S: {
|
||
uint32_t *target32 = (uint32_t *)reloc_addr;
|
||
uint64_t value = load_base + sym_value + rela[j].r_addend;
|
||
*target32 = (uint32_t)value;
|
||
break;
|
||
}
|
||
default:
|
||
return 0;
|
||
}
|
||
#elif defined(ARCH_AARCH64)
|
||
switch (type) {
|
||
case R_AARCH64_NONE:
|
||
break;
|
||
case R_AARCH64_RELATIVE: {
|
||
uint64_t *target = (uint64_t *)reloc_addr;
|
||
*target = load_base + rela[j].r_addend;
|
||
break;
|
||
}
|
||
case R_AARCH64_ABS64: {
|
||
uint64_t *target = (uint64_t *)reloc_addr;
|
||
*target = load_base + sym_value + rela[j].r_addend;
|
||
break;
|
||
}
|
||
default:
|
||
return 0;
|
||
}
|
||
#elif defined(ARCH_RISCV64)
|
||
switch (type) {
|
||
case R_RISCV_NONE:
|
||
break;
|
||
case R_RISCV_RELATIVE: {
|
||
uint64_t *target = (uint64_t *)reloc_addr;
|
||
*target = load_base + rela[j].r_addend;
|
||
break;
|
||
}
|
||
case R_RISCV_64: {
|
||
uint64_t *target = (uint64_t *)reloc_addr;
|
||
*target = load_base + sym_value + rela[j].r_addend;
|
||
break;
|
||
}
|
||
default:
|
||
return 0;
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
/**
|
||
* @brief Load, relocate, and return the entry point of a StarKernel ELF64 binary.
|
||
*
|
||
* Top-level ELF loader called by @c efi_main() (in the split-build path)
|
||
* after @c ExitBootServices(). Performs three operations in sequence:
|
||
*
|
||
* 1. **Header validation** via @c elf_validate_header() — checks magic,
|
||
* class, data encoding, version, type, and machine-specific architecture.
|
||
* Returns 0 immediately on failure.
|
||
*
|
||
* 2. **Segment loading** via @c elf_load_segments() — maps all @c PT_LOAD
|
||
* segments to their target addresses, initialises BSS to zero.
|
||
* The @p load_base is:
|
||
* - 0 for @c ET_EXEC (absolute-address kernel; typical for StarKernel).
|
||
* - 0x400000 (4 MB) for @c ET_DYN (position-independent kernel).
|
||
*
|
||
* 3. **Relocation processing** via @c elf_apply_relocations() — applies
|
||
* @c SHT_RELA sections to patch in the correct runtime addresses.
|
||
* Returns 0 on any relocation error.
|
||
*
|
||
* On success, stores @c load_base + @c e_entry in @c *entry_out — the
|
||
* virtual address of the kernel's C entry function @c kernel_main() — and
|
||
* returns 1. The caller performs an indirect call to this address.
|
||
*
|
||
* @c elf_size is currently unused (reserved for future bounds-checking of
|
||
* program-header and section-header offsets against the file boundary).
|
||
*
|
||
* @param elf_data Pointer to the ELF64 file image in UEFI loader memory;
|
||
* must remain valid until after the call returns.
|
||
* @param elf_size Size of the ELF image in bytes (currently unchecked).
|
||
* @param entry_out Output: receives the kernel entry point address on success.
|
||
* @return 1 on success, 0 if validation, segment loading, or relocation fails.
|
||
*/
|
||
int elf_load_kernel(const uint8_t *elf_data, uint64_t elf_size,
|
||
Elf64_Addr *entry_out)
|
||
{
|
||
(void)elf_size; /* Reserved for future bounds checking */
|
||
const Elf64_Ehdr *ehdr = (const Elf64_Ehdr *)elf_data;
|
||
|
||
/* Validate ELF header */
|
||
if (!elf_validate_header(ehdr)) {
|
||
return 0;
|
||
}
|
||
|
||
/* Determine load base address */
|
||
Elf64_Addr load_base = 0;
|
||
if (ehdr->e_type == ET_DYN) {
|
||
/* Position-independent: use a fixed base for now */
|
||
load_base = 0x400000; /* 4MB */
|
||
} else if (ehdr->e_type == ET_EXEC) {
|
||
/* Absolute addresses: load at 0 (segments have absolute vaddr) */
|
||
load_base = 0;
|
||
}
|
||
|
||
/* Load segments */
|
||
if (!elf_load_segments(elf_data, load_base)) {
|
||
return 0;
|
||
}
|
||
|
||
/* Apply relocations */
|
||
if (!elf_apply_relocations(elf_data, load_base)) {
|
||
return 0;
|
||
}
|
||
|
||
/* Calculate entry point */
|
||
*entry_out = load_base + ehdr->e_entry;
|
||
|
||
return 1;
|
||
}
|