diff options
| author | Arseney300 <Arseney300@gmail.com> | 2026-03-29 02:07:17 +0700 |
|---|---|---|
| committer | Arseney300 <Arseney300@gmail.com> | 2026-04-08 01:19:07 +0700 |
| commit | 4912796d2e88c6eb5d02fbf0fb9c39f8c9f7cd4c (patch) | |
| tree | 9ee8110e2c090c888f23797cda56c7e2df531695 | |
bastion: initial implementation
ready project skeleton
dual-arch build system with Linux-config style configuration
UEFI EFI stub loader (PE32+) for x86_64 and AArch64
ELF64 kernel parser
Temporary framebuffer console
freestanding string and c++ abi stubs
For now, kernel boots, prints banner, memory map and go halt
45 files changed, 4107 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acc8dbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Build output +result/ +*.o + +# Generated config (re-run make defconfig-xxx / make configure) +.config +config_generated.mk +include/kernel/config.h + +# Editor files +*.swp +*.swo +*~ +.vscode/ +.idea/ +.clangd/ +compile_commands.json + +# OS/platform files +.DS_Store +Thumbs.db + + +# Qemu +qemu_log* + + + +#AI tools +CLAUDE.md diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6fa1ecd --- /dev/null +++ b/Makefile @@ -0,0 +1,125 @@ +# ============================================================================ +# Bastion kernel - Top-Level Build System +# +# Usage: +# make ARCH=x86_64 defconfig — Create default .config for x86_64 +# make ARCH=aarch64 defconfig — Create default .config for aarch64 +# make ARCH=x86_64 build — Build for x86_64 +# make ARCH=aarch64 build — Build for aarch64 +# make ARCH=x86_64 run — Build + run in QEMU +# make ARCH=aarch64 run — Build + run in QEMU +# make all — Build both architectures +# make clean — Remove build artifacts and objects +# make distclean — Remove build + generated config +# ============================================================================ + +VERSION = 0 +PATCHLEVEL = 0 +SUBLEVEL = 1 +NAME = Husky + +.PHONY: all build clean distclean configure defconfig disk run check-config + +RESULT_DIR := result +CONFIG_FILE := .config +CONFIG_H := include/kernel/config.h +CONFIG_MK := config_generated.mk +CONFIGURE := python3 scripts/configure.py #TODO: change to $(python) + +-include $(CONFIG_MK) + +# ── ARCH validation ──────────────────────────────────────────────────────── + +# Default ARCH if not specified +ARCH ?= $(shell uname -m) + +ifneq ($(ARCH),x86_64) +ifneq ($(ARCH),aarch64) + $(error ARCH must be x86_64 or aarch64 (got: $(ARCH))) +endif +endif + +# Derive EFI binary name from ARCH +# TODO: maybe add name of this artifact to config +ifeq ($(ARCH),x86_64) + EFI_BINARY := BOOTX64.EFI +else + EFI_BINARY := BOOTAA64.EFI +endif + +ARCH_RESULT_DIR := $(RESULT_DIR)/$(ARCH) + +# ── Build ────────────────────────────────────────────────────────────────── + +all build: check-config + @echo "══════════════════════════════════════════" + @echo " Building Bastion — $(ARCH)" + @echo "══════════════════════════════════════════" + @echo " v.$(VERSION).$(PATCHLEVEL).$(SUBLEVEL) — $(NAME)" + @echo "══════════════════════════════════════════" + @echo " Build boot" + @echo "══════════════════════════════════════════" + $(MAKE) -C boot ARCH=$(ARCH) RESULT_DIR=$(CURDIR)/$(ARCH_RESULT_DIR)/boot + @echo "══════════════════════════════════════════" + @echo " Build kernel" + @echo "══════════════════════════════════════════" + $(MAKE) -C kernel ARCH=$(ARCH) RESULT_DIR=$(CURDIR)/$(ARCH_RESULT_DIR)/kernel + +# ── Configuration ─────────────────────────────────────────────────────────── + +defconfig: + @cp configs/default_$(ARCH) $(CONFIG_FILE) + @echo "Created .config from configs/default_$(ARCH)" + @$(CONFIGURE) --arch $(ARCH) + @echo "" + +# Regenerate config.h and config_generated.mk from .config +configure: $(CONFIG_FILE) + @$(CONFIGURE) --arch $(ARCH) + +# Auto-generate config.h if .config exists but config.h is stale or broken or missing +check-config: $(CONFIG_FILE) + @if [ ! -f $(CONFIG_H) ] || [ $(CONFIG_FILE) -nt $(CONFIG_H) ] || [ scripts/configure.py -nt $(CONFIG_H) ]; then \ + $(CONFIGURE) --arch $(ARCH); \ + fi + +# If .config doesn't exist, tell the user +$(CONFIG_FILE): + @echo "═══════════════════════════════════════════════════════════" + @echo " ERROR: No .config file found." + @echo "" + @echo " Create one with:" + @echo " make defconfig" + @echo " make ARCH=x86_64 defconfig" + @echo " make ARCH=aarch64 defconfig" + @echo "" + @echo " Or copy manually:" + @echo " cp configs/default_x86_64 .config" + @echo "═══════════════════════════════════════════════════════════" + @exit 1 + +# ── Disk images ───────────────────────────────────────────────────────────── + +disk: build + bash scripts/create_disk.sh $(ARCH) \ + $(ARCH_RESULT_DIR)/boot/$(EFI_BINARY) \ + $(ARCH_RESULT_DIR)/kernel/$(CONFIG_KERNEL_FILE_NAME) \ + $(ARCH_RESULT_DIR)/bastion.img + +# ── QEMU ──────────────────────────────────────────────────────────────────── + +run: disk + @bash scripts/run_qemu.sh $(ARCH) $(ARCH_RESULT_DIR)/bastion.img + +# ── Clean ─────────────────────────────────────────────────────────────────── + +clean: + rm -rf $(RESULT_DIR) + $(MAKE) -C boot ARCH=$(ARCH) clean + $(MAKE) -C kernel ARCH=$(ARCH) clean + @echo "Clean." + +# Remove everything including generated config (but not .config itself) +distclean: clean + rm -f $(CONFIG_H) $(CONFIG_MK) + @echo "Distclean." diff --git a/README.md b/README.md new file mode 100644 index 0000000..07cf3c4 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# Bastion — Custom UEFI Kernel + +A dual-architecture operating system kernel written in C++20 with a custom UEFI boot stub. + +## Architecture + +``` +bastion/ +├── Makefile Top-level build orchestration +├── config.mk Shared toolchain flags (Clang/LLD) +├── include/ +│ └── boot_info.h Loader ↔ kernel handoff contract +├── boot/ UEFI boot loader (PE32+) +│ ├── Makefile +│ ├── common/ +│ │ ├── efi.h Standalone UEFI type definitions +│ │ ├── elf.h ELF64 format definitions +│ │ ├── elf_parser.cpp ELF segment loader +│ │ └── efi_loader.cpp Core boot logic (GOP, mmap, ExitBootServices) +│ ├── x86_64/ +│ │ ├── entry.cpp efi_main → common loader → jump to kernel +│ │ └── linker.ld +│ └── aarch64/ +│ ├── entry.cpp +│ └── linker.ld +├── kernel/ Kernel (ELF64) +│ ├── Makefile +│ ├── arch/ +│ │ ├── x86_64/ +│ │ │ ├── entry.S Assembly entry: set stack, call kernel_main +│ │ │ └── linker.ld Kernel at 0x100000 +│ │ └── aarch64/ +│ │ ├── entry.S +│ │ └── linker.ld Kernel at 0x40100000 +│ ├── core/ +│ │ └── kernel_main.cpp First C++ code: init console, dump info, halt +│ ├── lib/ +│ │ ├── kprint.cpp Framebuffer console (8x16 VGA font) +│ │ ├── string.cpp Freestanding memcpy/memset/strlen +│ │ └── cxxabi.cpp C++ ABI stubs +│ └── include/kernel/ +│ ├── kprint.h +│ └── types.h +└── scripts/ + ├── create_disk.sh Creates GPT + FAT32 ESP disk image + └── run_qemu.sh Launches QEMU with OVMF/AAVMF firmware +``` + +## Boot Flow + +``` +UEFI Firmware + ↓ loads BOOTX64.EFI (PE32+) +EFI Loader (boot/) + ├── Opens ESP, reads kernel.elf + ├── Parses ELF64, loads PT_LOAD segments + ├── Gets GOP framebuffer + ├── Finds ACPI RSDP (x86) / FDT (arm64) + ├── GetMemoryMap() + ExitBootServices() + └── Jumps to kernel entry with BootInfo* + ↓ +Kernel entry.S (arch-specific) + ├── Sets up 16 KiB stack + └── Calls kernel_main(BootInfo*) + ↓ +kernel_main (C++) + ├── Initializes framebuffer console + ├── Prints banner, memory map, kernel info + └── Halts +``` + +## Build Requirements + +- **Clang/LLVM 15+** (clang++, lld-link, llvm-objcopy) +- **QEMU** (qemu-system-x86_64, qemu-system-aarch64) +- **OVMF/AAVMF** firmware for UEFI emulation +- **mtools** + **dosfstools** for disk image creation + +### Install (Ubuntu/Debian) + +```bash +sudo apt install clang lld llvm qemu-system-x86 qemu-system-arm \ + ovmf qemu-efi-aarch64 mtools dosfstools gdisk +``` + +## Build & Run + +```bash +# Build both architectures +make all + +# Build x86_64 only +make x86_64 + +# Build + create disk image + run in QEMU +make run-x86_64 +make run-aarch64 + +# Clean +make clean +``` + +## Design Decisions + +- **C++20, freestanding**: No exceptions, no RTTI, no libstdc++. RAII and templates are available. +- **Custom UEFI stub** (not Limine): Full control over boot process, like Linux's EFI stub. +- **Direct PE32+ compilation**: Clang targets `x86_64-unknown-windows` / `aarch64-unknown-windows`, linked with `lld-link` — no objcopy step needed. +- **Shared BootInfo contract**: Architecture-agnostic handoff struct in `include/boot_info.h`. +- **~80% shared boot code**: Only entry points and firmware table lookups are arch-specific. + +## Roadmap + +- [x] Phase 0: Project skeleton + dual-arch build system +- [x] Phase 1: UEFI boot loader + ELF loading + framebuffer console +- [ ] Phase 2: GDT/IDT (x86_64), exception vectors (aarch64) +- [ ] Phase 3: Physical + virtual memory management, kernel heap +- [ ] Phase 4: Timer, scheduler, context switching +- [ ] Phase 5: ELF loader, userspace transition, syscalls +- [ ] Phase 6: VFS, initramfs, drivers diff --git a/boot/Makefile b/boot/Makefile new file mode 100644 index 0000000..b49690c --- /dev/null +++ b/boot/Makefile @@ -0,0 +1,78 @@ +# ============================================================================ +# boot/Makefile - UEFI Loader Build +# Produces: $(RESULT_DIR)/BOOTX64.EFI (x86) or BOOTAA64.EFI(aarch64) +# +# Strategy: Compile with Clang targeting Windows PE32+, link with lld-link. +# This produces a native .efi (PE32+) directly - no objcopy needed. +# TODO: create PE header in the code, and get rid from clang dependency +# ============================================================================ + +include ../config.mk +-include ../config_generated.mk + +COMMON_SRC := \ + common/efi_loader.cpp \ + common/efi_elf_parser.cpp + +ARCH_SRC := \ + $(ARCH)/entry.cpp + +SRC := $(COMMON_SRC) $(ARCH_SRC) +OBJ := $(patsubst %.cpp,%.o,$(SRC)) + +# ── UEFI-specific flags ──────────────────────────────────────────────────── +# Compile as PE/COFF using Clang's Windows target with -fshort-wchar. + +ifeq ($(ARCH),x86_64) + EFI_TARGET := x86_64-unknown-windows + EFI_BINARY := BOOTX64.EFI + EFI_MFLAGS := -mno-red-zone #https://os.phil-opp.com/red-zone/#:~:text=Disable%20the%20Red%20Zone%20%7C%20Writing%20an%20OS%20in%20Rust +else ifeq ($(ARCH),aarch64) + EFI_TARGET := aarch64-unknown-windows + EFI_BINARY := BOOTAA64.EFI + EFI_MFLAGS := +endif + +EFI_CXXFLAGS := \ + -target $(EFI_TARGET) \ + -std=c++23 \ + -ffreestanding \ + -fno-exceptions \ + -fno-rtti \ + -fno-stack-protector \ + -fno-threadsafe-statics \ + -fno-use-cxa-atexit \ + -fshort-wchar \ + -nostdlib \ + -Wall -Wextra \ + -Werror=return-type \ + -O2 -g \ + $(EFI_MFLAGS) \ + -I../include \ + -include $(PROJECT_ROOT)/include/kernel/config.h \ + -DEFI_ARCH_$(shell echo $(ARCH) | tr a-z A-Z) + +EFI_LDFLAGS := \ + -flavor link \ + -subsystem:efi_application \ + -entry:efi_main \ + -nodefaultlib + +# ── Targets ───────────────────────────────────────────────────────────────── + +.PHONY: all clean + +all: $(RESULT_DIR)/$(EFI_BINARY) + +# Link directly to PE32+ .efi +$(RESULT_DIR)/$(EFI_BINARY): $(OBJ) + @mkdir -p $(dir $@) + @echo " LINK $@" + @lld-link $(EFI_LDFLAGS) $(OBJ) -out:$@ + +%.o: %.cpp + @echo " CXX $<" + @$(CXX) $(EFI_CXXFLAGS) -c $< -o $@ + +clean: + find . -name '*.o' -delete diff --git a/boot/aarch64/entry.cpp b/boot/aarch64/entry.cpp new file mode 100644 index 0000000..9e602a7 --- /dev/null +++ b/boot/aarch64/entry.cpp @@ -0,0 +1,36 @@ +// ============================================================================ +// aarch64/entry.cpp - UEFI entry point for AArch64 +// +// Kernel entry point: extern "C" [[noreturn]] void kernel_main(BootInfo*); +// Chain: UEFI -> efi_main -> jumps to _start (entry.S) -> calls kernel_main (from kernel_main.cpp) +// Why we need to run asm code: +// Because we haven't stack pointer, that needed for c/c++ +// In efi_main / efi_loader_main we have it, because UEFI setup it for us +// ============================================================================ + +#include <boot/efi.h> +#include <boot/boot_info.h> + +extern bool efi_loader_main(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE* system_table, BootInfo* boot_info); + +static BootInfo g_boot_info{}; + +typedef void (*KernelEntry)(BootInfo*); // _start + +extern "C" EFI_STATUS EFIAPI efi_main(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE* system_table) { + if (!efi_loader_main(image_handle, system_table, &g_boot_info)) + return EFI_LOAD_ERROR; + + /* + entry_offset - how far the entry point is from the start of the kernel image. + For example if kernel_entry_point is 0x100000 and kernel_virt_base is 0x100000, the offset is 0 + phys_entry - the actual physical address to jump to: kernel_phys_base + entry_offset + */ + uint64_t entry_offset = g_boot_info.kernel_entry_point - g_boot_info.kernel_virt_base; + uint64_t phys_entry = g_boot_info.kernel_phys_base + entry_offset; + + auto kernel_entry = reinterpret_cast<KernelEntry>(phys_entry); + kernel_entry(&g_boot_info); // X0 = &g_boot_info (AAPCS64) + + for (;;) asm volatile("wfi"); +} diff --git a/boot/aarch64/linker.ld b/boot/aarch64/linker.ld new file mode 100644 index 0000000..5073934 --- /dev/null +++ b/boot/aarch64/linker.ld @@ -0,0 +1,60 @@ +/* AArch64 UEFI Loader Linker Script */ + + /* Entry point */ +ENTRY(efi_main) + +SECTIONS +{ + /* + Base address is 0. The loader is a relocatable PE binary - UEFI loads it wherever it wants, + so addresses are relative offsets, not fixed. + */ + . = 0; + + /* + All code from all object files. + */ + .text : { *(.text .text.*) } + + /* + Pad to next page boundary before the next section. PE sections need page alignment. + As i know, PE sections bust be aligned to the page size (4096 bytes) because memory protection + works at page granularity - each page can be markec RWX indepentedly, and if sections weren't page-aligned, + two sections with different RWX could share the same page. + rodata is read-only data (for example string literals) + */ + . = ALIGN(4096); + .rodata : { *(.rodata .rodata.*) } + + /* + Pad. + .data and .sdata - init globals (like gST, gBS). .sdata is "small data" - some compilers put small globals + in .sdata segment for faster access + */ + . = ALIGN(4096); + .data : { *(.data .data.*) *(.sdata .sdata.*) } + + /* + Pad. + .bss - zero-init globals (like static g_boot_info or static new_memory_map) + COMMON is section for globals without init (int x; instead of int x = 0 or extern int x) + */ + . = ALIGN(4096); + .bss : { *(.bss .bss.*) *(COMMON) } + + /* + Pad. + .dynamic and .rela - dynamic linking and relocation entries. PE bins need relocations + so UEFI can load them at any address and fix up absolute refs. + */ + . = ALIGN(4096); + .dynamic : { *(.dynamic) } + .rela : { *(.rela .rela.*) } + + /* + /DISCARD/ throws away debug comments, notes, exception frames, and hash tables that the EFI binare doesn't need + */ + /DISCARD/ : { + *(.comment) *(.note.*) *(.eh_frame*) *(.gnu.hash) *(.hash) + } +} diff --git a/boot/common/efi_elf_parser.cpp b/boot/common/efi_elf_parser.cpp new file mode 100644 index 0000000..f4413df --- /dev/null +++ b/boot/common/efi_elf_parser.cpp @@ -0,0 +1,147 @@ +// ============================================================================ +// efi_elf_parser.cpp - Parse and load an ELF64 kernel binary +// +// Why we don't just execute the file in-place: +// 1) Layout mismatch — the on-disk ELF layout differs from the in-memory +// layout (segments are not necessarily contiguous or page-aligned). +// 2) Fixed link addresses — the kernel is ET_EXEC (non-relocatable) linked +// at a specific virtual address (e.g. 0x100000 for x86_64). +// 3) .bss - the .bss segment has p_filesz == 0 but p_memsz > 0; those +// zero-initialised globals occupy no space in the file but need real +// zeroed memory at runtime. +// +// So we load the file into a temporary buffer, then parse + copy segments +// into a freshly allocated region at the correct address. +// ============================================================================ + +#include <elf/elf.h> +#include <boot/efi.h> + +namespace { + +// Local memcpy/memset — we have no libc in the EFI environment. +static void* memcpy_local(void* dst, const void* src, uint64_t n) noexcept { + auto* d = static_cast<uint8_t*>(dst); + const auto* s = static_cast<const uint8_t*>(src); + for (uint64_t i = 0; i < n; ++i) d[i] = s[i]; + return dst; +} + +static void* memset_local(void* dst, int val, uint64_t n) noexcept { + auto* d = static_cast<uint8_t*>(dst); + for (uint64_t i = 0; i < n; ++i) d[i] = static_cast<uint8_t>(val); + return dst; +} + +} // anonymous namespace + +// Target machine type for ELF validation — set by the build system. +// TODO: somehow remove it to arch-specific code +#if defined(EFI_ARCH_X86_64) + static constexpr uint16_t EXPECTED_ARCH = EM_X86_64; +#elif defined(EFI_ARCH_AARCH64) + static constexpr uint16_t EXPECTED_ARCH = EM_AARCH64; +#else + #error "Unknown EFI_ARCH" +#endif + +ElfLoadResult elf_load(const uint8_t* file_data, uint64_t file_size, EFI_BOOT_SERVICES* bs) { + ElfLoadResult result{}; + result.success = false; + result.error = ELF_ERR_NONE; + + // The file must be at least as large as the ELF header. + if (file_size < sizeof(Elf64_Ehdr)) { + result.error = ELF_ERR_FILE_TOO_SMALL; + return result; + } + + // Got ELF header + const auto* ehdr = reinterpret_cast<const Elf64_Ehdr*>(file_data); + + // Validate magic, class, endianness, machine type, and object type. + if (*reinterpret_cast<const uint32_t*>(ehdr->e_ident) != ELF_MAGIC) { + result.error = ELF_ERR_INVALID_MAGIC; + return result; + } + if (ehdr->e_ident[EI_CLASS] != ELFCLASS64) { + result.error = ELF_ERR_INVALID_CLASS; + return result; + } + if (ehdr->e_ident[EI_DATA] != ELFDATA2LSB) { + result.error = ELF_ERR_INVALID_IDENT; + return result; + } + if (ehdr->e_machine != EXPECTED_ARCH) { + result.error = ELF_ERR_INVALID_ARCH; + return result; + } + if (ehdr->e_type != ET_EXEC) { + result.error = ELF_ERR_INVALID_TYPE; + return result; + } + + // Set entry point + result.entry_point = ehdr->e_entry; + + // First pass: find the virtual address span of all PT_LOAD segments. + // This tells us how many pages to allocate and where. + uint64_t vaddr_min = ~0ULL; + uint64_t vaddr_max = 0; + + /* Get Program Header */ + const auto* phdrs = reinterpret_cast<const Elf64_Phdr*>(file_data + ehdr->e_phoff); + + /* Iterates via headers and get only loadable segments and computes min and max vaddr + to determine the total memory footprint the kernel needs. + */ + for (uint16_t i = 0; i < ehdr->e_phnum; i++) { + const auto& ph = phdrs[i]; + if (ph.p_type != PT_LOAD || ph.p_memsz == 0) continue; + if (ph.p_vaddr < vaddr_min) vaddr_min = ph.p_vaddr; + if (ph.p_vaddr + ph.p_memsz > vaddr_max) vaddr_max = ph.p_vaddr + ph.p_memsz; + } + + if (vaddr_min >= vaddr_max) { + result.error = ELF_ERR_NO_LOAD_SEGS; + return result; + } + + result.virt_base = vaddr_min; + result.total_size = vaddr_max - vaddr_min; + + // Allocate pages at the kernel's linked virtual address so that + // absolute references in the non-relocatable ELF resolve correctly. + const uint64_t num_pages = (result.total_size + ELF_PAGE_MASK) / ELF_PAGE_SIZE; + EFI_PHYSICAL_ADDRESS phys_addr = vaddr_min; + + EFI_STATUS status = bs->AllocatePages(AllocateAddress, EfiLoaderData, num_pages, &phys_addr); + if (EFI_ERROR(status)) { + result.error = ELF_ERR_ALLOC_FAILED; + result.efi_alloc_status = status; + return result; + } + + result.phys_base = phys_addr; + + // Zero the entire region — this covers .bss and any gaps between segments. + memset_local(reinterpret_cast<void*>(phys_addr), 0, num_pages * ELF_PAGE_SIZE); + + // Second pass: load PT_LOAD segments + // Iterates via PT_LOAD segments and copying each segment's file data (p_filesz) to the + // correct offset within the allocted region. + // Segments where p_memsz > p_filesz (e.g. .bss) are already zeroed above. + for (uint16_t i = 0; i < ehdr->e_phnum; i++) { + const auto& ph = phdrs[i]; + if (ph.p_type != PT_LOAD || ph.p_memsz == 0) continue; + + const uint64_t offset_in_image = ph.p_vaddr - vaddr_min; + auto* dest = reinterpret_cast<uint8_t*>(phys_addr + offset_in_image); + + if (ph.p_filesz > 0) + memcpy_local(dest, file_data + ph.p_offset, ph.p_filesz); + } + + result.success = true; + return result; +} diff --git a/boot/common/efi_loader.cpp b/boot/common/efi_loader.cpp new file mode 100644 index 0000000..da10b91 --- /dev/null +++ b/boot/common/efi_loader.cpp @@ -0,0 +1,423 @@ +// ============================================================================ +// efi_loader.cpp - Core UEFI loader logic (architecture-independent) +// +// Sequence: +// 1. Load kernel ELF from ESP +// 2. Parse ELF and load segments into memory +// 3. Get framebuffer via GOP +// 4. Find ACPI/FDT tables +// 5. Get final memory map + ExitBootServices +// 6. Populate BootInfo and return to arch entry for the final jump +// ============================================================================ + +#include <boot/efi.h> +#include <elf/elf.h> +#include <boot/boot_info.h> + +// TODO: move it to include +// Stringify helper for kernel file path (wide-string literal from config macro). +#define _WIDE(x) L##x +#define WIDE(x) _WIDE(x) + +// Parse file and allocate memory for kernel +extern ElfLoadResult elf_load(const uint8_t* data, uint64_t size, EFI_BOOT_SERVICES* bs); + + +namespace efi { + +static EFI_SYSTEM_TABLE* gST = nullptr; /* global System Table, has pointers to console, boot services, runtime services and other*/ +static EFI_BOOT_SERVICES* gBS = nullptr; /* global Boot Services, function table, that has memory allocation, protocol discovery, GetMemoryMap, ExitBootServices and other */ + +// Max memory-map entries +//TODO: fix problem with that +static constexpr uint64_t MAX_MEMORY_REGIONS = 256; + +// Low-level halt - safe to call even after ExitBootServices +[[noreturn]] static void panic_halt() noexcept { + for (;;) { +#if defined(EFI_ARCH_X86_64) + asm volatile("hlt"); +#elif defined(EFI_ARCH_AARCH64) + asm volatile("wfi"); +#endif + } +} + +// Console helpers +void print(const CHAR16* msg) { + gST->ConOut->OutputString(gST->ConOut, const_cast<CHAR16*>(msg)); +} + +void print_hex(uint64_t val) { + CHAR16 buf[19]; + buf[0] = u'0'; + buf[1] = u'x'; + for (int i = 15; i >= 0; i--) { + uint8_t nibble = (val >> (i * 4)) & 0xFu; + buf[17 - i] = nibble < 10 ? (u'0' + nibble) : (u'A' + nibble - 10); + } + buf[18] = 0; + print(buf); +} + +[[noreturn]] void panic(const CHAR16* msg) { + print(L"[PANIC] "); + print(msg); + print(L"\r\n"); + panic_halt(); +} + +// Type-safe protocol lookup helpers +// +// UEFI's HandleProtocol/LocateProtocol take void** which requires an explicit +// reinterpret_cast at every call site. These templates absorb the cast. +// TODO: move it to efi.hpp +template<typename T> +[[nodiscard]] static EFI_STATUS handle_protocol(EFI_HANDLE h, EFI_GUID& guid, T** out) { + return gBS->HandleProtocol(h, &guid, reinterpret_cast<void**>(out)); +} + +template<typename T> +[[nodiscard]] static EFI_STATUS locate_protocol(EFI_GUID& guid, T** out) { + return gBS->LocateProtocol(&guid, nullptr, reinterpret_cast<void**>(out)); +} + +// RAII wrapper for EFI_FILE_PROTOCOL +// +// Closes the underlying file handle when it goes out of scope. +// Non-copyable; move is not needed in the loader's simple linear flow. +// TODO: maybe move it efi.h +class ScopedFile { +public: + ScopedFile() = default; + ScopedFile(const ScopedFile&) = delete; + ScopedFile& operator=(const ScopedFile&) = delete; + + ~ScopedFile() { if (h) h->Close(h); } + + EFI_FILE_PROTOCOL* operator->() const noexcept { return h; } + explicit operator bool() const noexcept { return h != nullptr; } + + EFI_FILE_PROTOCOL*& get(){ + return h; + } +private: + EFI_FILE_PROTOCOL* h = nullptr; +}; + +// Load kernel file from ESP +// +// Returns a pointer to the raw ELF bytes allocated with AllocatePages. +// The caller (efi_loader_main) is responsible for keeping the pages alive +// until ExitBootServices is called (they are freed automatically thereafter +// because they are typed EfiLoaderData). +// image_handle: opaque pointer, that UEFI firmware passes to efi_main - it identifies loaded EFI application (our loader) +// out_size: size of the file +// return: pointer to file in allocated memory + +uint8_t* load_kernel_file(EFI_HANDLE image_handle, uint64_t* out_size) { + EFI_STATUS status; + + // Find the boot device via the loaded-image protocol. + EFI_GUID lip_guid = EFI_LOADED_IMAGE_PROTOCOL_GUID; + EFI_LOADED_IMAGE_PROTOCOL* loaded_image = nullptr; + status = handle_protocol(image_handle, lip_guid, &loaded_image); + if (EFI_ERROR(status)) + panic(L"Failed to get LoadedImageProtocol"); + + // Open the filesystem on that device. + EFI_GUID sfsp_guid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID; + EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* fs = nullptr; + status = handle_protocol(loaded_image->DeviceHandle, sfsp_guid, &fs); + if (EFI_ERROR(status)) + panic(L"Failed to get SimpleFileSystemProtocol"); + + // Open the ESP root directory. + ScopedFile root; + status = fs->OpenVolume(fs, &root.get()); + if (EFI_ERROR(status)) + panic(L"Failed to open ESP volume"); + + // Open kernel.elf. + ScopedFile kernel_file; + status = root->Open(root.get(), &kernel_file.get(), + const_cast<CHAR16*>(WIDE("\\" CONFIG_KERNEL_FILE_NAME)), + EFI_FILE_MODE_READ, 0); + if (EFI_ERROR(status)) + panic(L"Failed to open " WIDE("\\" CONFIG_KERNEL_FILE_NAME)); + + // Query file size via GetInfo. + EFI_GUID fi_guid = EFI_FILE_INFO_ID; + UINTN info_size = sizeof(EFI_FILE_INFO) + 256; // 256 bytes extra for filename + uint8_t info_buf[sizeof(EFI_FILE_INFO) + 256]; + status = kernel_file->GetInfo(kernel_file.get(), &fi_guid, &info_size, info_buf); + if (EFI_ERROR(status)) + panic(L"Failed to get kernel file info"); + + const auto* file_info = reinterpret_cast<const EFI_FILE_INFO*>(info_buf); + const uint64_t file_size = file_info->FileSize; + + // Allocate pages to hold the file. + // Round-up division to convert a byte size into a number of 4 KiB pages + const UINTN pages = (file_size + ELF_PAGE_MASK) / ELF_PAGE_SIZE; + EFI_PHYSICAL_ADDRESS addr = 0; + status = gBS->AllocatePages(AllocateAnyPages, EfiLoaderData, pages, &addr); + if (EFI_ERROR(status)) + panic(L"Failed to allocate memory for kernel"); + + // Read the file into the allocated buffer. + auto* file_data = reinterpret_cast<uint8_t*>(addr); + UINTN read_size = file_size; + status = kernel_file->Read(kernel_file.get(), &read_size, file_data); + if (EFI_ERROR(status)) + panic(L"Failed to read kernel file"); + + *out_size = file_size; + return file_data; +} + +// Locate GOP framebuffer +// +// Graphics Output Protocol: https://uefi.org/specs/UEFI/2.10/12_Protocols_Console_Support.html +// Returns false if GOP is unavailable (non-fatal — the kernel can run headless). + +bool get_framebuffer(FramebufferInfo* fb) { + EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; + EFI_GRAPHICS_OUTPUT_PROTOCOL* gop = nullptr; + EFI_STATUS status = locate_protocol(gop_guid, &gop); + if (EFI_ERROR(status) || !gop || !gop->Mode || !gop->Mode->Info) + return false; + + const auto* mode = gop->Mode; + const auto* info = mode->Info; + + fb->base = mode->FrameBufferBase; + fb->width = info->HorizontalResolution; + fb->height = info->VerticalResolution; + fb->pitch = info->PixelsPerScanLine * 4; + + switch (info->PixelFormat) { + case PixelRedGreenBlueReserved8BitPerColor: fb->format = PixelFormat::RGB; break; + case PixelBlueGreenRedReserved8BitPerColor: fb->format = PixelFormat::BGR; break; + default: fb->format = PixelFormat::Mask; break; + } + return true; +} + +// Search the UEFI Configuration Table +// +// The System Table has an array of {GUID, pointer} pairs published by various +// firmware subsystems. Returns the VendorTable pointer, or 0 if not found. + +uint64_t find_config_table(const EFI_GUID& target_guid) { + for (UINTN i = 0; i < gST->NumberOfTableEntries; i++) { + if (guid_equal(gST->ConfigurationTable[i].VendorGuid, target_guid)) + return reinterpret_cast<uint64_t>(gST->ConfigurationTable[i].VendorTable); + } + return 0; +} + +// Convert UEFI memory descriptor type to MemoryRegionType + +MemoryRegionType convert_memory_type(uint32_t efi_type) { + switch (efi_type) { + case EfiConventionalMemory: return MemoryRegionType::Usable; /* Free RAM - the kernel can allocate this */ + case EfiACPIReclaimMemory: return MemoryRegionType::AcpiReclaimable; /* ACPI tables - usable after the kernel is done parsing them */ + case EfiACPIMemoryNVS: return MemoryRegionType::AcpiNvs; /* ACPI Non_volatile Storage - must be preserved */ + case EfiBootServicesCode: + case EfiBootServicesData: + case EfiLoaderCode: + case EfiLoaderData: return MemoryRegionType::BootloaderReclaimable; /* Memory used by our EFI loader - the kernel can reclaim it later */ + default: return MemoryRegionType::Reserved; /* Hardware-reserved, MMIO registers, firmware and other */ + } +} + +// Pointer arithmetic helper for the packed memory descriptor array +// +// UEFI memory descriptors are desc_size bytes each (not sizeof(EFI_MEMORY_DESCRIPTOR) +// firmware may use a larger struct with extra fields at the end). + +[[nodiscard]] static const EFI_MEMORY_DESCRIPTOR* mem_desc_at(const void* map, UINTN index, UINTN desc_size) noexcept { + return reinterpret_cast<const EFI_MEMORY_DESCRIPTOR*>( + reinterpret_cast<uintptr_t>(map) + index * desc_size + ); +} + +} // namespace efi + +// ============================================================================ +// Main loader — called by arch-specific entry.cpp +// ============================================================================ + +bool efi_loader_main(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE* system_table, BootInfo* boot_info) { + efi::gST = system_table; + efi::gBS = system_table->BootServices; + + efi::gST->ConOut->ClearScreen(efi::gST->ConOut); + efi::print(L"Bastion UEFI Loader\r\n"); + efi::print(L"=====================\r\n\r\n"); + + // 1. Load kernel ELF from ESP. + efi::print(L"Loading " WIDE("\\" CONFIG_KERNEL_FILE_NAME) " from ESP...\r\n"); + uint64_t kernel_file_size = 0; + uint8_t* kernel_file = efi::load_kernel_file(image_handle, &kernel_file_size); + efi::print(L" Size: "); efi::print_hex(kernel_file_size); efi::print(L"\r\n"); + + // 2. Parse and load ELF segments into memory. + efi::print(L"Parsing ELF...\r\n"); + ElfLoadResult elf = elf_load(kernel_file, kernel_file_size, efi::gBS); + if (!elf.success) { + switch (elf.error) { + case ELF_ERR_FILE_TOO_SMALL: efi::panic(L"ELF: file too small"); + case ELF_ERR_INVALID_MAGIC: efi::panic(L"ELF: bad magic"); + case ELF_ERR_INVALID_CLASS: efi::panic(L"ELF: bad class (need ELF64)"); + case ELF_ERR_INVALID_IDENT: efi::panic(L"ELF: bad endianness (need LE)"); + case ELF_ERR_INVALID_ARCH: efi::panic(L"ELF: wrong machine type"); + case ELF_ERR_INVALID_TYPE: efi::panic(L"ELF: not an executable"); + case ELF_ERR_NO_LOAD_SEGS: efi::panic(L"ELF: no loadable segments"); + case ELF_ERR_ALLOC_FAILED: + efi::print(L"ELF: AllocatePages failed at "); efi::print_hex(elf.virt_base); + efi::print(L" EFI status: "); efi::print_hex(elf.efi_alloc_status); + efi::panic(L"\r\n"); + default: + efi::panic(L"ELF: unknown error"); + } + } + efi::print(L" Entry: "); efi::print_hex(elf.entry_point); efi::print(L"\r\n"); + efi::print(L" Phys: "); efi::print_hex(elf.phys_base); efi::print(L"\r\n"); + + // 3. Acquire framebuffer (non-fatal if absent). + efi::print(L"Locating GOP...\r\n"); + if (!efi::get_framebuffer(&boot_info->framebuffer)) { + efi::print(L" WARNING: No GOP framebuffer found\r\n"); + #pragma message ("maybe panic too?") + } else { + efi::print(L" FB at: "); efi::print_hex(boot_info->framebuffer.base); efi::print(L"\r\n"); + } + + // 4. Find firmware tables (ACPI RSDP or FDT depending on arch). +#ifdef CONFIG_ARCH_X86 + EFI_GUID acpi_guid = EFI_ACPI_20_TABLE_GUID; + boot_info->rsdp_address = efi::find_config_table(acpi_guid); +#endif +#ifdef CONFIG_ARCH_AARCH64 + EFI_GUID fdt_guid = EFI_DTB_TABLE_GUID; + boot_info->fdt_address = efi::find_config_table(fdt_guid); +#endif + + // 5. GetMemoryMap → ExitBootServices + /* + Memory map is array of entries + each entry is + - Base addr - where the region starts + - Length - size of region + - Type - look MemoryRegionType + For example, 256MiB Qemu may have around 120 entreis. + For now kernel prints all of them at boot, then sums up the Usable regions to report total free RAM + + map_size - buffer size on input; We pass in how big our buffer is. UEFI fills in how many bytes it actuall used. + map_key - opaque snapshot token - must match when calling ExitBootServices. + (If an allocation happens between GetMemoryMap and ExitBootServices, + the key goes stale and ExitBootServices returns EFI_INVALID_PARAMETER) + desc_size - actual size of each EFI_MEMORY_DESCRIPTOR entry in the returned map + (may be larger than sizeof(EFI_MEMORY_DESCRIPTOR) in newer + firmware — never assume the struct size). + */ + efi::print(L"Exiting boot services...\r\n"); + + UINTN map_size = 0, map_key = 0, desc_size = 0; + [[maybe_unused]] uint32_t desc_version = 0; //unused + EFI_MEMORY_DESCRIPTOR* efi_map = nullptr; + + // Get required size + // First call: probe the required buffer size. + efi::gBS->GetMemoryMap(&map_size, nullptr, &map_key, &desc_size, &desc_version); + // We add desc_size*4 because next allocation will change memory map, and 4 descriptors mush be enough + map_size += desc_size * 4; + efi::gBS->AllocatePool(EfiLoaderData, map_size, reinterpret_cast<void**>(&efi_map)); + + // Second call: fill the buffer. + EFI_STATUS status = efi::gBS->GetMemoryMap(&map_size, efi_map, &map_key, &desc_size, &desc_version); + if (EFI_ERROR(status)) + efi::panic(L"GetMemoryMap failed"); + + // Point of No Return + efi::print(L"EFI Point of No Return\r\n"); + status = efi::gBS->ExitBootServices(image_handle, map_key); + if (EFI_ERROR(status)) { + // Map changed between GetMemoryMap and ExitBootServices — retry once. + // Do NOT allocate between this GetMemoryMap and ExitBootServices. + efi::gBS->GetMemoryMap(&map_size, efi_map, &map_key, &desc_size, &desc_version); + status = efi::gBS->ExitBootServices(image_handle, map_key); + if (EFI_ERROR(status)) { + // EFI services are in an unknown state — use the low-level halt; + // calling efi::panic() (which uses ConOut) is unsafe here. + efi::panic_halt(); + } + } + + // ═══ NO MORE UEFI CALLS FROM THIS POINT ═══ + + // 6. Convert the UEFI memory map to our compact MemoryRegion format. + // + // Convert big UEFI-specific struct (with fields like catching flags, vaddreses, attr and other) + // to our small map (maybe in future i will use more fierds from UEFI map) + // Note: MAX_MEMORY_REGIONS (256) covers typical hardware. Systems with + // many RAM sticks, large MMIO holes, or complex firmware may exceed this. + // TODO: compute the required count from map_size/desc_size before capping. + // TODO: read how Linux solves it + + static MemoryRegion new_memory_map[efi::MAX_MEMORY_REGIONS]; + uint64_t new_memory_map_count = 0; + + const UINTN entry_count = map_size / desc_size; + for (UINTN i = 0; i < entry_count && new_memory_map_count < efi::MAX_MEMORY_REGIONS; i++, new_memory_map_count++) { + const auto* desc = efi::mem_desc_at(efi_map, i, desc_size); + new_memory_map[new_memory_map_count].base = desc->PhysicalStart; + new_memory_map[new_memory_map_count].length = desc->NumberOfPages * ELF_PAGE_SIZE; + new_memory_map[new_memory_map_count].type = efi::convert_memory_type(desc->Type); + } + + // Mark the kernel's physical pages as KernelAndModules so the + // PMM (Phase 3) does not hand them out as free RAM. + // The kernel was allocated with EfiLoaderData, so convert_memory_type() + // classified it as BootLoaderReclaimable - the PMM would free it in Phase 3. + { + const uint64_t k_base = elf.phys_base; + const uint64_t k_end = k_base + elf.total_size; + for (uint64_t i = 0; i < new_memory_map_count; ++i) { + const uint64_t region_end = new_memory_map[i].base + new_memory_map[i].length; + if (new_memory_map[i].base < k_end && region_end > k_base) + new_memory_map[i].type = MemoryRegionType::KernelAndModules; + } + } + + // Mark the framebuffer region so the PMM never hands it out as free RAM. + // GOP framebuffer memory isn't guaranteed a distinct EFI type (some firmware + // reports it as EfiConventionalMemory), so we fix it up manually here now + // that we know the framebuffer address and size. + if (boot_info->framebuffer.base != 0) { + const uint64_t fb_base = boot_info->framebuffer.base; + const uint64_t fb_size = static_cast<uint64_t>(boot_info->framebuffer.height) + * boot_info->framebuffer.pitch; + const uint64_t fb_end = fb_base + fb_size; + + for (uint64_t i = 0; i < new_memory_map_count; i++) { + const uint64_t region_end = new_memory_map[i].base + new_memory_map[i].length; + if (new_memory_map[i].base < fb_end && region_end > fb_base) + new_memory_map[i].type = MemoryRegionType::Framebuffer; + } + } + + // 7. Populate BootInfo for the kernel. + boot_info->magic = BOOT_INFO_MAGIC; + boot_info->memory_map = new_memory_map; + boot_info->memory_map_count = new_memory_map_count; + boot_info->kernel_phys_base = elf.phys_base; + boot_info->kernel_virt_base = elf.virt_base; + boot_info->kernel_size = elf.total_size; + boot_info->kernel_entry_point = elf.entry_point; + boot_info->hhdm_base = 0; + + return true; +} diff --git a/boot/x86_64/entry.cpp b/boot/x86_64/entry.cpp new file mode 100644 index 0000000..b9f710d --- /dev/null +++ b/boot/x86_64/entry.cpp @@ -0,0 +1,29 @@ +// ============================================================================ +// x86_64/entry.cpp - UEFI entry point for x86_64 +// +// Kernel entry: extern "C" [[noreturn]] void kernel_main(BootInfo*); +// ============================================================================ + +#include <boot/efi.h> +#include <boot/boot_info.h> + +extern bool efi_loader_main(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE* system_table, BootInfo* boot_info); + +static BootInfo g_boot_info{}; + +typedef void (*KernelEntry)(BootInfo*); + +extern "C" EFI_STATUS EFIAPI efi_main(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE* system_table) { + if (!efi_loader_main(image_handle, system_table, &g_boot_info)) { + return EFI_LOAD_ERROR; + } + + // Post-ExitBootServices: compute physical entry and jump + uint64_t entry_offset = g_boot_info.kernel_entry_point - g_boot_info.kernel_virt_base; + uint64_t phys_entry = g_boot_info.kernel_phys_base + entry_offset; + + auto kernel_entry = reinterpret_cast<KernelEntry>(phys_entry); + kernel_entry(&g_boot_info); // RDI = &g_boot_info (System V ABI) + + for (;;) asm volatile("hlt"); +} diff --git a/boot/x86_64/linker.ld b/boot/x86_64/linker.ld new file mode 100644 index 0000000..b439380 --- /dev/null +++ b/boot/x86_64/linker.ld @@ -0,0 +1,32 @@ +/* x86_64 UEFI Loader Linker Script — ELF -> objcopy -> PE32+ */ + +ENTRY(efi_main) + +SECTIONS +{ + . = 0; + + .text : { *(.text .text.*) } + + . = ALIGN(4096); + .rodata : { *(.rodata .rodata.*) } + + . = ALIGN(4096); + .data : { *(.data .data.*) *(.sdata .sdata.*) } + + . = ALIGN(4096); + .bss : { *(.bss .bss.*) *(COMMON) } + + /* + .reloc - empty PE base relocation sections. PE format requires this section to exist, even if empty. + The aarch64 target doesn't need it explicitly. X86 does. TODO: i'm not sure about this!!!! + */ + . = ALIGN(4096); + .dynamic : { *(.dynamic) } + .rela : { *(.rela .rela.*) } + .reloc : { } + + /DISCARD/ : { + *(.comment) *(.note.*) *(.eh_frame*) *(.gnu.hash) *(.hash) + } +} diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..334ca1e --- /dev/null +++ b/config.mk @@ -0,0 +1,70 @@ +# ============================================================================ +# config.mk - Shared Build Configuration +# ============================================================================ + +PROJECT_ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) + +CC := clang +CXX := clang++ +AS := clang +LD := ld.lld +OBJCOPY := llvm-objcopy +AR := llvm-ar + +ifeq ($(ARCH),x86_64) + TARGET_TRIPLE := x86_64-elf + EFI_ARCH := x86_64 + EFI_BINARY := BOOTX64.EFI + ARCH_CFLAGS := -target x86_64-unknown-elf \ + -mno-red-zone \ + -mno-sse -mno-sse2 -mno-avx \ + -mcmodel=kernel + QEMU := qemu-system-x86_64 +else ifeq ($(ARCH),aarch64) + TARGET_TRIPLE := aarch64-elf + EFI_ARCH := aarch64 + EFI_BINARY := BOOTAA64.EFI + ARCH_CFLAGS := -target aarch64-unknown-elf \ + -mgeneral-regs-only + QEMU := qemu-system-aarch64 +else + $(error ARCH must be x86_64 or aarch64) +endif + +COMMON_CXXFLAGS := \ + -std=c++23 \ + -ffreestanding \ + -fno-exceptions \ + -fno-rtti \ + -fno-stack-protector \ + -fno-threadsafe-statics \ + -fno-use-cxa-atexit \ + -fno-pic \ + -fno-pie \ + -nostdlib \ + -nostdinc++ \ + -Wall -Wextra -Wpedantic \ + -Werror=return-type \ + -O2 \ + -g \ + -include $(PROJECT_ROOT)/include/kernel/config.h + +COMMON_CFLAGS := \ + -std=c17 \ + -ffreestanding \ + -fno-stack-protector \ + -fno-pic \ + -fno-pie \ + -nostdlib \ + -Wall -Wextra \ + -O2 \ + -g + +COMMON_ASFLAGS := \ + -g \ + -include $(PROJECT_ROOT)/include/kernel/config.h + +COMMON_LDFLAGS := \ + -nostdlib \ + --no-dynamic-linker \ + -z noexecstack diff --git a/configs/default_aarch64 b/configs/default_aarch64 new file mode 100644 index 0000000..9835e87 --- /dev/null +++ b/configs/default_aarch64 @@ -0,0 +1,54 @@ +# ============================================================================ +# MyOS Kernel Configuration — AArch64 Default +# +# Copy to project root as .config and edit: +# cp configs/default_aarch64 .config +# vim .config +# make (configure.py runs automatically) +# ============================================================================ + +# ── Basic ─────────────────────────────────────────────────────────────────── +KERNEL_VERSION="v0.0.1" +KERNEL_FILE_NAME="kernel.elf" + +# ── Architecture ──────────────────────────────────────────────────────────── +# Set by the build system via ARCH= variable. Do not set here. +AARCH64=y + +# ── Debugging ─────────────────────────────────────────────────────────────── +DEBUG_SERIAL=y +DEBUG_VERBOSE_BOOT=y +DEBUG_PAGE_ALLOC=n +DEBUG_SCHEDULER=n +DEBUG_SYSCALL_TRACE=n + +# ── Memory ────────────────────────────────────────────────────────────────── +MAX_CPUS=4 +KERNEL_STACK_SIZE=16384 +PMM_TYPE=buddy +HEAP_TYPE=slab + +# ── Drivers ───────────────────────────────────────────────────────────────── +DRIVER_FBCON=y +DRIVER_PL011_UART=y +DRIVER_PS2KBD=n +DRIVER_VIRTIO_BLK=y +DRIVER_AHCI=n +DRIVER_USB=n +DRIVER_VIRTIO_NET=n + +# ── Filesystems ───────────────────────────────────────────────────────────── +FS_INITRAMFS=y +FS_TMPFS=y +FS_EXT2=n +FS_FAT32=n + +# ── Console ───────────────────────────────────────────────────────────────── +FBCON_FG_COLOR=0x00CCCCCC +FBCON_BG_COLOR=0x001A1A2E + +# ── Kernel features ──────────────────────────────────────────────────────── +FEATURE_SMP=n +FEATURE_MODULES=n +FEATURE_NETWORK=n +FEATURE_POSIX_SIGNALS=n diff --git a/configs/default_x86_64 b/configs/default_x86_64 new file mode 100644 index 0000000..9787046 --- /dev/null +++ b/configs/default_x86_64 @@ -0,0 +1,57 @@ +# ============================================================================ +# MyOS Kernel Configuration — x86_64 Default +# +# Copy to project root as .config and edit: +# cp configs/default_x86_64 .config +# vim .config +# make (configure.py runs automatically) +# +# Values: y = enabled, n = disabled, or a number/string +# Lines starting with # are comments +# ============================================================================ + +# ── Basic ─────────────────────────────────────────────────────────────────── +KERNEL_VERSION="v0.0.1" +KERNEL_FILE_NAME="kernel.elf" + +# ── Architecture ──────────────────────────────────────────────────────────── +# Set by the build system via ARCH= variable. Do not set here. +X86=y + +# ── Debugging ─────────────────────────────────────────────────────────────── +DEBUG_SERIAL=y +DEBUG_VERBOSE_BOOT=y +DEBUG_PAGE_ALLOC=n +DEBUG_SCHEDULER=n +DEBUG_SYSCALL_TRACE=n + +# ── Memory ────────────────────────────────────────────────────────────────── +MAX_CPUS=4 +KERNEL_STACK_SIZE=16384 +PMM_TYPE=buddy +HEAP_TYPE=slab + +# ── Drivers ───────────────────────────────────────────────────────────────── +DRIVER_FBCON=y +DRIVER_UART_16550=y +DRIVER_PS2KBD=y +DRIVER_VIRTIO_BLK=y +DRIVER_AHCI=n +DRIVER_USB=n +DRIVER_VIRTIO_NET=n + +# ── Filesystems ───────────────────────────────────────────────────────────── +FS_INITRAMFS=y +FS_TMPFS=y +FS_EXT2=n +FS_FAT32=n + +# ── Console ───────────────────────────────────────────────────────────────── +FBCON_FG_COLOR=0x00CCCCCC +FBCON_BG_COLOR=0x001A1A2E + +# ── Kernel features ──────────────────────────────────────────────────────── +FEATURE_SMP=n +FEATURE_MODULES=n +FEATURE_NETWORK=n +FEATURE_POSIX_SIGNALS=n diff --git a/doc/boot/image_format.md b/doc/boot/image_format.md new file mode 100644 index 0000000..a286811 --- /dev/null +++ b/doc/boot/image_format.md @@ -0,0 +1,32 @@ +# Final Image format + +The main binary is bastion.elf (in some cases it calls kernel.elf). +It has format of ELF64 + +## Why ELF64? +ELF is the basic executable format for unix-like, and this format is very easy for understandable. +Also elf is what Clang/GCC emit by default when targeting Linux or bare-metal. +And GDB/LLDB has great understanding of ELF format. + +## What format is Linux image +Linux has its own format for each architecture. +For example for x86: +bzImage layout: +┌──────────────────────┐ +│ Real-mode boot header│ "Boot protocol" setup header +│ (struct boot_params) │ at offset 0x1F1 +├──────────────────────┤ +│ PE/COFF header │ Grafted on if CONFIG_EFI_STUB=y +│ (optional) │ so UEFI can load it directly +├──────────────────────┤ +│ Compressed kernel │ The actual vmlinux (ELF), compressed +│ (gzip/lz4/zstd) │ with a small decompressor stub +├──────────────────────┤ +│ Decompressor code │ Runs in protected/long mode, +│ │ decompresses → real vmlinux +└──────────────────────┘ + +for aarch64, kernel Image is a flat binary with 64-byte header defined by the ARM64 boot protocol (with efi_stub config it also can have PE header). + + + diff --git a/doc/boot/kernel_address.md b/doc/boot/kernel_address.md new file mode 100644 index 0000000..c7449fc --- /dev/null +++ b/doc/boot/kernel_address.md @@ -0,0 +1,53 @@ +# Kernel address +Kernel address is place in RAM, where kernel is located. (for example uefi can place kernel to it) + + +## x86_64 +0x100000 (1MiB) +On x86, the first 1MiB of physical memory is a minefield of legacy hardware reservations dating back to the orignal IBM PC (1981): +0x00000000 - 0x000003FF IVT (Interrupt Vector Table) — real mode +0x00000400 - 0x000004FF BIOS Data Area +0x00000500 - 0x00007BFF Conventional memory (usable, but tiny) +0x00007C00 - 0x00007DFF Boot sector load address (BIOS legacy) +0x00007E00 - 0x0007FFFF More conventional memory +0x00080000 - 0x0009FFFF EBDA (Extended BIOS Data Area) +0x000A0000 - 0x000BFFFF VGA video memory (framebuffer) +0x000C0000 - 0x000C7FFF VGA BIOS ROM +0x000C8000 - 0x000EFFFF Other option ROMs +0x000F0000 - 0x000FFFFF System BIOS ROM +───────────────────────────────────────────────── +0x00100000 FIRST SAFE ADDRESS (1 MiB) + +0x00100000 is the first address above all this legacy garbage. It's whene Linux loads its kernel too ("high memory" region). +Because of using UEFI (and not current support of BIOS) i don't need all this fields. + +## AArch64 +0x40100000 +AArch64 has no legacy baggage - theere's no fixed memory map baked into the arch (i think it's one of the problem of this arch). +Instead of it each board/machine defiens where RAM starts. For QEMU's virt machine (that i use for first time: qemu-system-aarch64 -machine virt) RAM begins: +0x00000000 - 0x3FFFFFFF Device MMIO region (GIC, UART, virtio, flash, etc.) +───────────────────────────────────────────────── +0x40000000 RAM starts here on QEMU virt + +Technically i can succesfully use 0x40000000 as the first byte, but i need to add 1MiB offset for the UEFI firmware's own use (page tables, runtime services data, memory map and other). The UEFI firmware itself is loaded into early RAM and may have data structs there. + +On a real AArch64 hrdware RAM starts a completely different address (for exmaple at 0x0 on the Pi). That's why we need to read device tree blob or UEFI memory map to figure out where RAM actually is, rather than hardcoding. + + +## Future solutions + +Right now kernel is identity-mapped (virtual == phisical), so the linker script address must match where the kernel phiscyally lives. Once we implement higher-half mapping in next Developing Phase (Phase II), the picture must changes: + +Current (identity-mapped): + Linker says 0x100000, loaded at 0x100000, runs at 0x100000 + +After higher-half (Phase 2): + Linker says 0xFFFFFFFF80100000 (virtual, higher-half) + Loaded at 0x100000 (physical, by EFI loader) + Page tables map virtual → physical + Kernel runs at its virtual address + + +At that point, the physical load address becomes flexible - the ELF loader can use AllocateAnyPages and the page tables handle the translation. The linkes script address becomes the virtual address, and the two are decoupled. +Linux works exactly this way: vmlinux is linked at a high virtual address like 0xFFFFFFFF81000000, but it's physically loaded wherever the bootloader puts it. + diff --git a/doc/boot/legacy_boot.md b/doc/boot/legacy_boot.md new file mode 100644 index 0000000..abae9b4 --- /dev/null +++ b/doc/boot/legacy_boot.md @@ -0,0 +1,2 @@ +#Legacy Boot +Legacy boot now in not supported diff --git a/doc/boot/limine.md b/doc/boot/limine.md new file mode 100644 index 0000000..91d98df --- /dev/null +++ b/doc/boot/limine.md @@ -0,0 +1,2 @@ +## Limine +Limine bootleader now is not supported diff --git a/doc/boot/u-boot.md b/doc/boot/u-boot.md new file mode 100644 index 0000000..f919c38 --- /dev/null +++ b/doc/boot/u-boot.md @@ -0,0 +1,57 @@ +# U-boot +U-boot is a bootloader, not a firmware standard like UEFI. + +## U-boot boot methods +### U-Boot's own boot protocol +- Loads a flat binary or uImage +- Passes device tree in X0 (or R2 on 32-bit) +- No UEFI involved at all + +### U-Boot as UEFI firmware +- U-boot builded with CONFIG_EFI_LOADER +- Loads and runs 3efi PE32+ applications +- Bastion kernel should work perfectly + +### booti/bootm commands +- Loads Linux-format Image with header +- Passes device tree pointer + +### Modify u-boot to run bastion kernel +Worst case + +## Easiest method +is using u-boot as uefi loader +For it i need to build u-boot with CONFIG_EFI_LOADER. +U-boot provides the same EFI_SYSTEM_TABLE, EFI_BOOT_SERVICES, GOP, memory map and ExitBootServices() that real UEFI firmware does. +A lot of ARM Linux platforms use it - they run GRUB(grubaa64.efi) from u-boot and then run kernel. + +## Not-easiest method +Use u-boot's own boot proto +Here's how it works: +- The kernel is loaded as a flat bin (or a uImage wrapper) +- CPU is en EL2 or EL1, MMU off, caches off +- X0 = physical address of the device tree blof (FDT) +- No memory map - we need to parse dt's /memory node +- No framebuffer setup - we need to configure it from dt +- No ExitBootServices - u-boot is already gone when we get control + +### Kernel image format +U-boot expects same as Linux's Image format - a flat binary with 64-byte header: +``` +// ARM64 Image header (at offset 0 of the binary) +struct arm64_image_header { + uint32_t code0; // Executable code (branch instruction) + uint32_t code1; // Executable code + uint64_t text_offset; // Image load offset from start of RAM + uint64_t image_size; // Effective image size (0 = unknown) + uint64_t flags; // Kernel flags + uint64_t res2; // Reserved + uint64_t res3; // Reserved + uint64_t res4; // Reserved + uint32_t magic; // Magic: 0x644D5241 ("ARM\x64") + uint32_t res5; // Reserved (used for PE offset if EFI stub) +}; +``` + +# Current state +Bastion kernel supports only UEFI boot loading and not support own u-boot protos diff --git a/doc/boot/uefi.md b/doc/boot/uefi.md new file mode 100644 index 0000000..0b20299 --- /dev/null +++ b/doc/boot/uefi.md @@ -0,0 +1,144 @@ +# UEFI +UEFI is Unified Extensible Firmware Interface + + + +## How Linux is booting with uefi (without grub) +It has own built-in UEFI stub application +How it works (i hope): + +linux has EFI stub, that located in arch/x86/boot/compressed/efi_stub_entry.S and drivers/firmware/efi/libstub/* +when we build a kernel with CONFIG_EFI_STUB=y, the final vmlinuz is PE32+ executable (https://docs.kernel.org/admin-guide/efi-stub.html). +What this stub does before handing off to the real kernel: +1) Calls Graphics Output Protocol to set up a framebuffer +2) Retrieves the UEFI memory mpa via GetMemoryMap() +3) Locates the ACPI tables (RSDP) and passes them forward +4) Loads the initrd/initramfs into memory (via UEFI file protocol or passed by a bootloader like grub) +5) Calls ExitBootServices() - this is the point of no return where UEFI loses control of hardware +6) Jumps to real kernel entry point with all collected info in a boot_params structure (arch/x86/include/uapi/asm/bootparam.h) + + +## How Linux is booting with uefi with GRUB +Here bootloader are itself UEFI application (we can check it via looking at grubx64.efi on boot EFI partition). +Se the chain now looks like: UEFI firmware -> GRUB (EFI app) -> Linux EFI stub -> real kernel. + +BTW, we can boot linux directly by placing kernel as EFI/Boot/bootx64.efi on the EFI partition (it will run kernel, but without initram) + + + +## Core Problem +Because of bastion kernel is in ELF format, we can't simply run it as efi application. +So, because of it i decided to split booting into two files: +1) loader.efi - efi application, that do all efi staff and run kernel file +2) bastion.elf (or kernel.elf) - kernel itself + +## Boot chain + +1) UEFI firmware loads loader.efi + +2) GET EFI_SYSTEM_TABLE pointer (passed by UEFI firmware) + +3) Open the kernel ELF file from the EFI partition (Use EFI_SIMPLE_FILE_SYSTEM_PROTOCOL -> open volume -> open file) + +4) Read and parse ELF64 headers, validate arch + +5) Allocate pages for each PT_LOAD segment (Use BootServices->AllocatePages()) + +6) Load segments into allocated memory at their p_vaddr + +7) Locate the GOP (Graphic Output Protocol) (Get framebuffer base, resolution, pitch, pixel format) + +8) Locate ACPI tables (for x86_64) or Device Tree pointer (for arm) (Seach EFI Configuration Table for ACPI2_GUId or FDT_GUID) + +9) Get the final memory map via GetMemoryMap() (MUST be the last Boot Services call befofe exit) + +10) Call ExitBootServices(ImageHandle, MapKey) (it's point of no return - no more uefi calls) + +11) Set up initial page tables + +12) Jump to kernel entry point, passing a BootInfo struct (locates in boot_info.h) + + +## Key UEFI Protocols i use + +| Protocol | GUID | Purpose | +|---|---|---| +| `EFI_LOADED_IMAGE_PROTOCOL` | `5B1B31A1-...` | Get the device handle loader was loaded from | +| `EFI_SIMPLE_FILE_SYSTEM_PROTOCOL` | `964E5B22-...` | Open the ESP volume to read kernel file | +| `EFI_FILE_PROTOCOL` | — | Open/read/close files on the ESP | +| `EFI_GRAPHICS_OUTPUT_PROTOCOL` | `9042A9DE-...` | Get framebuffer address, set video mode | +| `EFI_BOOT_SERVICES` | — | `AllocatePages`, `GetMemoryMap`, `LocateProtocol`, `ExitBootServices` | + +--- + + + +## The Toolchain Problem + +EFI loader must be a **PE32+** binary. Two ways to handle this: + +**Option 1 — `gnu-efi`** — A lightweight library that provides EFI headers and a CRT0 that wraps `efi_main()`. Compile with GCC/Clang targeting ELF, then `objcopy` converts it to PE32+. This is the simplest approach. + +**Option 2 — Clang with MSVC target** — Clang can directly emit PE32+ if use `--target=x86_64-unknown-windows` with `-fno-stack-protector -fshort-wchar`. No conversion step, but i need to handle the entry point conventions manually. I don't like this optien + +**Option 1 with `gnu-efi` is more common** in the simple OS world and works for both architectures. + +### How Linux solves it +#### Approach 1: objcopy conrevsion (the old way) +This is what gnu-efi does and what Linux used to do. +``` +gcc -c -o stub.o stub.c # Compile as ELF + +ld -shared -o stub.so stub.o # Link as ELF shared object + +objcopy --target=elf-app-x86_64 stub.so stub.efi # Convert ELF to PE32+ +``` +But there is a problem: objcopy's PE support is fragile, relocations can break in subtle ways, and debugging is so painful because the format the debugger sees(PE) doesn't match watht the compiler emited (ELF). +#### Approach 2: PE header in assembly +look image_format.md for more information +in two words, Linux manually writes PE header into final Image (look arch/x86/boot/header.S) + +### ARM64 (case with EFI slub) +Linux arm64 header (arch/arm64/kernel/efi-header.S). +The first two bytes are 0x4D 0x5A. So UEFI sees this bytes, then reads it as PE, follows PE header to the entry point. ( + +### How I solve it (go via Option 2) +Clang by default supports Windows/PE targets, so i can simply do: +``` +# Compile directly as PE/COFF +clang++ --target=x86_64-unknown-windows -c -o entry.o entry.cpp + +# Link directly as PE32+ EFI application via lld-link (llvm-objcopy doesn't support efi-app-x86_64) +lld-link -subsystem:efi_application -entry:efi_main entry.o -out:BOOTX64.EFI +``` +### Future +In future i want to use hand-written PE (like in Linux) without clang dependency and without gnu-efi + + +## Dual-Arch Boot Strategy +The UEFI spec is architecturally neutral — the same protocols exist on x86_64 and aarch64. So loader code will be ~80% shared +boot/ +├── common/ +│ ├── efi_loader.cpp # Main logic: load ELF, get GOP, get mmap +│ ├── elf_parser.cpp # Parse ELF64 headers and segments +│ └── boot_info.h # Shared handoff structure +├── x86_64/ +│ ├── entry.cpp # efi_main() → call common loader → jump to kernel +│ └── linker.ld # PE32+ layout for x86_64 +└── aarch64/ + ├── entry.cpp # efi_main() → call common loader → jump to kernel + └── linker.ld # PE32+ layout for aarch64 + + + + +## ExitBootService staff + +The sequence must be: +1) Call GetMemoryMap() -- returns MapKey +2) Immediately call ExitBootService(ImageHandle, MapKey); +3) If it fails (returns EFI_INVALID_PARAMETR), the memory map changed between steps 1 and 2, so we must call GetMemoryMap() again (go to first step) +4) After success, we **can not** call any Boot Service - the firmware is gone + + + diff --git a/doc/build/final_bins.md b/doc/build/final_bins.md new file mode 100644 index 0000000..eb0c2ef --- /dev/null +++ b/doc/build/final_bins.md @@ -0,0 +1,9 @@ +# Final Binaries file + +## X86_64 +build/x86_64/boot/BOOTX64.EFI - PE32+ EFI application for x86_64 (size around 8KB) +build/x86_64/kernel/bastion.elf - ELF64, x86_64, entry 0x100000 (size around 41 KB at moment of writing this md) + +## AArch64 (EFI case) +build/aarch64/boot/BOOTAA64.EFI - PE32+ EFI application, AAarch64 (size around 7KB) +bulid/aarch64/kernel/bastion.elf - ELF64, AAarch64, entry 0x40100000 (size around 93 KB) diff --git a/doc/build/how_to_build.md b/doc/build/how_to_build.md new file mode 100644 index 0000000..f966309 --- /dev/null +++ b/doc/build/how_to_build.md @@ -0,0 +1,10 @@ +# How to build kernel + +## Dependencies: +sudo apt-get install clang lld llvm qemu-system-x86 ovmf mtools dosfstools gdisk + +## Build & run +make run-x86_64 + +## Only build +make all diff --git a/doc/develop/file_summary.md b/doc/develop/file_summary.md new file mode 100644 index 0000000..de7df15 --- /dev/null +++ b/doc/develop/file_summary.md @@ -0,0 +1,28 @@ +Makefile - Top-level Makefile, deligates to boot/ and kernel/ Makefiles with ARCH=x86_64 or ARCH=aarch64. Provides make all, make disk-aarch64, make run-x86_64 and other +config.mk - Shared toolchain config - defines Clang/LLD paths, per-arch target triples and flags, common c++20 freestanding flags (-fno-exception, -fno-rtti, -nostlib and other) + +include/boot_info.h - The loader <-> kernel boot_info struct. + +boot/Makefile - Builds .efi binary via ELF->PE32+ objcopy. Compiles c++ with --target=x86_64-unknown-windows (or aarch64), links with lld-link to produce a PE32+ .efi binary directly +boot/common/efi.h - Standalone UEFI types (without gnu-efi or EDK2 dependency). It defines EFI_SYSTEM_TABLE, EFI_BOOT_SERVICE, GOP, FIle Protocol, Loaded Image Protocol, GUIDs, status codes. +boot/common/elf.h - ELF64 format definitions. It defines Elf64_Ehdr, Elf64_Phdr, segment types(PT_LOAD), machine types(EM_X86_64, EM_AARCH64), and the ElfLoadResult struct. +boot/common/elf_parser.cpp - Reads ELF64 bin from memory, validates headers, calculates virtual address span, allocates phisycal pages via UEFI AllocatePages, copies PT_LOAD segments, zeroes BSS. Returns entry point and load addresses. +boot/common/efi_loader.cpp - Main boot logic: load ELF, GOP, memory map, ExitBootService. Opens the ESP filesystem, reads bastion.elf, calls the ELF parser, locates GOF framebuffer, finds ACPI/FDT config tables, does GetMemoryMap -> ExitBootServices, converts UEFI memory map to our format, populates BootInfo. +boot/x86_64/entry.cpp - efi_main() -> common loader -> jump to kernel. Calls efi_loader_main(), then computes the physical entry address from the ELF virtual entry and jumps to the kernel with BootInfo* in RDI register. +boot/x86_64/linker.ld - PE32+ layout for x86_64 loader. Linker script for this loader - section layout for text/rodata/data/bss. +boot/aarch64/entry.cpp - efi_main() -> common loader -> jump to kernel but for aarch64. Instead of RDI uses X0(AAPCS64) +boot/aarch64/linkel.ld - PE32+ layout for aarch64 loader + +kernel/Makefile - Builds kernel.elf - auto-discovers .cpp asd .S sources via wildcard, compiles as freestanding ELF, links with the arch-specifc linker script +kernel/arch/x86_64/entry.S - Assembly entry: set stack, call kernel_main. Sets up a 16KiB stack, calls ```kernel_main(BootInfo*)``` . Written in AT&T syntax for Clang's integrated assembler. +kernel/arch/x86_64/linker.ld - Places kernel at phiscal 0x100000 (1MiB). Defines .text, .rodata .data .bss sections with section boundary symbols (__bss_start, __kernel_end) +kernel/arch/aarch64/entry.S - Same for AArch64. Masks interrupts, sets stack from _stack_top, calls kernel_main. +kernel/arch/aarch64/linkel.ld - Same, but at 0x40100000 (QEMU virt machine convention) +kernel/core/kernel_main.cpp - First c++ code: init console, dump memory map, halt. Validate BootInfo magic, init the framebuffer console, prints a banner with arch name, dumps framebuffer info, firmware table addresses, memory map with region types and total usable RAM, then halts. +kernel/lib/kprint.cpp - Framebuffer console with 8x16 VGA bitmap font (ASCII 32-126). +kernel/lib/string.cpp - Freestanding memcpy/memset/strlet and other +kernel/lib/cxxabi.cpp - C++ ABI stubs(```__ctx_atexit``` and other). Has placeholder for new/delete operators. Needed because the compiler emits references to these symbols even in freestanding mode. +kernel/include/kernel/kprint.h - Console API header +kernel/include/kernel/types.h - PhysAddr, VirtAddr, aligment helpers +scripts/create_disk.sh - Creates 64 MiB GPT + FAT32 ESP disp image. Copies BOOTX64.efi to EFI/BOOT/ and bastion.elf to the root. Uses sgdisk + mkfs.vfat + mtools +scripts/run_qemu.sh - Finds OVMF/AAVMF firmware on the system and launches QEMU with the disk image, serial on stdio, interrupt logging enabled. diff --git a/doc/develop/overall.md b/doc/develop/overall.md new file mode 100644 index 0000000..0ea2508 --- /dev/null +++ b/doc/develop/overall.md @@ -0,0 +1,123 @@ +# Overall stages of developen BastionOS kernel + +## I Phase +UEFI boot + +My main task here is create bootable efi application. + + +First kernel booting stage is running PE32+ efi binary. It calls UEFI Boot Services to get the memory map, framebuffer(GOP) and ACPI/device_tree_pointer (for arm64 if it will supports dts). Then it loads my ELF kernel into memory and jumps to it after calling ExitBootServices(). +GNU-EFI (https://github.com/ncroxon/gnu-efi.git) should help me somehow to do it. + + +So, what we need to have after uefi: +1) Physical memory map (which regions are usable) +2) Framebuffer address and pitch (for console) +3) RSDP pointer (for ACPI table parsing) +4) Device Tree pointer (for arm) +5) Kernel's own physical/virtual address + +## II Phase +Arch-Specific CPU setup + +# For x86_64 +- Load a GDT (minimal: null, kernel code64, kernel data, user code64, user data, TSS) +- Set up IDT - 256 entries, wire ISR stubs in assembly, that push error codes uniformly, then call dispatch_interrupt(InterruptFrame&) handler +- Configure paging: PML4 page table hierarchy, higher-half kernel mapping(canonical address like 0xFFFF800000000000+), recursive or direct-map strategy for page table self-reference +- Enable and configure the local APIC + I/O APIC (from MADT ACPI table), replace the legacy PIC + +# For AArch64: +- Set up exception vectors(VBAR_EL1) - 4 exception types x 4 source levels = 16 vectors +- Configure the MMU: TCR_EL1, MAIR_EL1, TTBR0_EL1/ TTBR1_EL1 (user/kernel split), 4-level page tables (4KB granule, 48-bit VA) +- Set up the GIC(Generic Interrupt Controller) v2 or v3 from device tree info + + +Because of using c++ as main language i can create abstraction for it: +``` +namespace arch { + void init_interrupts(); + void enable_interrupts(); + void disable_interrupts(); + void set_page_table(PhysAddr root); + void invalidate_page(VirtAddr addr); + [[noreturn]] void halt(); +} +``` + + +## III Phase + +### PMM - Physical Memory Manager +- Parse the boot memory map (that we did in I phase), build a buddy allocator or bitmap allocator over free regions +- Track allocation in page-sized (4KiB) granules +- Provide alloc_page() / free_page functions + +### VMM - Virtual Memory Manager +- Implement VirtualAddressSpace object, that wraps a page table root +- Operations map(VirtAddr, PhysAddr, flags), unmap(VirtAddr), translate(VirtAddr) -> PhysAddr +- Kernel its own address space; each process will get one later +- Both archs use 4-level tables with similar structure - abstract the entry format + +### Kernel Heap +- Implement a slab allocator or a simple kmalloc/kfree on tho of the VMM +- Overload global operator new/delete to use it - this unlocks C++ STL + + +## IV Phase +### Timer +- x86_64: APIC Timer (calibrated against HPET or PIT) or TSC deadline mode +- AArch64: Generic Timer (CNTPCT_EL0, CNTP_TVAL_EL0) + +### Scheduler +- At begining, i want to use simple round-robit with a reade one queue +- Each task has: a kernel stack, saved register context, an address space +- Context switch is arch-specific assembly: save/restore registers + swap stack pointer + swap page table root (the best arch for context switching is still riscV with only one simple command, x86 will be very hard(considering Linux code), but if i will not use hash it can be easy and understandable) +- Preemption via timer interrupt + +## V Phase +### ELF Parser +- Parse ELF64 header, validate e_ident magic, check EM_X86_64 or EM_AARCH64 +- Iterate program headers(PT_LOAD segments), map them into the process address space at their p_vaddr with correct permissions (rwx from p_flags) +- Set entry point from e_entry + +### Userspace transition +- Allocate a user stack, set up the initial stack frame (argc, argv, envp, auxv) +- x86_64: sysretq or iretq to ring3 +- aarch64: eret to EL0 + +### SysCall +- x86_64: syscall/sysret via MSRr(LSTAR, STAR, SFMASK) +- aarch64: svc instruction, handled in the EL1 syncronous exception vector +- Define a syscall table - start with basic write(), read(), exit(), mmap(), fork()/spawn()/clone() + + +## VI Phase +### Essential drivers +- UART/Serial +- Framebuffer console +- USB keyboard (or PS/2 for qemu testing) +- Virtio-blk (block device in QEMU - much simpler than AHCI/NVMe) + +### Filesystem +- Implement a VFS layer (struct Inode, struct File, open()/read()/write()/close()) +- Start with in-memory initramfs (USTAR or CPIO) baked into the boot image +- Later: ext2 read support (very simple) +- Later: normal ext4 +- Sometime: fat +- Never: ntfs + +## VII Phase +## dynamic linking and shared libs +## porting full libc (or mlibc, that designed for hobby kernels) +## do full POSIX support + +## VIII Phase +### Network (virtio-net + tcp/ip stack) + +## IX Phase +### multicore/SMP + +## X Phase +## Window drawing + + diff --git a/doc/develop/study_resources.md b/doc/develop/study_resources.md new file mode 100644 index 0000000..251f06a --- /dev/null +++ b/doc/develop/study_resources.md @@ -0,0 +1,15 @@ +# Sites +- OSDev Wiki (wiki.osdev.org) +- uefi.org + +# Books +- Tannebaum +- Operating Systems: Three Easy Pieces + +# Repos +- managarm +- LemonOS +- Limine Boot Loader + +# Specifications +- ARM Architecture Reference Manual diff --git a/doc/file_structure.md b/doc/file_structure.md new file mode 100644 index 0000000..0ece193 --- /dev/null +++ b/doc/file_structure.md @@ -0,0 +1,16 @@ +# General Structure of project + +/ +|--- arch/ + |--- x86_64/ # GDT, IDT, paging, APIC, arch-specific boot (for more information look at doc/arch/x86_64.md) + |--- aarch64/ # MMU setup, GIC, exception vectors and other (fore more information look at doc/arch/aarch64.md) +|--- core/ # Scheduler, IPC, syscalls, processes +|--- mm/ # VMM, PMM, head and stack +|--- fs/ # VFS, initramfs, interfaces for fs drivers +|--- drivers/ # Framebuffer, UART, block devices, fs, graphic, network +|--- loader/ # ELF parser and process loader +|--- lib/ # kprint, string ops, main data structures +|--- boot/ # UEFI application (and maybe adding something like Limine support and legacy support) +|--- include/ # header files +|--- scripts/ # Scripts +|--- Makefile # Main build file diff --git a/include/boot/boot_info.h b/include/boot/boot_info.h new file mode 100644 index 0000000..cc02ae8 --- /dev/null +++ b/include/boot/boot_info.h @@ -0,0 +1,76 @@ +#pragma once +// ============================================================================ +// boot_info.h - Boot handoff struct +// Shared between the UEFI loader and the kernel. +// The loader fills it, the kernel consumes it. Simple. +// In future i will expand this struct +// ============================================================================ + +#include <stdint.h> + +// ── Boot Magic Number─────────────────────────────────────────────────────── +inline constexpr uint64_t BOOT_INFO_MAGIC = 0xDEADDEADDEADDEADULL; + +// ── Framebuffer (GOP) ─────────────────────────────────────────────────────── +// TODO: remove Framebuffer to driver +enum class PixelFormat : uint32_t { + RGB = 0, // PixelRedGreenBlueReserved8BitPerColor + BGR = 1, // PixelBlueGreenRedReserved8BitPerColor + Mask = 2, // PixelBitMask — need to check masks +}; + +struct FramebufferInfo { + uint64_t base; // Physical address of framebuffer + uint32_t width; // Horizontal resolution in pixels + uint32_t height; // Vertical resolution in pixels + uint32_t pitch; // Bytes per scanline (>= width * 4) + PixelFormat format; +}; + +// ── Memory map ───────────────────────────────────────────────────────────── +//TODO: remove FrameBuffer +enum class MemoryRegionType : uint32_t { + Usable = 0, // Free RAM - kernel can use + Reserved = 1, // Firmware/hardware reserved + AcpiReclaimable = 2, // ACPI tables - free after parsing + AcpiNvs = 3, // ACPI non-volatile storage + BootloaderReclaimable = 4, // Loader code/data - free after kernel init + KernelAndModules = 5, // Kernel image + any loaded modules + Framebuffer = 6, // Framebuffer memory - do not use as RAM +}; + +struct MemoryRegion { + uint64_t base; + uint64_t length; + MemoryRegionType type; + uint32_t _reserved; // Padding to 24 bytes +}; + +// ── Boot info structure ──────────────────────────────────────────────────── + +struct BootInfo { + uint64_t magic; // Must be BOOT_INFO_MAGIC + + // Framebuffer + // Todo: remove + FramebufferInfo framebuffer; + + // Memory map (array of MemoryRegion) + MemoryRegion* memory_map; + uint64_t memory_map_count; + + // Platform-specific firmware tables (both always present to keep layout uniform) + uint64_t rsdp_address; // ACPI RSDP (x86_64), 0 if absent + uint64_t fdt_address; // Flattened Device Tree (aarch64), 0 if absent + + // Kernel load info + uint64_t kernel_phys_base; // Where the kernel was loaded physically + uint64_t kernel_virt_base; // Kernel's virtual base (from ELF) + uint64_t kernel_size; // Total size of kernel in memory + + // Kernel entry point (virtual address from ELF e_entry) + uint64_t kernel_entry_point; + + // Higher-half direct map base (if set up by loader) + uint64_t hhdm_base; // e.g., 0xFFFF800000000000 +}; diff --git a/include/boot/efi.h b/include/boot/efi.h new file mode 100644 index 0000000..bcb8854 --- /dev/null +++ b/include/boot/efi.h @@ -0,0 +1,365 @@ +#pragma once +// ============================================================================ +// efi.h - Minimal UEFI type and protocol definitions +// +// Only what we need for our boot loader. No external dependencies (like gnu-efi). +// Reference: UEFI Specification 2.10, https://uefi.org/specs/UEFI/2.10/ +// I generate it with Cloude, because i don't want to manually rewrite whole spec +// ============================================================================ + +#include <stdint.h> +#include <stddef.h> + +// ── Base types ───────────────────────────────────────────────────────────── + +typedef uint64_t UINTN; +typedef int64_t INTN; +typedef uint64_t EFI_STATUS; +typedef void* EFI_HANDLE; +typedef void* EFI_EVENT; +typedef uint64_t EFI_PHYSICAL_ADDRESS; +typedef uint64_t EFI_VIRTUAL_ADDRESS; +typedef wchar_t CHAR16; // With -fshort-wchar, wchar_t is 16-bit +typedef uint8_t BOOLEAN; + +#define TRUE 1 +#define FALSE 0 +#define IN +#define OUT +#define OPTIONAL +#define EFIAPI + +// ── Status codes ─────────────────────────────────────────────────────────── + +#define EFI_SUCCESS 0ULL +#define EFI_ERROR_BIT (1ULL << 63) +#define EFI_LOAD_ERROR (EFI_ERROR_BIT | 1) +#define EFI_INVALID_PARAMETER (EFI_ERROR_BIT | 2) +#define EFI_UNSUPPORTED (EFI_ERROR_BIT | 3) +#define EFI_BAD_BUFFER_SIZE (EFI_ERROR_BIT | 4) +#define EFI_BUFFER_TOO_SMALL (EFI_ERROR_BIT | 5) +#define EFI_NOT_FOUND (EFI_ERROR_BIT | 14) + +#define EFI_ERROR(status) ((status) & EFI_ERROR_BIT) + +// ── GUIDs ────────────────────────────────────────────────────────────────── + +struct EFI_GUID { + uint32_t Data1; + uint16_t Data2; + uint16_t Data3; + uint8_t Data4[8]; +}; + +#define EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID \ + { 0x9042A9DE, 0x23DC, 0x4A38, { 0x96, 0xFB, 0x7A, 0xDE, 0xD0, 0x80, 0x51, 0x6A } } + +#define EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID \ + { 0x964E5B22, 0x6459, 0x11D2, { 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B } } + +#define EFI_LOADED_IMAGE_PROTOCOL_GUID \ + { 0x5B1B31A1, 0x9562, 0x11D2, { 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B } } + +#define EFI_FILE_INFO_ID \ + { 0x09576E92, 0x6D3F, 0x11D2, { 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B } } + +#define EFI_ACPI_20_TABLE_GUID \ + { 0x8868E871, 0xE4F1, 0x11D3, { 0xBC, 0x22, 0x00, 0x80, 0xC7, 0x3C, 0x88, 0x81 } } + +#define EFI_DTB_TABLE_GUID \ + { 0xB1B621D5, 0xF19C, 0x41A5, { 0x83, 0x0B, 0xD9, 0x15, 0x2C, 0x69, 0xAA, 0xE0 } } + +// ── Memory types ─────────────────────────────────────────────────────────── + +typedef uint32_t EFI_MEMORY_TYPE; +#define EfiReservedMemoryType 0 +#define EfiLoaderCode 1 +#define EfiLoaderData 2 +#define EfiBootServicesCode 3 +#define EfiBootServicesData 4 +#define EfiRuntimeServicesCode 5 +#define EfiRuntimeServicesData 6 +#define EfiConventionalMemory 7 +#define EfiUnusableMemory 8 +#define EfiACPIReclaimMemory 9 +#define EfiACPIMemoryNVS 10 +#define EfiMemoryMappedIO 11 +#define EfiMemoryMappedIOPortSpace 12 +#define EfiPalCode 13 +#define EfiPersistentMemory 14 +#define EfiMaxMemoryType 15 + +typedef uint32_t EFI_ALLOCATE_TYPE; +#define AllocateAnyPages 0 +#define AllocateMaxAddress 1 +#define AllocateAddress 2 + +struct EFI_MEMORY_DESCRIPTOR { + uint32_t Type; + EFI_PHYSICAL_ADDRESS PhysicalStart; + EFI_VIRTUAL_ADDRESS VirtualStart; + uint64_t NumberOfPages; + uint64_t Attribute; +}; + +// ── Simple Text Output Protocol ──────────────────────────────────────────── + +struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL { + void* Reset; + EFI_STATUS (EFIAPI *OutputString)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* This, + CHAR16* String + ); + void* TestString; + void* QueryMode; + void* SetMode; + void* SetAttribute; + EFI_STATUS (EFIAPI *ClearScreen)( + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* This + ); + void* SetCursorPosition; + void* EnableCursor; + void* Mode; +}; + +// ── Graphics Output Protocol ─────────────────────────────────────────────── + +typedef uint32_t EFI_GRAPHICS_PIXEL_FORMAT; +#define PixelRedGreenBlueReserved8BitPerColor 0 +#define PixelBlueGreenRedReserved8BitPerColor 1 +#define PixelBitMask 2 +#define PixelBltOnly 3 + +struct EFI_PIXEL_BITMASK { + uint32_t RedMask; + uint32_t GreenMask; + uint32_t BlueMask; + uint32_t ReservedMask; +}; + +struct EFI_GRAPHICS_OUTPUT_MODE_INFORMATION { + uint32_t Version; + uint32_t HorizontalResolution; + uint32_t VerticalResolution; + EFI_GRAPHICS_PIXEL_FORMAT PixelFormat; + EFI_PIXEL_BITMASK PixelInformation; + uint32_t PixelsPerScanLine; +}; + +struct EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE { + uint32_t MaxMode; + uint32_t Mode; + EFI_GRAPHICS_OUTPUT_MODE_INFORMATION* Info; + UINTN SizeOfInfo; + EFI_PHYSICAL_ADDRESS FrameBufferBase; + UINTN FrameBufferSize; +}; + +struct EFI_GRAPHICS_OUTPUT_PROTOCOL { + void* QueryMode; + void* SetMode; + void* Blt; + EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE* Mode; +}; + +// ── File Protocol ────────────────────────────────────────────────────────── + +#define EFI_FILE_MODE_READ 0x0000000000000001ULL +#define EFI_FILE_READ_ONLY 0x0000000000000001ULL + +struct EFI_FILE_PROTOCOL { + uint64_t Revision; + EFI_STATUS (EFIAPI *Open)( + EFI_FILE_PROTOCOL* This, + EFI_FILE_PROTOCOL** NewHandle, + CHAR16* FileName, + uint64_t OpenMode, + uint64_t Attributes + ); + EFI_STATUS (EFIAPI *Close)( + EFI_FILE_PROTOCOL* This + ); + void* Delete; + EFI_STATUS (EFIAPI *Read)( + EFI_FILE_PROTOCOL* This, + UINTN* BufferSize, + void* Buffer + ); + void* Write; + void* GetPosition; + void* SetPosition; + EFI_STATUS (EFIAPI *GetInfo)( + EFI_FILE_PROTOCOL* This, + EFI_GUID* InformationType, + UINTN* BufferSize, + void* Buffer + ); +}; + +struct EFI_FILE_INFO { + uint64_t Size; + uint64_t FileSize; + uint64_t PhysicalSize; + uint8_t _time_padding[48]; // Skip EFI_TIME fields + uint64_t Attribute; + CHAR16 FileName[1]; // Variable length +}; + +// ── Simple File System Protocol ──────────────────────────────────────────── + +struct EFI_SIMPLE_FILE_SYSTEM_PROTOCOL { + uint64_t Revision; + EFI_STATUS (EFIAPI *OpenVolume)( + EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* This, + EFI_FILE_PROTOCOL** Root + ); +}; + +// ── Loaded Image Protocol ────────────────────────────────────────────────── + +struct EFI_LOADED_IMAGE_PROTOCOL { + uint32_t Revision; + EFI_HANDLE ParentHandle; + void* SystemTable; + EFI_HANDLE DeviceHandle; + void* FilePath; + void* Reserved; + uint32_t LoadOptionsSize; + void* LoadOptions; + void* ImageBase; + uint64_t ImageSize; + EFI_MEMORY_TYPE ImageCodeType; + EFI_MEMORY_TYPE ImageDataType; + void* Unload; +}; + +// ── Boot Services ────────────────────────────────────────────────────────── + +struct EFI_BOOT_SERVICES { + char _hdr[24]; + + // Task Priority (2) + void* RaiseTPL; + void* RestoreTPL; + + // Memory Services + EFI_STATUS (EFIAPI *AllocatePages)( + EFI_ALLOCATE_TYPE Type, + EFI_MEMORY_TYPE MemoryType, + UINTN Pages, + EFI_PHYSICAL_ADDRESS* Memory + ); + EFI_STATUS (EFIAPI *FreePages)( + EFI_PHYSICAL_ADDRESS Memory, + UINTN Pages + ); + EFI_STATUS (EFIAPI *GetMemoryMap)( + UINTN* MemoryMapSize, + EFI_MEMORY_DESCRIPTOR* MemoryMap, + UINTN* MapKey, + UINTN* DescriptorSize, + uint32_t* DescriptorVersion + ); + EFI_STATUS (EFIAPI *AllocatePool)( + EFI_MEMORY_TYPE PoolType, + UINTN Size, + void** Buffer + ); + EFI_STATUS (EFIAPI *FreePool)( + void* Buffer + ); + + // Event & Timer (6) + void* CreateEvent; + void* SetTimer; + void* WaitForEvent; + void* SignalEvent; + void* CloseEvent; + void* CheckEvent; + + // Protocol Handler (6 + 3) + void* InstallProtocolInterface; + void* ReinstallProtocolInterface; + void* UninstallProtocolInterface; + EFI_STATUS (EFIAPI *HandleProtocol)( + EFI_HANDLE Handle, + EFI_GUID* Protocol, + void** Interface + ); + void* Reserved; + void* RegisterProtocolNotify; + void* LocateHandle; + void* LocateDevicePath; + void* InstallConfigurationTable; + + // Image Services (5) + void* LoadImage; + void* StartImage; + void* Exit; + void* UnloadImage; + EFI_STATUS (EFIAPI *ExitBootServices)( + EFI_HANDLE ImageHandle, + UINTN MapKey + ); + + // Misc (3) + void* GetNextMonotonicCount; + void* Stall; + void* SetWatchdogTimer; + + // DriverSupport (2) + void* ConnectController; + void* DisconnectController; + + // Open/Close Protocol (3) + void* OpenProtocol; + void* CloseProtocol; + void* OpenProtocolInformation; + + // Library (3) + void* ProtocolsPerHandle; + void* LocateHandleBuffer; + EFI_STATUS (EFIAPI *LocateProtocol)( + EFI_GUID* Protocol, + void* Registration, + void** Interface + ); +}; + +// ── Configuration Table ──────────────────────────────────────────────────── + +struct EFI_CONFIGURATION_TABLE { + EFI_GUID VendorGuid; + void* VendorTable; +}; + +// ── System Table ─────────────────────────────────────────────────────────── + +struct EFI_SYSTEM_TABLE { + char _hdr[24]; + + CHAR16* FirmwareVendor; + uint32_t FirmwareRevision; + uint32_t _pad; + + EFI_HANDLE ConsoleInHandle; + void* ConIn; + EFI_HANDLE ConsoleOutHandle; + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* ConOut; + EFI_HANDLE StandardErrorHandle; + void* StdErr; + void* RuntimeServices; + EFI_BOOT_SERVICES* BootServices; + UINTN NumberOfTableEntries; + EFI_CONFIGURATION_TABLE* ConfigurationTable; +}; + +// ── Utility ──────────────────────────────────────────────────────────────── + +inline bool guid_equal(const EFI_GUID& a, const EFI_GUID& b) { + return a.Data1 == b.Data1 && a.Data2 == b.Data2 && a.Data3 == b.Data3 && + a.Data4[0] == b.Data4[0] && a.Data4[1] == b.Data4[1] && + a.Data4[2] == b.Data4[2] && a.Data4[3] == b.Data4[3] && + a.Data4[4] == b.Data4[4] && a.Data4[5] == b.Data4[5] && + a.Data4[6] == b.Data4[6] && a.Data4[7] == b.Data4[7]; +} diff --git a/include/elf/elf.h b/include/elf/elf.h new file mode 100644 index 0000000..512ba14 --- /dev/null +++ b/include/elf/elf.h @@ -0,0 +1,331 @@ +#pragma once + +// ============================================================================ +// elf.h - ELF format definitions +// https://refspecs.linuxbase.org/elf/elf.pdf +// https://gist.github.com/x0nu11byt3/bcb35c3de461e5fb66173071a2379779 +// https://docs.oracle.com/cd/E19683-01/816-1386/6m7qcoblh/index.html +// ============================================================================ + +#include <stdint.h> + +/* ELF Types */ +#pragma message("TODO: describe ELF types from specification") + +// ============================================================================ +// =================================== ELF Header ============================= +// ============================================================================ +#define EI_NIDENT (16) + +/* e_ident offset (https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#ELF_header) */ +#define EI_MAG0 0 +#define EI_MAG1 1 +#define EI_MAG2 2 +#define EI_MAG3 3 +#define EI_CLASS 4 +#define EI_DATA 5 +#define EI_VERSION 6 +#define EI_OSABI 7 +#define EI_ABIVERSION 8 +#define EI_PAD 9 + +/* ELF Magic value */ +#define ELF_MAGIC 0x464C457F // "\x7FELF" little-endian + +/* ELF Class */ +#define ELFCLASS32 0x1 +#define ELFCLASS64 0x2 + +/* Data format: or LSB(Least Significant Bit, little-endian) or MSB(Most Significant Bit, big-endian) */ +#define ELFDATA2LSB 0x1 +#define ELFDATA2MSB 0x2 + +/* ELF version */ +#define ELFVERSION 0x1 + +/* Target ABI */ +#define ELFABISYSTEMV 0x00 +#define ELFABIHPUX 0x01 +#define ELFABINETBSD 0x02 +#define ELFABILINUX 0x03 +#define ELFABIGNUHURD 0x04 +#define ELFABISOLARIS 0x06 +#define ELFABIAIX 0x07 +#define ELFABIIRIX 0x08 +#define ELFABIFREEBSD 0x09 +#define ELFABITRU64 0x0A +#define ELFABINM 0x0B +#define ELFABIOPENBSD 0x0C +#define ELFABIOPENVMS 0x0D +#define ELFABINSKERNEL 0x0E +#define ELFABIAROS 0x0F +#define ELFABIFENIXOS 0x10 +#define ELFABICLOUDABI 0x11 +#define ELFABISTOPENVOS 0x12 + +/* ABI version */ +#pragma message("TODO: do something with ABI version") +/* Currently i don't use ABI version */ + +/* Object File Types, e_type field */ +#define ET_NONE 0x0 /* Unknown */ +#define ET_REL 0x1 /* Relocatable. (output of gcc -c). Contains sections + symbol tables + relocations. No program header */ +#define ET_EXEC 0x2 /* Executable. Statically-linked executable with fixed virtual addresses. Has program headers. */ +#define ET_DYN 0x3 /* Shared object. Shared library or PIE executable. Postitoin-independent, relocated at load time. */ +#define ET_CORE 0x4 /* Core dump. Process memory image for post-mortem debugging */ +#define ET_LOOS 0xFE00 /* Reserved inclusive range. Operating system specific */ +#define ET_HIOS 0xFEFF /* Reserved inclusive range. Operating system specific */ +#define ET_LOPROC 0xFF00 /* Reserved inclusive range. Processor specific */ +#define ET_HIPROC 0xFFFF /* Reserved inclusive range. Processor specific */ + +/* Arch types, e_machine field */ +#pragma message("TODO: add other architecture defines, copy from github") +#define EM_X86 0x03 +#define EM_X86_64 0x3E +#define EM_ARM 0x28 /* up to Armv7/AArch32 */ +#define EM_AARCH64 0xB7 /* Armv8/AArch64 */ + +/* Version value, e_version field */ +#define EV_NONE 0x00 +#define EV_CURRENT 0x01 +#define EV_NUM 0x02 + +/* Other header fields: + e_entry: Memory address of the entry point. + e_phoff: Pointer to the start of the program header table. + e_shoff: Pointer to the start of section header table. + e_flags: Interpretation of this field depends on the target arch. + e_ehsize: Contains the size of header (normally 64 bytes for 64-bit and 52 Bytes for 32-bit) + e_phentsize: Contains the size of program header table entry. Or 0x20 (for 32-bit) or 0x38 (for 64-bit). + e_phnum: Contains the number of entries in the program header table. + e_shentsize: Contains the size of a section header table entry. Or 0x28 (for 32-bit) or 0x40 (for 64-bit) + e_shnum: Contains the number of entries in the section header table. + e_shstrndx: Contains index of the section header table entry, that contains the section names. +*/ + +/* ELF header (atm only for 64 bit) */ +#pragma message("TODO: add elf header for 32 bit") +struct Elf64_Ehdr { + uint8_t e_ident[EI_NIDENT]; + uint16_t e_type; + uint16_t e_machine; + uint32_t e_version; + uint64_t e_entry; + uint64_t e_phoff; + uint64_t e_shoff; + uint32_t e_flags; + uint16_t e_ehsize; + uint16_t e_phentsize; + uint16_t e_phnum; + uint16_t e_shentsize; + uint16_t e_shnum; + uint16_t e_shstrndx; +}; + +// ============================================================================ +// =================================== Program Header ========================= +// ============================================================================ + +/* Type, p_type field */ +#define PT_NULL 0x00000000 /* Program header table entry unused */ +#define PT_LOAD 0x00000001 /* Loadable segment */ +#define PT_DYNAMIC 0x00000002 /* Dynamic linking information */ +#define PT_INTERP 0x00000003 /* Interpreter information */ +#define PT_NOTE 0x00000004 /* Auxiliary information */ +#define PT_SHLIB 0x00000005 /* Reserved */ +#define PT_PHDR 0x00000006 /* Segment containing program header table itself */ +#define PT_TLS 0x00000007 /* Thread-Local Storage template */ +#define PT_NUM 0x00000008 /* Number of defined types */ +#define PT_LOOS 0x60000000 /* Reserved inclusive range. Operating system specific: */ +#define PT_GNU_EH_FRAME 0x6474e550 /* GCC .eh_frame_hdr segment */ +#define PT_GNU_STACK 0x6474e551 /* Indicates stack executability */ +#define PT_GNU_RELRO 0x6474e552 /* Read-only after relocation */ +#define PT_LOSUNW 0x6ffffffa +#define PT_SUNWBSS 0x6ffffffa /* Sun Specific segment */ +#define PT_SUNWSTACK 0x6ffffffb /* Stack segment */ +#define PT_HISUNW 0x6fffffff +#define PT_HIOS 0x6FFFFFFF /* Reserved inclusive range. Operating system specific end*/ +#define PT_LOPROC 0x70000000 /* Reserved inclusive range. Processor specific */ +#define PT_HIPROC 0x7FFFFFFF /* Reserved inclusive range. Processor specific */ + +/* Flags, p_flags 64-bit field */ +#define PF_X 0x1 /* Executable segment */ +#define PF_W 0x2 /* Writeable segment */ +#define PF_R 0x4 /* Readable segment */ +#define PF_MASKOS 0x0FF00000 /* OS-specific */ +#define PF_MASKPROC 0xF0000000 /* Processor-specific */ + +/* Other Program Header fields: + p_offset: Offset of the segment in the file image. + p_vaddr: Virtual address of the segment in memory. + p_paddr: On systems where physical address is relevant, reserved for segment's physical address. + p_filesz: Size in bytes of the segment in the file image. May be 0. + p_memsz: Size in bytes of the segment in memory. May be 0. + p_flags: 32-bit flags (don't exist on 64 bit header) + p_align: 0 and 1 specify no alignment. Otherwise should be a positive, integral power of 2, with p_vaddr equating p_offset modules p_align +*/ + +/* Elf64 Program Header */ +struct Elf64_Phdr { + uint32_t p_type; + uint32_t p_flags; + uint64_t p_offset; + uint64_t p_vaddr; + uint64_t p_paddr; + uint64_t p_filesz; + uint64_t p_memsz; + uint64_t p_align; +}; + +// ============================================================================ +// =================================== Section Header ============================= +// ============================================================================ + +/* Section Header Type, sh_type field */ +#define SHT_NULL 0x0 /* Section header table entry unused. First entry in the section table must be SHT_NULL according the stanard */ +#define SHT_PROGBITS 0x1 /* Program data */ +#define SHT_SYMTAB 0x2 /* Symbol table */ +#define SHT_STRTAB 0x3 /* String table */ +#define SHT_RELA 0x4 /* Relocation entries with addends */ +#define SHT_HASH 0x5 /* Symbol hash table */ +#define SHT_DYNAMIC 0x6 /* Dynamic linking information */ +#define SHT_NOTE 0x7 /* Notes */ +#define SHT_NOBITS 0x8 /* Program space with no data (bss) */ +#define SHT_REL 0x9 /* Relocation entries, no addends */ +#define SHT_SHLIB 0x0A /* Reserved */ +#define SHT_DYNSYM 0x0B /* Dynamic linker symbol table */ +#define SHT_INIT_ARRAY 0x0E /* Array of constructors */ +#define SHT_FINI_ARRAY 0x0F /* Array of destructors */ +#define SHT_PREINIT_ARRAY 0x10 /* Array of pre-constructors */ +#define SHT_GROUP 0x11 /* Section group */ +#define SHT_SYMTAB_SHNDX 0x12 /* Extended section indices */ +#define SHT_NUM 0x13 /* Number of defined types */ +#define SHT_LOOS 0x60000000 /* Start OS-specific */ +#define SHT_GNU_ATTRIBUTES 0x6ffffff5 /* Object attributes. */ +#define SHT_GNU_HASH 0x6ffffff6 /* GNU-style hash table. */ +#define SHT_GNU_LIBLIST 0x6ffffff7 /* Prelink library list */ +#define SHT_CHECKSUM 0x6ffffff8 /* Checksum for DSO content. */ +#define SHT_LOSUNW 0x6ffffffa /* Sun-specific low bound. */ +#define SHT_SUNW_move 0x6ffffffa +#define SHT_SUNW_COMDAT 0x6ffffffb +#define SHT_SUNW_syminfo 0x6ffffffc +#define SHT_GNU_verdef 0x6ffffffd /* Version definition section. */ +#define SHT_GNU_verneed 0x6ffffffe /* Version needs section. */ +#define SHT_GNU_versym 0x6fffffff /* Version symbol table. */ +#define SHT_HISUNW 0x6fffffff /* Sun-specific high bound. */ +#define SHT_HIOS 0x6fffffff /* End OS-specific type */ +#define SHT_LOPROC 0x70000000 /* Start of processor-specific */ +#define SHT_HIPROC 0x7fffffff /* End of processor-specific */ +#define SHT_LOUSER 0x80000000 /* Start of application-specific */ +#define SHT_HIUSER 0x8fffffff /* End of application-specific */ + +/* Section Header flags, sh_flags field */ +#define SHF_WRITE 0x1 /*Writable*/ +#define SHF_ALLOC 0x2 /*Occupies memory during execution*/ +#define SHF_EXECINSTR 0x4 /*Executable*/ +#define SHF_MERGE 0x10 /*Might be merged*/ +#define SHF_STRINGS 0x20 /*Contains null-terminated strings*/ +#define SHF_INFO_LINK 0x40 /*'sh_info' contains SHT index*/ +#define SHF_LINK_ORDER 0x80 /*Preserve order after combining*/ +#define SHF_OS_NONCONFORMING 0x100 /*Non-standard OS specific handling required*/ +#define SHF_GROUP 0x200 /*Section is member of a group*/ +#define SHF_TLS 0x400 /*Section hold thread-local data*/ +#define SHF_COMPRESSED 0x800 /*Section with compressed data*/ +#define SHF_MASKOS 0x0FF00000 /*OS-specific*/ +#define SHF_MASKPROC 0xF0000000 /*Processor-specific*/ +#define SHF_ORDERED 0x4000000 /*Special ordering requirement (Solaris)*/ +#define SHF_EXCLUDE 0x8000000 /*Section is excluded unless referenced or allocated (Solaris)*/ + +/* Other Section Header fields: + sh_name: An offset to a string in the .shstrab section that represents the name of this section. Zero means no name. + sh_addr: Virtual address of the section in memory, for sections that are loaded. + sh_offset: Offset of the section in the file image. + sh_size: Size in bytes of the section. May be 0. + sh_link: Contains the section index of an associated section. This field is used for several purposes, depending on the type of section. + sh_info: Contains extra information about the section. This field is used for several purposes, depending on the type of section. + sh_addralign: Contains the required alignment of the section. This field must be a power of two. + sh_entsize: Contains the size, in bytes, of each entry, for sections that contain fixed-size entries. Otherwise, this field contains zero. +*/ +/* + The most common ELF sections: + .text - contains executable code. Packed with Read and Execute flags. Load only one times. Can't be changed. + .data - initalized data. Read and Write flags. + .rodata - initalized read only data. Read flag. + .bss - unititlized data. Read and Write flags. + other sections you can see here: https://gist.github.com/x0nu11byt3/bcb35c3de461e5fb66173071a2379779#sections + + Also, there is something like "group of sections" (readelf -g), but it's very rare. +*/ + +/* Elf64 Section Header */ +struct Elf64_Shdr { + uint32_t sh_name; + uint32_t sh_type; + uint64_t sh_flags; + uint64_t sh_addr; + uint64_t sh_offset; + uint64_t sh_size; + uint32_t sh_link; + uint32_t sh_info; + uint64_t sh_addralign; + uint64_t sh_entsize; +}; + +// ============================================================================ +// =================================== Symbols ================================ +// ============================================================================ +// https://docs.oracle.com/cd/E19683-01/816-1386/chapter6-79797/index.html +#pragma message("TODO: do") + +/* Symbol fields: + st_name: Symbol name + st_info: Symbol type and binding. It is calculated using macros + st_other: Symbol visibility. + st_shndx: Section index. + st_value: Symbol value. + st_size: Symbol size; +*/ + +/* Symbol struct */ +struct Elf64_Sym { + uint32_t st_name; + uint8_t st_info; + uint8_t st_other; + uint16_t st_shndx; + uint64_t st_value; + uint64_t st_size; +}; + + +// ============================================================================ +// =================================== ELF Page size ========================== +// ============================================================================ +#define ELF_PAGE_SIZE 0x1000 +#define ELF_PAGE_MASK (ELF_PAGE_SIZE - 1) + +// ============================================================================ +// =================================== ELF Load Result ======================== +// ============================================================================ +// ATM it uses in efi_elf_parser.cpp + +/* Error codes for ElfLoadResult.error */ +#define ELF_ERR_NONE 0 /* success */ +#define ELF_ERR_FILE_TOO_SMALL 1 /* file smaller than ELF header */ +#define ELF_ERR_INVALID_MAGIC 2 /* bad magic */ +#define ELF_ERR_INVALID_CLASS 3 /* bad class */ +#define ELF_ERR_INVALID_IDENT 4 /* bad ident */ +#define ELF_ERR_INVALID_ARCH 5 /* bad machine */ +#define ELF_ERR_INVALID_TYPE 6 /* bad type */ +#define ELF_ERR_NO_LOAD_SEGS 7 /* no PT_LOAD segments found */ +#define ELF_ERR_ALLOC_FAILED 8 /* AllocatePages failed (check efi_alloc_status) */ + +/* Elf Load Result object */ +struct ElfLoadResult { + uint64_t entry_point; + uint64_t phys_base; + uint64_t virt_base; + uint64_t total_size; + uint64_t efi_alloc_status; /* EFI_STATUS from AllocatePages, valid when error == ELF_ERR_ALLOC_FAILED */ + uint8_t error; /* ELF_ERR_* code above */ + bool success; +}; diff --git a/kernel/Makefile b/kernel/Makefile new file mode 100644 index 0000000..a5f6741 --- /dev/null +++ b/kernel/Makefile @@ -0,0 +1,115 @@ +# ============================================================================ +# kernel/Makefile - Kernel Build +# Produces: $(RESULT_DIR)/(CONFIG_KERNEL_FILE_NAME)).elf +# +# ATM: uses config_generated.mk for compilation of drivers/filesystems/entyties. +# I hard-code it in this Makefile, but in future (TODO) i should remove it +# ============================================================================ + +include ../config.mk +-include ../config_generated.mk + +INCLUDES := \ + -Iinclude \ + -I../include + +# ── Always-compiled sources ───────────────────────────────────────────────── + +ARCH_CPP_SRC := $(wildcard arch/$(ARCH)/*.cpp) +ARCH_ASM_SRC := $(wildcard arch/$(ARCH)/*.S) + +#TODO: move it to special Makefiles +CORE_SRC := $(wildcard core/*.cpp) +MM_SRC := $(wildcard mm/*.cpp) +LDR_SRC := $(wildcard loader/*.cpp) +LIB_SRC := $(wildcard lib/*.cpp) + +# ── Conditionally-compiled sources (from .config) ─────────────────────────── +# TODO: change it to Driver Makefiles +DRV_SRC := +FS_SRC := + +# Drivers — add src based on CONFIG_ variables +ifeq ($(CONFIG_DRIVER_FBCON),y) + DRV_SRC += $(wildcard drivers/console/*.cpp) +endif +ifeq ($(CONFIG_DRIVER_UART_16550),y) + DRV_SRC += $(wildcard drivers/char/uart_16550*.cpp) +endif +ifeq ($(CONFIG_DRIVER_PL011_UART),y) + DRV_SRC += $(wildcard drivers/char/pl011*.cpp) +endif +ifeq ($(CONFIG_DRIVER_PS2KBD),y) + DRV_SRC += $(wildcard drivers/input/ps2kbd*.cpp) +endif +ifeq ($(CONFIG_DRIVER_VIRTIO_BLK),y) + DRV_SRC += $(wildcard drivers/block/virtio_blk*.cpp) +endif +ifeq ($(CONFIG_DRIVER_AHCI),y) + DRV_SRC += $(wildcard drivers/block/ahci*.cpp) +endif +ifeq ($(CONFIG_DRIVER_VIRTIO_NET),y) + DRV_SRC += $(wildcard drivers/net/virtio_net*.cpp) +endif + +# Filesystems(just for example) +ifeq ($(CONFIG_FS_INITRAMFS),y) + FS_SRC += $(wildcard fs/initramfs*.cpp) +endif +ifeq ($(CONFIG_FS_TMPFS),y) + FS_SRC += $(wildcard fs/tmpfs*.cpp) +endif +ifeq ($(CONFIG_FS_EXT2),y) + FS_SRC += $(wildcard fs/ext2*.cpp) +endif +ifeq ($(CONFIG_FS_FAT32),y) + FS_SRC += $(wildcard fs/fat32*.cpp) +endif + +# ── Collect all sources ───────────────────────────────────────────────────── + +ALL_CPP_SRC := $(ARCH_CPP_SRC) $(CORE_SRC) $(MM_SRC) $(DRV_SRC) $(FS_SRC) $(LDR_SRC) $(LIB_SRC) +ALL_ASM_SRC := $(ARCH_ASM_SRC) + +ALL_CPP_OBJ := $(patsubst %.cpp,%.o,$(ALL_CPP_SRC)) +ALL_ASM_OBJ := $(patsubst %.S,%.o,$(ALL_ASM_SRC)) +ALL_OBJ := $(ALL_ASM_OBJ) $(ALL_CPP_OBJ) + +# ── Flags ─────────────────────────────────────────────────────────────────── + +KERNEL_CXXFLAGS := \ + $(COMMON_CXXFLAGS) \ + $(ARCH_CFLAGS) \ + $(INCLUDES) \ + -DARCH_$(shell echo $(ARCH) | tr a-z A-Z) + +KERNEL_ASFLAGS := \ + $(COMMON_ASFLAGS) \ + $(ARCH_CFLAGS) + +KERNEL_LDFLAGS := \ + $(COMMON_LDFLAGS) \ + -T arch/$(ARCH)/linker.ld + +# ── Targets ───────────────────────────────────────────────────────────────── + +.PHONY: all clean + +all: $(RESULT_DIR)/$(CONFIG_KERNEL_FILE_NAME) + +$(RESULT_DIR)/$(CONFIG_KERNEL_FILE_NAME): $(ALL_OBJ) arch/$(ARCH)/linker.ld + @mkdir -p $(dir $@) + @echo " LD $@" + @$(LD) $(KERNEL_LDFLAGS) -o $@ $(ALL_OBJ) + +%.o: %.cpp + @echo " CXX $<" + @$(CXX) $(KERNEL_CXXFLAGS) -c $< -o $@ + +%.o: %.S + @echo " AS $<" + @$(CXX) $(KERNEL_ASFLAGS) -c $< -o $@ + +clean: + rm -rf $(RESULT_DIR) + find . -name '*.o' -delete diff --git a/kernel/arch/aarch64/arch.cpp b/kernel/arch/aarch64/arch.cpp new file mode 100644 index 0000000..e35b769 --- /dev/null +++ b/kernel/arch/aarch64/arch.cpp @@ -0,0 +1,17 @@ +// ============================================================================ +// arch/aarch64/arch.cpp — AArch64 architecture-specific primitives +// ============================================================================ + +#include <kernel/arch.h> + +namespace arch { + +[[noreturn]] void halt() { + for (;;) asm volatile("wfi"); +} + +const char* name() { + return "AArch64"; +} + +} // namespace arch diff --git a/kernel/arch/aarch64/entry.S b/kernel/arch/aarch64/entry.S new file mode 100644 index 0000000..1ca9b0c --- /dev/null +++ b/kernel/arch/aarch64/entry.S @@ -0,0 +1,46 @@ +// ============================================================================ +// arch/aarch64/entry.S - Kernel entry point (AArch64) +// +// Called by the UEFI loader after ExitBootServices. +// - EL1, MMU on (identity map from UEFI) +// - X0 = pointer to BootInfo structure (AAPCS64 6.8.2 C.9 Pointer located in X[NGRN]) +// - Interrupts masked (DAIF) +// ============================================================================ + +.section .text +.global _start +.extern kernel_main + +_start: + // Mask all interrupts + msr daifset, #0xF + + // Set up kernel stack pointer to Stack Pointer(SP) + adrp x1, _stack_top + add x1, x1, :lo12:_stack_top + mov sp, x1 + + // Clear frame pointer for clean stack traces + // x29 is Frame Pointer (FP), points to the prev stack frame. Make kerne_main is root of the call chain (remove UEFI calls) + // x30 is Link Register (LR), holds the return address from the last bl. Same logic. + mov x29, #0 + mov x30, #0 + + // X0 already holds BootInfo* + bl kernel_main + + // Should never return +.Lhalt: + wfi + b .Lhalt + +// ============================================================================ +// Kernel stack (16 KiB) +// Maybe in future i will increase this value, but i hope it's not necessary +// ============================================================================ + +.section .bss +.balign 4096 +_stack_bottom: + .space CONFIG_KERNEL_STACK_SIZE +_stack_top: diff --git a/kernel/arch/aarch64/linker.ld b/kernel/arch/aarch64/linker.ld new file mode 100644 index 0000000..4eb0ced --- /dev/null +++ b/kernel/arch/aarch64/linker.ld @@ -0,0 +1,55 @@ +/* + * kernel/arch/aarch64/linker.ld + * + * Linker script for the AArch64 kernel ELF binary. + * Loaded at 0x40100000 (typical for QEMU virt machine). + * + * 4K align is same reason with boot linker scripts - page-aligned sections allow per-section memory protection + * TODO: rewrite it after i will add normal paging + */ + +ENTRY(_start) + +/* + KERNEL_PHYS_BASE - kernel base address, for aarch64 it's 0x40100000 because + in QEMU RAM starts at 0x4000000, and we add 1MiB for BIOS data +*/ +KERNEL_PHYS_BASE = 0x40100000; + +SECTIONS +{ + . = KERNEL_PHYS_BASE; + + .text ALIGN(4K) : { + __text_start = .; + *(.text .text.*) + __text_end = .; + } + + .rodata ALIGN(4K) : { + __rodata_start = .; + *(.rodata .rodata.*) + __rodata_end = .; + } + + .data ALIGN(4K) : { + __data_start = .; + *(.data .data.*) + __data_end = .; + } + + .bss ALIGN(4K) : { + __bss_start = .; + *(.bss .bss.*) + *(COMMON) + __bss_end = .; + } + + __kernel_end = .; + + /DISCARD/ : { + *(.comment) + *(.note.*) + *(.eh_frame*) + } +} diff --git a/kernel/arch/x86_64/arch.cpp b/kernel/arch/x86_64/arch.cpp new file mode 100644 index 0000000..3e6705b --- /dev/null +++ b/kernel/arch/x86_64/arch.cpp @@ -0,0 +1,17 @@ +// ============================================================================ +// arch/x86_64/arch.cpp — x86_64 architecture-specific primitives +// ============================================================================ + +#include <kernel/arch.h> + +namespace arch { + +[[noreturn]] void halt() { + for (;;) asm volatile("hlt"); +} + +const char* name() { + return "x86_64"; +} + +} // namespace arch diff --git a/kernel/arch/x86_64/entry.S b/kernel/arch/x86_64/entry.S new file mode 100644 index 0000000..50cdd47 --- /dev/null +++ b/kernel/arch/x86_64/entry.S @@ -0,0 +1,48 @@ +// ============================================================================ +// arch/x86_64/entry.S - Kernel entry point (x86_64) +// +// Called by the UEFI loader after ExitBootServices. +// - Long mode active, paging on (identity map from UEFI) +// - RDI = pointer to BootInfo structure +// - Interrupts disabled +// ============================================================================ + +.section .text +.global _start +.extern kernel_main + +_start: + // Disable interrupts + cli + + // Set up kernel stack + // Load Effective Address quadword + // the address RIP (current exec instruction) + offset_to_stack_top + // to the Stack pointer register (rsp) + //(AT&T syntax: lea symbol(%rip), %reg) + leaq _stack_top(%rip), %rsp + + // Align stack to 16 bytes (System V ABI) + // Because ABI mandates that rsp must be 16-byte aligned at the point of a call instruction + andq $-16, %rsp + + // The UEFI boot loader (PE/MS x64 ABI) passes BootInfo* in RCX. + // kernel_main uses System V ABI and expects it in RDI. + movq %rcx, %rdi + call kernel_main + + // Should never return +.Lhalt: + cli + hlt + jmp .Lhalt + +// ============================================================================ +// Kernel stack (16 KiB) +// ============================================================================ + +.section .bss +.align 4096 +_stack_bottom: + .space CONFIG_KERNEL_STACK_SIZE +_stack_top: diff --git a/kernel/arch/x86_64/linker.ld b/kernel/arch/x86_64/linker.ld new file mode 100644 index 0000000..ca418bf --- /dev/null +++ b/kernel/arch/x86_64/linker.ld @@ -0,0 +1,62 @@ +/* + * kernel/arch/x86_64/linker.ld + * + * Linker script for the x86_64 kernel ELF binary. + * + * The kernel is loaded at 0x100000 (1 MiB) physical. + * Once paging is set up, it will be remapped to higher-half + * (e.g., 0xFFFFFFFF80000000 or 0xFFFF800000000000). + * + * For now we use identity mapping - the EFI loader loads + * segments at their p_paddr and jumps to e_entry. + */ + +ENTRY(_start) + +/* + KERNEL_PHYS_BASE - kernel base address, for x86 it's 1MiB for BIOS data +*/ +KERNEL_PHYS_BASE = 0x100000; + +SECTIONS +{ + . = KERNEL_PHYS_BASE; + + /* Code */ + .text ALIGN(4K) : { + __text_start = .; + *(.text .text.*) + __text_end = .; + } + + /* Read-only data */ + .rodata ALIGN(4K) : { + __rodata_start = .; + *(.rodata .rodata.*) + __rodata_end = .; + } + + /* Initialized data */ + .data ALIGN(4K) : { + __data_start = .; + *(.data .data.*) + __data_end = .; + } + + /* Uninitialized data (BSS) */ + .bss ALIGN(4K) : { + __bss_start = .; + *(.bss .bss.*) + *(COMMON) + __bss_end = .; + } + + __kernel_end = .; + + /* Discard unnecessary sections */ + /DISCARD/ : { + *(.comment) + *(.note.*) + *(.eh_frame*) + } +} diff --git a/kernel/core/kernel_main.cpp b/kernel/core/kernel_main.cpp new file mode 100644 index 0000000..9c744c7 --- /dev/null +++ b/kernel/core/kernel_main.cpp @@ -0,0 +1,113 @@ +// ============================================================================ +// core/kernel_main.cpp - Kernel entry point +// +// Called by arch-specific entry.S after stack setup. +// Receives BootInfo* by the UEFI loader (or in future from other loader). +// ============================================================================ + +#include <boot/boot_info.h> +#include <kernel/kprint.h> +#include <kernel/arch.h> + +// ── Memory region type names ─────────────────────────────────────────────── + +namespace { +//TODO: Move it to another cpp/h file +//TODO: use reflection to get field name +[[nodiscard]] constexpr const char* mem_type_name(MemoryRegionType type) { + switch (type) { + case MemoryRegionType::Usable: return "Usable"; + case MemoryRegionType::Reserved: return "Reserved"; + case MemoryRegionType::AcpiReclaimable: return "ACPI Reclaimable"; + case MemoryRegionType::AcpiNvs: return "ACPI NVS"; + case MemoryRegionType::BootloaderReclaimable: return "Boot Reclaimable"; + case MemoryRegionType::KernelAndModules: return "Kernel"; + case MemoryRegionType::Framebuffer: return "Framebuffer"; + } + return "Unknown"; +} + +} // anonymous namespace + +// ── Kernel entry ─────────────────────────────────────────────────────────── +// Called from entry.S with BootInfo* in RDI (x86_64) / X0 (AArch64). +extern "C" void kernel_main(BootInfo* boot_info) { + // Validate magic — if wrong we can't print, just halt. + if (boot_info->magic != BOOT_INFO_MAGIC) + arch::halt(); + + // Initialize framebuffer console. + // TODO: Add driver skeleton and move framebuffer to early drivers. + kcon::init(boot_info->framebuffer); + + // ── Banner ───────────────────────────────────────────────────────── + + kcon::println("========================================"); + kcon::println(" Bastion Kernel " CONFIG_KERNEL_VERSION); + kcon::kprintf(" Architecture: %s\n", arch::name()); + kcon::println("========================================"); + kcon::putchar('\n'); + + // ── Framebuffer info ─────────────────────────────────────────────── + + kcon::kprintf("Framebuffer: %ux%u, pitch=%u, base=%x\n", + boot_info->framebuffer.width, + boot_info->framebuffer.height, + boot_info->framebuffer.pitch, + boot_info->framebuffer.base); + kcon::putchar('\n'); + + // ── Firmware tables ──────────────────────────────────────────────── + +#ifdef CONFIG_X86 + if (boot_info->rsdp_address) + kcon::kprintf("ACPI RSDP at: %x\n", boot_info->rsdp_address); +#elif CONFIG_AARCH64 + if (boot_info->fdt_address) + kcon::kprintf("Device Tree at: %x\n", boot_info->fdt_address); +#endif + kcon::putchar('\n'); + + // ── Memory map ───────────────────────────────────────────────────── + + kcon::kprintf("Memory map (%u entries):\n", boot_info->memory_map_count); + + uint64_t total_usable = 0; + for (uint64_t i = 0; i < boot_info->memory_map_count; i++) { + const auto& region = boot_info->memory_map[i]; + + // Skip tiny reserved regions to avoid flooding the console. + if (region.type == MemoryRegionType::Reserved && region.length < 0x10000) + continue; + + kcon::kprintf(" %x - %x [%s]\n", + region.base, + region.base + region.length, + mem_type_name(region.type)); + + if (region.type == MemoryRegionType::Usable) + total_usable += region.length; + } + + kcon::putchar('\n'); + kcon::kprintf("Total usable memory: %u MiB\n", total_usable / (1024u * 1024u)); + + // ── Kernel info ──────────────────────────────────────────────────── + + kcon::putchar('\n'); + kcon::kprintf("Kernel loaded at phys %x, virt %x, size %u KiB\n", + boot_info->kernel_phys_base, + boot_info->kernel_virt_base, + boot_info->kernel_size / 1024u); + + // ── Done (for now) ───────────────────────────────────────────────── + + kcon::putchar('\n'); + kcon::println("Hello, World from BastionOS!"); + kcon::putchar('\n'); + kcon::println("Kernel initialized successfully."); + kcon::println("Next: GDT, IDT, paging, memory allocator..."); + kcon::println("Halting."); + + arch::halt(); // [[noreturn]] — never comes back +} diff --git a/kernel/include/kernel/arch.h b/kernel/include/kernel/arch.h new file mode 100644 index 0000000..a30e03e --- /dev/null +++ b/kernel/include/kernel/arch.h @@ -0,0 +1,18 @@ +// ============================================================================ +// kernel/arch.h - Architecture abstraction interface +// +// Provides a uniform API over arch-specific CPU operations. +// Implementations live in kernel/arch/<arch>/arch.cpp. +// ============================================================================ + +#pragma once + +namespace arch { + + // Halt the CPU permanently. Never returns. + [[noreturn]] void halt(); + + // Return the architecture name as a C string (e.g. "x86_64", "AArch64"). + const char* name(); + +} diff --git a/kernel/include/kernel/kprint.h b/kernel/include/kernel/kprint.h new file mode 100644 index 0000000..9228f8b --- /dev/null +++ b/kernel/include/kernel/kprint.h @@ -0,0 +1,43 @@ +#pragma once + +// ============================================================================ +// kernel/kprint.h - Early kernel console output +// +// Currently writes directly to the framebuffer using a built-in bitmap font. +// This is the first thing you want working - essential for debugging +// everything that follows. +// +// In future, UART console will use the same interface. +// ============================================================================ + +#include <stdint.h> +#include <boot/boot_info.h> + +namespace kcon { + +// Initialize the console with framebuffer info from BootInfo. +void init(const FramebufferInfo& fb); + +// Print a single character. +void putchar(char c); + +// Print a null-terminated string (no-op if s == nullptr). +void puts(const char* s); + +// Print a string followed by a newline. +void println(const char* s); + +// Print a 64-bit value as "0x<hex>" (leading zeros suppressed). +void put_hex(uint64_t val); + +// Print a 64-bit decimal value. +void put_dec(uint64_t val); + +// Minimal printf — supports %s, %d, %u, %x, %p, %%. +// Note: %d and %u expect int64_t/uint64_t arguments (not int/unsigned int). +void kprintf(const char* fmt, ...); + +// Clear screen to background color. +void clear(); + +} // namespace kcon diff --git a/kernel/include/kernel/types.h b/kernel/include/kernel/types.h new file mode 100644 index 0000000..8eaf7cf --- /dev/null +++ b/kernel/include/kernel/types.h @@ -0,0 +1,25 @@ +#pragma once + +// ============================================================================ +// kernel/types.h - Core kernel type definitions +// ============================================================================ + +#include <stdint.h> +#include <stddef.h> + +// Physical and virtual address types for clarity +using PhysAddr = uint64_t; +using VirtAddr = uint64_t; + +// Page size constant +inline constexpr uint64_t PAGE_SIZE = 4096; +inline constexpr uint64_t PAGE_MASK = ~(PAGE_SIZE - 1); + +// Align up/down helpers +constexpr uint64_t align_up(uint64_t val, uint64_t align) { + return (val + align - 1) & ~(align - 1); +} + +constexpr uint64_t align_down(uint64_t val, uint64_t align) { + return val & ~(align - 1); +} diff --git a/kernel/lib/cxxabi.cpp b/kernel/lib/cxxabi.cpp new file mode 100644 index 0000000..e38813f --- /dev/null +++ b/kernel/lib/cxxabi.cpp @@ -0,0 +1,85 @@ +/* + * kernel/lib/cxxabi.cpp + * + * C++ ABI runtime support for a freestanding kernel. + * + * The compiler emits calls to these functions for: + * - Pure virtual function calls (__cxa_pure_virtual) + * - Global object destructors (__cxa_atexit, __dso_handle) + * - new/delete operators + * + * Compile with -fno-exceptions -fno-rtti, so we don't need + * exception handling or typeinfo support. + */ + +#include <stddef.h> +#include <stdint.h> + +extern "C" { + +/* + * Called when a pure virtual function is invoked. + * This should never happen in correct code — it's a fatal error. + */ +[[noreturn]] void __cxa_pure_virtual() { + /* TODO: call kernel_panic("pure virtual function called") */ + for (;;) { + #if defined(__x86_64__) + asm volatile("hlt"); + #elif defined(__aarch64__) + asm volatile("wfi"); + #endif + } +} + +/* + * Static local variable guards (thread-safe init in the Itanium ABI). + * + * The ABI stores the "initialized" flag in the least-significant byte of the + * 64-bit guard variable (Itanium C++ ABI 3.3.2 and 3.3.3 ). On little-endian targets + * (x86_64, AArch64) that byte is at the lowest address, so casting the + * guard pointer to uint8_t* and reading/writing byte 0 is correct. + * + * In a single-CPU kernel with no preemption, the full acquire/release + * protocol is unnecessary — we just check and set the byte. + */ +int __cxa_guard_acquire(uint64_t* guard) { + return !(*reinterpret_cast<const uint8_t*>(guard)); +} + +void __cxa_guard_release(uint64_t* guard) { + *reinterpret_cast<uint8_t*>(guard) = 1; +} + +void __cxa_guard_abort(uint64_t* guard) { + (void)guard; +} + +/* + * atexit support — for global object destructors. + * In a kernel, we never "exit", so this is a no-op. + */ +int __cxa_atexit(void (*)(void*), void*, void*) { + return 0; +} + +void* __dso_handle = nullptr; + +} /* extern "C" */ + +/* + * Placement new — always needed. + * Sized new/delete — routed through kernel heap once available (Phase 3). + */ + +// [[nodiscard]]: ignoring the returned pointer from new is always a bug. +[[nodiscard]] void* operator new(size_t size) { (void)size; return nullptr; /* TODO: kmalloc */ } +[[nodiscard]] void* operator new[](size_t size) { (void)size; return nullptr; /* TODO: kmalloc */ } +void operator delete(void* ptr) noexcept { (void)ptr; /* TODO: kfree */ } +void operator delete[](void* ptr) noexcept { (void)ptr; /* TODO: kfree */ } +void operator delete(void* ptr, size_t) noexcept { (void)ptr; /* TODO: kfree */ } +void operator delete[](void* ptr, size_t) noexcept { (void)ptr; /* TODO: kfree */ } + +/* Placement new — doesn't allocate, just returns the pointer */ +inline void* operator new(size_t, void* ptr) noexcept { return ptr; } +inline void* operator new[](size_t, void* ptr) noexcept { return ptr; } diff --git a/kernel/lib/framebuffer.cpp b/kernel/lib/framebuffer.cpp new file mode 100644 index 0000000..476abaa --- /dev/null +++ b/kernel/lib/framebuffer.cpp @@ -0,0 +1,374 @@ +// ============================================================================ +// lib/framebuffer.cpp - Early kernel framebuffer console +// +// Renders text directly to the linear framebuffer using a minimal +// built-in 8x16 bitmap font (PC VGA style, ASCII 32-126). +// ============================================================================ + +#include <kernel/kprint.h> +#include <stdarg.h> + +namespace kcon { + +// ── Character cell dimensions ────────────────────────────────────────────── + +static constexpr uint32_t CHAR_W = 8; // Font glyph width in pixels +static constexpr uint32_t CHAR_H = 16; // Font glyph height in pixels +static constexpr uint32_t TAB_WIDTH = 4; // Tab stop width in character cells + +// ── Colors (0x00RRGGBB) ──────────────────────────────────────────────────── + +static constexpr uint32_t FG_COLOR = CONFIG_FBCON_FG_COLOR; // Light grey +static constexpr uint32_t BG_COLOR = CONFIG_FBCON_BG_COLOR; // Dark blue + +// ── Console state ────────────────────────────────────────────────────────── +// +// All mutable state lives in one struct so callers can reason about it as +// a unit and we avoid scattered bare globals. +#pragma message("when i add drivers abstract, move it to another console interface and use framebuffer as driver") + +struct Console { +public: + uint32_t* fb = nullptr; // Framebuffer base (as 32-bit pixels) + uint32_t width = 0; // Pixels per row + uint32_t height = 0; // Rows in pixels + uint32_t pitch = 0; // Bytes per scanline (>= width * 4) + uint32_t col = 0; // Cursor column in character cells + uint32_t row = 0; // Cursor row in character cells + bool is_bgr = false; + + // C++23 multi-dimensional subscript: direct reference to pixel (x, y). + // pitch is in bytes; each pixel is 4 bytes. No bounds checking — callers + // must validate coordinates before calling (put_pixel does this). + [[nodiscard]] uint32_t& operator[](uint32_t x, uint32_t y) noexcept { + auto* row_ptr = reinterpret_cast<uint32_t*>( + reinterpret_cast<uintptr_t>(fb) + static_cast<uintptr_t>(y) * pitch); + return row_ptr[x]; + } +}; + +static Console g_con{}; + +// ── Minimal 8x16 bitmap font (ASCII 32–126) ─────────────────────────────── +// Each character is 16 bytes (one byte per row, 8 pixels wide). +// We store a blank (space) for anything outside the range. + +// clang-format off +static const uint8_t g_font[][16] = { + // 32: space + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + // 33: ! + {0x00,0x00,0x18,0x3C,0x3C,0x3C,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00}, + // 34: " + {0x00,0x66,0x66,0x66,0x24,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + // 35: # + {0x00,0x00,0x00,0x6C,0x6C,0xFE,0x6C,0x6C,0xFE,0x6C,0x6C,0x00,0x00,0x00,0x00,0x00}, + // 36: $ + {0x00,0x10,0x10,0x7C,0xD6,0xD0,0x7C,0x16,0xD6,0x7C,0x10,0x10,0x00,0x00,0x00,0x00}, + // 37: % + {0x00,0x00,0x00,0x00,0xC2,0xC6,0x0C,0x18,0x30,0x60,0xC6,0x86,0x00,0x00,0x00,0x00}, + // 38: & + {0x00,0x00,0x38,0x6C,0x6C,0x38,0x76,0xDC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00}, + // 39: ' + {0x00,0x30,0x30,0x30,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + // 40: ( + {0x00,0x00,0x0C,0x18,0x30,0x30,0x30,0x30,0x30,0x30,0x18,0x0C,0x00,0x00,0x00,0x00}, + // 41: ) + {0x00,0x00,0x30,0x18,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x18,0x30,0x00,0x00,0x00,0x00}, + // 42: * + {0x00,0x00,0x00,0x00,0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00,0x00,0x00,0x00,0x00}, + // 43: + + {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00,0x00,0x00,0x00,0x00}, + // 44: , + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x30,0x00,0x00,0x00}, + // 45: - + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + // 46: . + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00}, + // 47: / + {0x00,0x00,0x00,0x00,0x02,0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00,0x00,0x00,0x00}, + // 48-57: 0-9 + {0x00,0x00,0x7C,0xC6,0xC6,0xCE,0xDE,0xF6,0xE6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x18,0x38,0x78,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x7C,0xC6,0x06,0x0C,0x18,0x30,0x60,0xC0,0xC6,0xFE,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x7C,0xC6,0x06,0x06,0x3C,0x06,0x06,0x06,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x0C,0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x0C,0x1E,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xFE,0xC0,0xC0,0xC0,0xFC,0x06,0x06,0x06,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x38,0x60,0xC0,0xC0,0xFC,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xFE,0xC6,0x06,0x06,0x0C,0x18,0x30,0x30,0x30,0x30,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x7C,0xC6,0xC6,0xC6,0x7C,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x7C,0xC6,0xC6,0xC6,0x7E,0x06,0x06,0x06,0x0C,0x78,0x00,0x00,0x00,0x00}, + // 58: : + {0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x00}, + // 59: ; + {0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x30,0x00,0x00,0x00,0x00}, + // 60: < + {0x00,0x00,0x00,0x06,0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x06,0x00,0x00,0x00,0x00}, + // 61: = + {0x00,0x00,0x00,0x00,0x00,0x7E,0x00,0x00,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + // 62: > + {0x00,0x00,0x00,0x60,0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x60,0x00,0x00,0x00,0x00}, + // 63: ? + {0x00,0x00,0x7C,0xC6,0xC6,0x0C,0x18,0x18,0x18,0x00,0x18,0x18,0x00,0x00,0x00,0x00}, + // 64: @ + {0x00,0x00,0x7C,0xC6,0xC6,0xDE,0xDE,0xDE,0xDC,0xC0,0xC0,0x7C,0x00,0x00,0x00,0x00}, + // 65-90: A-Z + {0x00,0x00,0x10,0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x66,0x66,0x66,0x66,0xFC,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x3C,0x66,0xC2,0xC0,0xC0,0xC0,0xC0,0xC2,0x66,0x3C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xF8,0x6C,0x66,0x66,0x66,0x66,0x66,0x66,0x6C,0xF8,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xFE,0x66,0x62,0x68,0x78,0x68,0x60,0x62,0x66,0xFE,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xFE,0x66,0x62,0x68,0x78,0x68,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x3C,0x66,0xC2,0xC0,0xC0,0xDE,0xC6,0xC6,0x66,0x3A,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x3C,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x1E,0x0C,0x0C,0x0C,0x0C,0x0C,0xCC,0xCC,0xCC,0x78,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xE6,0x66,0x66,0x6C,0x78,0x78,0x6C,0x66,0x66,0xE6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xF0,0x60,0x60,0x60,0x60,0x60,0x60,0x62,0x66,0xFE,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xC6,0xEE,0xFE,0xFE,0xD6,0xC6,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xC6,0xE6,0xF6,0xFE,0xDE,0xCE,0xC6,0xC6,0xC6,0xC6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x60,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xD6,0xDE,0x7C,0x0C,0x0E,0x00,0x00}, + {0x00,0x00,0xFC,0x66,0x66,0x66,0x7C,0x6C,0x66,0x66,0x66,0xE6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x7C,0xC6,0xC6,0x60,0x38,0x0C,0x06,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xFF,0xDB,0x99,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xD6,0xD6,0xD6,0xFE,0xEE,0x6C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xC6,0xC6,0x6C,0x7C,0x38,0x38,0x7C,0x6C,0xC6,0xC6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xCC,0xCC,0xCC,0xCC,0x78,0x30,0x30,0x30,0x30,0x78,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xFE,0xC6,0x86,0x0C,0x18,0x30,0x60,0xC2,0xC6,0xFE,0x00,0x00,0x00,0x00}, + // 91: [ + {0x00,0x00,0x3C,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x3C,0x00,0x00,0x00,0x00}, + // 92: backslash + {0x00,0x00,0x00,0x80,0xC0,0x60,0x30,0x18,0x0C,0x06,0x02,0x00,0x00,0x00,0x00,0x00}, + // 93: ] + {0x00,0x00,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00,0x00,0x00,0x00}, + // 94: ^ + {0x10,0x38,0x6C,0xC6,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + // 95: _ + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00}, + // 96: ` + {0x00,0x30,0x18,0x0C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + // 97-122: a-z + {0x00,0x00,0x00,0x00,0x00,0x78,0x0C,0x7C,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00}, + {0x00,0x00,0xE0,0x60,0x60,0x78,0x6C,0x66,0x66,0x66,0x66,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x1C,0x0C,0x0C,0x3C,0x6C,0xCC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xFE,0xC0,0xC0,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x38,0x6C,0x64,0x60,0xF0,0x60,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x76,0xCC,0xCC,0xCC,0xCC,0xCC,0x7C,0x0C,0xCC,0x78,0x00}, + {0x00,0x00,0xE0,0x60,0x60,0x6C,0x76,0x66,0x66,0x66,0x66,0xE6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x18,0x18,0x00,0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x06,0x06,0x00,0x0E,0x06,0x06,0x06,0x06,0x06,0x06,0x66,0x66,0x3C,0x00}, + {0x00,0x00,0xE0,0x60,0x60,0x66,0x6C,0x78,0x78,0x6C,0x66,0xE6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0xEC,0xFE,0xD6,0xD6,0xD6,0xD6,0xC6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0xDC,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0xDC,0x66,0x66,0x66,0x66,0x66,0x7C,0x60,0x60,0xF0,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x76,0xCC,0xCC,0xCC,0xCC,0xCC,0x7C,0x0C,0x0C,0x1E,0x00}, + {0x00,0x00,0x00,0x00,0x00,0xDC,0x76,0x66,0x60,0x60,0x60,0xF0,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x7C,0xC6,0x60,0x38,0x0C,0xC6,0x7C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x10,0x30,0x30,0xFC,0x30,0x30,0x30,0x30,0x36,0x1C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0x76,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xD6,0xD6,0xD6,0xFE,0x6C,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0xC6,0x6C,0x38,0x38,0x38,0x6C,0xC6,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x00,0x00,0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7E,0x06,0x0C,0xF8,0x00}, + {0x00,0x00,0x00,0x00,0x00,0xFE,0xCC,0x18,0x30,0x60,0xC6,0xFE,0x00,0x00,0x00,0x00}, + // 123: { + {0x00,0x00,0x0E,0x18,0x18,0x18,0x70,0x18,0x18,0x18,0x18,0x0E,0x00,0x00,0x00,0x00}, + // 124: | + {0x00,0x00,0x18,0x18,0x18,0x18,0x00,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00}, + // 125: } + {0x00,0x00,0x70,0x18,0x18,0x18,0x0E,0x18,0x18,0x18,0x18,0x70,0x00,0x00,0x00,0x00}, + // 126: ~ + {0x00,0x00,0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, +}; +// clang-format on + +static constexpr int FONT_FIRST = 32; +static constexpr int FONT_LAST = 126; + +// ── Pixel helpers ────────────────────────────────────────────────────────── + +static void put_pixel(uint32_t x, uint32_t y, uint32_t rgb) noexcept { + if (x >= g_con.width || y >= g_con.height) return; + + // Swap R and B channels if the framebuffer uses BGR layout. + const uint32_t color = g_con.is_bgr + ? ((rgb & 0xFFu) << 16) | (rgb & 0xFF00u) | ((rgb >> 16) & 0xFFu) + : rgb; + + g_con[x, y] = color; // C++23 multi-dimensional subscript +} + +// ── Scroll the screen up by one character row ────────────────────────────── + +static void scroll() noexcept { + const uint32_t shift = CHAR_H * g_con.pitch; + const uint32_t total = g_con.height * g_con.pitch; + auto* const fb_bytes = reinterpret_cast<uint8_t*>(g_con.fb); + + // Shift everything up by CHAR_H rows (raw byte memmove equivalent). + for (uint32_t i = 0; i < total - shift; ++i) + fb_bytes[i] = fb_bytes[i + shift]; + + // Fill the newly exposed bottom rows with the background color. + for (uint32_t row = g_con.height - CHAR_H; row < g_con.height; ++row) + for (uint32_t col = 0; col < g_con.width; ++col) + put_pixel(col, row, BG_COLOR); +} + +// ── Render a glyph at character-cell position (col, row) ────────────────── + +static void render_char(uint32_t col, uint32_t row, char c) noexcept { + // Treat char as unsigned to avoid negative indices for high-ASCII. + int idx = static_cast<int>(static_cast<unsigned char>(c)) - FONT_FIRST; + if (idx < 0 || idx > (FONT_LAST - FONT_FIRST)) idx = 0; // fall back to space + + const uint32_t px = col * CHAR_W; + const uint32_t py = row * CHAR_H; + + for (uint32_t y = 0; y < CHAR_H; ++y) { + const uint8_t bits = g_font[idx][y]; + for (uint32_t x = 0; x < CHAR_W; ++x) { + const uint32_t color = (bits & (0x80u >> x)) ? FG_COLOR : BG_COLOR; + put_pixel(px + x, py + y, color); + } + } +} + +// ── Public API ───────────────────────────────────────────────────────────── + +void init(const FramebufferInfo& fb) { + g_con.fb = reinterpret_cast<uint32_t*>(fb.base); + g_con.width = fb.width; + g_con.height = fb.height; + g_con.pitch = fb.pitch; + g_con.is_bgr = (fb.format == PixelFormat::BGR); + g_con.col = 0; + g_con.row = 0; + clear(); +} + +void clear() { + for (uint32_t y = 0; y < g_con.height; ++y) + for (uint32_t x = 0; x < g_con.width; ++x) + put_pixel(x, y, BG_COLOR); + g_con.col = 0; + g_con.row = 0; +} + +void putchar(char c) { + const uint32_t max_cols = g_con.width / CHAR_W; + const uint32_t max_rows = g_con.height / CHAR_H; + + if (c == '\n') { + g_con.col = 0; + ++g_con.row; + } else if (c == '\r') { + g_con.col = 0; + } else if (c == '\t') { + // Advance to the next TAB_WIDTH-aligned column. + g_con.col = (g_con.col + TAB_WIDTH) & ~(TAB_WIDTH - 1u); + } else { + render_char(g_con.col, g_con.row, c); + ++g_con.col; + } + + if (g_con.col >= max_cols) { + g_con.col = 0; + ++g_con.row; + } + + if (g_con.row >= max_rows) { + scroll(); + g_con.row = max_rows - 1; + } +} + +void puts(const char* s) { + if (!s) return; + while (*s) putchar(*s++); +} + +void println(const char* s) { + puts(s); + putchar('\n'); +} + +void put_hex(uint64_t val) { + puts("0x"); + bool leading = true; + for (int i = 60; i >= 0; i -= 4) { + const uint8_t nibble = static_cast<uint8_t>((val >> i) & 0xFu); + if (nibble == 0 && leading && i > 0) continue; + leading = false; + putchar(nibble < 10 ? '0' + nibble : 'A' + nibble - 10); + } + if (leading) putchar('0'); +} + +void put_dec(uint64_t val) { + if (val == 0) { putchar('0'); return; } + char buf[20]; + int len = 0; + while (val > 0) { + buf[len++] = static_cast<char>('0' + val % 10); + val /= 10; + } + for (int j = len - 1; j >= 0; --j) putchar(buf[j]); +} + +void kprintf(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + + while (*fmt) { + if (*fmt != '%') { + putchar(*fmt++); + continue; + } + ++fmt; // skip '%' + + switch (*fmt) { + case 's': { + const char* s = va_arg(args, const char*); + puts(s ? s : "(null)"); + break; + } + case 'd': { + int64_t v = va_arg(args, int64_t); + if (v < 0) { putchar('-'); v = -v; } + put_dec(static_cast<uint64_t>(v)); + break; + } + case 'u': { + put_dec(va_arg(args, uint64_t)); + break; + } + case 'x': + case 'p': { + put_hex(va_arg(args, uint64_t)); + break; + } + case '%': + putchar('%'); + break; + case '\0': + goto done; + default: + putchar('%'); + putchar(*fmt); + break; + } + ++fmt; + } +done: + va_end(args); +} + +} // namespace kcon diff --git a/kernel/lib/string.cpp b/kernel/lib/string.cpp new file mode 100644 index 0000000..3bd60fa --- /dev/null +++ b/kernel/lib/string.cpp @@ -0,0 +1,78 @@ +// ============================================================================ +// lib/string.cpp - Freestanding memory and string operations +// +// These are required because we compile with -nostdlib. +// The compiler may generate implicit calls to memcpy/memset/memmove +// (e.g., for struct copies, array init), so these must exist with +// standard C linkage and standard names. +// ============================================================================ + +#include <stdint.h> +#include <stddef.h> + +extern "C" { + +void* memcpy(void* dest, const void* src, size_t n) { + auto* d = static_cast<uint8_t*>(dest); + auto* s = static_cast<const uint8_t*>(src); + for (size_t i = 0; i < n; i++) d[i] = s[i]; + return dest; +} + +void* memmove(void* dest, const void* src, size_t n) { + auto* d = static_cast<uint8_t*>(dest); + auto* s = static_cast<const uint8_t*>(src); + if (d < s) { + for (size_t i = 0; i < n; i++) d[i] = s[i]; + } else { + for (size_t i = n; i > 0; i--) d[i - 1] = s[i - 1]; + } + return dest; +} + +void* memset(void* dest, int val, size_t n) { + auto* d = static_cast<uint8_t*>(dest); + for (size_t i = 0; i < n; i++) d[i] = static_cast<uint8_t>(val); + return dest; +} + +int memcmp(const void* s1, const void* s2, size_t n) { + auto* a = static_cast<const uint8_t*>(s1); + auto* b = static_cast<const uint8_t*>(s2); + for (size_t i = 0; i < n; i++) { + // Cast to int before subtracting: uint8_t → int is safe, range [0, 255] + if (a[i] != b[i]) return static_cast<int>(a[i]) - static_cast<int>(b[i]); + } + return 0; +} + +size_t strlen(const char* s) { + size_t len = 0; + while (s[len]) len++; + return len; +} + +int strcmp(const char* s1, const char* s2) { + // Cast to unsigned before comparison — char may be signed, + // and unsigned subtraction gives the correct lexicographic ordering. + const auto* u1 = reinterpret_cast<const uint8_t*>(s1); + const auto* u2 = reinterpret_cast<const uint8_t*>(s2); + while (*u1 && *u1 == *u2) { ++u1; ++u2; } + return static_cast<int>(*u1) - static_cast<int>(*u2); +} + +char* strcpy(char* dest, const char* src) { + char* ret = dest; + while (*src) *dest++ = *src++; + *dest = '\0'; + return ret; +} + +char* strncpy(char* dest, const char* src, size_t n) { + size_t i = 0; + for (; i < n && src[i]; i++) dest[i] = src[i]; + for (; i < n; i++) dest[i] = '\0'; + return dest; +} + +} // extern "C" diff --git a/scripts/configure.py b/scripts/configure.py new file mode 100755 index 0000000..053aac9 --- /dev/null +++ b/scripts/configure.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +""" +configure.py — MyOS Kernel Configuration Generator + +Reads .config (or a specified config file), validates dependencies, +and generates: + - include/kernel/config.h (C++ preprocessor defines) + - config_generated.mk (Makefile variables) + +Usage: + python3 scripts/configure.py # reads .config + python3 scripts/configure.py myconfig # reads myconfig + python3 scripts/configure.py --arch x86_64 # set arch, use default config + python3 scripts/configure.py --check # validate only, don't generate + +The ARCH variable is NOT set in .config — it comes from the build system. +This script receives it via --arch and injects it into the generated output. +""" + +import sys +import os +import argparse +from pathlib import Path +from datetime import datetime + +# ============================================================================ +# Dependency rules +# +# Format: (option, requires, description) +# If 'option' is 'y', then 'requires' must also be 'y' +# ============================================================================ + +DEPENDENCIES = [ + ("FS_EXT2", "DRIVER_VIRTIO_BLK", "ext2 requires a block device driver"), + ("FS_FAT32", "DRIVER_VIRTIO_BLK", "FAT32 requires a block device driver"), + ("FEATURE_NETWORK", "DRIVER_VIRTIO_NET", "networking requires a network driver"), + ("FEATURE_MODULES", "FS_INITRAMFS", "loadable modules require a filesystem"), + ("FEATURE_SMP", None, None), # no deps, just a marker +] + +# ============================================================================ +# Conflict rules +# +# Format: (option_a, option_b, description) +# Both cannot be 'y' at the same time +# ============================================================================ + +CONFLICTS = [ + # None yet — add as needed +] + +# ============================================================================ +# Valid values for non-boolean options +# ============================================================================ + +VALID_VALUES = { + "PMM_TYPE": ["buddy", "bitmap"], + "HEAP_TYPE": ["slab", "simple"], +} + +# ============================================================================ +# Options that must be present (with defaults if missing) +# ============================================================================ + +DEFAULTS = { + "DEBUG_SERIAL": "n", + "DEBUG_VERBOSE_BOOT": "n", + "DEBUG_PAGE_ALLOC": "n", + "DEBUG_SCHEDULER": "n", + "DEBUG_SYSCALL_TRACE": "n", + "MAX_CPUS": "4", + "KERNEL_STACK_SIZE": "16384", + "PMM_TYPE": "buddy", + "HEAP_TYPE": "slab", + "DRIVER_FBCON": "y", + "DRIVER_UART_16550": "n", + "DRIVER_PL011_UART": "n", + "DRIVER_PS2KBD": "n", + "DRIVER_VIRTIO_BLK": "n", + "DRIVER_AHCI": "n", + "DRIVER_USB": "n", + "DRIVER_VIRTIO_NET": "n", + "FS_INITRAMFS": "y", + "FS_TMPFS": "y", + "FS_EXT2": "n", + "FS_FAT32": "n", + "FBCON_FG_COLOR": "0x00CCCCCC", + "FBCON_BG_COLOR": "0x001A1A2E", + "FEATURE_SMP": "n", + "FEATURE_MODULES": "n", + "FEATURE_NETWORK": "n", + "FEATURE_POSIX_SIGNALS": "n", + "KERNEL_FILE_NAME": "kernel.elf", +} + + +def parse_config(path: str) -> dict: + """Parse a .config file into a dict.""" + config = {} + with open(path) as f: + for lineno, line in enumerate(f, 1): + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" not in line: + print(f"WARNING: {path}:{lineno}: malformed line: {line}", + file=sys.stderr) + continue + key, val = line.split("=", 1) + key = key.strip() + val = val.strip() + # Strip surrounding quotes from string values + if len(val) >= 2 and val[0] == '"' and val[-1] == '"': + val = val[1:-1] + config[key] = val + return config + + +def validate(config: dict) -> list: + """Validate config. Returns list of error strings (empty = ok).""" + errors = [] + + # Check dependencies + for opt, requires, desc in DEPENDENCIES: + if requires is None: + continue + if config.get(opt) == "y" and config.get(requires) != "y": + errors.append(f"{opt}=y requires {requires}=y ({desc})") + + # Check conflicts + for opt_a, opt_b, desc in CONFLICTS: + if config.get(opt_a) == "y" and config.get(opt_b) == "y": + errors.append(f"{opt_a} conflicts with {opt_b} ({desc})") + + # Check valid values + for key, valid in VALID_VALUES.items(): + val = config.get(key) + if val is not None and val not in valid: + errors.append(f"{key}={val} is invalid. Must be one of: {valid}") + + # Check numeric values + for key in ("MAX_CPUS", "KERNEL_STACK_SIZE"): + val = config.get(key) + if val is not None: + try: + int(val) + except ValueError: + errors.append(f"{key}={val} must be a number") + + return errors + + +def generate_config_h(config: dict, arch: str, output: str): + """Generate include/kernel/config.h from config dict.""" + lines = [ + "#pragma once", + "", + "// ============================================================================", + "// config.h — Auto-generated by scripts/configure.py", + f"// Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + f"// Architecture: {arch}", + "//", + "// DO NOT EDIT — edit .config and re-run make instead.", + "// ============================================================================", + "", + ] + + # Group by prefix for readability + groups = {} + for key, val in sorted(config.items()): + prefix = key.split("_")[0] + if prefix not in groups: + groups[prefix] = [] + groups[prefix].append((key, val)) + + for prefix, items in groups.items(): + lines.append(f"// ── {prefix} " + "─" * (60 - len(prefix))) + for key, val in items: + if val == "y": + lines.append(f"#define CONFIG_{key} 1") + elif val == "n": + lines.append(f"/* #undef CONFIG_{key} */") + elif val.startswith("0x"): + # Hex constant + lines.append(f"#define CONFIG_{key} {val}") + else: + # Try numeric + try: + int(val) + lines.append(f"#define CONFIG_{key} {val}") + except ValueError: + # String value — quote it for some, raw for others + lines.append(f'#define CONFIG_{key} "{val}"') + # For enum-like options, also define a variant for preprocessor comparisons + if key in VALID_VALUES: + lines.append(f'#define CONFIG_{key}_{val.upper()} 1') + + lines.append("") + + os.makedirs(os.path.dirname(output), exist_ok=True) + with open(output, "w") as f: + f.write("\n".join(lines) + "\n") + + +def generate_config_mk(config: dict, arch: str, output: str): + """Generate config_generated.mk from config dict.""" + lines = [ + "# ============================================================================", + "# config_generated.mk — Auto-generated by scripts/configure.py", + f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + "# DO NOT EDIT — edit .config and re-run make instead.", + "# ============================================================================", + "", + ] + + for key, val in sorted(config.items()): + lines.append(f"CONFIG_{key} := {val}") + + with open(output, "w") as f: + f.write("\n".join(lines) + "\n") + + +def main(): + parser = argparse.ArgumentParser(description="MyOS kernel configuration generator") + parser.add_argument("config_file", nargs="?", default=".config", + help="Path to config file (default: .config)") + parser.add_argument("--arch", default=None, + help="Target architecture (x86_64 or aarch64)") + parser.add_argument("--check", action="store_true", + help="Validate only, don't generate files") + parser.add_argument("--config-h", default="include/kernel/config.h", + help="Output path for config.h") + parser.add_argument("--config-mk", default="config_generated.mk", + help="Output path for config_generated.mk") + args = parser.parse_args() + + # ── Read config ───────────────────────────────────────────────────── + + if not os.path.exists(args.config_file): + print(f"ERROR: Config file '{args.config_file}' not found.", + file=sys.stderr) + print(f" Create one with: cp configs/default_x86_64 .config", + file=sys.stderr) + sys.exit(1) + + config = parse_config(args.config_file) + + # ── Apply defaults for missing keys ───────────────────────────────── + + for key, default_val in DEFAULTS.items(): + if key not in config: + config[key] = default_val + + # ── Determine architecture ────────────────────────────────────────── + + arch = args.arch or os.environ.get("ARCH", "x86_64") + + # ── Validate ──────────────────────────────────────────────────────── + + errors = validate(config) + if errors: + print("Configuration errors:", file=sys.stderr) + for e in errors: + print(f" ERROR: {e}", file=sys.stderr) + sys.exit(1) + + if args.check: + print("Configuration is valid.") + sys.exit(0) + + # ── Generate ──────────────────────────────────────────────────────── + + generate_config_h(config, arch, args.config_h) + generate_config_mk(config, arch, args.config_mk) + + # ── Summary ───────────────────────────────────────────────────────── + + enabled = [k for k, v in config.items() if v == "y"] + disabled = [k for k, v in config.items() if v == "n"] + other = [k for k, v in config.items() if v not in ("y", "n")] + + print(f"Configuration generated for {arch}:") + print(f" {len(enabled)} options enabled, {len(disabled)} disabled, " + f"{len(other)} custom values") + print(f" → {args.config_h}") + print(f" → {args.config_mk}") + + +if __name__ == "__main__": + main() diff --git a/scripts/create_disk.sh b/scripts/create_disk.sh new file mode 100755 index 0000000..1e5d1f3 --- /dev/null +++ b/scripts/create_disk.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# ============================================================================ +# create_disk.sh — Create a bootable UEFI disk image +# +# Usage: create_disk.sh <arch> <efi_binary> <kernel_elf> <output_img> +# +# Creates a GPT disk image with a FAT32 EFI System Partition containing: +# - EFI/BOOT/BOOTx64.EFI (or BOOTAA64.EFI) — our UEFI loader +# - kernel.elf — the kernel binary +# ============================================================================ + +set -e + +ARCH="$1" +EFI_BIN="$2" +KERNEL_ELF="$3" +OUTPUT="$4" + +if [ -z "$ARCH" ] || [ -z "$EFI_BIN" ] || [ -z "$KERNEL_ELF" ] || [ -z "$OUTPUT" ]; then + echo "Usage: $0 <arch> <efi_binary> <kernel_elf> <output_img>" + exit 1 +fi + +# Determine the default EFI boot path +if [ "$ARCH" = "x86_64" ]; then + EFI_BOOT_NAME="BOOTX64.EFI" +elif [ "$ARCH" = "aarch64" ]; then + EFI_BOOT_NAME="BOOTAA64.EFI" +else + echo "Unknown architecture: $ARCH" + exit 1 +fi + +echo "Creating disk image for $ARCH..." + +# Image size: 64 MiB (more than enough) +IMG_SIZE=$((64 * 1024 * 1024)) + +# Create empty image +dd if=/dev/zero of="$OUTPUT" bs=1M count=64 status=none + +# Create GPT partition table with one EFI System Partition +# Use sgdisk if available, otherwise fall back to parted +if command -v sgdisk &> /dev/null; then + sgdisk --clear \ + --new=1:2048:131038 --typecode=1:ef00 --change-name=1:"EFI System" \ + "$OUTPUT" > /dev/null 2>&1 +elif command -v parted &> /dev/null; then + parted -s "$OUTPUT" mklabel gpt + parted -s "$OUTPUT" mkpart "EFI System" fat32 1MiB 63MiB + parted -s "$OUTPUT" set 1 esp on +else + echo "ERROR: Need sgdisk or parted to create GPT partition" + exit 1 +fi + +# Format the ESP partition as FAT32 +# Extract partition offset (sector 2048 * 512 = 1 MiB) +PART_OFFSET=$((2048 * 512)) +PART_SIZE=$(((131038 - 2048 + 1) * 512)) + +# Create a separate FAT32 filesystem image +FAT_IMG="${OUTPUT}.fat" +dd if=/dev/zero of="$FAT_IMG" bs=512 count=$((PART_SIZE / 512)) status=none +mkfs.vfat -F 32 "$FAT_IMG" > /dev/null 2>&1 + +# Copy files into the FAT filesystem using mtools +# Set up mtools config for this image +export MTOOLSRC="$(mktemp)" +echo "drive c: file=\"$FAT_IMG\" offset=0" > "$MTOOLSRC" + +# Create EFI boot directory and copy files +mmd -i "$FAT_IMG" ::EFI +mmd -i "$FAT_IMG" ::EFI/BOOT +mcopy -i "$FAT_IMG" "$EFI_BIN" "::EFI/BOOT/$EFI_BOOT_NAME" +mcopy -i "$FAT_IMG" "$KERNEL_ELF" "::kernel.elf" + +# List contents for verification +echo "ESP contents:" +mdir -i "$FAT_IMG" ::/ -/ 2>/dev/null || true + +# Write the FAT image into the partition slot +dd if="$FAT_IMG" of="$OUTPUT" bs=512 seek=2048 conv=notrunc status=none + +# Clean up +rm -f "$FAT_IMG" "$MTOOLSRC" + +echo "Disk image created: $OUTPUT" diff --git a/scripts/run_qemu.sh b/scripts/run_qemu.sh new file mode 100755 index 0000000..f48f445 --- /dev/null +++ b/scripts/run_qemu.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# ============================================================================ +# run_qemu.sh — Launch QEMU with UEFI firmware +# +# Usage: run_qemu.sh <arch> <disk_image> +# +# Requirements: +# x86_64: OVMF firmware (usually in /usr/share/OVMF/ or /usr/share/edk2/) +# aarch64: AAVMF firmware (usually in /usr/share/AAVMF/ or qemu-efi-aarch64) +# ============================================================================ + +set -e + +ARCH="$1" +DISK="$2" + +if [ -z "$ARCH" ] || [ -z "$DISK" ]; then + echo "Usage: $0 <arch> <disk_image>" + exit 1 +fi + +# ── Find UEFI firmware ───────────────────────────────────────────────────── + +find_firmware() { + local candidates=("$@") + for path in "${candidates[@]}"; do + if [ -f "$path" ]; then + echo "$path" + return 0 + fi + done + return 1 +} + +if [ "$ARCH" = "x86_64" ]; then + QEMU=qemu-system-x86_64 + + # Common OVMF paths across distros + OVMF=$(find_firmware \ + /usr/share/OVMF/OVMF_CODE.fd \ + /usr/share/edk2/ovmf/OVMF_CODE.fd \ + /usr/share/edk2-ovmf/x64/OVMF_CODE.fd \ + /usr/share/qemu/OVMF_CODE.fd \ + /usr/share/OVMF/OVMF_CODE_4M.fd \ + ) || { + echo "ERROR: Cannot find OVMF firmware." + echo "Install it: apt install ovmf OR dnf install edk2-ovmf" + exit 1 + } + + echo "Using OVMF: $OVMF" + echo "Starting QEMU x86_64..." + echo "Press Ctrl+A, X to exit QEMU" + echo "─────────────────────────────────────" + + exec $QEMU \ + -machine q35 \ + -cpu qemu64 \ + -m 256M \ + -drive if=pflash,format=raw,readonly=on,file="$OVMF" \ + -drive format=raw,file="$DISK" \ + -serial stdio \ + -no-reboot \ + -no-shutdown \ + -d int,cpu_reset \ + -D qemu_log.txt + +elif [ "$ARCH" = "aarch64" ]; then + QEMU=qemu-system-aarch64 + + # Common AAVMF / EDK2 paths + AAVMF=$(find_firmware \ + /usr/share/AAVMF/AAVMF_CODE.fd \ + /usr/share/qemu-efi-aarch64/QEMU_EFI.fd \ + /usr/share/edk2/aarch64/QEMU_EFI.fd \ + /usr/share/edk2-aarch64/QEMU_EFI.fd \ + ) || { + echo "ERROR: Cannot find AAVMF firmware." + echo "Install it: apt install qemu-efi-aarch64 OR dnf install edk2-aarch64" + exit 1 + } + + echo "Using AAVMF: $AAVMF" + echo "Starting QEMU aarch64..." + echo "Press Ctrl+A, X to exit QEMU" + echo "─────────────────────────────────────" + + # Create a pflash variable store (AAVMF needs separate code + vars) + VARS_FILE="${DISK}.vars.fd" + if [ ! -f "$VARS_FILE" ]; then + dd if=/dev/zero of="$VARS_FILE" bs=1M count=64 status=none + fi + + exec $QEMU \ + -machine virt \ + -cpu cortex-a72 \ + -m 256M \ + -drive if=pflash,format=raw,readonly=on,file="$AAVMF" \ + -drive if=pflash,format=raw,file="$VARS_FILE" \ + -drive format=raw,file="$DISK" \ + -serial stdio \ + -no-reboot \ + -no-shutdown \ + -d int,cpu_reset \ + -D qemu_log.txt + +else + echo "Unknown architecture: $ARCH" + exit 1 +fi |
