core: Add a simple ELF loader

We only need the ELF loader for init, and possibly a few other modules. The bulk of loading executables is going to be done by userspace, so we only need the basics here.
This commit is contained in:
Gabriel 2025-02-15 22:46:28 +01:00
parent ed324fcf9e
commit a76e94c517
2 changed files with 228 additions and 20 deletions

152
core/src/elf.zig Normal file
View File

@ -0,0 +1,152 @@
const std = @import("std");
const target = @import("builtin").target;
const vmm = @import("arch/vmm.zig").arch;
const platform = @import("arch/platform.zig").arch;
const pmm = @import("pmm.zig");
const debug = @import("arch/debug.zig");
const ELFMAG = "\x7fELF";
const SELFMAG = 4;
const EI_CLASS = 4; // File class byte index
const ELFCLASS64 = 2; // 64-bit objects
const EI_DATA = 5; // Data encoding byte index
const ELFDATA2LSB = 1; // 2's complement, little endian
const ET_EXEC = 2; // Executable file
const PT_LOAD = 1; // Loadable program segment
const EM_MACH = switch (target.cpu.arch) {
.x86_64 => 62,
else => @compileError("unsupported architecture"),
};
const Elf64_Ehdr align(8) = packed struct {
e_ident: u128, // Magic number and other info
e_type: u16, // Object file type
e_machine: u16, // Architecture
e_version: u32, // Object file version
e_entry: u64, // Entry point virtual address
e_phoff: u64, // Program header table file offset
e_shoff: u64, // Section header table file offset
e_flags: u32, // Processor-specific flags
e_ehsize: u16, // ELF header size in bytes
e_phentsize: u16, // Program header table entry size
e_phnum: u16, // Program header table entry count
e_shentsize: u16, // Section header table entry size
e_shnum: u16, // Section header table entry count
e_shstrndx: u16, // Section header string table index
};
const Elf64_Phdr align(8) = packed struct {
p_type: u32, // Segment type
p_flags: u32, // Segment flags
p_offset: u64, // Segment file offset
p_vaddr: u64, // Segment virtual address
p_paddr: u64, // Segment physical address
p_filesz: u64, // Segment size in file
p_memsz: u64, // Segment size in memory
p_align: u64, // Segment alignment
};
const ElfError = error{
InvalidExecutable,
};
fn canExecuteSegment(flags: u32) bool {
return (flags & 1) > 0;
}
fn canWriteSegment(flags: u32) bool {
return (flags & 2) > 0;
}
pub fn loadElf(allocator: *pmm.FrameAllocator, directory: *vmm.PageDirectory, base_address: pmm.PhysFrame) !usize {
const address = base_address.virtualAddress(vmm.PHYSICAL_MAPPING_BASE);
debug.print("Address: {}\n", .{address});
const elf_header: *Elf64_Ehdr = @ptrFromInt(address);
debug.print("ELF header: {}\n", .{elf_header});
const e_ident: [*]u8 = @ptrFromInt(address);
if (!std.mem.eql(u8, e_ident[0..SELFMAG], ELFMAG[0..SELFMAG])) {
debug.print("Error while loading ELF: ELF header has no valid magic\n", .{});
return error.InvalidExecutable;
}
if (e_ident[EI_CLASS] != ELFCLASS64) {
debug.print("Error while loading ELF: ELF object is not 64-bit\n", .{});
return error.InvalidExecutable;
}
if (e_ident[EI_DATA] != ELFDATA2LSB) {
debug.print("Error while loading ELF: ELF object is not 2's complement little-endian\n", .{});
return error.InvalidExecutable;
}
if (elf_header.e_type != ET_EXEC) {
debug.print("Error while loading ELF: ELF object is not an executable\n", .{});
return error.InvalidExecutable;
}
if (elf_header.e_machine != EM_MACH) {
debug.print("Error while loading ELF: ELF object's target architecture does not match the current one\n", .{});
return error.InvalidExecutable;
}
if (elf_header.e_phnum == 0) {
debug.print("Error while loading ELF: ELF object has no program headers\n", .{});
return error.InvalidExecutable;
}
var i: usize = 0;
var program_header: *align(1) Elf64_Phdr = @ptrFromInt(address + elf_header.e_phoff);
debug.print("Program header address: {x}\n", .{address + elf_header.e_phoff});
while (i < elf_header.e_phnum) {
debug.print("Program header: {}\n", .{program_header.*});
if (program_header.p_type == PT_LOAD) {
debug.print("ELF: Loading segment (offset={d}, base={x}, filesize={d}, memsize={d})\n", .{ program_header.p_offset, program_header.p_vaddr, program_header.p_filesz, program_header.p_memsz });
const vaddr_diff: u64 = @rem(program_header.p_vaddr, platform.PAGE_SIZE);
const base_vaddr: u64 = program_header.p_vaddr - vaddr_diff;
var flags: u32 = @intFromEnum(vmm.Flags.User) | @intFromEnum(vmm.Flags.NoExecute);
if (canWriteSegment(program_header.p_flags)) flags |= @intFromEnum(vmm.Flags.ReadWrite);
if (canExecuteSegment(program_header.p_flags)) flags &= ~@as(u32, @intFromEnum(vmm.Flags.NoExecute));
// Allocate physical memory for the segment
try vmm.allocAndMap(allocator, directory, base_vaddr, try std.math.divCeil(usize, program_header.p_memsz + vaddr_diff, platform.PAGE_SIZE), flags);
try vmm.memsetUser(directory, vmm.PHYSICAL_MAPPING_BASE, base_vaddr, 0, vaddr_diff);
try vmm.copyToUser(directory, vmm.PHYSICAL_MAPPING_BASE, program_header.p_vaddr, @ptrFromInt(address + program_header.p_offset), program_header.p_filesz);
const bss_size = program_header.p_memsz - program_header.p_filesz;
try vmm.memsetUser(directory, vmm.PHYSICAL_MAPPING_BASE, program_header.p_vaddr + program_header.p_filesz, 0, bss_size);
} else {
debug.print("ELF: Encountered non-loadable program header, skipping\n", .{});
}
i += 1;
const new_address = address + elf_header.e_phoff + (i * elf_header.e_phentsize);
debug.print("Program header address: {x}\n", .{new_address});
program_header = @ptrFromInt(new_address);
}
return elf_header.e_entry;
}
pub fn allocateStack(allocator: *pmm.FrameAllocator, directory: *vmm.PageDirectory, stack_top: usize, stack_size: usize) !usize {
const pages = try std.math.divCeil(usize, stack_size, platform.PAGE_SIZE);
const stack_bottom = stack_top - (pages * platform.PAGE_SIZE);
try vmm.allocAndMap(allocator, directory, stack_bottom, pages, @intFromEnum(vmm.Flags.ReadWrite) | @intFromEnum(vmm.Flags.User) | @intFromEnum(vmm.Flags.NoExecute));
return stack_top - 16;
}

