Implement kernel memory allocator (kmalloc/kfree) and freestanding string library (AI)

- Added first-fit free-list allocator with block splitting and coalescing.
  Provides kmalloc(), kfree(), and kcalloc() for kernel-space dynamic memory.
- Each block carries an inline header with a magic value (0xCAFEBABE) for
  heap corruption detection, plus double-free checking.
- Memory is obtained from the paging subsystem in 4 KiB page increments.
  All allocations are 8-byte aligned with a 16-byte minimum block size.
- Created freestanding string.c with memset, memcpy, memmove, memcmp,
  strlen, strcmp, strncmp, strcpy, strncpy — replacing the unavailable
  libc implementations.
- Added documentation in docs/kmalloc.md.

Tested: kmalloc(64) returns 0xD0001010 (in kernel heap) and kfree succeeds.
Works with both 4 MiB and 128 MiB RAM.
This commit is contained in:
AI
2026-02-23 11:06:52 +00:00
parent fb61ab7c15
commit f63cd9eb3f
7 changed files with 551 additions and 1 deletions

View File

@@ -46,7 +46,7 @@ Once a task is completed, it should be checked off.
- [x] Implement a PIC handler
- [x] Create a physical memory allocator and mapper. The kernel should live in the upper last gigabyte of virtual memory. It should support different zones (e.g.: `SUB_16M`, `DEFAULT`, ...) These zones describe the region of memory that memory should be allocated in. If it is not possible to allocate in that region (because it is full, or has 0 capacity to begin with), it should fallback to another zone.
- [x] Create a paging subsystem. It should allow drivers to allocate and deallocate pages at will.
- [ ] Create a memory allocator. This should provide the kernel with `malloc` and `free`. Internally, it should use the paging subsystem to ensure that the address it returns have actual RAM paged to them.
- [x] Create a memory allocator. This should provide the kernel with `malloc` and `free`. Internally, it should use the paging subsystem to ensure that the address it returns have actual RAM paged to them.
- [ ] Create an initial driver architecture, allowing different drivers included in the kernel to test whether they should load or not.
- [ ] Create a VGA driver. On startup, some memory statistics should be displayed, as well as boot progress.
- [ ] Create subsystem for loading new processes in Ring 3.

61
docs/kmalloc.md Normal file
View File

@@ -0,0 +1,61 @@
# Kernel Memory Allocator (kmalloc)
## Overview
The kernel memory allocator provides `kmalloc()` and `kfree()` for dynamic memory allocation within the kernel. It uses the paging subsystem to obtain physical memory on demand.
## Architecture
### Free-List Allocator
The allocator maintains a singly-linked free list of available memory blocks, ordered by address. Each block carries an inline header:
```c
typedef struct block_header {
uint32_t size; // Usable bytes (excludes header)
uint32_t magic; // 0xCAFEBABE for integrity checking
struct block_header *next; // Next free block (free list only)
uint8_t is_free; // 1 = free, 0 = allocated
} block_header_t;
```
### Allocation Strategy
1. **First-fit search:** Walk the free list for the first block with `size >= requested`.
2. **Block splitting:** If the found block is significantly larger than needed, it is split into the allocated portion and a new free block.
3. **Page expansion:** If no suitable block exists, a new 4 KiB page is obtained from `paging_alloc_page()`.
All allocations are 8-byte aligned. Minimum block size is 16 bytes to limit fragmentation.
### Deallocation
1. The block header is located by subtracting `sizeof(block_header_t)` from the user pointer.
2. The block's magic number is verified to detect corruption.
3. The block is inserted back into the free list in address order.
4. **Coalescing:** Adjacent free blocks are merged to reduce fragmentation.
### Integrity Checks
- A magic value (`0xCAFEBABE`) in each block header detects heap corruption.
- Double-free is detected by checking the `is_free` flag.
## API
```c
void init_kmalloc(void);
void *kmalloc(size_t size);
void kfree(void *ptr);
void *kcalloc(size_t count, size_t size);
```
## Key Files
- `src/kmalloc.c` / `src/kmalloc.h` — Allocator implementation.
- `src/string.c` — Freestanding `memset`, `memcpy`, `strlen`, etc.
- `src/paging.c` — Provides physical page backing.
## Limitations
- Maximum single allocation is ~4080 bytes (one page minus header).
- No multi-page allocations for large objects.
- Free virtual addresses are not reused after `paging_free_page()`.

