Built heap allocation
This commit is contained in:
parent
20b4661ff3
commit
269729f2f2
|
|
@ -18,7 +18,7 @@ rather than restate it. Roughly in the order things happen at runtime:
|
||||||
rather than leaking UEFI's memory descriptors across the boundary.
|
rather than leaking UEFI's memory descriptors across the boundary.
|
||||||
5. **[frame-allocator.md](frame-allocator.md) — the physical frame allocator.** The
|
5. **[frame-allocator.md](frame-allocator.md) — the physical frame allocator.** The
|
||||||
bitmap allocator that hands out and reclaims 4 KiB physical frames from that
|
bitmap allocator that hands out and reclaims 4 KiB physical frames from that
|
||||||
map — the primitive page tables and the heap will be built on.
|
map — the primitive page tables and the heap are built on.
|
||||||
6. **[interrupts.md](interrupts.md) — interrupts and exceptions.** The GDT, IDT and
|
6. **[interrupts.md](interrupts.md) — interrupts and exceptions.** The GDT, IDT and
|
||||||
TSS, the exception stubs, and the handler that reports a CPU fault in red instead
|
TSS, the exception stubs, and the handler that reports a CPU fault in red instead
|
||||||
of letting it triple-fault into a silent reset.
|
of letting it triple-fault into a silent reset.
|
||||||
|
|
@ -28,7 +28,10 @@ rather than restate it. Roughly in the order things happen at runtime:
|
||||||
8. **[device-interrupts.md](device-interrupts.md) — device interrupts.** The Local
|
8. **[device-interrupts.md](device-interrupts.md) — device interrupts.** The Local
|
||||||
APIC and its timer — the kernel's first interrupt that is *handled and returned
|
APIC and its timer — the kernel's first interrupt that is *handled and returned
|
||||||
from*, giving it a heartbeat.
|
from*, giving it a heartbeat.
|
||||||
9. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
9. **[heap.md](heap.md) — the kernel heap.** A growable free-list allocator built on
|
||||||
|
the VMM, exposed as a `std.mem.Allocator` so std containers work — dynamic
|
||||||
|
allocation for the kernel.
|
||||||
|
10. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||||
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
||||||
|
|
||||||
Cutting across all of these:
|
Cutting across all of these:
|
||||||
|
|
@ -51,7 +54,8 @@ map** of physical RAM ([memory-map.md](memory-map.md)); the kernel turns that ma
|
||||||
into a **frame allocator** ([frame-allocator.md](frame-allocator.md)), installs
|
into a **frame allocator** ([frame-allocator.md](frame-allocator.md)), installs
|
||||||
its **descriptor tables** so CPU faults are caught ([interrupts.md](interrupts.md)),
|
its **descriptor tables** so CPU faults are caught ([interrupts.md](interrupts.md)),
|
||||||
builds its own **page tables** and switches onto them ([paging.md](paging.md)),
|
builds its own **page tables** and switches onto them ([paging.md](paging.md)),
|
||||||
starts the **timer** so it has a heartbeat ([device-interrupts.md](device-interrupts.md)),
|
brings up the **heap** for dynamic allocation ([heap.md](heap.md)), starts the
|
||||||
|
**timer** so it has a heartbeat ([device-interrupts.md](device-interrupts.md)),
|
||||||
runs — its CPU-specific bits behind the [arch](arch.md) boundary — and when it has
|
runs — its CPU-specific bits behind the [arch](arch.md) boundary — and when it has
|
||||||
finished, or panics, it **halts** ([halting.md](halting.md)).
|
finished, or panics, it **halts** ([halting.md](halting.md)).
|
||||||
|
|
||||||
|
|
@ -63,6 +67,7 @@ finished, or panics, it **halts** ([halting.md](halting.md)).
|
||||||
| Kernel entry, panic, bring-up | `src/main.zig` |
|
| Kernel entry, panic, bring-up | `src/main.zig` |
|
||||||
| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, ABI) | `src/root.zig` |
|
| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, ABI) | `src/root.zig` |
|
||||||
| Physical frame allocator | `src/pmm.zig` |
|
| Physical frame allocator | `src/pmm.zig` |
|
||||||
|
| Kernel heap (`std.mem.Allocator`) | `src/heap.zig` |
|
||||||
| Framebuffer text console (mirrors to serial) | `src/console.zig` |
|
| Framebuffer text console (mirrors to serial) | `src/console.zig` |
|
||||||
| In-kernel test cases | `src/tests.zig` |
|
| In-kernel test cases | `src/tests.zig` |
|
||||||
| Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/timer, serial, linker script) | `src/arch/x86_64/` |
|
| Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/timer, serial, linker script) | `src/arch/x86_64/` |
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
# The kernel heap
|
||||||
|
|
||||||
|
The [frame allocator](frame-allocator.md) hands out fixed 4 KiB physical frames;
|
||||||
|
the [VMM](paging.md) maps pages into virtual addresses. The **kernel heap** sits on
|
||||||
|
top of both to provide what the rest of the kernel actually wants: `alloc(n)` /
|
||||||
|
`free(p)` for arbitrary byte sizes. It's the first real consumer of `map()`, and
|
||||||
|
the thing that unlocks dynamic data structures — lists, hash maps, driver state,
|
||||||
|
eventually a process table.
|
||||||
|
|
||||||
|
It's generic kernel code (`src/heap.zig`): the allocator logic is
|
||||||
|
architecture-neutral, using `arch.mapPage` and the frame allocator underneath.
|
||||||
|
|
||||||
|
## A growable free-list allocator
|
||||||
|
|
||||||
|
The algorithm is a classic **first-fit free list**:
|
||||||
|
|
||||||
|
- The heap owns a virtual region. Free space is tracked as an **address-ordered
|
||||||
|
singly linked list** of free blocks; each block begins with a 16-byte header
|
||||||
|
(`size`, and a `next` link used while free).
|
||||||
|
- **alloc(n)** walks the list for the first block big enough. If the block is much
|
||||||
|
larger it's **split** — the front becomes the allocation, the remainder stays
|
||||||
|
free. If nothing fits, the heap **grows** (below) and the search retries.
|
||||||
|
- **free(p)** finds the block header just before `p` and inserts it back into the
|
||||||
|
list, **coalescing** with the physically adjacent free blocks on either side so
|
||||||
|
the space can be reused as one region rather than fragmenting away.
|
||||||
|
|
||||||
|
Allocations are 16-byte aligned; larger alignments aren't supported yet (the
|
||||||
|
`std.mem.Allocator` `alloc` returns `null` for them).
|
||||||
|
|
||||||
|
## Growing on demand
|
||||||
|
|
||||||
|
The heap lives in the **higher half** of the address space (virtual base
|
||||||
|
`0xFFFF_8000_0000_0000`) — unmapped, well clear of the identity-mapped low half,
|
||||||
|
and leaving the low half free for a future user address space. (That base is
|
||||||
|
x86_64-canonical; another architecture would pick its own.)
|
||||||
|
|
||||||
|
When the free list can't satisfy a request, `grow` extends the mapped region: it
|
||||||
|
pulls fresh frames from the [frame allocator](frame-allocator.md) and `map`s each
|
||||||
|
onto the end of the heap, then adds the new span as a free block (coalescing with
|
||||||
|
the current tail). So the heap starts at one page and expands page-by-page as
|
||||||
|
demand requires, up to a cap. This is exactly what the VMM's on-demand `map` was
|
||||||
|
built for.
|
||||||
|
|
||||||
|
## A std.mem.Allocator
|
||||||
|
|
||||||
|
The heap is exposed as a **`std.mem.Allocator`** (`heap.allocator()`), Zig's
|
||||||
|
standard allocator interface. That's a deliberate multiplier: it means the whole of
|
||||||
|
Zig's standard library — `ArrayList`, `AutoHashMap`, `std.fmt.allocPrint`, and the
|
||||||
|
rest — works directly on the kernel heap, no bespoke containers required.
|
||||||
|
|
||||||
|
## Verifying it
|
||||||
|
|
||||||
|
The `heap` test (see [testing.md](testing.md)) exercises the allocator end to end:
|
||||||
|
|
||||||
|
```
|
||||||
|
[PASS] alloc 4096 bytes
|
||||||
|
[PASS] heap memory is writable and reads back
|
||||||
|
[PASS] freed block is reused <- free list + coalescing works
|
||||||
|
[PASS] many allocations (heap growth) stay valid <- grow() maps fresh frames
|
||||||
|
[PASS] std.ArrayList on the kernel heap <- std containers work on it
|
||||||
|
```
|
||||||
|
|
||||||
|
The "freed block is reused" check (free then re-alloc returns the same address) is
|
||||||
|
the proof that free and the free list actually work, not just alloc; "heap growth"
|
||||||
|
forces allocation past the initial page so `grow`/`map` runs; and the `ArrayList`
|
||||||
|
check is the std-integration payoff.
|
||||||
|
|
||||||
|
## What's next (not done here)
|
||||||
|
|
||||||
|
- **Thread/interrupt safety.** The heap assumes a single caller — no lock yet.
|
||||||
|
It's safe now (nothing allocates from interrupt handlers), but threads or an
|
||||||
|
allocating IRQ handler will need a lock (or `cli` around the critical section).
|
||||||
|
- **Larger alignments** than 16 (for page-aligned buffers, DMA regions).
|
||||||
|
- **`resize`/`remap` in place**, so growing an `ArrayList` needn't always copy.
|
||||||
|
- **Reclaiming empty tail pages** back to the frame allocator when the heap shrinks.
|
||||||
|
|
@ -48,6 +48,7 @@ Current cases:
|
||||||
| `smoke` | memory map has usable RAM; frame alloc/free; paging active | `DANOS-TEST-RESULT: PASS` |
|
| `smoke` | memory map has usable RAM; frame alloc/free; paging active | `DANOS-TEST-RESULT: PASS` |
|
||||||
| `timer` | device interrupts fire and return (tick count advances) | `DANOS-TEST-RESULT: PASS` |
|
| `timer` | device interrupts fire and return (tick count advances) | `DANOS-TEST-RESULT: PASS` |
|
||||||
| `vmm` | on-demand `map` works: a mapped page is writable and reads back | `DANOS-TEST-RESULT: PASS` |
|
| `vmm` | on-demand `map` works: a mapped page is writable and reads back | `DANOS-TEST-RESULT: PASS` |
|
||||||
|
| `heap` | kernel heap: alloc/free, block reuse, growth, and a std container on it | `DANOS-TEST-RESULT: PASS` |
|
||||||
| `fault-ud` | invalid-opcode exception is caught | serial shows `invalid opcode (vector 6)` |
|
| `fault-ud` | invalid-opcode exception is caught | serial shows `invalid opcode (vector 6)` |
|
||||||
| `fault-pf` | page fault caught with CR2 | `page fault (vector 14)` |
|
| `fault-pf` | page fault caught with CR2 | `page fault (vector 14)` |
|
||||||
| `fault-df` | double fault caught on IST1 (not a triple-fault reset) | `double fault (vector 8)` |
|
| `fault-df` | double fault caught on IST1 (not a triple-fault reset) | `double fault (vector 8)` |
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
//! The kernel heap: dynamic allocation for the kernel.
|
||||||
|
//!
|
||||||
|
//! Where the frame allocator ([pmm]) hands out fixed 4 KiB physical frames, the
|
||||||
|
//! heap hands out arbitrary byte-sized blocks from a virtual region, growing on
|
||||||
|
//! demand by mapping fresh frames into it (arch.mapPage) — the first real user of
|
||||||
|
//! the VMM (see docs/paging.md).
|
||||||
|
//!
|
||||||
|
//! The algorithm is a first-fit free list: an address-ordered singly linked list
|
||||||
|
//! of free blocks, split on allocation and coalesced with neighbours on free. It
|
||||||
|
//! is exposed as a std.mem.Allocator, so the kernel can use std containers.
|
||||||
|
//!
|
||||||
|
//! Not yet concurrency-safe: it assumes a single caller and no allocation from
|
||||||
|
//! interrupt handlers (ours don't). A lock comes with threads/SMP.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const danos = @import("danos");
|
||||||
|
const arch = @import("arch");
|
||||||
|
const pmm = @import("pmm.zig");
|
||||||
|
|
||||||
|
const page_size = danos.page_size;
|
||||||
|
|
||||||
|
/// Virtual base of the heap: the start of the higher half, which is unmapped and
|
||||||
|
/// well clear of the identity-mapped low half. (Canonical on x86_64; an arch that
|
||||||
|
/// splits the address space differently would choose its own.)
|
||||||
|
const heap_base: usize = 0xFFFF_8000_0000_0000;
|
||||||
|
/// Cap on heap growth for now.
|
||||||
|
const heap_max: usize = 64 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// A block header, placed at the start of every block. While the block is free it
|
||||||
|
/// also links into the free list via `next`.
|
||||||
|
const Block = extern struct {
|
||||||
|
size: usize, // total block size in bytes, including this header; a multiple of 16
|
||||||
|
next: ?*Block, // free-list link (only meaningful while free)
|
||||||
|
};
|
||||||
|
|
||||||
|
const header_size = @sizeOf(Block); // 16
|
||||||
|
const min_block = header_size + 16; // smallest block worth splitting off
|
||||||
|
|
||||||
|
var free_list: ?*Block = null;
|
||||||
|
var heap_end: usize = heap_base; // [heap_base, heap_end) is currently mapped
|
||||||
|
|
||||||
|
fn alignUp(value: usize, alignment: usize) usize {
|
||||||
|
return (value + alignment - 1) & ~(alignment - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payloadOf(block: *Block) [*]u8 {
|
||||||
|
return @ptrFromInt(@intFromPtr(block) + header_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bring the heap up with an initial mapped region.
|
||||||
|
pub fn init() void {
|
||||||
|
free_list = null;
|
||||||
|
heap_end = heap_base;
|
||||||
|
_ = grow(page_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map more pages onto the end of the heap and add them as a free block. Returns
|
||||||
|
/// false if out of heap virtual space or out of physical frames.
|
||||||
|
fn grow(min_bytes: usize) bool {
|
||||||
|
const start = heap_end;
|
||||||
|
const bytes = alignUp(min_bytes, page_size);
|
||||||
|
if (start + bytes > heap_base + heap_max) return false;
|
||||||
|
|
||||||
|
var virt = start;
|
||||||
|
while (virt < start + bytes) : (virt += page_size) {
|
||||||
|
const frame = pmm.alloc() orelse return false;
|
||||||
|
arch.mapPage(virt, frame, true);
|
||||||
|
}
|
||||||
|
heap_end = start + bytes;
|
||||||
|
|
||||||
|
const block: *Block = @ptrFromInt(start);
|
||||||
|
block.size = bytes;
|
||||||
|
insertFree(block); // coalesces with the previous tail block if adjacent
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a block into the address-ordered free list, coalescing with the
|
||||||
|
/// physically adjacent free blocks on either side.
|
||||||
|
fn insertFree(block: *Block) void {
|
||||||
|
var prev: ?*Block = null;
|
||||||
|
var cur = free_list;
|
||||||
|
while (cur) |c| : (cur = c.next) {
|
||||||
|
if (@intFromPtr(c) > @intFromPtr(block)) break;
|
||||||
|
prev = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
block.next = cur;
|
||||||
|
if (prev) |p| p.next = block else free_list = block;
|
||||||
|
|
||||||
|
// Merge forward into `cur` if they're contiguous.
|
||||||
|
if (cur) |c| {
|
||||||
|
if (@intFromPtr(block) + block.size == @intFromPtr(c)) {
|
||||||
|
block.size += c.size;
|
||||||
|
block.next = c.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Merge `prev` forward into `block` if they're contiguous.
|
||||||
|
if (prev) |p| {
|
||||||
|
if (@intFromPtr(p) + p.size == @intFromPtr(block)) {
|
||||||
|
p.size += block.size;
|
||||||
|
p.next = block.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate `len` bytes (16-byte aligned), or null if out of memory.
|
||||||
|
fn rawAlloc(len: usize) ?[*]u8 {
|
||||||
|
const need = alignUp(header_size + len, 16);
|
||||||
|
|
||||||
|
var attempts: u32 = 0;
|
||||||
|
while (attempts < 2) : (attempts += 1) {
|
||||||
|
var prev: ?*Block = null;
|
||||||
|
var cur = free_list;
|
||||||
|
while (cur) |block| : ({
|
||||||
|
prev = block;
|
||||||
|
cur = block.next;
|
||||||
|
}) {
|
||||||
|
if (block.size < need) continue;
|
||||||
|
|
||||||
|
if (block.size >= need + min_block) {
|
||||||
|
// Split: carve `need` off the front, leave the rest free.
|
||||||
|
const rest: *Block = @ptrFromInt(@intFromPtr(block) + need);
|
||||||
|
rest.size = block.size - need;
|
||||||
|
rest.next = block.next;
|
||||||
|
if (prev) |p| p.next = rest else free_list = rest;
|
||||||
|
block.size = need;
|
||||||
|
} else {
|
||||||
|
// Take the whole block.
|
||||||
|
if (prev) |p| p.next = block.next else free_list = block.next;
|
||||||
|
}
|
||||||
|
return payloadOf(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing fit: grow and try once more.
|
||||||
|
if (!grow(need)) return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rawFree(ptr: [*]u8) void {
|
||||||
|
const block: *Block = @ptrFromInt(@intFromPtr(ptr) - header_size);
|
||||||
|
insertFree(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- std.mem.Allocator interface -----------------------------------------
|
||||||
|
|
||||||
|
pub fn allocator() std.mem.Allocator {
|
||||||
|
return .{ .ptr = undefined, .vtable = &vtable };
|
||||||
|
}
|
||||||
|
|
||||||
|
const vtable = std.mem.Allocator.VTable{
|
||||||
|
.alloc = allocImpl,
|
||||||
|
.resize = resizeImpl,
|
||||||
|
.remap = remapImpl,
|
||||||
|
.free = freeImpl,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn allocImpl(_: *anyopaque, len: usize, alignment: std.mem.Alignment, _: usize) ?[*]u8 {
|
||||||
|
// Blocks are 16-byte aligned; larger alignments aren't supported yet.
|
||||||
|
if (alignment.toByteUnits() > 16) return null;
|
||||||
|
return rawAlloc(len);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resizeImpl(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) bool {
|
||||||
|
return false; // no in-place resize; the caller reallocates
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remapImpl(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) ?[*]u8 {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn freeImpl(_: *anyopaque, memory: []u8, _: std.mem.Alignment, _: usize) void {
|
||||||
|
rawFree(memory.ptr);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ const danos = @import("danos");
|
||||||
const arch = @import("arch");
|
const arch = @import("arch");
|
||||||
const console = @import("console.zig");
|
const console = @import("console.zig");
|
||||||
const pmm = @import("pmm.zig");
|
const pmm = @import("pmm.zig");
|
||||||
|
const heap = @import("heap.zig");
|
||||||
const tests = @import("tests.zig");
|
const tests = @import("tests.zig");
|
||||||
const build_options = @import("build_options");
|
const build_options = @import("build_options");
|
||||||
const BootInfo = danos.BootInfo;
|
const BootInfo = danos.BootInfo;
|
||||||
|
|
@ -92,10 +93,14 @@ fn kmain(boot_info: *const BootInfo) noreturn {
|
||||||
con.print(" page tables: CR3 = 0x{x:0>16}\n", .{arch.readCr3()});
|
con.print(" page tables: CR3 = 0x{x:0>16}\n", .{arch.readCr3()});
|
||||||
con.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_info.kernel_segment_count});
|
con.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_info.kernel_segment_count});
|
||||||
|
|
||||||
|
// Bring up the kernel heap (dynamic allocation), built on the VMM.
|
||||||
|
heap.init();
|
||||||
|
con.write("\ndanos: kernel heap online\n");
|
||||||
|
|
||||||
// Start the timer and unmask interrupts — the kernel now has a heartbeat.
|
// Start the timer and unmask interrupts — the kernel now has a heartbeat.
|
||||||
arch.startTimer();
|
arch.startTimer();
|
||||||
arch.enableInterrupts();
|
arch.enableInterrupts();
|
||||||
con.write("\ndanos: timer interrupts enabled\n");
|
con.write("danos: timer interrupts enabled\n");
|
||||||
|
|
||||||
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
|
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
|
||||||
// Normal builds fall through to the idle halt.
|
// Normal builds fall through to the idle halt.
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const std = @import("std");
|
||||||
const danos = @import("danos");
|
const danos = @import("danos");
|
||||||
const arch = @import("arch");
|
const arch = @import("arch");
|
||||||
const pmm = @import("pmm.zig");
|
const pmm = @import("pmm.zig");
|
||||||
|
const heap = @import("heap.zig");
|
||||||
|
|
||||||
/// Formatted write straight to serial, independent of the framebuffer console.
|
/// Formatted write straight to serial, independent of the framebuffer console.
|
||||||
fn log(comptime fmt: []const u8, args: anytype) void {
|
fn log(comptime fmt: []const u8, args: anytype) void {
|
||||||
|
|
@ -50,6 +51,8 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void {
|
||||||
timer();
|
timer();
|
||||||
} else if (eql(case, "vmm")) {
|
} else if (eql(case, "vmm")) {
|
||||||
vmm();
|
vmm();
|
||||||
|
} else if (eql(case, "heap")) {
|
||||||
|
heapTest();
|
||||||
} else if (eql(case, "fault-ud")) {
|
} else if (eql(case, "fault-ud")) {
|
||||||
faultInvalidOpcode();
|
faultInvalidOpcode();
|
||||||
} else if (eql(case, "fault-pf")) {
|
} else if (eql(case, "fault-pf")) {
|
||||||
|
|
@ -138,6 +141,69 @@ fn vmm() void {
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Exercise the kernel heap: basic alloc/write/free, reuse, growth beyond the
|
||||||
|
/// initial region, and a std container backed by it.
|
||||||
|
fn heapTest() void {
|
||||||
|
log("DANOS-TEST-BEGIN: heap\n", .{});
|
||||||
|
const a = heap.allocator();
|
||||||
|
|
||||||
|
// Allocate, write a pattern, read it back, free.
|
||||||
|
const buf = a.alloc(u8, 4096) catch null;
|
||||||
|
check("alloc 4096 bytes", buf != null);
|
||||||
|
if (buf) |b| {
|
||||||
|
@memset(b, 0xAB);
|
||||||
|
check("heap memory is writable and reads back", b[0] == 0xAB and b[4095] == 0xAB);
|
||||||
|
a.free(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Freeing then re-allocating the same size should reuse the block.
|
||||||
|
const p1 = a.alloc(u64, 8) catch null;
|
||||||
|
const addr1 = if (p1) |p| @intFromPtr(p.ptr) else 0;
|
||||||
|
if (p1) |p| a.free(p);
|
||||||
|
const p2 = a.alloc(u64, 8) catch null;
|
||||||
|
const addr2 = if (p2) |p| @intFromPtr(p.ptr) else 0;
|
||||||
|
check("freed block is reused", addr1 != 0 and addr1 == addr2);
|
||||||
|
if (p2) |p| a.free(p);
|
||||||
|
|
||||||
|
// Force growth past the initial page and check every block is usable.
|
||||||
|
var blocks: [64]?[]u8 = .{null} ** 64;
|
||||||
|
var ok = true;
|
||||||
|
for (&blocks, 0..) |*slot, i| {
|
||||||
|
const b = a.alloc(u8, 4096) catch null;
|
||||||
|
slot.* = b;
|
||||||
|
if (b) |bb| @memset(bb, @intCast(i & 0xff)) else {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (blocks, 0..) |slot, i| {
|
||||||
|
if (slot) |bb| {
|
||||||
|
if (bb[0] != @as(u8, @intCast(i & 0xff)) or bb[4095] != @as(u8, @intCast(i & 0xff))) ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check("many allocations (heap growth) stay valid", ok);
|
||||||
|
for (blocks) |slot| {
|
||||||
|
if (slot) |bb| a.free(bb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A std container backed by the kernel heap.
|
||||||
|
var list: std.ArrayList(u32) = .empty;
|
||||||
|
var sum: u64 = 0;
|
||||||
|
var expected: u64 = 0;
|
||||||
|
var i: u32 = 0;
|
||||||
|
var list_ok = true;
|
||||||
|
while (i < 1000) : (i += 1) {
|
||||||
|
list.append(a, i) catch {
|
||||||
|
list_ok = false;
|
||||||
|
};
|
||||||
|
expected += i;
|
||||||
|
}
|
||||||
|
for (list.items) |v| sum += v;
|
||||||
|
list.deinit(a);
|
||||||
|
check("std.ArrayList on the kernel heap", list_ok and sum == expected);
|
||||||
|
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
fn faultInvalidOpcode() void {
|
fn faultInvalidOpcode() void {
|
||||||
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
||||||
asm volatile ("ud2");
|
asm volatile ("ud2");
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,9 @@ CASES = [
|
||||||
{"name": "vmm",
|
{"name": "vmm",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
{"name": "heap",
|
||||||
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
{"name": "fault-ud", "expect": r"invalid opcode \(vector 6\)"},
|
{"name": "fault-ud", "expect": r"invalid opcode \(vector 6\)"},
|
||||||
{"name": "fault-pf", "expect": r"page fault \(vector 14\)"},
|
{"name": "fault-pf", "expect": r"page fault \(vector 14\)"},
|
||||||
{"name": "fault-df", "expect": r"double fault \(vector 8\)"},
|
{"name": "fault-df", "expect": r"double fault \(vector 8\)"},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue