summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Matthew Wozniak <me@woz.blue> 2026-05-29 21:59:53 -0400
committerGravatar Matthew Wozniak <me@woz.blue> 2026-05-29 22:04:38 -0400
commitfe456c2014c8d1d88b7fd5b24ebda7f7b4c53460 (patch)
treea1ae3682aede1ea7550f4be76a79118bd6502902
downloadquail-fe456c2014c8d1d88b7fd5b24ebda7f7b4c53460.tar.gz
quail-fe456c2014c8d1d88b7fd5b24ebda7f7b4c53460.zip
basic physical memory page allocator
Signed-off-by: Matthew Wozniak <me@woz.blue>
-rw-r--r--.gitignore2
-rw-r--r--build.zig50
-rw-r--r--build.zig.zon81
-rw-r--r--src/asm/boot.S42
-rw-r--r--src/asm/trap.S6
-rw-r--r--src/ld/virt.ld46
-rw-r--r--src/main.zig39
-rw-r--r--src/mem.zig41
-rw-r--r--src/uart.zig80
9 files changed, 387 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dca1103
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+zig-out/
+.zig-cache/
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..536762d
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,50 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const target = b.resolveTargetQuery(.{
+ .cpu_arch = .riscv64,
+ .abi = .none,
+ .os_tag = .freestanding,
+ .ofmt = .elf,
+ });
+ const optimize = b.standardOptimizeOption(.{});
+
+ const exe = b.addExecutable(.{
+ .name = "kernel",
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ .code_model = .medany,
+ }),
+ });
+ exe.root_module.addAssemblyFile(b.path("src/asm/boot.S"));
+ exe.root_module.addAssemblyFile(b.path("src/asm/trap.S"));
+ exe.linker_script = b.path("src/ld/virt.ld");
+ b.installArtifact(exe);
+
+ const run_step = b.step("run", "Run the app");
+ const run_cmd = b.addSystemCommand(&.{
+ // zig fmt: off
+ "qemu-system-riscv64",
+ "-machine", "virt",
+ "-cpu", "rv64",
+ "-smp", "4",
+ "-m", "128M",
+ // "-drive", "if=none,format=raw,file=hdd.dsk,id=foo",
+ // "-device", "virtio-blk-device,scsi=off,drive=foo",
+ "-nographic",
+ "-serial", "mon:stdio",
+ "-bios", "none",
+ "-device", "virtio-rng-device",
+ "-device", "virtio-net-device",
+ "-device", "virtio-tablet-device",
+ "-device", "virtio-keyboard-device",
+ "-S", "-s",
+ "-kernel"
+ // zig fmt: on
+ });
+ run_cmd.addArtifactArg(exe);
+ run_step.dependOn(&run_cmd.step);
+ run_cmd.step.dependOn(b.getInstallStep());
+}
diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000..bc1fe44
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,81 @@
+.{
+ // This is the default name used by packages depending on this one. For
+ // example, when a user runs `zig fetch --save <url>`, this field is used
+ // as the key in the `dependencies` table. Although the user can choose a
+ // different name, most users will stick with this provided value.
+ //
+ // It is redundant to include "zig" in this name because it is already
+ // within the Zig package namespace.
+ .name = .os,
+ // This is a [Semantic Version](https://semver.org/).
+ // In a future version of Zig it will be used for package deduplication.
+ .version = "0.0.0",
+ // Together with name, this represents a globally unique package
+ // identifier. This field is generated by the Zig toolchain when the
+ // package is first created, and then *never changes*. This allows
+ // unambiguous detection of one package being an updated version of
+ // another.
+ //
+ // When forking a Zig project, this id should be regenerated (delete the
+ // field and run `zig build`) if the upstream project is still maintained.
+ // Otherwise, the fork is *hostile*, attempting to take control over the
+ // original project's identity. Thus it is recommended to leave the comment
+ // on the following line intact, so that it shows up in code reviews that
+ // modify the field.
+ .fingerprint = 0x6ab04511e57166b1, // Changing this has security and trust implications.
+ // Tracks the earliest Zig version that the package considers to be a
+ // supported use case.
+ .minimum_zig_version = "0.17.0-dev.387+31f157d80",
+ // This field is optional.
+ // Each dependency must either provide a `url` and `hash`, or a `path`.
+ // `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
+ // Once all dependencies are fetched, `zig build` no longer requires
+ // internet connectivity.
+ .dependencies = .{
+ // See `zig fetch --save <url>` for a command-line interface for adding dependencies.
+ //.example = .{
+ // // When updating this field to a new URL, be sure to delete the corresponding
+ // // `hash`, otherwise you are communicating that you expect to find the old hash at
+ // // the new URL. If the contents of a URL change this will result in a hash mismatch
+ // // which will prevent zig from using it.
+ // .url = "https://example.com/foo.tar.gz",
+ //
+ // // This is computed from the file contents of the directory of files that is
+ // // obtained after fetching `url` and applying the inclusion rules given by
+ // // `paths`.
+ // //
+ // // This field is the source of truth; packages do not come from a `url`; they
+ // // come from a `hash`. `url` is just one of many possible mirrors for how to
+ // // obtain a package matching this `hash`.
+ // //
+ // // Uses the [multihash](https://multiformats.io/multihash/) format.
+ // .hash = "...",
+ //
+ // // When this is provided, the package is found in a directory relative to the
+ // // build root. In this case the package's hash is irrelevant and therefore not
+ // // computed. This field and `url` are mutually exclusive.
+ // .path = "foo",
+ //
+ // // When this is set to `true`, a package is declared to be lazily
+ // // fetched. This makes the dependency only get fetched if it is
+ // // actually used.
+ // .lazy = false,
+ //},
+ },
+ // Specifies the set of files and directories that are included in this package.
+ // Only files and directories listed here are included in the `hash` that
+ // is computed for this package. Only files listed here will remain on disk
+ // when using the zig package manager. As a rule of thumb, one should list
+ // files required for compilation plus any license(s).
+ // Paths are relative to the build root. Use the empty string (`""`) to refer to
+ // the build root itself.
+ // A directory listed here means that all files within, recursively, are included.
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ // For example...
+ //"LICENSE",
+ //"README.md",
+ },
+}
diff --git a/src/asm/boot.S b/src/asm/boot.S
new file mode 100644
index 0000000..bd50b4c
--- /dev/null
+++ b/src/asm/boot.S
@@ -0,0 +1,42 @@
+# bootloader for my kernel
+.option norvc
+.section .data
+.section .text.init
+.global _start
+_start:
+ # any harts not bootstrapping need to wait for an IPI
+ csrr t0, mhartid
+ bnez t0, 3f
+ # satp should be zero, but make sure
+ csrw satp, zero
+.option push
+.option norelax
+ la gp, __global_pointer$
+.option pop
+ # clear bss
+ la a0, _bss_start
+ la a1, _bss_end
+ bgeu a0, a1, 2f
+1:
+ sd zero, (a0)
+ addi a0, a0, 8
+ bltu a0, a1, 1b
+2:
+ # set up stack
+ la sp, _stack_end
+ # set kmain to the return address and then return
+ li t0, (0b11 << 11) | (1 << 7) | (1 << 3)
+ la t1, kmain
+ la t2, asm_trap_vector
+ li t3, 0 # (1 << 3) | (1 << 7) | (1 << 11)
+ csrw mstatus, t0
+ csrw mepc, t1
+ csrw mtvec, t2
+ csrw mie, t3
+ mret
+
+
+# wait for interrupt
+3:
+ wfi
+ j 3b
diff --git a/src/asm/trap.S b/src/asm/trap.S
new file mode 100644
index 0000000..672efcd
--- /dev/null
+++ b/src/asm/trap.S
@@ -0,0 +1,6 @@
+.section .text
+.global asm_trap_vector
+asm_trap_vector:
+ # We get here when the CPU is interrupted
+ # for any reason.
+ mret
diff --git a/src/ld/virt.ld b/src/ld/virt.ld
new file mode 100644
index 0000000..33532c4
--- /dev/null
+++ b/src/ld/virt.ld
@@ -0,0 +1,46 @@
+OUTPUT_ARCH( "riscv" )
+ENTRY( _start )
+
+MEMORY
+{
+ ram : org = 0x80000000, len = 128M
+}
+
+SECTIONS
+{
+ .text : {
+ _text_start = .;
+ *(.text.init)
+ *(.text .text.*)
+ _text_end = .;
+ } >ram
+
+ .rodata : {
+ _rodata_start = .;
+ *(.rodata .rodata.*)
+ _rodata_end = .;
+ } >ram
+
+ .data : {
+ . = ALIGN(4096);
+ _data_start = .;
+ *(.sdata .sdata.*)
+ *(.data .data.*)
+ _data_end = .;
+ } >ram
+
+ .bss : {
+ _bss_start = .;
+ *(.sbss .sbss.*)
+ *(.bss .bss.*)
+ _bss_end = .;
+ } >ram
+
+ . = ALIGN(4096);
+ _stack_start = .;
+ _stack_end = . + 0x80000;
+ _heap_start = _stack_end;
+ _memory_end = ORIGIN(ram) + LENGTH(ram);
+ _heap_size = _memory_end - _heap_start;
+}
+
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..7896a09
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,39 @@
+const uart = @import("uart.zig");
+const mem = @import("mem.zig");
+const std = @import("std");
+
+fn logFn(
+ comptime message_level: std.log.Level,
+ comptime scope: @EnumLiteral(),
+ comptime format: []const u8,
+ args: anytype,
+) void {
+ return std.log.defaultLogFileTerminal(
+ message_level,
+ scope,
+ format,
+ args,
+ uart.terminal,
+ ) catch {};
+}
+
+pub const std_options: std.Options = .{
+ .logFn = logFn,
+ .page_size_max = 4096,
+ .page_size_min = 4096,
+};
+
+pub const panic = std.debug.FullPanic(panicFn);
+
+fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {
+ std.log.err("kernel panic at {?}", .{first_trace_addr});
+ std.log.err("{s}", .{msg});
+ while (true) {}
+}
+
+export fn kmain() callconv(.c) noreturn {
+ uart.init(0x1000_0000);
+ std.log.info("hello, world", .{});
+ mem.init();
+ while (true) {}
+}
diff --git a/src/mem.zig b/src/mem.zig
new file mode 100644
index 0000000..b361b88
--- /dev/null
+++ b/src/mem.zig
@@ -0,0 +1,41 @@
+const std = @import("std");
+
+const Page = extern struct {
+ next: *Page,
+};
+
+var head = @extern(*align(4096) Page, .{ .name = "_heap_start" });
+
+pub fn init() void {
+ // how many pages do we have?
+ const page_count = @intFromPtr(@extern(
+ *const anyopaque,
+ .{ .name = "_heap_size" },
+ )) / 4096;
+
+ // go through every page and make it point it to the next one
+ const pages: []Page = @as([*]Page, @ptrCast(head))[0..page_count];
+ for (0..page_count - 1) |i| {
+ pages[i].next = &pages[i + 1];
+ }
+
+ std.log.scoped(.mem).info(
+ "{} physical pages of 4096 B = {} B free",
+ .{ page_count, page_count * 4096 },
+ );
+}
+
+// allocates 1 page by taking it off the front of the linked list
+pub fn alloc() []align(4096) u8 {
+ const page = @as([*]align(4096) u8, @ptrCast(head))[0..4096];
+ head = head.next;
+ return page;
+}
+
+// free the page by putting it back on the front of the linked list
+pub fn free(mem: []align(4096) u8) void {
+ std.debug.assert(mem.len == 4096);
+ const page: *Page = @ptrCast(mem.ptr);
+ page.next = head;
+ head = page;
+}
diff --git a/src/uart.zig b/src/uart.zig
new file mode 100644
index 0000000..661084c
--- /dev/null
+++ b/src/uart.zig
@@ -0,0 +1,80 @@
+const std = @import("std");
+
+var uart_ptr: ?[*]u8 = null;
+
+pub fn init(base_addr: usize) void {
+ uart_ptr = @ptrFromInt(base_addr);
+ // Set the word length to 8 bits. Offset 3 is the LCR (line control
+ // register).
+ //
+ // The bottom 2 bits set the word length:
+ // 0 = 5 bits
+ // 1 = 6 bits
+ // 2 = 7 bits
+ // 3 = 8 bits
+ const lcr: u8 = 3 << 0;
+ uart_ptr.?[3] = lcr;
+ // Now enable the FIFO, which is the bottom bit of the FIFO control
+ // register (offset 2)
+ uart_ptr.?[2] = 1 << 0;
+ // Enable receiver buffer interrupts, which is the bottom bit of the IER
+ // (interrupt enable register)
+ uart_ptr.?[1] = 1 << 0;
+ // Per the datasheet, we are to set the clock divisor to:
+ // divisor = ceil(clock_hz/(baud_sps * 16))
+ // divisor = ceil(22_729_000/(115200 * 16)) = 13
+ const divisor: u16 = 13;
+ const divisor_least: u8 = divisor & 0xff;
+ const divisor_most: u8 = divisor >> 8;
+ // To write the divisor, we have to open the divisor latch on bit 7 of the
+ // line control register.
+ uart_ptr.?[3] = lcr | 1 << 7;
+ // Now we write the divisor
+ uart_ptr.?[0] = divisor_least;
+ uart_ptr.?[1] = divisor_most;
+ // Now we lock it again
+ uart_ptr.?[3] = lcr;
+}
+
+pub fn get() ?u8 {
+ if (uart_ptr.?[5] & 1 != 0) {
+ return uart_ptr.?[0];
+ } else {
+ return null;
+ }
+}
+
+pub fn put(c: u8) void {
+ uart_ptr.?[0] = c;
+}
+
+fn drain(_: *std.Io.Writer, data: []const []const u8, splat: usize) !usize {
+ if (uart_ptr == null) return std.Io.Writer.Error.WriteFailed;
+ var written: u32 = 0;
+ for (data, 0..) |item, i| {
+ if (i == data.len - 1) {
+ for (0..splat) |_| for (item) |c| {
+ put(c);
+ written += 1;
+ };
+ } else {
+ for (item) |c| {
+ put(c);
+ written += 1;
+ }
+ }
+ }
+ return written;
+}
+
+pub var writer: std.Io.Writer = .{
+ .buffer = &.{},
+ .vtable = &.{
+ .drain = drain,
+ },
+};
+
+pub var terminal: std.Io.Terminal = .{
+ .writer = &writer,
+ .mode = .escape_codes,
+};