diff --git a/.gitignore b/.gitignore index d575976..0e94061 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /build .DS_Store -/docs \ No newline at end of file +/docs +map_file.map diff --git a/Makefile b/Makefile index 12e8753..65b395e 100644 --- a/Makefile +++ b/Makefile @@ -68,4 +68,7 @@ docs: doc: open "docs/html/index.html" +debug: + make KFLAGS="-DUSE_KTESTS" + .PHONY: all clean qemu docker docs diff --git a/README.md b/README.md index 68ead10..0060975 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # AstraKernel ~ A minimal ARM kernel for QEMU [![GitHub release (including pre-releases)](https://img.shields.io/github/v/release/sandbox-science/AstraKernel?include_prereleases)](https://github.com/sandbox-science/AstraKernel/releases) +[![Doxygen Docs](https://github.com/sandbox-science/AstraKernel/actions/workflows/static.yml/badge.svg?branch=main)](https://github.com/sandbox-science/AstraKernel/actions/workflows/static.yml) AstraKernel is a minimal experimental kernel written in modern C and ARM assembly, designed to run on **QEMU’s Versatile AB/PB board with a Cortex‑A8 CPU override (-cpu cortex-a8)**. This setup keeps the diff --git a/include/memory.h b/include/memory.h index 4aa40cf..d0c1e5b 100644 --- a/include/memory.h +++ b/include/memory.h @@ -7,11 +7,37 @@ extern "C" { #endif + /** + * @enum block_state + * @brief Enumeration of block states in the memory allocator. + * + * This enum defines the possible states of a memory block in the + * kernel memory allocator. + */ + enum block_state { + BLOCK_FREE = 0, /**< Block is free and available for allocation. */ + BLOCK_USED /**< Block is currently allocated and in use. */ + }; - void kmalloc_init(void *start, void *limit); - void* kmalloc(size_t size); - size_t kmalloc_remaining(void); + /** + * @brief Header structure for memory blocks in the allocator. + * + * Each allocated or free block in the kernel memory allocator is + * preceded by a header that contains metadata about the block. + */ + struct header { + size_t size; /**< Size of the memory block (excluding header). */ + enum block_state state; /**< State of the block (free or used). */ + struct header *next; /**< Pointer to the next block in the linked list. */ + struct header *prev; /**< Pointer to the previous block in the linked list. */ + }; + struct header *kmalloc_get_head(void); + + void kmalloc_init(void *start, void *limit); + void* kmalloc(size_t size); + void kfree(void *block); + int kmalloc_test(void); #ifdef __cplusplus } #endif diff --git a/src/kernel/kernel.c b/src/kernel/kernel.c index 3dc3c5d..fb84841 100644 --- a/src/kernel/kernel.c +++ b/src/kernel/kernel.c @@ -71,6 +71,11 @@ void not_main(void) void *p2 = kmalloc(48); printf("kmalloc(10) addr: %p\n", p); printf("kmalloc(48) addr: %p\n", p2); + struct header *h = (struct header *)p - 1; + struct header *h2 = (struct header *)p2 - 1; + printf("size of kmalloc(10) %d\nsize of kmalloc(48) %d\n", h->size, h2->size); + kfree(p); + kfree(p2); char *buf = kmalloc(1); if (buf) @@ -114,20 +119,21 @@ void not_main(void) #define TIMER_TICK_TEST not_main() #define SANITY_CHECK irq_sanity_check() #define CALL_SVC_0 __asm__ volatile ("svc #0") +#define KMALLOC_TEST kmalloc_test() // Entry point for the kernel void kernel_main(void) { clear(); kmalloc_init(&__heap_start__, &__heap_end__); - kmalloc_remaining(); /* TESTS */ - #ifdef USE_KTESTS - SANITY_CHECK; - CALL_SVC_0; - TIMER_TICK_TEST; - #endif +#ifdef USE_KTESTS + SANITY_CHECK; + CALL_SVC_0; + KMALLOC_TEST; + TIMER_TICK_TEST; +#endif /* Back to normal operations */ init_message(); diff --git a/src/kernel/memory.c b/src/kernel/memory.c index c1510a1..331d187 100644 --- a/src/kernel/memory.c +++ b/src/kernel/memory.c @@ -1,21 +1,71 @@ +/** + * @copyright Copyright (c) 2025 SandBox Science + * @license GPL-3.0 license + * @author Christopher Dedman Rollet + * + * @file memory.c + * @brief Simple kernel memory allocator (kmalloc/kfree) implementation. + * + * This file implements a basic dynamic memory allocator for the kernel, + * providing functions to allocate and free memory blocks. The allocator + * uses a linked list of memory block headers to manage free and used + * memory regions within a predefined heap area. + * + * The implementation includes: + * - `kmalloc_init()`: Initializes the heap region for dynamic allocation. + * - `kmalloc()`: Allocates a block of memory of a specified size. + * - `kfree()`: Frees a previously allocated block of memory. + * + * The allocator uses a first-fit strategy and merges adjacent free blocks + * to reduce fragmentation. + * + * @note These implementations are inspired by + * https://github.com/dthain/basekernel/blob/master/kernel/kmalloc.c + */ #include "memory.h" #include "panic.h" #include "printf.h" -#include - -// Default alignment: at least pointer size; 16 is a good general default. -static const size_t KMALLOC_ALIGN = (16 < sizeof(void*) ? sizeof(void*) : 16); - -static _Atomic uintptr_t heap_cur = 0; // atomic in case of future multi core -static uintptr_t heap_end = 0; +static struct header *head = NULL; +/**< Default alignment: at least pointer size; 16 is a good general default. */ +static const size_t KMALLOC_ALIGN = (16 < sizeof(void*) ? sizeof(void*) : 16); +/** + * @brief Align a pointer up to the next multiple of a given alignment. + * + * This function takes a pointer value `x` and aligns it up to the next + * multiple of `align`. The `align` parameter must be a power of 2. + * + * @param x The pointer value to align. + * @param align The alignment value (must be a power of 2). + * + * @return uintptr_t The aligned pointer value. + */ static inline uintptr_t align_up_uintptr(uintptr_t x, size_t align) { return (x + (align - 1)) & ~(uintptr_t)(align - 1); } -void kmalloc_init(void *start, void *limit) +/** + * @brief Initialize the kernel heap for dynamic memory allocation. + * + * This function sets up the initial heap region for `kmalloc`. It aligns + * the start and end addresses according to the kernel alignment + * requirements and initializes the first free memory block. + * + * @param start Pointer to the beginning of the heap memory region. + * Must be less than `limit`. + * @param limit Pointer to the end of the heap memory region. + * Must be greater than `start`. + * + * @note If the heap range is invalid or too small, this function + * will call `kernel_panic()`. + * + * @note The pointers `start` and `limit` are marked `restrict` to indicate + * that their memory regions do not overlap, allowing the compiler + * to optimize pointer arithmetic safely. + */ +void kmalloc_init(void *restrict start, void *restrict limit) { const uintptr_t s = (uintptr_t)start; const uintptr_t l = (uintptr_t)limit; @@ -23,24 +73,77 @@ void kmalloc_init(void *start, void *limit) { kernel_panic("kmalloc_init: invalid heap range"); } - + const uintptr_t aligned_start = align_up_uintptr(s, KMALLOC_ALIGN); const uintptr_t aligned_end = l & ~(KMALLOC_ALIGN - 1); - if (aligned_end <= aligned_start) + if ( (aligned_end - aligned_start) <= sizeof(struct header) ) { kernel_panic("kmalloc_init: heap too small after alignment"); } - heap_cur = aligned_start; - heap_end = aligned_end; + head = (struct header *)aligned_start; + *head = (struct header) + { + .size = aligned_end - aligned_start - sizeof(struct header), + .state = BLOCK_FREE, + .next = NULL, + .prev = NULL, + }; puts("kmalloc init\n"); } +/** + * @brief Split a large free memory block into two parts if it is larger than the requested size. + * + * This function takes a pointer to a free memory block and divides it into + * two smaller blocks if the block's size is greater than the requested size + * plus the size needed for the allocator's bookkeeping structure. + * + * The first part of the block will be used to satisfy the allocation request, + * while the second part remains free and is linked back into the free list. + * + * @param curr Pointer to the current free block to be split. + * @param size Requested size in bytes for allocation. + */ +static void ksplit_block(struct header *curr, size_t size) +{ + struct header *new = (struct header *)((char *)curr + sizeof(struct header) + size); + *new = (struct header) + { + .state = BLOCK_FREE, + .size = curr->size - size - sizeof(struct header), + .prev = curr, + .next = curr->next + }; + + if (curr->next) + { + curr->next->prev = new; + } + curr->next = new; + curr->size = size; +} + +/** + * @brief Allocate a block of memory from the kernel heap. + * + * This function searches the heap for the first free block that is large + * enough to satisfy the requested `size`. If the block is larger than needed, + * it is split into an allocated block and a new free block. + * @param size The number of bytes to allocate. Must be > 0. + * + * @return Pointer to the usable memory area of the allocated block, or + * NULL if `size` is zero. + * + * @note If no suitable block is found, the function will call `kernel_panic`. + * @note The returned pointer points immediately after the block header. + * + */ void *kmalloc(size_t size) { - if (size == 0 || heap_cur == 0 || heap_end == 0) + if (size == 0) { return NULL; // not initialized } @@ -48,18 +151,295 @@ void *kmalloc(size_t size) // Round size up to alignment size = (size + (KMALLOC_ALIGN - 1)) & ~(size_t)(KMALLOC_ALIGN - 1); - if (size > (size_t)(heap_end - heap_cur)) + struct header *curr = head; + while(curr != NULL) + { + if (curr->state != BLOCK_USED && curr->size >= size) + { + break; + } + curr = curr->next; + } + + if (curr == NULL) { kernel_panic("kmalloc: out of memory"); } - void *ptr = (void *)heap_cur; - heap_cur += (uintptr_t)size; + if (curr->size >= size + sizeof(struct header) + KMALLOC_ALIGN) + { + ksplit_block(curr, size); + } + curr->state = BLOCK_USED; + + return (void*)((char*)curr + sizeof(struct header)); +} + +struct header *kmalloc_get_head(void) +{ + return head; +} + +/** + * @brief Merge a block with the its next free block. + * + * if the the block exist and both the current and next + * block are free, merge the next block with the current block. + * Keep merging while the next block is free. + * + * @param curr Pointer to the current free block. + */ +static void kmerge(struct header *curr) +{ + if (!curr || curr->state != BLOCK_FREE) + { + return; + } + + while (curr->next && curr->next->state == BLOCK_FREE) + { + curr->size += sizeof(struct header) + curr->next->size; + curr->next = curr->next->next; + if (curr->next) + { + curr->next->prev = curr; + } + } +} + +/** + * @brief Free a previously allocated block of memory. + * + * Marks the block as free and attempts to merge it with adjacent free blocks + * to reduce fragmentation. + * + * @param block Pointer to the memory previously returned by `kmalloc`. + * Must not be NULL. + * + * @note If `block` is NULL, the function does nothing. + * @note If `block` does not correspond to a valid allocated block, + * the function calls `kernel_panic`. + */ +void kfree(void *block) +{ + if (!block) + { + return; + } + + struct header *curr = (struct header*)((char*)block - sizeof(struct header)); + + if (curr->state != BLOCK_USED) + { + kernel_panic("kfree: invalid kfree"); + } + + curr->state = BLOCK_FREE; + kmerge(curr); + kmerge(curr->prev); +} + + +/** --------------------------------------------- + * kmalloc/kfree tests + * @todo Find a better way for testing functions + * --------------------------------------------- + */ +#define TEST_HEAP_SIZE (1024 * 1024) +static uint8_t heap_space[TEST_HEAP_SIZE]; +static size_t initial_heap_size = 0; + +// --- Setup and teardown --- +static void setup() +{ + kmalloc_init(heap_space, heap_space + sizeof(heap_space)); + initial_heap_size = kmalloc_get_head()->size; +} + +static void tear_down() +{ +} + +// --- Basic alloc/free correctness --- +static int kmalloc_test_single_alloc() +{ + void *ptr = kmalloc(128); + if (!ptr) + { + printf("kmalloc returned NULL\n"); + return 0; + } + struct header *head = kmalloc_get_head(); + if (head->state != BLOCK_USED) + { + printf("Block not marked as used after kmalloc\n"); + return 0; + } + return 1; +} + +static int kmalloc_test_single_alloc_and_free() +{ + void *ptr = kmalloc(128); + kfree(ptr); + struct header *head = kmalloc_get_head(); + if (head->state != BLOCK_FREE) + { + printf("Block not free after kfree\n"); + return 0; + } + if (head->size != initial_heap_size) + { + printf("Block size incorrect after kfree: got %lu expected %lu\n", head->size, initial_heap_size); + return 0; + } + return 1; +} - return ptr; +static int kmalloc_test_merge_free_blocks() +{ + void *a = kmalloc(128); + void *b = kmalloc(128); + kfree(a); + kfree(b); + struct header *head = kmalloc_get_head(); + if (head->state != BLOCK_FREE) + { + printf("Head not free after merging\n"); + return 0; + } + if (head->size != initial_heap_size) + { + printf("Merged size incorrect: got %lu expected %lu\n", head->size, initial_heap_size); + return 0; + } + return 1; } -size_t kmalloc_remaining(void) +// --- Extended kfree edge case tests --- +// static int kfree_null_pointer_test() +// { +// struct header *before = kmalloc_get_head(); +// kfree(NULL); +// struct header *after = kmalloc_get_head(); +// if (panic_called) +// { +// printf("PANIC triggered on kfree(NULL)\n"); +// return 0; +// } +// if (before != after) +// return 0; +// return 1; +// } + +// static int kfree_double_free_test() +// { +// void *ptr = kmalloc(128); +// kfree(ptr); +// kfree(ptr); // second free should panic +// if (!panic_called) +// { +// printf("Expected panic on double free, but none occurred\n"); +// return 0; +// } +// return 1; +// } + +// static int kfree_invalid_pointer_inside_heap_test() +// { +// void *ptr = kmalloc(128); +// uint8_t *invalid = (uint8_t *)ptr + 8; +// kfree(invalid); +// if (!panic_called) +// { +// printf("Expected panic on invalid pointer inside heap\n"); +// return 0; +// } +// return 1; +// } + +// static int kfree_invalid_pointer_outside_heap_test() +// { +// uint8_t *invalid = heap_space - 32; // outside heap +// kfree(invalid); +// if (!panic_called) +// { +// printf("Expected panic on invalid pointer outside heap\n"); +// return 0; +// } +// return 1; +// } + +static int kfree_merge_order_test() { - return (heap_end > heap_cur) ? (size_t)(heap_end - heap_cur) : 0; + void *a = kmalloc(128); + void *b = kmalloc(128); + void *c = kmalloc(128); + + // Free middle first, then ends + kfree(b); + kfree(a); + kfree(c); + + struct header *head = kmalloc_get_head(); + if (head->state != BLOCK_FREE) + { + printf("Heap not free after out-of-order merges\n"); + return 0; + } + if (head->size != initial_heap_size) + { + printf("Heap size incorrect after out-of-order merge: got %lu expected %lu\n", head->size, initial_heap_size); + return 0; + } + return 1; +} + +// --- Main test runner --- +int kmalloc_test() +{ + printf("Running kmalloc tests...\n"); + + int (*tests[])(void) = { + kmalloc_test_single_alloc, + kmalloc_test_single_alloc_and_free, + kmalloc_test_merge_free_blocks, + // kfree_null_pointer_test, + // kfree_double_free_test, + // kfree_invalid_pointer_inside_heap_test, + // kfree_invalid_pointer_outside_heap_test, + kfree_merge_order_test, + }; + + const char *names[] = { + "single_alloc", + "single_alloc_and_free", + "merge_free_blocks", + "kfree_null_pointer", + "kfree_double_free", + "kfree_invalid_inside_heap", + "kfree_invalid_outside_heap", + "kfree_merge_order", + }; + + int num_tests = sizeof(tests) / sizeof(tests[0]); + int test_passed = 0; + + for (int i = 0; i < num_tests; i++) + { + printf("Running test %d (%s): ", i, names[i]); + setup(); + int result = tests[i](); + tear_down(); + + if (!result) + { + printf("FAILED\n"); + return 1; + } + printf("PASSED\n"); + test_passed++; + } + printf("%d/%d tests passed\n", test_passed, num_tests); + return 0; } diff --git a/src/user/printf.c b/src/user/printf.c index 471156f..dd0dbb0 100644 --- a/src/user/printf.c +++ b/src/user/printf.c @@ -4,6 +4,8 @@ * * This file implements formatted output and simple line input using * the QEMU VersatileAB UART0. It is designed for early boot and debugging. + * + * @todo Refactor this file for cleaner function implementation. */ #include "printf.h"