View File

@ -1,14 +1,23 @@
const std = @import("std"); const std = @import("std");
const easyboot = @cImport(@cInclude("easyboot.h")); const easyboot = @cImport(@cInclude("easyboot.h"));
const debug = @import("arch/debug.zig"); const debug = @import("arch/debug.zig");
const cpu = @import("arch/cpu.zig");
const platform = @import("arch/platform.zig").arch; const platform = @import("arch/platform.zig").arch;
const interrupts = @import("arch/interrupts.zig").arch; const interrupts = @import("arch/interrupts.zig").arch;
const vmm = @import("arch/vmm.zig").arch; const vmm = @import("arch/vmm.zig").arch;
const multiboot = @import("multiboot.zig"); const multiboot = @import("multiboot.zig");
const pmm = @import("pmm.zig"); const pmm = @import("pmm.zig");
const thread = @import("thread.zig");
const elf = @import("elf.zig");
const MultibootInfo = [*c]u8; const MultibootInfo = [*c]u8;
const Context = struct {
allocator: *pmm.FrameAllocator,
directory: *vmm.PageDirectory,
regs: *interrupts.InterruptStackFrame,
};
export fn _start(magic: u32, info: MultibootInfo) callconv(.C) noreturn { export fn _start(magic: u32, info: MultibootInfo) callconv(.C) noreturn {
interrupts.disableInterrupts(); interrupts.disableInterrupts();
@ -17,40 +26,87 @@ export fn _start(magic: u32, info: MultibootInfo) callconv(.C) noreturn {
while (true) {} while (true) {}
} }
debug.print("Hello world from the kernel!\n", .{});
multiboot.parseMultibootTags(@ptrCast(info)); multiboot.parseMultibootTags(@ptrCast(info));
platform.platformInit(); platform.platformInit();
debug.print("GDT initialized\n", .{}); const tag = multiboot.findMultibootTag(easyboot.multiboot_tag_mmap_t, @ptrCast(info)) orelse {
debug.print("error: No memory map multiboot tag found!\n", .{});
while (true) {}
unreachable;
};
if (multiboot.findMultibootTag(easyboot.multiboot_tag_mmap_t, @ptrCast(info))) |tag| { var allocator = pmm.initializeFrameAllocator(tag) catch |err| {
var allocator = pmm.initializeFrameAllocator(tag) catch |err| { debug.print("Error while initializing frame allocator: {}\n", .{err});
debug.print("Error while initializing frame allocator: {}\n", .{err}); while (true) {}
while (true) {} };
};
var init_directory = std.mem.zeroes(vmm.PageDirectory); var dir: vmm.PageDirectory = std.mem.zeroes(vmm.PageDirectory);
const base: usize = vmm.createInitialMappings(&allocator, tag, &init_directory) catch |err| { const base: usize = vmm.createInitialMappings(&allocator, tag, &dir) catch |err| {
debug.print("Error while creating initial mappings: {}\n", .{err}); debug.print("Error while creating initial mappings: {}\n", .{err});
while (true) {} while (true) {}
}; };
debug.print("Physical memory base mapping for init: {x}\n", .{base}); debug.print("Physical memory base mapping for init: {x}\n", .{base});
} else {
debug.print("No memory map multiboot tag found!\n", .{}); const frame = pmm.allocFrame(&allocator) catch |err| {
} debug.print("Error while creating frame for user page directory: {}\n", .{err});
while (true) {}
};
// At this point the physical address space is already mapped into kernel virtual memory.
const init_directory: *vmm.PageDirectory = @ptrFromInt(frame.virtualAddress(vmm.PHYSICAL_MAPPING_BASE));
init_directory.* = dir;
cpu.setupCore(&allocator) catch |err| {
debug.print("Error while setting up core-specific scheduler structures: {}\n", .{err});
while (true) {}
};
const init = thread.createThreadControlBlock(&allocator) catch |err| {
debug.print("Error while creating thread control block for init: {}\n", .{err});
while (true) {}
};
init.directory = init_directory;
thread.arch.initUserRegisters(&init.regs);
thread.arch.setArgument(&init.regs, base);
thread.addThreadToScheduler(cpu.thisCore(), init);
const ctx = Context{ .allocator = &allocator, .directory = init_directory, .regs = &init.regs };
multiboot.findMultibootTags(easyboot.multiboot_tag_module_t, @ptrCast(info), struct {
fn handler(mod: *easyboot.multiboot_tag_module_t, c: *const anyopaque) void {
const context: *const Context = @alignCast(@ptrCast(c));
const name = "init";
if (std.mem.eql(u8, mod.string()[0..name.len], name[0..name.len])) {
const phys_frame = pmm.PhysFrame{ .address = mod.mod_start };
debug.print("Loading init from module at address {x}, virtual {x}\n", .{ mod.mod_start, phys_frame.virtualAddress(vmm.PHYSICAL_MAPPING_BASE) });
const entry = elf.loadElf(context.allocator, context.directory, pmm.PhysFrame{ .address = mod.mod_start }) catch |err| {
debug.print("Error while loading ELF file for init: {}\n", .{err});
while (true) {}
};
thread.arch.setAddress(context.regs, entry);
}
}
}.handler, &ctx);
const default_stack_size = 0x80000; // 512 KiB.
const stack = elf.allocateStack(&allocator, init_directory, base - platform.PAGE_SIZE, default_stack_size) catch |err| {
debug.print("Error while creating stack for init: {}\n", .{err});
while (true) {}
};
thread.arch.setStack(&init.regs, stack);
platform.platformEndInit(); platform.platformEndInit();
asm volatile ("int3"); thread.enterTask(init);
while (true) {}
} }
pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn {
debug.print("--- KERNEL PANIC! ---\n", .{}); debug.print("--- KERNEL PANIC! ---\n", .{});
debug.print("{s}\n", .{message}); debug.print("{s}\n", .{message});
debug.print("return address: {x}\n", .{@returnAddress()});
while (true) {} while (true) {}
} }