commit 13ec4bee8784fbce137afd0b3f1a22cd25484686 Author: Gabriel Date: Thu Feb 13 22:39:48 2025 +0100 Ready. Set. Go! Microkernel development in Zig, should be fun! =] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..624b320 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +**/.zig-cache +boot/core +boot/init +tools/bin/ +tools/include/ +tools/share/ +astryon.iso diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0e8b6a4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "easyboot"] + path = easyboot + url = https://gitlab.com/bztsrc/easyboot diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5e0956a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.formatOnSave": true, + "editor.tabSize": 4, + "files.trimFinalNewlines": true, + "files.insertFinalNewline": true, + "git.inputValidationLength": 72, + "git.inputValidationSubjectLength": 72, +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..caf1ba8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2025, asleepymoon. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..254f1cb --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Astryon - a microkernel-based operating system project in Zig + +Note: not guaranteed to be the project's final name. + +## Goals + +This project is in its very early stages, so don't expect much yet. + +I've started this project to try something new in the world of OS development, after working on a classic monolithic system for some time. I've been wanting to make a microkernel-based system for a while. I've also been wanting to try out Zig, so this was a perfect opportunity to combine both. + +- [x] Fully written in Zig +- [ ] Simple microkernel that only manages memory, scheduling, and basic IPC (in progress) +- [ ] IPC system using shared memory ring buffers +- [ ] Init process that can load other services and connect processes to each other (sort of like dbus) +- [ ] Permission manager, VFS system, etc... all in userspace +- [ ] Decently POSIX-compatible (with a compatibility layer and libc) +- [ ] Window server and GUI system +- [ ] Sandbox most regular userspace processes for security + +## Setup + +Install [Zig](https://ziglang.org/), version `0.13.0`. + +When cloning the repo, make sure to use `git clone --recursive` or run `git submodule update --init` after cloning. + +If done correctly, you should have the bootloader cloned as a submodule in the `easyboot` folder. Extract `easyboot/distrib/easyboot-x86_64-linux.tgz` into the `tools` folder (Linux only). + +The directory tree should look like this: +```tools + - bin + - include + - share + iso.sh + run.sh + ... +``` + +On other operating systems, you're going to have to build the bootloader manually. + +## Building + +Simply run `zig build -p .` + +Built binaries will end up in `base/usr/bin`, with the exception of the kernel and core modules, which will be installed in `boot`. + +## Running + +### Creating the image +Use `tools/iso.sh` to generate an ISO image containing the previously built binaries. + +This script assumes that you have the easyboot tool installed at `tools/bin/easyboot`. If this is not the case, you'll have to run easyboot manually. Here's the command: + +`/path/to/easyboot -e boot astryon.iso` + +### Running the image in QEMU +Then, to run the image in QEMU, you can use the convenience script `tools/run.sh` or run the following command: + +`qemu-system-x86_64 -cdrom astryon.iso -serial stdio -enable-kvm` + +If you prefer another virtualization system (like Oracle VirtualBox or VMWare), simply import `astryon.iso` into it. Keep in mind you're going to have to do this every time you build a new image. + +## License + +The bootloader, `easyboot` by [bzt](https://gitlab.com/bztsrc/), is licensed under the GPLv3+ [LICENSE](easyboot/LICENSE). + +The Astryon operating system is licensed under the BSD-2-Clause [LICENSE](LICENSE). diff --git a/boot/easyboot/menu.cfg b/boot/easyboot/menu.cfg new file mode 100644 index 0000000..4883fa1 --- /dev/null +++ b/boot/easyboot/menu.cfg @@ -0,0 +1,7 @@ +framebuffer 800 600 32 + +default 1 1000 + +menuentry Astryon default + kernel core + module init diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..2f5e0f7 --- /dev/null +++ b/build.zig @@ -0,0 +1,14 @@ +const std = @import("std"); +const core = @import("core/build.zig"); +const system = @import("system/build.zig"); + +pub fn build(b: *std.Build) void { + const build_step = b.step("all", "Build and install everything"); + + const optimize = b.standardOptimizeOption(.{}); + + core.build(b, build_step, optimize); + system.build(b, build_step, optimize); + + b.default_step = build_step; +} diff --git a/core/build.zig b/core/build.zig new file mode 100644 index 0000000..5acde36 --- /dev/null +++ b/core/build.zig @@ -0,0 +1,46 @@ +const std = @import("std"); + +const here = "core"; + +pub fn build(b: *std.Build, build_step: *std.Build.Step, optimize: std.builtin.OptimizeMode) void { + var disabled_features = std.Target.Cpu.Feature.Set.empty; + var enabled_features = std.Target.Cpu.Feature.Set.empty; + + disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.mmx)); + disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.sse)); + disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.sse2)); + disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.avx)); + disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.avx2)); + enabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.soft_float)); + + const target_query = std.Target.Query{ + .cpu_arch = std.Target.Cpu.Arch.x86_64, + .os_tag = std.Target.Os.Tag.freestanding, + .abi = std.Target.Abi.none, + .cpu_features_sub = disabled_features, + .cpu_features_add = enabled_features, + }; + + const core = b.addExecutable(.{ + .name = "core", + .root_source_file = b.path(here ++ "/src/main.zig"), + .target = b.resolveTargetQuery(target_query), + .optimize = optimize, + .code_model = .kernel, + }); + + core.addIncludePath(b.path(here ++ "/../easyboot/")); + + core.setLinkerScript(b.path(here ++ "/src/link.ld")); + const install = b.addInstallArtifact(core, .{ + .dest_dir = .{ + .override = .{ .custom = "boot/" }, + }, + }); + + var kernel_step = b.step("core", "Build the core microkernel"); + kernel_step.dependOn(&core.step); + kernel_step.dependOn(&install.step); + + build_step.dependOn(kernel_step); +} diff --git a/core/src/arch/debug.zig b/core/src/arch/debug.zig new file mode 100644 index 0000000..ae14b44 --- /dev/null +++ b/core/src/arch/debug.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const target = @import("builtin").target; + +const arch = switch (target.cpu.arch) { + .x86_64 => @import("x86_64/debug.zig"), + else => { + @compileError("unsupported architecture"); + }, +}; + +const DebugWriter = struct { + const Writer = std.io.Writer( + *DebugWriter, + error{}, + write, + ); + + fn write( + _: *DebugWriter, + data: []const u8, + ) error{}!usize { + return arch.write(data); + } + + fn writer(self: *DebugWriter) Writer { + return .{ .context = self }; + } +}; + +/// Print a formatted string to the platform's debug output. +pub fn print(comptime fmt: []const u8, args: anytype) void { + var debug_writer = DebugWriter{}; + var writer = debug_writer.writer(); + writer.print(fmt, args) catch return; +} diff --git a/core/src/arch/interrupts.zig b/core/src/arch/interrupts.zig new file mode 100644 index 0000000..f9d1b19 --- /dev/null +++ b/core/src/arch/interrupts.zig @@ -0,0 +1,9 @@ +const std = @import("std"); +const target = @import("builtin").target; + +pub const arch = switch (target.cpu.arch) { + .x86_64 => @import("x86_64/interrupts.zig"), + else => { + @compileError("unsupported architecture"); + }, +}; diff --git a/core/src/arch/platform.zig b/core/src/arch/platform.zig new file mode 100644 index 0000000..16cbe14 --- /dev/null +++ b/core/src/arch/platform.zig @@ -0,0 +1,9 @@ +const std = @import("std"); +const target = @import("builtin").target; + +pub const arch = switch (target.cpu.arch) { + .x86_64 => @import("x86_64/platform.zig"), + else => { + @compileError("unsupported architecture"); + }, +}; diff --git a/core/src/arch/vmm.zig b/core/src/arch/vmm.zig new file mode 100644 index 0000000..8d396ce --- /dev/null +++ b/core/src/arch/vmm.zig @@ -0,0 +1,9 @@ +const std = @import("std"); +const target = @import("builtin").target; + +pub const arch = switch (target.cpu.arch) { + .x86_64 => @import("x86_64/vmm.zig"), + else => { + @compileError("unsupported architecture"); + }, +}; diff --git a/core/src/arch/x86_64/debug.zig b/core/src/arch/x86_64/debug.zig new file mode 100644 index 0000000..47c7e0a --- /dev/null +++ b/core/src/arch/x86_64/debug.zig @@ -0,0 +1,23 @@ +const io = @import("ioports.zig"); + +const COM1: u16 = 0x3f8; + +fn serialWait() void { + while ((io.inb(COM1 + 5) & 0x20) == 0) { + asm volatile ("pause"); + } +} + +fn serialPutchar(c: u8) void { + serialWait(); + io.outb(COM1, c); +} + +/// Write data to the platform's debug output. +pub fn write(s: []const u8) usize { + for (s) |character| { + serialPutchar(character); + } + + return s.len; +} diff --git a/core/src/arch/x86_64/gdt.zig b/core/src/arch/x86_64/gdt.zig new file mode 100644 index 0000000..db4e053 --- /dev/null +++ b/core/src/arch/x86_64/gdt.zig @@ -0,0 +1,135 @@ +const std = @import("std"); +const platform = @import("platform.zig"); + +const GDTR align(4096) = packed struct { + size: u16, + offset: u64, +}; + +const GDTEntry = packed struct { + limit0: u16, + base0: u16, + base1: u8, + access: u8, + limit1_flags: u8, + base2: u8, +}; + +fn createGDTEntry(limit0: u16, base0: u16, base1: u8, access: u8, limit1_flags: u8, base2: u8) GDTEntry { + return GDTEntry{ + .limit0 = limit0, + .base0 = base0, + .base1 = base1, + .access = access, + .limit1_flags = limit1_flags, + .base2 = base2, + }; +} + +const HighGDTEntry = packed struct { + base_high: u32, + reserved: u32, +}; + +const GlobalDescriptorTable = packed struct { + null: GDTEntry, + kernel_code: GDTEntry, + kernel_data: GDTEntry, + user_code: GDTEntry, + user_data: GDTEntry, + tss: GDTEntry, + tss2: HighGDTEntry, +}; + +fn setBase(entry: *GDTEntry, base: u32) void { + entry.base0 = @intCast(base & 0xFFFF); + entry.base1 = @intCast((base >> 16) & 0xFF); + entry.base2 = @intCast((base >> 24) & 0xFF); +} + +fn setLimit(entry: *GDTEntry, limit: u20) void { + entry.limit0 = @intCast(limit & 0xFFFF); + entry.limit1_flags = (entry.limit1_flags & 0xF0) | (@as(u8, @intCast(limit >> 16)) & 0xF); +} + +fn setTSSBase(tss1: *GDTEntry, tss2: *HighGDTEntry, addr: u64) void { + setBase(tss1, @intCast(addr & 0xffffffff)); + tss2.base_high = @intCast(addr >> 32); +} + +const TSS = packed struct { + reserved0: u32, + rsp0: u64, + rsp1: u64, + rsp2: u64, + reserved1: u64, + ist0: u64, + ist1: u64, + ist2: u64, + ist3: u64, + ist4: u64, + ist5: u64, + ist6: u64, + reserved2: u64, + reserved3: u16, + iomap_base: u16, +}; + +fn stackTop(begin: usize, size: usize) usize { + return (begin + size) - 16; +} + +fn setupTSS(gdt: *GlobalDescriptorTable, tss: *TSS, stack: [*]u8, stack_length: usize) void { + tss.iomap_base = @sizeOf(TSS); + tss.ist0 = stackTop(@intFromPtr(stack), stack_length); + setTSSBase(&gdt.tss, &gdt.tss2, @intFromPtr(tss)); + setLimit(&gdt.tss, @sizeOf(TSS) - 1); +} + +fn loadGDT() callconv(.Naked) void { + asm volatile ( + \\ cli + \\ lgdt (%rdi) + \\ mov $0x10, %ax + \\ mov %ax, %ds + \\ mov %ax, %es + \\ mov %ax, %fs + \\ mov %ax, %gs + \\ mov %ax, %ss + \\ push $8 + \\ lea .reload_CS(%rip), %rax + \\ push %rax + \\ lretq + \\.reload_CS: + \\ ret + ); +} + +fn loadTR() callconv(.Naked) void { + asm volatile ( + \\ mov %rdi, %rax + \\ ltr %ax + \\ ret + ); +} + +/// Setup the Global Descriptor Table. +pub fn setupGDT() void { + // Store all these as static variables, as they won't be needed outside this function but need to stay alive. + const state = struct { + var gdt = GlobalDescriptorTable{ .null = std.mem.zeroes(GDTEntry), .kernel_code = createGDTEntry(0xffff, 0x0000, 0x00, 0x9a, 0xaf, 0x00), .kernel_data = createGDTEntry(0xffff, 0x0000, 0x00, 0x92, 0xcf, 0x00), .user_code = createGDTEntry(0xffff, 0x0000, 0x00, 0xfa, 0xaf, 0x00), .user_data = createGDTEntry(0xffff, 0x0000, 0x00, 0xf2, 0xcf, 0x00), .tss = createGDTEntry(0x0000, 0x0000, 0x00, 0xe9, 0x0f, 0x00), .tss2 = HighGDTEntry{ .base_high = 0x00000000, .reserved = 0x00000000 } }; + var gdtr = std.mem.zeroes(GDTR); + var tss = std.mem.zeroes(TSS); + var alternate_stack: [platform.PAGE_SIZE * 4]u8 = std.mem.zeroes([platform.PAGE_SIZE * 4]u8); + }; + + state.gdtr.offset = @intFromPtr(&state.gdt); + state.gdtr.size = @sizeOf(GlobalDescriptorTable); + setupTSS(&state.gdt, &state.tss, @ptrCast(&state.alternate_stack[0]), @sizeOf(@TypeOf(state.alternate_stack))); + + // Hackish way to call naked functions which we know conform to SysV ABI. + const lgdt: *const fn (g: *GDTR) callconv(.C) void = @ptrCast(&loadGDT); + lgdt(&state.gdtr); + const ltr: *const fn (t: u16) callconv(.C) void = @ptrCast(&loadTR); + ltr(0x2b); +} diff --git a/core/src/arch/x86_64/idt.zig b/core/src/arch/x86_64/idt.zig new file mode 100644 index 0000000..107185e --- /dev/null +++ b/core/src/arch/x86_64/idt.zig @@ -0,0 +1,156 @@ +const std = @import("std"); + +const IDTEntry = packed struct { + offset0: u16, + selector: u16, + ist: u8, + type_attr: u8, + offset1: u16, + offset2: u32, + ignore: u32, +}; + +fn setOffset(entry: *IDTEntry, offset: u64) void { + entry.offset0 = @as(u16, @intCast(offset & 0x000000000000ffff)); + entry.offset1 = @as(u16, @intCast((offset & 0x00000000ffff0000) >> 16)); + entry.offset2 = @as(u32, @intCast((offset & 0xffffffff00000000) >> 32)); +} + +fn getOffset(entry: *IDTEntry) u64 { + var offset: u64 = 0; + offset |= @as(u64, entry.offset0); + offset |= @as(u64, entry.offset1) << 16; + offset |= @as(u64, entry.offset2) << 32; + return offset; +} + +const IDT_TA_InterruptGate = 0b10001110; +const IDT_TA_UserCallableInterruptGate = 0b11101110; +const IDT_TA_TrapGate = 0b10001111; + +const IDTR = packed struct { + limit: u16, + offset: u64, +}; + +fn addIDTHandler(idt: *[256]IDTEntry, num: u32, handler: *const anyopaque, type_attr: u8, ist: u8) void { + var entry_for_handler: *IDTEntry = &idt.*[num]; + entry_for_handler.selector = 0x08; + entry_for_handler.type_attr = type_attr; + entry_for_handler.ist = ist; + setOffset(entry_for_handler, @intFromPtr(handler)); +} + +fn createISRHandler(comptime num: u32) *const fn () callconv(.Naked) void { + return struct { + fn handler() callconv(.Naked) void { + asm volatile ( + \\ push $0 + \\ push %[num] + \\ jmp asmInterruptEntry + : + : [num] "n" (num), + ); + } + }.handler; +} + +fn createISRHandlerWithErrorCode(comptime num: u32) *const fn () callconv(.Naked) void { + return struct { + fn handler() callconv(.Naked) void { + asm volatile ( + \\ push %[num] + \\ jmp asmInterruptEntry + : + : [num] "n" (num), + ); + } + }.handler; +} + +fn createIRQHandler(comptime num: u32, comptime irq: u32) *const fn () callconv(.Naked) void { + return struct { + fn handler() callconv(.Naked) void { + asm volatile ( + \\ push %[irq] + \\ push %[num] + \\ jmp asmInterruptEntry + : + : [num] "n" (num), + [irq] "n" (irq), + ); + } + }.handler; +} + +/// Setup the Interrupt Descriptor Table. +pub fn setupIDT() void { + // Store these as static variables, as they won't be needed outside this function but need to stay alive. + const state = struct { + var idtr = std.mem.zeroes(IDTR); + var idt: [256]IDTEntry = std.mem.zeroes([256]IDTEntry); + }; + + comptime var i: u32 = 0; + + // ISR 0-7 (no error code) + inline while (i < 8) : (i += 1) { + const handler: *const anyopaque = @ptrCast(createISRHandler(i)); + addIDTHandler(&state.idt, i, handler, IDT_TA_TrapGate, 1); + } + + // ISR 8 #DF (error code) + const handler_8: *const anyopaque = @ptrCast(createISRHandlerWithErrorCode(8)); + addIDTHandler(&state.idt, 8, handler_8, IDT_TA_TrapGate, 1); + + // ISR 9 obsolete + + i = 10; + // ISR 10-14 (error code) + inline while (i < 15) : (i += 1) { + const handler: *const anyopaque = @ptrCast(createISRHandlerWithErrorCode(i)); + addIDTHandler(&state.idt, i, handler, IDT_TA_TrapGate, 1); + } + + // ISR 15 reserved + + // ISR 16 #MF (no error code) + const handler_16: *const anyopaque = @ptrCast(createISRHandler(16)); + addIDTHandler(&state.idt, 16, handler_16, IDT_TA_TrapGate, 1); + + // ISR 17 #AC (error code) + const handler_17: *const anyopaque = @ptrCast(createISRHandlerWithErrorCode(17)); + addIDTHandler(&state.idt, 17, handler_17, IDT_TA_TrapGate, 1); + + i = 18; + // ISR 18-20 (no error code) + inline while (i < 21) : (i += 1) { + const handler: *const anyopaque = @ptrCast(createISRHandler(i)); + addIDTHandler(&state.idt, i, handler, IDT_TA_TrapGate, 1); + } + + // ISR 21 #CP (error code) + const handler_21: *const anyopaque = @ptrCast(createISRHandlerWithErrorCode(21)); + addIDTHandler(&state.idt, 21, handler_21, IDT_TA_TrapGate, 1); + + // ISR 22-31 reserved + + i = 0; + // ISR 32-47 (IRQs 0-16 after remapping the PIC) + inline while (i < 16) : (i += 1) { + const handler: *const anyopaque = @ptrCast(createIRQHandler(32 + i, i)); + addIDTHandler(&state.idt, 32 + i, handler, IDT_TA_InterruptGate, 0); + } + + // ISR 66 (syscall) + const sys_handler: *const anyopaque = @ptrCast(createISRHandler(66)); + addIDTHandler(&state.idt, 66, sys_handler, IDT_TA_UserCallableInterruptGate, 0); + + state.idtr.limit = 0x0FFF; + state.idtr.offset = @intFromPtr(&state.idt[0]); + + asm volatile ("lidt (%[idtr])" + : + : [idtr] "{rax}" (&state.idtr), + ); +} diff --git a/core/src/arch/x86_64/interrupts.zig b/core/src/arch/x86_64/interrupts.zig new file mode 100644 index 0000000..fd9a61e --- /dev/null +++ b/core/src/arch/x86_64/interrupts.zig @@ -0,0 +1,147 @@ +const std = @import("std"); +const pic = @import("pic.zig"); +const debug = @import("../debug.zig"); +const sys = @import("../../sys/syscall.zig"); + +pub const InterruptStackFrame = packed struct { + r15: u64, + r14: u64, + r13: u64, + r12: u64, + r11: u64, + r10: u64, + r9: u64, + r8: u64, + rbp: u64, + rdi: u64, + rsi: u64, + rdx: u64, + rcx: u64, + rbx: u64, + rax: u64, + isr: u64, + error_or_irq: u64, + rip: u64, + cs: u64, + rflags: u64, + rsp: u64, + ss: u64, +}; + +const IRQHandler = *const fn (u32, *InterruptStackFrame) void; + +var irq_handlers: [16]?IRQHandler = std.mem.zeroes([16]?IRQHandler); + +export fn asmInterruptEntry() callconv(.Naked) void { + asm volatile ( + \\ push %rax + \\ push %rbx + \\ push %rcx + \\ push %rdx + \\ push %rsi + \\ push %rdi + \\ push %rbp + \\ push %r8 + \\ push %r9 + \\ push %r10 + \\ push %r11 + \\ push %r12 + \\ push %r13 + \\ push %r14 + \\ push %r15 + \\ cld + \\ mov %rsp, %rdi + \\ call interruptEntry + \\asmInterruptExit: + \\ pop %r15 + \\ pop %r14 + \\ pop %r13 + \\ pop %r12 + \\ pop %r11 + \\ pop %r10 + \\ pop %r9 + \\ pop %r8 + \\ pop %rbp + \\ pop %rdi + \\ pop %rsi + \\ pop %rdx + \\ pop %rcx + \\ pop %rbx + \\ pop %rax + \\ add $16, %rsp + \\ iretq + ); +} + +const SYSCALL_INTERRUPT = 66; + +export fn interruptEntry(frame: *InterruptStackFrame) callconv(.C) void { + debug.print("Caught interrupt {d}\n", .{frame.isr}); + switch (frame.isr) { + SYSCALL_INTERRUPT => { + var args = sys.Arguments{ .arg0 = frame.rdi, .arg1 = frame.rsi, .arg2 = frame.rdx, .arg3 = frame.r10, .arg4 = frame.r8, .arg5 = frame.r9 }; + sys.invokeSyscall(frame.rax, frame, &args, @ptrFromInt(@as(usize, @intFromPtr(&frame.rax)))); + }, + else => {}, + } +} + +/// Disable interrupts (except for NMIs). +pub fn disableInterrupts() void { + asm volatile ("cli"); +} + +/// Enable interrupts. +pub fn enableInterrupts() void { + asm volatile ("sti"); +} + +/// Check whether interrupts are enabled. +pub fn saveInterrupts() bool { + var flags: u64 = 0; + asm volatile ("pushfq; pop %[flags]" + : [flags] "=r" (flags), + ); + return (flags & 0x200) != 0; +} + +/// Enable or disable interrupts depending on the boolean value passed. +pub fn restoreInterrupts(saved: bool) void { + switch (saved) { + true => { + enableInterrupts(); + }, + false => { + disableInterrupts(); + }, + } +} + +/// Update the PIC masks according to which IRQ handlers are registered. +pub fn syncInterrupts() void { + var pic1_mask: u8 = 0b11111111; + var pic2_mask: u8 = 0b11111111; + var i: u8 = 0; + while (i < 8) : (i += 1) { + if (irq_handlers[i] != null) pic1_mask &= (~(@as(u8, 1) << @as(u3, @intCast(i)))); + if (irq_handlers[i + 8] != null) pic2_mask &= (~(@as(u8, 1) << @as(u3, @intCast(i)))); + } + + if (pic2_mask != 0b11111111) pic1_mask &= 0b11111011; + + const saved: bool = saveInterrupts(); + disableInterrupts(); + pic.changePICMasks(pic1_mask, pic2_mask); + restoreInterrupts(saved); +} + +/// Register an IRQ handler. +pub fn registerIRQ(num: u32, handler: IRQHandler) bool { + if (irq_handlers[num] != null) return false; + + irq_handlers[num] = handler; + + syncInterrupts(); + + return true; +} diff --git a/core/src/arch/x86_64/ioports.zig b/core/src/arch/x86_64/ioports.zig new file mode 100644 index 0000000..d028d35 --- /dev/null +++ b/core/src/arch/x86_64/ioports.zig @@ -0,0 +1,44 @@ +pub fn inb(port: u16) u8 { + return asm volatile ("inb %[port], %[result]" + : [result] "={al}" (-> u8), + : [port] "N{dx}" (port), + ); +} + +pub fn outb(port: u16, value: u8) void { + return asm volatile ("outb %[data], %[port]" + : + : [port] "{dx}" (port), + [data] "{al}" (value), + ); +} + +pub fn inw(port: u16) u16 { + return asm volatile ("inw %[port], %[result]" + : [result] "={ax}" (-> u16), + : [port] "N{dx}" (port), + ); +} + +pub fn outw(port: u16, value: u16) void { + return asm volatile ("outw %[data], %[port]" + : + : [port] "{dx}" (port), + [data] "{ax}" (value), + ); +} + +pub fn inl(port: u16) u32 { + return asm volatile ("inl %[port], %[result]" + : [result] "={eax}" (-> u16), + : [port] "N{dx}" (port), + ); +} + +pub fn outl(port: u16, value: u32) void { + return asm volatile ("outw %[data], %[port]" + : + : [port] "{dx}" (port), + [data] "{eax}" (value), + ); +} diff --git a/core/src/arch/x86_64/pic.zig b/core/src/arch/x86_64/pic.zig new file mode 100644 index 0000000..be06e2b --- /dev/null +++ b/core/src/arch/x86_64/pic.zig @@ -0,0 +1,55 @@ +const io = @import("ioports.zig"); + +const PIC1_COMMAND = 0x20; +const PIC1_DATA = 0x21; +const PIC2_COMMAND = 0xA0; +const PIC2_DATA = 0xA1; +const PIC_EOI = 0x20; + +const ICW1_INIT = 0x10; +const ICW1_ICW4 = 0x01; +const ICW4_8086 = 0x01; + +inline fn ioDelay() void { + io.outb(0x80, 0); +} + +/// Remap the PIC so that all IRQs are remapped to 0x20-0x2f. +pub fn remapPIC() void { + io.outb(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4); + ioDelay(); + io.outb(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4); + ioDelay(); + + io.outb(PIC1_DATA, 0x20); + ioDelay(); + + io.outb(PIC2_DATA, 0x28); + ioDelay(); + + io.outb(PIC1_DATA, 4); + ioDelay(); + io.outb(PIC2_DATA, 2); + ioDelay(); + + io.outb(PIC1_DATA, ICW4_8086); + ioDelay(); + io.outb(PIC2_DATA, ICW4_8086); + ioDelay(); + + changePICMasks(0b11111111, 0b11111111); +} + +/// Update the PIC masks. +pub fn changePICMasks(pic1_mask: u8, pic2_mask: u8) void { + io.outb(PIC1_DATA, pic1_mask); + ioDelay(); + io.outb(PIC2_DATA, pic2_mask); + ioDelay(); +} + +/// Send an end-of-interrupt signal to the PIC. +pub fn picEOI(irq: u8) void { + if (irq >= 8) io.outb(PIC2_COMMAND, PIC_EOI); + io.outb(PIC1_COMMAND, PIC_EOI); +} diff --git a/core/src/arch/x86_64/platform.zig b/core/src/arch/x86_64/platform.zig new file mode 100644 index 0000000..0aee444 --- /dev/null +++ b/core/src/arch/x86_64/platform.zig @@ -0,0 +1,18 @@ +const gdt = @import("gdt.zig"); +const idt = @import("idt.zig"); +const pic = @import("pic.zig"); +const interrupts = @import("interrupts.zig"); + +pub const PAGE_SIZE = 4096; + +// Initialize platform-specific components. +pub fn platformInit() void { + gdt.setupGDT(); + idt.setupIDT(); +} + +// Initialize platform-specific components just before beginning multitasking. +pub fn platformEndInit() void { + pic.remapPIC(); + interrupts.syncInterrupts(); +} diff --git a/core/src/arch/x86_64/vmm.zig b/core/src/arch/x86_64/vmm.zig new file mode 100644 index 0000000..37ee1da --- /dev/null +++ b/core/src/arch/x86_64/vmm.zig @@ -0,0 +1,203 @@ +const std = @import("std"); +const easyboot = @cImport(@cInclude("easyboot.h")); +const mmap = @import("../../mmap.zig"); +const pmm = @import("../../pmm.zig"); + +const USER_ADDRESS_RANGE_END = 0x0000_7fff_ffff_ffff; +const PHYSICAL_MAPPING_BASE = 0xffff_8000_0000_0000; +const HUGE_PAGE_SIZE = 0x200000; // 2 MiB + +pub const PageTableEntry = packed struct { + present: u1, + read_write: u1, + user: u1, + write_through: u1, + cache_disabled: u1, + accessed: u1, + ignore0: u1, + larger_pages: u1, + global: u1, + available: u3, + address: u48, + available2: u3, + no_execute: u1, + + pub fn set_address(self: *PageTableEntry, address: u64) void { + self.address = @intCast(address >> 12); + } + + pub fn get_address(self: *PageTableEntry) u64 { + return self.address << 12; + } + + pub fn clear(self: *PageTableEntry) void { + self = std.mem.zeroes(@TypeOf(self)); + } +}; + +pub const PageDirectory = struct { + entries: [512]PageTableEntry, +}; + +const Flags = enum(u32) { + None = 0, + ReadWrite = 1, + User = 2, + NoExecute = 4, + WriteThrough = 8, + CacheDisable = 16, + Global = 32, +}; + +const PageTableIndexes = struct { + level4: u24, + level3: u24, + level2: u24, + level1: u24, +}; + +fn calculatePageTableIndexes(address: usize) PageTableIndexes { + return .{ .level4 = @intCast((address >> 39) & 0o777), .level3 = @intCast((address >> 30) & 0o777), .level2 = @intCast((address >> 21) & 0o777), .level1 = @intCast((address >> 12) & 0o777) }; +} + +fn hasFlag(flags: u32, flag: Flags) u1 { + return switch ((flags & @intFromEnum(flag)) > 0) { + true => 1, + false => 0, + }; +} + +fn updatePageTableEntry(entry: *PageTableEntry, phys: pmm.PhysFrame, flags: u32) void { + entry.present = 1; + entry.read_write = hasFlag(flags, Flags.ReadWrite); + entry.user = hasFlag(flags, Flags.User); + entry.write_through = hasFlag(flags, Flags.WriteThrough); + entry.cache_disabled = hasFlag(flags, Flags.CacheDisable); + entry.no_execute = hasFlag(flags, Flags.NoExecute); + entry.global = hasFlag(flags, Flags.Global); + entry.set_address(phys.address); +} + +fn setUpParentPageTableEntry(allocator: *pmm.FrameAllocator, pte: *PageTableEntry, flags: u32, base: usize) !void { + if (pte.present == 0) { + const frame = try pmm.allocFrame(allocator); + pte.present = 1; + pte.set_address(frame.address); + getTable(pte, base).* = std.mem.zeroes(PageDirectory); + } + if (hasFlag(flags, Flags.ReadWrite) == 1) pte.read_write = 1; + if (hasFlag(flags, Flags.User) == 1) pte.user = 1; +} + +fn getTable(pte: *PageTableEntry, base: usize) *allowzero PageDirectory { + const frame = pmm.PhysFrame{ .address = pte.get_address() }; + return @ptrFromInt(frame.virtualAddress(base)); +} + +pub fn map(allocator: *pmm.FrameAllocator, directory: *PageDirectory, base: usize, virt_address: u64, phys: pmm.PhysFrame, flags: u32, use_huge_pages: bool) !void { + const indexes = calculatePageTableIndexes(virt_address); + const l4 = &directory.entries[indexes.level4]; + try setUpParentPageTableEntry(allocator, l4, flags, base); + + const l3 = &getTable(l4, base).entries[indexes.level3]; + if (l3.larger_pages == 1) return error.MemoryAlreadyInUse; + try setUpParentPageTableEntry(allocator, l3, flags, base); + + const l2 = &getTable(l3, base).entries[indexes.level2]; + if (l2.larger_pages == 1) return error.MemoryAlreadyInUse; + + if (use_huge_pages) { + l2.larger_pages = 1; + updatePageTableEntry(l2, phys, flags); + return; + } + + try setUpParentPageTableEntry(allocator, l2, flags, base); + + const l1 = &getTable(l2, base).entries[indexes.level1]; + if (l1.present == 1) return error.MemoryAlreadyInUse; + updatePageTableEntry(l1, phys, flags); +} + +fn mapPhysicalMemory(allocator: *pmm.FrameAllocator, tag: *easyboot.multiboot_tag_mmap_t, directory: *PageDirectory, base: usize, flags: u32) !void { + const address_space_size = mmap.getAddressSpaceSize(tag) orelse return error.InvalidMemoryMap; + const address_space_pages = address_space_size / HUGE_PAGE_SIZE; + + var index: usize = 0; + while (index < address_space_pages) : (index += 1) { + try map(allocator, directory, 0, base + index * HUGE_PAGE_SIZE, pmm.PhysFrame{ .address = index * HUGE_PAGE_SIZE }, flags, true); + } +} + +fn lockPageDirectoryFrames(allocator: *pmm.FrameAllocator, directory: *PageDirectory, index: u8) !void { + if (index > 1) { + var i: u64 = 0; + while (i < 512) : (i += 1) { + const pte = &directory.entries[i]; + if (pte.present == 0) continue; + if ((index < 4) and (pte.larger_pages == 1)) continue; + + try pmm.lockFrame(allocator, pte.get_address()); + + const child_table: *PageDirectory = @ptrFromInt(pte.get_address()); + + try lockPageDirectoryFrames(allocator, child_table, index - 1); + } + } +} + +fn lockPageDirectory(allocator: *pmm.FrameAllocator, directory: *PageDirectory) !void { + try pmm.lockFrame(allocator, @intFromPtr(directory)); + try lockPageDirectoryFrames(allocator, directory, 4); +} + +fn setUpKernelPageDirectory(allocator: *pmm.FrameAllocator, tag: *easyboot.multiboot_tag_mmap_t) !*PageDirectory { + const directory = readPageDirectory(); + + try lockPageDirectory(allocator, directory); + try mapPhysicalMemory(allocator, tag, directory, PHYSICAL_MAPPING_BASE, @intFromEnum(Flags.ReadWrite) | @intFromEnum(Flags.NoExecute) | @intFromEnum(Flags.Global)); + + return directory; +} + +fn setUpInitialUserPageDirectory(allocator: *pmm.FrameAllocator, tag: *easyboot.multiboot_tag_mmap_t, kernel_directory: *PageDirectory, user_directory: *PageDirectory) !usize { + const physical_address_space_size = mmap.getAddressSpaceSize(tag) orelse return error.InvalidMemoryMap; + + user_directory.* = std.mem.zeroes(PageDirectory); + + const directory_upper_half: *[256]PageTableEntry = kernel_directory.entries[256..]; + const user_directory_upper_half: *[256]PageTableEntry = user_directory.entries[256..]; + @memcpy(user_directory_upper_half, directory_upper_half); + + const user_physical_address_base = (USER_ADDRESS_RANGE_END + 1) - physical_address_space_size; + + try mapPhysicalMemory(allocator, tag, user_directory, user_physical_address_base, @intFromEnum(Flags.ReadWrite) | @intFromEnum(Flags.NoExecute) | @intFromEnum(Flags.User)); + + return user_physical_address_base; +} + +pub fn createInitialMappings(allocator: *pmm.FrameAllocator, tag: *easyboot.multiboot_tag_mmap_t, user_directory: *PageDirectory) !usize { + const directory = try setUpKernelPageDirectory(allocator, tag); + const base = try setUpInitialUserPageDirectory(allocator, tag, directory, user_directory); + + setPageDirectory(directory); + + allocator.bitmap.location = @ptrFromInt(@as(usize, PHYSICAL_MAPPING_BASE) + @intFromPtr(allocator.bitmap.location)); + + return base; +} + +pub fn readPageDirectory() *PageDirectory { + var directory: *PageDirectory = undefined; + asm volatile ("mov %%cr3, %[dir]" + : [dir] "=r" (directory), + ); + return directory; +} + +pub fn setPageDirectory(directory: *PageDirectory) void { + asm volatile ("mov %[dir], %%cr3" + : + : [dir] "{rdi}" (directory), + ); +} diff --git a/core/src/lib/bitmap.zig b/core/src/lib/bitmap.zig new file mode 100644 index 0000000..3f065b3 --- /dev/null +++ b/core/src/lib/bitmap.zig @@ -0,0 +1,138 @@ +const std = @import("std"); + +const BitmapError = error{ + OutOfRange, +}; + +pub const Bitmap = struct { + location: [*]u8, + byte_size: usize, + + pub fn bit_size(self: *Bitmap) usize { + return self.byte_size * 8; + } + + pub fn set(self: *Bitmap, index: usize, value: u1) BitmapError!void { + if (index >= self.bit_size()) return error.OutOfRange; + + const byte_index = index / 8; + const bit_mask = @as(u8, 0b10000000) >> @as(u3, @intCast(index % 8)); + self.location[byte_index] &= ~bit_mask; + if (value == 1) { + self.location[byte_index] |= bit_mask; + } + } + + pub fn get(self: *Bitmap, index: usize) BitmapError!u1 { + if (index >= self.bit_size()) return error.OutOfRange; + + const byte_index = index / 8; + const bit_mask = @as(u8, 0b10000000) >> @as(u3, @intCast(index % 8)); + if ((self.location[byte_index] & bit_mask) > 0) return 1; + return 0; + } + + pub fn clear(self: *Bitmap, value: u1) void { + @memset(self.location[0..self.byte_size], byteThatOnlyContainsBit(value)); + } +}; + +pub fn createBitmap(location: [*]u8, byte_size: usize) Bitmap { + return Bitmap{ .location = location, .byte_size = byte_size }; +} + +// Self-explanatory. +fn byteThatDoesNotContainBit(value: u1) u8 { + return switch (value) { + 1 => 0x00, + 0 => 0xff, + }; +} + +fn byteThatOnlyContainsBit(value: u1) u8 { + return switch (value) { + 1 => 0xff, + 0 => 0x00, + }; +} + +pub fn findInBitmap(bitmap: *Bitmap, value: u1, begin: usize) BitmapError!?usize { + if (begin >= bitmap.bit_size()) return error.OutOfRange; + + var index = begin; + + while ((index % 8) != 0) { + if (try bitmap.get(index) == value) return index; + index += 1; + } + + if (index == bitmap.bit_size()) return null; + + var i: usize = index / 8; + const byte_that_does_not_contain_value = byteThatDoesNotContainBit(value); + while (i < bitmap.byte_size) { + if (bitmap.location[i] == byte_that_does_not_contain_value) { + i += 1; + continue; + } + + var j: usize = i * 8; + const end: usize = j + 8; + while (j < end) { + if (try bitmap.get(j) == value) return j; + j += 1; + } + + // Once we've located a byte that contains the value, we should succeed in finding it. + unreachable; + } + + return null; +} + +pub fn findInBitmapAndToggle(bitmap: *Bitmap, value: u1, begin: usize) BitmapError!?usize { + const index = try findInBitmap(bitmap, value, begin); + + switch (value) { + 0 => try bitmap.set(index, 1), + 1 => try bitmap.set(index, 0), + } + + return index; +} + +pub fn updateBitmapRegion(bitmap: *Bitmap, begin: usize, count: usize, value: u1) BitmapError!void { + if ((begin + count) > bitmap.bit_size()) return error.OutOfRange; + + if (count == 0) return; + + var index = begin; // The bit index we're updating. + var bits_remaining = count; // The number of bits left to update. + + // If the index is in the middle of a byte, update individual bits until we reach a byte. + while ((index % 8) > 0 and bits_remaining > 0) { + try bitmap.set(index, value); + index += 1; + bits_remaining -= 1; + } + + // Clear out the rest in bytes. We calculate the number of bytes to update, and then memset them all. + const bytes: usize = bits_remaining / 8; + + if (bytes > 0) { + const start = index / 8; + const end = start + bytes; + @memset(bitmap.location[start..end], byteThatOnlyContainsBit(value)); + + // Update the counting variables after the memset. + index += bytes * 8; + bits_remaining -= bytes * 8; + } + + // Set the remaining individual bits. + while (bits_remaining > 0) { + try bitmap.set(index, value); + index += 1; + bits_remaining -= 1; + } +} diff --git a/core/src/link.ld b/core/src/link.ld new file mode 100644 index 0000000..aa89d7d --- /dev/null +++ b/core/src/link.ld @@ -0,0 +1,33 @@ +ENTRY(_start) +OUTPUT_FORMAT(elf64-x86-64) + +PHDRS +{ + boot PT_LOAD; /* one single loadable segment */ +} +SECTIONS +{ + . = 0xffffffffffe00000; + kernel_start = .; + .text : { + KEEP(*(.text.boot)) *(.text .text.*) /* code */ + + . = ALIGN(0x1000); + start_of_kernel_rodata = .; + *(.rodata .rodata.*) /* read-only data */ + end_of_kernel_rodata = .; + + . = ALIGN(0x1000); + start_of_kernel_data = .; /* data */ + *(.data .data.*) + + } :boot + .bss (NOLOAD) : { /* bss */ + *(.bss .bss.*) + *(COMMON) + } :boot + end_of_kernel_data = .; + kernel_end = .; + + /DISCARD/ : { *(.eh_frame) *(.comment) } +} diff --git a/core/src/main.zig b/core/src/main.zig new file mode 100644 index 0000000..5fb71db --- /dev/null +++ b/core/src/main.zig @@ -0,0 +1,56 @@ +const std = @import("std"); +const easyboot = @cImport(@cInclude("easyboot.h")); +const debug = @import("arch/debug.zig"); +const platform = @import("arch/platform.zig").arch; +const interrupts = @import("arch/interrupts.zig").arch; +const vmm = @import("arch/vmm.zig").arch; +const multiboot = @import("multiboot.zig"); +const pmm = @import("pmm.zig"); + +const MultibootInfo = [*c]u8; + +export fn _start(magic: u32, info: MultibootInfo) callconv(.C) noreturn { + if (magic != easyboot.MULTIBOOT2_BOOTLOADER_MAGIC) { + debug.print("Invalid magic number: {x}\n", .{magic}); + while (true) {} + } + + debug.print("Hello world from the kernel!\n", .{}); + + multiboot.parseMultibootTags(@ptrCast(info)); + + interrupts.disableInterrupts(); + platform.platformInit(); + + debug.print("GDT initialized\n", .{}); + + platform.platformEndInit(); + interrupts.enableInterrupts(); + + if (multiboot.findMultibootTag(easyboot.multiboot_tag_mmap_t, @ptrCast(info))) |tag| { + var allocator = pmm.initializeFrameAllocator(tag) catch |err| { + debug.print("Error while initializing frame allocator: {}\n", .{err}); + while (true) {} + }; + + var init_directory = std.mem.zeroes(vmm.PageDirectory); + const base: usize = vmm.createInitialMappings(&allocator, tag, &init_directory) catch |err| { + debug.print("Error while creating initial mappings: {}\n", .{err}); + while (true) {} + }; + + debug.print("Physical memory base mapping for init: {x}\n", .{base}); + } else { + debug.print("No memory map multiboot tag found!\n", .{}); + } + + asm volatile ("int3"); + + while (true) {} +} + +pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn { + debug.print("--- KERNEL PANIC! ---\n", .{}); + debug.print("{s}\n", .{message}); + while (true) {} +} diff --git a/core/src/mmap.zig b/core/src/mmap.zig new file mode 100644 index 0000000..e9622e5 --- /dev/null +++ b/core/src/mmap.zig @@ -0,0 +1,61 @@ +const easyboot = @cImport(@cInclude("easyboot.h")); + +const MemoryMapIterator = struct { + tag: *easyboot.multiboot_tag_mmap_t, + entry: ?*easyboot.multiboot_mmap_entry_t, + end: usize, + + pub fn next(self: *MemoryMapIterator) ?*easyboot.multiboot_mmap_entry_t { + if (self.entry) |e| { + const current_entry = self.entry; + + var new_entry: [*c]u8 = @ptrCast(e); + new_entry += self.tag.entry_size; + self.entry = @alignCast(@ptrCast(new_entry)); + + if (@intFromPtr(self.entry) >= self.end) self.entry = null; + + return current_entry; + } + + return null; + } +}; + +pub fn createMemoryMapIterator(tag: *easyboot.multiboot_tag_mmap_t) MemoryMapIterator { + return MemoryMapIterator{ .tag = tag, .entry = @alignCast(@ptrCast(tag.entries())), .end = @intFromPtr(tag) + tag.size }; +} + +pub fn findLargestFreeEntry(tag: *easyboot.multiboot_tag_mmap_t) ?*easyboot.multiboot_mmap_entry_t { + var max_length: u64 = 0; + var biggest_entry: ?*easyboot.multiboot_mmap_entry_t = null; + + var iter = createMemoryMapIterator(tag); + + while (iter.next()) |entry| { + if (entry.type == easyboot.MULTIBOOT_MEMORY_AVAILABLE and entry.length > max_length) { + max_length = entry.length; + biggest_entry = entry; + } + } + + return biggest_entry; +} + +pub fn findHighestEntry(tag: *easyboot.multiboot_tag_mmap_t) ?*easyboot.multiboot_mmap_entry_t { + var highest_entry: ?*easyboot.multiboot_mmap_entry_t = null; + + var iter = createMemoryMapIterator(tag); + + while (iter.next()) |entry| { + highest_entry = entry; + } + + return highest_entry; +} + +pub fn getAddressSpaceSize(tag: *easyboot.multiboot_tag_mmap_t) ?usize { + const highest_entry = findHighestEntry(tag) orelse return null; + + return highest_entry.base_addr + highest_entry.length; +} diff --git a/core/src/multiboot.zig b/core/src/multiboot.zig new file mode 100644 index 0000000..dcfc0a8 --- /dev/null +++ b/core/src/multiboot.zig @@ -0,0 +1,231 @@ +const std = @import("std"); +const easyboot = @cImport(@cInclude("easyboot.h")); +const debug = @import("arch/debug.zig"); + +fn dumpUUID(uuid: [16]u8) void { + debug.print("{x:0^2}{x:0^2}{x:0^2}{x:0^2}-{x:0^2}{x:0^2}-{x:0^2}{x:0^2}-{x:0^2}{x:0^2}{x:0^2}{x:0^2}{x:0^2}{x:0^2}{x:0^2}{x:0^2}\n", .{ uuid[3], uuid[2], uuid[1], uuid[0], uuid[5], uuid[4], uuid[7], uuid[6], uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15] }); +} + +const MultibootInfo = [*]u8; + +/// Return the first multiboot tag of the given type. +pub fn findMultibootTag(comptime Type: type, info: MultibootInfo) ?*Type { + const mb_tag: *easyboot.multiboot_info_t = @alignCast(@ptrCast(info)); + const mb_size = mb_tag.total_size; + + var tag: *easyboot.multiboot_tag_t = @alignCast(@ptrCast(info + 8)); + const last = @intFromPtr(info) + mb_size; + while ((@intFromPtr(tag) < last) and (tag.type != easyboot.MULTIBOOT_TAG_TYPE_END)) { + switch (tag.type) { + easyboot.MULTIBOOT_TAG_TYPE_CMDLINE => { + if (Type == easyboot.multiboot_tag_cmdline_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_BOOT_LOADER_NAME => { + if (Type == easyboot.multiboot_tag_loader_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_MODULE => { + if (Type == easyboot.multiboot_tag_module_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_MMAP => { + if (Type == easyboot.multiboot_tag_mmap_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_FRAMEBUFFER => { + if (Type == easyboot.multiboot_tag_framebuffer_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_EFI64 => { + if (Type == easyboot.multiboot_tag_efi64_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_EFI64_IH => { + if (Type == easyboot.multiboot_tag_efi64_ih_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_SMBIOS => { + if (Type == easyboot.multiboot_tag_smbios_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_ACPI_OLD => { + if (Type == easyboot.multiboot_tag_old_acpi_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_ACPI_NEW => { + if (Type == easyboot.multiboot_tag_new_acpi_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_SMP => { + if (Type == easyboot.multiboot_tag_smp_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_PARTUUID => { + if (Type == easyboot.multiboot_tag_partuuid_t) return @alignCast(@ptrCast(tag)); + }, + easyboot.MULTIBOOT_TAG_TYPE_EDID => { + if (Type == easyboot.multiboot_tag_edid_t) return @alignCast(@ptrCast(tag)); + }, + else => {}, + } + + var new_tag: [*]u8 = @ptrCast(tag); + new_tag += ((tag.size + 7) & ~@as(usize, 7)); + tag = @alignCast(@ptrCast(new_tag)); + } + + return null; +} + +/// Find every multiboot tag of the given type. +pub fn findMultibootTags(comptime Type: type, info: MultibootInfo, callback: *const fn (tag: *Type) void) void { + const mb_tag: *easyboot.multiboot_info_t = @alignCast(@ptrCast(info)); + const mb_size = mb_tag.total_size; + + var tag: *easyboot.multiboot_tag_t = @alignCast(@ptrCast(info + 8)); + const last = @intFromPtr(info) + mb_size; + while ((@intFromPtr(tag) < last) and (tag.type != easyboot.MULTIBOOT_TAG_TYPE_END)) { + switch (tag.type) { + easyboot.MULTIBOOT_TAG_TYPE_CMDLINE => { + if (Type == easyboot.multiboot_tag_cmdline_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_BOOT_LOADER_NAME => { + if (Type == easyboot.multiboot_tag_loader_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_MODULE => { + if (Type == easyboot.multiboot_tag_module_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_MMAP => { + if (Type == easyboot.multiboot_tag_mmap_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_FRAMEBUFFER => { + if (Type == easyboot.multiboot_tag_framebuffer_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_EFI64 => { + if (Type == easyboot.multiboot_tag_efi64_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_EFI64_IH => { + if (Type == easyboot.multiboot_tag_efi64_ih_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_SMBIOS => { + if (Type == easyboot.multiboot_tag_smbios_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_ACPI_OLD => { + if (Type == easyboot.multiboot_tag_old_acpi_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_ACPI_NEW => { + if (Type == easyboot.multiboot_tag_new_acpi_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_SMP => { + if (Type == easyboot.multiboot_tag_smp_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_PARTUUID => { + if (Type == easyboot.multiboot_tag_partuuid_t) callback(@alignCast(@ptrCast(tag))); + }, + easyboot.MULTIBOOT_TAG_TYPE_EDID => { + if (Type == easyboot.multiboot_tag_edid_t) callback(@alignCast(@ptrCast(tag))); + }, + else => {}, + } + + var new_tag: [*]u8 = @ptrCast(tag); + new_tag += ((tag.size + 7) & ~@as(usize, 7)); + tag = @alignCast(@ptrCast(new_tag)); + } +} + +/// Log every multiboot tag in a multiboot struct. +pub fn parseMultibootTags(info: MultibootInfo) void { + const mb_tag: *easyboot.multiboot_info_t = @alignCast(@ptrCast(info)); + const mb_size = mb_tag.total_size; + + var tag: *easyboot.multiboot_tag_t = @alignCast(@ptrCast(info + 8)); + const last = @intFromPtr(info) + mb_size; + while ((@intFromPtr(tag) < last) and (tag.type != easyboot.MULTIBOOT_TAG_TYPE_END)) { + switch (tag.type) { + easyboot.MULTIBOOT_TAG_TYPE_CMDLINE => { + var cmdline: *easyboot.multiboot_tag_cmdline_t = @alignCast(@ptrCast(tag)); + debug.print("Command line = {s}\n", .{std.mem.sliceTo(cmdline.string(), 0)}); + }, + easyboot.MULTIBOOT_TAG_TYPE_BOOT_LOADER_NAME => { + var bootloader: *easyboot.multiboot_tag_loader_t = @alignCast(@ptrCast(tag)); + debug.print("Boot loader name = {s}\n", .{std.mem.sliceTo(bootloader.string(), 0)}); + }, + easyboot.MULTIBOOT_TAG_TYPE_MODULE => { + var module: *easyboot.multiboot_tag_module_t = @alignCast(@ptrCast(tag)); + debug.print("Module at {x}-{x}. Command line {s}\n", .{ module.mod_start, module.mod_end, std.mem.sliceTo(module.string(), 0) }); + }, + easyboot.MULTIBOOT_TAG_TYPE_MMAP => { + var mmap: *easyboot.multiboot_tag_mmap_t = @alignCast(@ptrCast(tag)); + debug.print("Memory map:\n", .{}); + var entry: *easyboot.multiboot_mmap_entry_t = @alignCast(@ptrCast(mmap.entries())); + const end = @intFromPtr(tag) + tag.size; + while (@intFromPtr(entry) < end) { + debug.print(" base_addr = {x}, length = {x}, type = {x} {s}, res = {x}\n", .{ entry.base_addr, entry.length, entry.type, switch (entry.type) { + easyboot.MULTIBOOT_MEMORY_AVAILABLE => "free", + easyboot.MULTIBOOT_MEMORY_ACPI_RECLAIMABLE => "ACPI", + easyboot.MULTIBOOT_MEMORY_NVS => "ACPI NVS", + else => "used", + }, entry.reserved }); + + var new_entry: [*c]u8 = @ptrCast(entry); + new_entry += mmap.entry_size; + entry = @alignCast(@ptrCast(new_entry)); + } + }, + easyboot.MULTIBOOT_TAG_TYPE_FRAMEBUFFER => { + const fb: *easyboot.multiboot_tag_framebuffer_t = @alignCast(@ptrCast(tag)); + debug.print("Framebuffer: \n", .{}); + debug.print(" address {x} pitch {d}\n", .{ fb.framebuffer_addr, fb.framebuffer_pitch }); + debug.print(" width {d} height {d} depth {d} bpp\n", .{ fb.framebuffer_width, fb.framebuffer_height, fb.framebuffer_bpp }); + debug.print(" red channel: at {d}, {d} bits\n", .{ fb.framebuffer_red_field_position, fb.framebuffer_red_mask_size }); + debug.print(" green channel: at {d}, {d} bits\n", .{ fb.framebuffer_green_field_position, fb.framebuffer_green_mask_size }); + debug.print(" blue channel: at {d}, {d} bits\n", .{ fb.framebuffer_blue_field_position, fb.framebuffer_blue_mask_size }); + }, + easyboot.MULTIBOOT_TAG_TYPE_EFI64 => { + const efi: *easyboot.multiboot_tag_efi64_t = @alignCast(@ptrCast(tag)); + debug.print("EFI system table {x}\n", .{efi.pointer}); + }, + easyboot.MULTIBOOT_TAG_TYPE_EFI64_IH => { + const efi_ih: *easyboot.multiboot_tag_efi64_t = @alignCast(@ptrCast(tag)); + debug.print("EFI image handle {x}\n", .{efi_ih.pointer}); + }, + easyboot.MULTIBOOT_TAG_TYPE_SMBIOS => { + const smbios: *easyboot.multiboot_tag_smbios_t = @alignCast(@ptrCast(tag)); + debug.print("SMBIOS table major {d} minor {d}\n", .{ smbios.major, smbios.minor }); + }, + easyboot.MULTIBOOT_TAG_TYPE_ACPI_OLD => { + const acpi: *easyboot.multiboot_tag_old_acpi_t = @alignCast(@ptrCast(tag)); + const rsdp = @intFromPtr(acpi.rsdp()); + debug.print("ACPI table (1.0, old RSDP) at {x}\n", .{rsdp}); + }, + easyboot.MULTIBOOT_TAG_TYPE_ACPI_NEW => { + const acpi: *easyboot.multiboot_tag_new_acpi_t = @alignCast(@ptrCast(tag)); + const rsdp = @intFromPtr(acpi.rsdp()); + debug.print("ACPI table (2.0, new RSDP) at {x}\n", .{rsdp}); + }, + easyboot.MULTIBOOT_TAG_TYPE_SMP => { + const smp: *easyboot.multiboot_tag_smp_t = @alignCast(@ptrCast(tag)); + debug.print("SMP supported\n", .{}); + debug.print(" {d} core(s)\n", .{smp.numcores}); + debug.print(" {d} running\n", .{smp.running}); + debug.print(" {x} bsp id\n", .{smp.bspid}); + }, + easyboot.MULTIBOOT_TAG_TYPE_PARTUUID => { + const part: *easyboot.multiboot_tag_partuuid_t = @alignCast(@ptrCast(tag)); + debug.print("Partition UUIDs\n", .{}); + debug.print(" boot ", .{}); + dumpUUID(part.bootuuid); + if (tag.size >= 40) { + debug.print(" root ", .{}); + dumpUUID(part.rootuuid); + } + }, + easyboot.MULTIBOOT_TAG_TYPE_EDID => { + const edid_tag: *easyboot.multiboot_tag_edid_t = @alignCast(@ptrCast(tag)); + const edid: []u8 = edid_tag.edid()[0 .. tag.size - @sizeOf(easyboot.multiboot_tag_t)]; + debug.print("EDID info\n", .{}); + debug.print(" manufacturer ID {x}{x}\n", .{ edid[8], edid[9] }); + debug.print(" EDID ID {x}{x} Version {d} Rev {d}\n", .{ edid[10], edid[11], edid[18], edid[19] }); + debug.print(" monitor type {x} size {d} cm x {d} cm\n", .{ edid[20], edid[21], edid[22] }); + }, + else => { + debug.print("Unknown MBI tag, this shouldn't happen with Simpleboot/Easyboot!---\n", .{}); + }, + } + + var new_tag: [*]u8 = @ptrCast(tag); + new_tag += ((tag.size + 7) & ~@as(usize, 7)); + tag = @alignCast(@ptrCast(new_tag)); + } +} diff --git a/core/src/pmm.zig b/core/src/pmm.zig new file mode 100644 index 0000000..6d0fd5a --- /dev/null +++ b/core/src/pmm.zig @@ -0,0 +1,103 @@ +const std = @import("std"); +const easyboot = @cImport(@cInclude("easyboot.h")); +const platform = @import("arch/platform.zig").arch; +const mmap = @import("mmap.zig"); +const bmap = @import("lib/bitmap.zig"); + +const FrameAllocatorError = error{ + InvalidMemoryMap, + MemoryAlreadyInUse, + MemoryNotInUse, + OutOfMemory, +}; + +pub const FrameAllocator = struct { + bitmap: bmap.Bitmap, + free_memory: u64, + used_memory: u64, + reserved_memory: u64, + start_index: usize, +}; + +pub const PhysFrame = struct { + address: usize, + + pub fn virtualAddress(self: *const PhysFrame, base: usize) usize { + return base + self.address; + } +}; + +pub fn lockFrame(allocator: *FrameAllocator, address: usize) !void { + const index = address / platform.PAGE_SIZE; + if (try allocator.bitmap.get(index) == 1) return error.MemoryAlreadyInUse; + try allocator.bitmap.set(index, 1); + allocator.used_memory += platform.PAGE_SIZE; + allocator.free_memory -= platform.PAGE_SIZE; +} + +pub fn lockFrames(allocator: *FrameAllocator, address: usize, pages: usize) !void { + var index: usize = 0; + while (index < pages) : (index += 1) { + try lockFrame(allocator, address + (index * platform.PAGE_SIZE)); + } +} + +pub fn freeFrame(allocator: *FrameAllocator, address: usize) !void { + const index = address / platform.PAGE_SIZE; + if (try allocator.bitmap.get(index) == 0) return error.MemoryNotInUse; + try allocator.bitmap.set(index, 0); + allocator.used_memory -= platform.PAGE_SIZE; + allocator.free_memory += platform.PAGE_SIZE; + + if (allocator.start_index > index) allocator.start_index = index; +} + +pub fn freeFrames(allocator: *FrameAllocator, address: usize, pages: usize) !void { + const index: usize = 0; + while (index < pages) : (index += 1) { + try freeFrame(allocator, address + (index * platform.PAGE_SIZE)); + } +} + +pub fn allocFrame(allocator: *FrameAllocator) !PhysFrame { + const index: usize = try bmap.findInBitmap(&allocator.bitmap, 0, allocator.start_index) orelse return error.OutOfMemory; + const address = index * platform.PAGE_SIZE; + try lockFrame(allocator, address); + + allocator.start_index = index + 1; + + return PhysFrame{ .address = address }; +} + +pub fn initializeFrameAllocator(tag: *easyboot.multiboot_tag_mmap_t) !FrameAllocator { + const largest_free = mmap.findLargestFreeEntry(tag) orelse return error.InvalidMemoryMap; + const physical_address_space_size = mmap.getAddressSpaceSize(tag) orelse return error.InvalidMemoryMap; + + const bitmap_base_address: [*]u8 = @ptrFromInt(largest_free.base_addr); + + const bitmap_bit_size = physical_address_space_size / @as(usize, platform.PAGE_SIZE); + const bitmap_size: usize = try std.math.divCeil(usize, bitmap_bit_size, 8); + + var allocator: FrameAllocator = FrameAllocator{ .bitmap = bmap.createBitmap(bitmap_base_address, bitmap_size), .free_memory = 0, .used_memory = 0, .reserved_memory = 0, .start_index = 0 }; + + allocator.bitmap.clear(1); // Set all pages to used/reserved by default, then clear out the free ones + + var iter = mmap.createMemoryMapIterator(tag); + while (iter.next()) |entry| { + const index = entry.base_addr / platform.PAGE_SIZE; + const pages = entry.length / platform.PAGE_SIZE; + + if (entry.type != easyboot.MULTIBOOT_MEMORY_AVAILABLE) { + allocator.reserved_memory += entry.length; + continue; + } + + allocator.free_memory += entry.length; + try bmap.updateBitmapRegion(&allocator.bitmap, index, pages, 0); + } + + const frames_to_lock = try std.math.divCeil(usize, bitmap_size, platform.PAGE_SIZE); + try lockFrames(&allocator, @intFromPtr(bitmap_base_address), frames_to_lock); + + return allocator; +} diff --git a/core/src/sys/print.zig b/core/src/sys/print.zig new file mode 100644 index 0000000..53a1c42 --- /dev/null +++ b/core/src/sys/print.zig @@ -0,0 +1,7 @@ +const interrupts = @import("../arch/interrupts.zig").arch; +const sys = @import("syscall.zig"); +const debug = @import("../arch/debug.zig"); + +pub fn print(_: *interrupts.InterruptStackFrame, args: *sys.Arguments, _: *isize) anyerror!void { + debug.print("The userspace program gave us the number {x}\n", .{args.arg0}); +} diff --git a/core/src/sys/syscall.zig b/core/src/sys/syscall.zig new file mode 100644 index 0000000..aac85be --- /dev/null +++ b/core/src/sys/syscall.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const interrupts = @import("../arch/interrupts.zig").arch; +const print = @import("print.zig").print; + +pub const Arguments = struct { + arg0: usize, + arg1: usize, + arg2: usize, + arg3: usize, + arg4: usize, + arg5: usize, +}; + +const SystemCall = *const fn (frame: *interrupts.InterruptStackFrame, args: *Arguments, retval: *isize) anyerror!void; + +const syscalls = [_]SystemCall{print}; + +pub fn invokeSyscall(number: usize, frame: *interrupts.InterruptStackFrame, args: *Arguments, retval: *isize) void { + if (number >= syscalls.len) { + retval.* = -1; + return; + } + + syscalls[number](frame, args, retval) catch { + retval.* = -1; + return; + }; +} diff --git a/easyboot b/easyboot new file mode 160000 index 0000000..31d2059 --- /dev/null +++ b/easyboot @@ -0,0 +1 @@ +Subproject commit 31d20593cf83bb212bbe73418211d8058bb23007 diff --git a/system/build.zig b/system/build.zig new file mode 100644 index 0000000..04851d0 --- /dev/null +++ b/system/build.zig @@ -0,0 +1,9 @@ +const std = @import("std"); +const init = @import("init/build.zig"); + +pub fn build(b: *std.Build, build_step: *std.Build.Step, optimize: std.builtin.OptimizeMode) void { + const system_step = b.step("system", "Build core system services"); + init.build(b, system_step, optimize); + + build_step.dependOn(system_step); +} diff --git a/system/init/build.zig b/system/init/build.zig new file mode 100644 index 0000000..2938f19 --- /dev/null +++ b/system/init/build.zig @@ -0,0 +1,43 @@ +const std = @import("std"); + +const here = "system/init"; + +pub fn build(b: *std.Build, build_step: *std.Build.Step, optimize: std.builtin.OptimizeMode) void { + var disabled_features = std.Target.Cpu.Feature.Set.empty; + var enabled_features = std.Target.Cpu.Feature.Set.empty; + + disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.mmx)); + disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.sse)); + disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.sse2)); + disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.avx)); + disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.avx2)); + enabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.soft_float)); + + const target_query = std.Target.Query{ + .cpu_arch = std.Target.Cpu.Arch.x86_64, + .os_tag = std.Target.Os.Tag.freestanding, + .abi = std.Target.Abi.none, + .cpu_features_sub = disabled_features, + .cpu_features_add = enabled_features, + }; + + const init = b.addExecutable(.{ + .name = "init", + .root_source_file = b.path(here ++ "/main.zig"), + .target = b.resolveTargetQuery(target_query), + .optimize = optimize, + .code_model = .default, + }); + + const install = b.addInstallArtifact(init, .{ + .dest_dir = .{ + .override = .{ .custom = "boot/" }, + }, + }); + + var init_step = b.step("init", "Build the init service"); + init_step.dependOn(&init.step); + init_step.dependOn(&install.step); + + build_step.dependOn(init_step); +} diff --git a/system/init/main.zig b/system/init/main.zig new file mode 100644 index 0000000..c79eb4e --- /dev/null +++ b/system/init/main.zig @@ -0,0 +1,13 @@ +fn syscall(num: u64, arg: u64) void { + asm volatile ("int $66" + : + : [num] "{rax}" (num), + [arg] "{rdi}" (arg), + ); +} + +export fn _start(base: u64) callconv(.C) noreturn { + syscall(0, base); + + while (true) {} +} diff --git a/tools/iso.sh b/tools/iso.sh new file mode 100755 index 0000000..3fae27c --- /dev/null +++ b/tools/iso.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +cd $(realpath $(dirname $0)/..) + +tools/bin/easyboot -e boot astryon.iso diff --git a/tools/run.sh b/tools/run.sh new file mode 100755 index 0000000..d650930 --- /dev/null +++ b/tools/run.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +cd $(realpath $(dirname $0)/..) + +qemu-system-x86_64 -cdrom astryon.iso -serial stdio -enable-kvm $@