View File

@@ -9,6 +9,8 @@ add_executable(kernel
pic.c
pmm.c
paging.c
kmalloc.c
string.c
interrupts.S
kernel.c
)

View File

@@ -7,6 +7,7 @@
#include "port_io.h"
#include "pmm.h"
#include "paging.h"
#include "kmalloc.h"
void offset_print(const char *str)
{
@@ -63,6 +64,21 @@ void kernel_main(uint32_t magic, uint32_t addr) {
offset_print("FAILED to allocate virtual page\n");
}
init_kmalloc();
offset_print("Memory allocator initialized\n");
/* Test kmalloc/kfree */
uint32_t *test_alloc = (uint32_t *)kmalloc(64);
if (test_alloc) {
*test_alloc = 42;
offset_print("kmalloc(64) = ");
print_hex((uint32_t)test_alloc);
kfree(test_alloc);
offset_print("kfree OK\n");
} else {
offset_print("FAILED to kmalloc\n");
}
/* Enable interrupts */
asm volatile("sti");
offset_print("Interrupts enabled\n");

253
src/kmalloc.c Normal file
View File

@@ -0,0 +1,253 @@
/**
* @file kmalloc.c
* @brief Kernel memory allocator implementation.
*
* A simple first-fit free-list allocator. Memory is obtained from the paging
* subsystem in 4 KiB page increments. The allocator maintains a linked list
* of free blocks. On allocation, the first block large enough is split if
* needed. On free, adjacent blocks are coalesced to reduce fragmentation.
*
* The allocator metadata (block headers) are stored inline at the beginning
* of each block, so the minimum allocation overhead is sizeof(block_header_t).
*/
#include "kmalloc.h"
#include "paging.h"
#include <stdint.h>
#include <string.h>
/* Debug print helpers defined in kernel.c */
extern void offset_print(const char *str);
extern void print_hex(uint32_t val);
/**
* Block header stored at the start of every allocated or free block.
* The usable memory starts immediately after this header.
*/
typedef struct block_header {
uint32_t size; /**< Size of the usable area (excludes header). */
uint32_t magic; /**< Magic number for integrity checking. */
struct block_header *next; /**< Next block in the free list (free blocks only). */
uint8_t is_free; /**< 1 if block is free, 0 if allocated. */
} block_header_t;
/** Magic value to detect heap corruption. */
#define BLOCK_MAGIC 0xCAFEBABE
/** Minimum block size to avoid excessive fragmentation. */
#define MIN_BLOCK_SIZE 16
/** Alignment for all allocations (8-byte aligned). */
#define ALIGNMENT 8
/** Round up to alignment boundary. */
#define ALIGN_UP(x, a) (((x) + (a) - 1) & ~((a) - 1))
/** Head of the free list. */
static block_header_t *free_list = NULL;
/** Number of pages currently allocated for the heap. */
static uint32_t heap_pages = 0;
/**
* Request a new page from the paging subsystem and add it to the free list.
*
* @return Pointer to the new block header, or NULL on failure.
*/
static block_header_t *request_page(void) {
void *page = paging_alloc_page();
if (!page) {
return NULL;
}
heap_pages++;
block_header_t *block = (block_header_t *)page;
block->size = 4096 - sizeof(block_header_t);
block->magic = BLOCK_MAGIC;
block->next = NULL;
block->is_free = 1;
return block;
}
/**
* Insert a block into the free list, maintaining address order.
* Then attempt to coalesce with adjacent blocks.
*
* @param block The block to insert.
*/
static void insert_free_block(block_header_t *block) {
block->is_free = 1;
/* Find insertion point (maintain address order for coalescing) */
block_header_t *prev = NULL;
block_header_t *curr = free_list;
while (curr && curr < block) {
prev = curr;
curr = curr->next;
}
/* Insert between prev and curr */
block->next = curr;
if (prev) {
prev->next = block;
} else {
free_list = block;
}
/* Coalesce with next block if adjacent */
if (block->next) {
uint8_t *block_end = (uint8_t *)block + sizeof(block_header_t) + block->size;
if (block_end == (uint8_t *)block->next) {
block->size += sizeof(block_header_t) + block->next->size;
block->next = block->next->next;
}
}
/* Coalesce with previous block if adjacent */
if (prev) {
uint8_t *prev_end = (uint8_t *)prev + sizeof(block_header_t) + prev->size;
if (prev_end == (uint8_t *)block) {
prev->size += sizeof(block_header_t) + block->size;
prev->next = block->next;
}
}
}
/**
* Split a block if it is large enough to hold the requested size
* plus a new block header and minimum block.
*
* @param block The block to potentially split.
* @param size The requested allocation size (aligned).
*/
static void split_block(block_header_t *block, uint32_t size) {
uint32_t remaining = block->size - size - sizeof(block_header_t);
if (remaining >= MIN_BLOCK_SIZE) {
block_header_t *new_block = (block_header_t *)((uint8_t *)block + sizeof(block_header_t) + size);
new_block->size = remaining;
new_block->magic = BLOCK_MAGIC;
new_block->is_free = 1;
new_block->next = block->next;
block->size = size;
block->next = new_block;
}
}
void init_kmalloc(void) {
/* Allocate the initial heap page */
block_header_t *initial = request_page();
if (!initial) {
offset_print(" KMALLOC: FATAL - could not allocate initial heap page\n");
return;
}
free_list = initial;
offset_print(" KMALLOC: initialized with 1 page\n");
}
void *kmalloc(size_t size) {
if (size == 0) {
return NULL;
}
/* Align the requested size */
size = ALIGN_UP(size, ALIGNMENT);
/* First-fit search through free list */
block_header_t *prev = NULL;
block_header_t *curr = free_list;
while (curr) {
if (curr->magic != BLOCK_MAGIC) {
offset_print(" KMALLOC: HEAP CORRUPTION detected!\n");
return NULL;
}
if (curr->is_free && curr->size >= size) {
/* Found a suitable block */
split_block(curr, size);
/* Remove from free list */
if (prev) {
prev->next = curr->next;
} else {
free_list = curr->next;
}
curr->is_free = 0;
curr->next = NULL;
/* Return pointer past the header */
return (void *)((uint8_t *)curr + sizeof(block_header_t));
}
prev = curr;
curr = curr->next;
}
/* No suitable block found; request a new page */
block_header_t *new_block = request_page();
if (!new_block) {
offset_print(" KMALLOC: out of memory\n");
return NULL;
}
/* If the new page is large enough, use it directly */
if (new_block->size >= size) {
split_block(new_block, size);
/* If there's a remainder, add it to the free list */
if (new_block->next && new_block->next->is_free) {
insert_free_block(new_block->next);
}
new_block->is_free = 0;
new_block->next = NULL;
return (void *)((uint8_t *)new_block + sizeof(block_header_t));
}
/* Page too small (shouldn't happen for reasonable sizes) */
offset_print(" KMALLOC: requested size too large for single page\n");
insert_free_block(new_block);
return NULL;
}
void kfree(void *ptr) {
if (!ptr) {
return;
}
/* Get the block header */
block_header_t *block = (block_header_t *)((uint8_t *)ptr - sizeof(block_header_t));
if (block->magic != BLOCK_MAGIC) {
offset_print(" KMALLOC: kfree() invalid pointer or corruption!\n");
return;
}
if (block->is_free) {
offset_print(" KMALLOC: kfree() double free detected!\n");
return;
}
insert_free_block(block);
}
void *kcalloc(size_t count, size_t size) {
size_t total = count * size;
/* Overflow check */
if (count != 0 && total / count != size) {
return NULL;
}
void *ptr = kmalloc(total);
if (ptr) {
memset(ptr, 0, total);
}
return ptr;
}

46
src/kmalloc.h Normal file
View File

@@ -0,0 +1,46 @@
/**
* @file kmalloc.h
* @brief Kernel memory allocator.
*
* Provides malloc/free for the kernel. Internally uses the paging subsystem
* to ensure returned addresses are backed by physical RAM. Uses a simple
* first-fit free-list allocator with block splitting and coalescing.
*/
#ifndef KMALLOC_H
#define KMALLOC_H
#include <stddef.h>
/**
* Initialize the kernel memory allocator.
*
* Must be called after init_paging(). Allocates the initial heap pages.
*/
void init_kmalloc(void);
/**
* Allocate a block of kernel memory.
*
* @param size Number of bytes to allocate.
* @return Pointer to the allocated memory, or NULL on failure.
*/
void *kmalloc(size_t size);
/**
* Free a previously allocated block of kernel memory.
*
* @param ptr Pointer returned by kmalloc. NULL is safely ignored.
*/
void kfree(void *ptr);
/**
* Allocate a block of zero-initialized kernel memory.
*
* @param count Number of elements.
* @param size Size of each element in bytes.
* @return Pointer to the allocated and zeroed memory, or NULL on failure.
*/
void *kcalloc(size_t count, size_t size);
#endif /* KMALLOC_H */

172
src/string.c Normal file
View File

@@ -0,0 +1,172 @@
/**
* @file string.c
* @brief Minimal C string/memory functions for the freestanding kernel.
*
* These implementations replace the standard library versions since the
* kernel is compiled with -ffreestanding and does not link against libc.
*/
#include <stddef.h>
#include <stdint.h>
/**
* Fill a region of memory with a byte value.
*
* @param s Destination pointer.
* @param c Byte value to fill (only low 8 bits used).
* @param n Number of bytes to fill.
* @return The destination pointer.
*/
void *memset(void *s, int c, size_t n) {
unsigned char *p = (unsigned char *)s;
while (n--) {
*p++ = (unsigned char)c;
}
return s;
}
/**
* Copy a region of memory (non-overlapping).
*
* @param dest Destination pointer.
* @param src Source pointer.
* @param n Number of bytes to copy.
* @return The destination pointer.
*/
void *memcpy(void *dest, const void *src, size_t n) {
unsigned char *d = (unsigned char *)dest;
const unsigned char *s = (const unsigned char *)src;
while (n--) {
*d++ = *s++;
}
return dest;
}
/**
* Copy a region of memory, handling overlaps correctly.
*
* @param dest Destination pointer.
* @param src Source pointer.
* @param n Number of bytes to copy.
* @return The destination pointer.
*/
void *memmove(void *dest, const void *src, size_t n) {
unsigned char *d = (unsigned char *)dest;
const unsigned char *s = (const unsigned char *)src;
if (d < s) {
while (n--) {
*d++ = *s++;
}
} else {
d += n;
s += n;
while (n--) {
*--d = *--s;
}
}
return dest;
}
/**
* Compare two regions of memory.
*
* @param s1 First memory region.
* @param s2 Second memory region.
* @param n Number of bytes to compare.
* @return 0 if equal, negative if s1 < s2, positive if s1 > s2.
*/
int memcmp(const void *s1, const void *s2, size_t n) {
const unsigned char *a = (const unsigned char *)s1;
const unsigned char *b = (const unsigned char *)s2;
while (n--) {
if (*a != *b) {
return *a - *b;
}
a++;
b++;
}
return 0;
}
/**
* Calculate the length of a null-terminated string.
*
* @param s The string.
* @return Number of characters before the null terminator.
*/
size_t strlen(const char *s) {
size_t len = 0;
while (*s++) {
len++;
}
return len;
}
/**
* Compare two null-terminated strings.
*
* @param s1 First string.
* @param s2 Second string.
* @return 0 if equal, negative if s1 < s2, positive if s1 > s2.
*/
int strcmp(const char *s1, const char *s2) {
while (*s1 && *s1 == *s2) {
s1++;
s2++;
}
return *(unsigned char *)s1 - *(unsigned char *)s2;
}
/**
* Compare at most n characters of two strings.
*
* @param s1 First string.
* @param s2 Second string.
* @param n Maximum number of characters to compare.
* @return 0 if equal, negative if s1 < s2, positive if s1 > s2.
*/
int strncmp(const char *s1, const char *s2, size_t n) {
while (n && *s1 && *s1 == *s2) {
s1++;
s2++;
n--;
}
if (n == 0) {
return 0;
}
return *(unsigned char *)s1 - *(unsigned char *)s2;
}
/**
* Copy a null-terminated string.
*
* @param dest Destination buffer (must be large enough).
* @param src Source string.
* @return The destination pointer.
*/
char *strcpy(char *dest, const char *src) {
char *d = dest;
while ((*d++ = *src++))
;
return dest;
}
/**
* Copy at most n characters of a string, null-padding if shorter.
*
* @param dest Destination buffer.
* @param src Source string.
* @param n Maximum characters to copy.
* @return The destination pointer.
*/
char *strncpy(char *dest, const char *src, size_t n) {
char *d = dest;
while (n && (*d++ = *src++)) {
n--;
}
while (n--) {
*d++ = '\0';
}
return dest;
}