From 08c95397293854cb7c8bb8959e509c2656ba8339 Mon Sep 17 00:00:00 2001 From: Mazunki Hoksaas Date: Tue, 17 Mar 2026 11:44:55 +0100 Subject: [PATCH 1/5] halt on unexpected syscalls `Expects()` doesn't actually shut down the service, causing an infinite loop of strace errors --- src/musl/common.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/musl/common.hpp b/src/musl/common.hpp index 40ac85391..1970f65b7 100644 --- a/src/musl/common.hpp +++ b/src/musl/common.hpp @@ -74,6 +74,7 @@ template inline auto strace(Fn func, [[maybe_unused]]const char* name, Args&&... args) { if (!kernel::state().allow_syscalls) { fprintf(stderr, "Syscalls not allowed here. Unexpected call to %s - terminating\n", name); + os::halt(); Expects(kernel::state().allow_syscalls); } From 3b0b641d9940452ac942272b030d79871cc71a63 Mon Sep 17 00:00:00 2001 From: Mazunki Hoksaas Date: Tue, 24 Mar 2026 14:31:59 +0100 Subject: [PATCH 2/5] support memory above 4G The multiboot specification explicitly states that `mem_upper` is not guaranteed to be the correct value for mem_upper. Additionally, placing the heap in-between `mem_lower` and `mem_upper` only really makes sense on 32-bit systems. By using memory mapped pages we can pick the largest consecutive region of memory available. Selecting 1_000_000 as the minimum address is a temporary hack since it's not immediately obvious how to get the correct address for virtual mappings that are still not initialized (such as LiveUpdate and SystemLog). The correct solution here would be to make these regions dynamic in size, initialize them earlier so the regions are already claimed, or re-initialize the heap. This is only necessary for systems that have less than 4G of memory. See-Also: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html See-Also: https://wiki.osdev.org/Multiboot --- api/boot/multiboot.h | 11 ++++- src/arch/x86_64/paging.cpp | 4 +- src/kernel/multiboot.cpp | 97 ++++++++++++++++++++++++++++---------- src/platform/x86_pc/os.cpp | 35 +++++++++----- 4 files changed, 108 insertions(+), 39 deletions(-) diff --git a/api/boot/multiboot.h b/api/boot/multiboot.h index ab4318778..240d6b9b6 100644 --- a/api/boot/multiboot.h +++ b/api/boot/multiboot.h @@ -195,12 +195,19 @@ typedef struct multiboot_info multiboot_info_t; struct multiboot_mmap_entry { - multiboot_uint32_t size; + multiboot_uint32_t size; // size of struct multiboot_uint64_t addr; - multiboot_uint64_t len; + multiboot_uint64_t len; // bytes available #define MULTIBOOT_MEMORY_AVAILABLE 1 #define MULTIBOOT_MEMORY_RESERVED 2 + #define MULTIBOOT_MEMORY_ACPI_RECLAIMABLE 3 + #define MULTIBOOT_MEMORY_NVS 4 + #define MULTIBOOT_MEMORY_BADRAM 5 multiboot_uint32_t type; + + [[nodiscard]] constexpr bool is_available() const noexcept { + return type == MULTIBOOT_MEMORY_AVAILABLE; + } } __attribute__((packed)); typedef struct multiboot_mmap_entry multiboot_memory_map_t; diff --git a/src/arch/x86_64/paging.cpp b/src/arch/x86_64/paging.cpp index cd3c78918..eeed3433e 100644 --- a/src/arch/x86_64/paging.cpp +++ b/src/arch/x86_64/paging.cpp @@ -376,6 +376,8 @@ uintptr_t mem::active_page_size(uintptr_t addr){ void allow_executable() { + // FIXME: this seems weird. is this only used for tests? + INFO2("* Allowing execute on %p -> %p", (void*) __exec_begin, (void*)__exec_end); @@ -391,7 +393,7 @@ void allow_executable() m.page_sizes = os::mem::Map::any_size; m.flags = os::mem::Access::execute | os::mem::Access::read; - os::mem::map(m, "ELF .text"); + os::mem::map(m, "Executable"); } /* TODO: Compiler warning unused diff --git a/src/kernel/multiboot.cpp b/src/kernel/multiboot.cpp index 295982016..491447430 100644 --- a/src/kernel/multiboot.cpp +++ b/src/kernel/multiboot.cpp @@ -22,6 +22,7 @@ #include #include #include +#include template static inline void _kfmt(fmt::string_view prefix, fmt::format_string fmtstr, Args&&... args) { @@ -122,9 +123,41 @@ uintptr_t _multiboot_free_begin(uintptr_t boot_addr) return multi_end; } +constexpr static inline const char* multiboot_memory_type_str(uint32_t type) noexcept { + // TODO: convert multiboot types to enum class + switch (type) { + case MULTIBOOT_MEMORY_AVAILABLE: + return "Available"; + case MULTIBOOT_MEMORY_ACPI_RECLAIMABLE: + return "ACPI Reclaimable"; + case MULTIBOOT_MEMORY_NVS: + return "ACPI Non-volatile Storage"; + case MULTIBOOT_MEMORY_BADRAM: + return "Bad RAM"; + case MULTIBOOT_MEMORY_RESERVED: + return "Reserved"; + default: + return "UNKNOWN"; + } +} + + +std::span _multiboot_memory_maps() { + auto* info = kernel::bootinfo(); + + auto* hardware_map = reinterpret_cast(info->mmap_addr); + const size_t entry_count = static_cast(info->mmap_length / sizeof(multiboot_memory_map_t)); + + return std::span { hardware_map, entry_count }; +} + void kernel::multiboot(uint32_t boot_addr) { - MYINFO("Booted with multiboot"); +#if defined(__x86_64) + MYINFO("Booted with multiboot x86_64"); +#else + MYINFO("Booted with multiboot x86"); +#endif auto* info = ::bootinfo(boot_addr); INFO2("* Boot flags: {:#x}", info->flags); @@ -152,36 +185,50 @@ void kernel::multiboot(uint32_t boot_addr) } if (info->flags & MULTIBOOT_INFO_MEM_MAP) { - INFO2("* Multiboot provided memory map ({} entries @ {})", - info->mmap_length / sizeof(multiboot_memory_map_t), - (const void*)(uintptr_t)info->mmap_addr); - std::span mmap { - reinterpret_cast(info->mmap_addr), - static_cast(info->mmap_length / sizeof(multiboot_memory_map_t)) - }; - - for (auto map : mmap) + auto* hardware_map = reinterpret_cast(info->mmap_addr); + const size_t entry_count = static_cast(info->mmap_length / sizeof(multiboot_memory_map_t)); + + INFO2("* Multiboot provided memory map ({} entries @ {})\n", entry_count, reinterpret_cast(hardware_map)); + + for (auto map : std::span{ hardware_map, entry_count }) { - const char* str_type = map.type & MULTIBOOT_MEMORY_AVAILABLE ? "FREE" : "RESERVED"; - const uintptr_t addr = map.addr; + const uintptr_t start = map.addr; const uintptr_t size = map.len; - INFO2(" {:#x} - {:#x} {} ({} KiB)", addr, addr + size - 1, str_type, size / 1024); - - if (not (map.type & MULTIBOOT_MEMORY_AVAILABLE)) { + const uintptr_t end = start + size - 1; - if (util::bits::is_aligned<4_KiB>(map.addr)) { - os::mem::map({addr, addr, os::mem::Access::read | os::mem::Access::write, size}, - "Reserved (Multiboot)"); - continue; - } + INFO2(" {:#16x} - {:#16x} ({} KiB): {}", start, end, size / 1024, multiboot_memory_type_str(map.type)); - // For non-aligned addresses, assign - os::mem::vmmap().assign_range({addr, addr + size - 1, "Reserved (Multiboot)"}); + // os::mem::map() does not accept non-aligned page addresses + if (not util::bits::is_aligned<4_KiB>(map.addr)) { + os::mem::vmmap().assign_range({start, start + size - 1, "UNALIGNED"}); + continue; } - else + + os::mem::Map rw_map = { /*.linear=*/start, /*.physical=*/start, /*.fl=*/os::mem::Access::read | os::mem::Access::write, /*.sz=*/size }; + switch (map.type) { - // Map as free memory - //os::mem::map_avail({map.addr, map.addr, {os::mem::Access::read | os::mem::Access::write}, map.len}, "Reserved (Multiboot)"); + case MULTIBOOT_MEMORY_ACPI_RECLAIMABLE: + os::mem::map(rw_map, "Multiboot (ACPI Reclaimable)"); + break; + case MULTIBOOT_MEMORY_NVS: + os::mem::map(rw_map, "Multiboot (ACPI Non-volatile Storage)"); + break; + case MULTIBOOT_MEMORY_BADRAM: + os::mem::map(rw_map, "Multiboot (Bad RAM)"); + break; + case MULTIBOOT_MEMORY_RESERVED: + os::mem::map(rw_map, "Multiboot (Reserved)"); + break; + + case MULTIBOOT_MEMORY_AVAILABLE: { + // these are mapped in src/platform/${platform}/os.cpp + break; + } + default: { + char buf[32]; // libc is not entirely initialized at this point + std::snprintf(buf, sizeof(buf), "Unknown memory map type: %d", map.type); + os::panic(buf); + } } } INFO2(""); diff --git a/src/platform/x86_pc/os.cpp b/src/platform/x86_pc/os.cpp index 9b2097889..ddb1b119d 100644 --- a/src/platform/x86_pc/os.cpp +++ b/src/platform/x86_pc/os.cpp @@ -48,6 +48,8 @@ extern uintptr_t _ELF_END_; // in kernel/os.cpp extern bool os_default_stdout; +extern std::span _multiboot_memory_maps(); + struct alignas(SMP_ALIGN) OS_CPU { uint64_t cycles_hlt = 0; }; @@ -125,10 +127,6 @@ void kernel::start(uint32_t boot_magic, uint32_t boot_addr) MYINFO("Total memory detected as %s ", util::Byte_r(kernel::memory_end()).to_string().c_str()); - // Give the rest of physical memory to heap - kernel::state().heap_max = kernel::memory_end() - 1; - assert(kernel::heap_begin() != 0x0 and kernel::heap_max() != 0x0); - PROFILE("Memory map"); // Assign memory ranges used by the kernel auto& memmap = os::mem::vmmap(); @@ -145,13 +143,28 @@ void kernel::start(uint32_t boot_magic, uint32_t boot_addr) memmap.assign_range({0x10000, 0x9d3ff, "Stack"}); #endif - // heap (physical) area - uintptr_t span_max = std::numeric_limits::max(); - uintptr_t heap_range_max_ = std::min(span_max, kernel::heap_max()); + multiboot_memory_map_t heap_map = {0,0,0,0}; + for (auto entry : _multiboot_memory_maps()) + { + if (not entry.is_available()) continue; + + if (entry.len > heap_map.len) { + heap_map = entry; + } + } + uintptr_t end = heap_map.addr + heap_map.len - 1; + + if (heap_map.addr < 0x1'000'000) { + kernel::state().heap_begin = std::max((uintptr_t)0x1'000'000, (uintptr_t)heap_map.addr); + kernel::state().heap_max = std::min(kernel::heap_max(), end); + } else { + kernel::state().heap_begin = heap_map.addr; + kernel::state().heap_max = end; + } - INFO2("* Assigning heap 0x%zx -> 0x%zx", kernel::heap_begin(), heap_range_max_); - memmap.assign_range({kernel::heap_begin(), heap_range_max_, - "Dynamic memory", kernel::heap_usage }); + + INFO2("* Assigning heap 0x%lx -> 0x%lx", kernel::heap_begin(), kernel::heap_max()); + memmap.assign_range({kernel::heap_begin(), kernel::heap_max(), "Heap", kernel::heap_usage }); MYINFO("Virtual memory map"); for (const auto& entry : memmap) @@ -182,7 +195,7 @@ void os::event_loop() __arch_poweroff(); } - +/* legacy boot is used when MULTIBOOT_BOOTLOADER_MAGIC is unset, see x86_pc/kernel_start.cpp */ void kernel::legacy_boot() { // Fetch CMOS memory info (unfortunately this is maximally 10^16 kb) From 8a395586bcf03e02a128b78fc67bd0626f35dd73 Mon Sep 17 00:00:00 2001 From: Mazunki Hoksaas Date: Fri, 27 Mar 2026 09:00:09 +0100 Subject: [PATCH 3/5] add missing namespace --- api/util/bitops.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/util/bitops.hpp b/api/util/bitops.hpp index 4c9c0e617..76e9088ac 100644 --- a/api/util/bitops.hpp +++ b/api/util/bitops.hpp @@ -247,7 +247,7 @@ inline bool is_aligned(uintptr_t A, uintptr_t ptr) noexcept return (ptr & (A - 1)) == 0; } -inline size_t upercent(size_t a, size_t b) noexcept +inline std::size_t upercent(std::size_t a, std::size_t b) noexcept { return (100 * a + b / 2) / b; } From b3e365f8b65660b9fc6dd4913f61a9e4c0377f52 Mon Sep 17 00:00:00 2001 From: Mazunki Hoksaas Date: Tue, 24 Mar 2026 14:43:19 +0100 Subject: [PATCH 4/5] update example unikernel to show mounted mappings --- example/src/main.cpp | 13 ++++++++++--- example/src/vm.json | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 example/src/vm.json diff --git a/example/src/main.cpp b/example/src/main.cpp index dabb79362..b2bfb97fb 100644 --- a/example/src/main.cpp +++ b/example/src/main.cpp @@ -1,9 +1,16 @@ +#include "kernel/memory.hpp" #include #include void Service::start(const std::string& args){ - printf("Args = %s\n", args.c_str()); - printf("Try giving the service less memory, eg. 10MB in vm.json\n"); - printf("Service done. Shutting down...\n"); + std::println("Hello from the example unikernel!"); + + std::println("Current virtual mappings:"); + for (const auto& entry : os::mem::vmmap()) + std::println(" {}", entry.second.to_string()); + + std::println("Tip: Try changing how much memory you give to the service in vm.json"); + std::println("Service done. Shutting down..."); + os::shutdown(); } diff --git a/example/src/vm.json b/example/src/vm.json new file mode 100644 index 000000000..7b976178d --- /dev/null +++ b/example/src/vm.json @@ -0,0 +1,3 @@ +{ + "mem": 16384 +} From b49ef65e140210e0c32223e918b1394be3a68351 Mon Sep 17 00:00:00 2001 From: Mazunki Hoksaas Date: Tue, 24 Mar 2026 14:44:07 +0100 Subject: [PATCH 5/5] add support for unassigning a mapping This will permit repurposing an already assigned mapping, allowing multiboot to preemptively assigning "free memory" regions, resizing ranges by deleting and recreating them, or making temporary regions. Could also be useful for NUMA. --- api/kernel/memmap.hpp | 6 ++++++ src/kernel/memmap.cpp | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/api/kernel/memmap.hpp b/api/kernel/memmap.hpp index 1009c2388..ac6cef6c0 100644 --- a/api/kernel/memmap.hpp +++ b/api/kernel/memmap.hpp @@ -339,6 +339,12 @@ class Memory_map { */ Fixed_memory_range& assign_range(const Fixed_memory_range::size_type size); + /** + * Removes a memory range previously defined + * Useful to redefine the purpose of a range + */ + void unassign_range(const Fixed_memory_range& range); + /** * Check if an address is within a range in the map * diff --git a/src/kernel/memmap.cpp b/src/kernel/memmap.cpp index 38ca1dd00..1c52cabfb 100644 --- a/src/kernel/memmap.cpp +++ b/src/kernel/memmap.cpp @@ -173,6 +173,21 @@ Fixed_memory_range& Memory_map::assign_range(Fixed_memory_range&& rng) { return new_entry.first->second; } +/////////////////////////////////////////////////////////////////////////////// +void Memory_map::unassign_range(const Fixed_memory_range& range) { + auto it = map_.find(range.addr_start()); + + if (it != map_.end()) { + if (it->second.addr_end() == range.addr_end()) { + map_.erase(it); + } else { + throw Memory_range_exception{"Range mismatch at address " + std::to_string(range.addr_start())}; + } + } else { + throw Memory_range_exception{"No range found to erase at " + std::to_string(range.addr_start())}; + } +} + /////////////////////////////////////////////////////////////////////////////// Fixed_memory_range& Memory_map::at(const Key key) { return const_cast(static_cast(this)->at(key));