commit 467e4a944a7bc136bbeb87137a827784c763ecd8 Author: Mark Poliakov Date: Thu Mar 13 18:06:14 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02689f6 --- /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..e4723b1 --- /dev/null +++ b/build.zig @@ -0,0 +1,74 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const optimize = .Debug; + const target = b.standardTargetOptions(.{ + .default_target = .{ + .cpu_arch = .riscv64, + .os_tag = .freestanding, + .abi = .none, + } + }); + + const kernel_module = b.addModule("kernel", .{ + .optimize = optimize, + .target = target, + .pic = true, + .red_zone = false, + .code_model = .medium, + .root_source_file = b.path("src/kernel.zig") + }); + + const kernel = b.addExecutable(.{ + .name = "kernel", + .root_module = kernel_module, + .pic = true + }); + kernel.pie = true; + + kernel.entry = .{.symbol_name = "__rv64_entry"}; + + kernel.setLinkerScript(b.path("etc/riscv64-unknown-none.ld")); + kernel.addCSourceFiles(.{ + .files = &.{ + "src/arch/riscv64/entry.S" + }, + .flags = &.{}, + }); + b.installArtifact(kernel); + + const elf2bin = b.addSystemCommand(&.{ + "llvm-objcopy", + "-O", "binary", + "zig-out/bin/kernel", + "zig-out/bin/kernel.bin" + }); + + // TODO QEMU binary override + const qemu_info = switch (target.result.cpu.arch) { + .riscv64 => .{ "qemu-system-riscv64", "rv64" }, + else => unreachable, + }; + + const qemu_cmd = b.addSystemCommand(&.{ + qemu_info[0], + "-M", "virt", + "-kernel", "zig-out/bin/kernel.bin", + "-m", "256M", + "-cpu", qemu_info[1], + "-serial", "mon:stdio", + "-display", "none" + }); + + if (target.result.cpu.arch == .riscv64) { + qemu_cmd.addArgs(&.{ + "-bios", "etc/boot/rv64_fw_jump.bin" + }); + } + + elf2bin.step.dependOn(b.getInstallStep()); + qemu_cmd.step.dependOn(&elf2bin.step); + if (b.args) |args| qemu_cmd.addArgs(args); + const run_step = b.step("run", "Start the OS in qemu"); + run_step.dependOn(&qemu_cmd.step); +} diff --git a/etc/boot/rv64_fw_jump.bin b/etc/boot/rv64_fw_jump.bin new file mode 100644 index 0000000..e9cfb9f Binary files /dev/null and b/etc/boot/rv64_fw_jump.bin differ diff --git a/etc/riscv64-unknown-none.ld b/etc/riscv64-unknown-none.ld new file mode 100644 index 0000000..d2e55ca --- /dev/null +++ b/etc/riscv64-unknown-none.ld @@ -0,0 +1,39 @@ +ENTRY(__rv64_entry); + +SECTIONS { + . = 0x0; + + .text : ALIGN(4K) { + *(.text.entry*) + *(.text*) + } + + .rodata : ALIGN(4K) { + *(.rodata*) + *(.got*) + *(.plt*) + } + + .dynamic : ALIGN(4K) { + *(.dynamic*) + } + + .rela : ALIGN(4K) { + PROVIDE(__rela_start = .); + *(.rela*) + PROVIDE(__rela_end = .); + } + + .data : ALIGN(4K) { + *(.data*) + } + + .bss : { + . = ALIGN(4K); + PROVIDE(__bss_start = .); + *(COMMON) + *(.bss*) + . = ALIGN(4K); + PROVIDE(__bss_end = .); + } +} diff --git a/etc/riscv64.lldb b/etc/riscv64.lldb new file mode 100644 index 0000000..a7aa4a3 --- /dev/null +++ b/etc/riscv64.lldb @@ -0,0 +1,7 @@ +gdb-remote localhost:1234 + +target modules add -s kernel zig-out/bin/kernel +# target modules load -f zig-out/bin/kernel -s 0x80200000 +target modules load -f zig-out/bin/kernel -s 0x200200000 + +breakpoint set -n arch.riscv64.boot.rv64BspEntryLower diff --git a/src/arch.zig b/src/arch.zig new file mode 100644 index 0000000..e7d7a5d --- /dev/null +++ b/src/arch.zig @@ -0,0 +1,12 @@ +pub fn arch() type { + const builtin = @import("builtin"); + + switch (comptime builtin.cpu.arch) { + .riscv64 => { + return @import("arch/riscv64.zig").arch(); + }, + else => { + @panic("Architecture is not supported"); + }, + } +} diff --git a/src/arch/riscv64.zig b/src/arch/riscv64.zig new file mode 100644 index 0000000..08d0289 --- /dev/null +++ b/src/arch/riscv64.zig @@ -0,0 +1,23 @@ +const boot = @import("riscv64/boot.zig"); +export const _ = boot.rv64BspLowerEntry; + +pub fn arch() type { + return struct { + pub inline fn halt() noreturn { + while (true) { + _ = setInterruptMask(true); + pause(); + } + } + + pub inline fn setInterruptMask(mask: bool) bool { + // TODO + _ = mask; + return true; + } + + pub inline fn pause() void { + asm volatile ("wfi"); + } + }; +} diff --git a/src/arch/riscv64/boot.zig b/src/arch/riscv64/boot.zig new file mode 100644 index 0000000..75b12b3 --- /dev/null +++ b/src/arch/riscv64/boot.zig @@ -0,0 +1,115 @@ +const sbi = @import("sbi.zig"); +const debug = @import("../../debug.zig"); +const arch = @import("../../kernel.zig").arch; +const vmm = @import("vmm.zig"); +const regs = @import("regs.zig"); + +const log = debug.log; + +extern const __rela_start: u8; +extern const __rela_end: u8; +extern const __rv64_bsp_stack_top: u8; + +pub export fn rv64RelocateKernel(imageBase: usize, relaStart: usize, relaEnd: usize) void { + const elf = @import("std").elf; + + const relaTablePtr = @as([*]elf.Rela, @ptrFromInt(relaStart)); + const relaCount = (relaEnd - relaStart) / @sizeOf(elf.Rela); + const relaTable = relaTablePtr[0..relaCount]; + for (relaTable) |entry| { + if (entry.r_type() == 0x03) { + const value = @as(*isize, @ptrFromInt(imageBase + entry.r_offset)); + value.* = @as(isize, @bitCast(imageBase)) + entry.r_addend; + } else { + arch.halt(); + } + } +} + +fn bspUpperEntry(a0: usize, a1: usize) callconv(.C) noreturn { + asm volatile ("":::"memory"); + + _ = a1; + // Relocate the kernel yet again, this time to another base + const relaStart = @intFromPtr(&__rela_start); + const relaEnd = @intFromPtr(&__rela_end); + const relOffset = vmm.KERNEL_VIRTUAL_BASE + vmm.L1.offset(a0); + + asm volatile ("":::"memory"); + + rv64RelocateKernel(relOffset, relaStart, relaEnd); + + asm volatile ("":::"memory"); + + // Can unmap lower half now + for (0..4) |i| { + vmm.fixed.entry(i).* = .INVALID; + } + + asm volatile ("":::"memory"); + + debug.log.setWriteFn(&sbi.debugPrintByte); + + log.info("Still alive", .{}); + + arch.halt(); +} + +fn longJump(pc: usize, sp: usize, a0: usize, a1: usize) noreturn { + asm volatile ( + \\ mv sp, %[sp] + \\ jr %[pc] + : + : [a0]"{a0}"(a0), + [a1]"{a1}"(a1), + [pc]"r"(pc), + [sp]"r"(sp) + :"memory" + ); + unreachable; +} + +fn setupMmu(realAddress: usize) void { + var table = &vmm.fixed; + const realL1 = vmm.L1.index(realAddress); + + // Lower half + for (0..4) |i| { + table.entry(i).* = vmm.TableEntry(vmm.L1).page( + .{ .raw = i * vmm.L1.SIZE }, + .{ .r = true, .w = true, .x = true } + ); + } + + // Map 1GiB at KERNEL_VIRTUAL_BASE -> physical 1GiB where the kernel is loaded + table.entry(vmm.KERNEL_VIRTUAL_L1I).* = vmm.TableEntry(vmm.L1).page( + .{ .raw = vmm.L1.address(realL1) }, + .{ .r = true, .w = true, .x = true } + ); + + const address = @as(usize, @intFromPtr(table)); + regs.SATP.write(.{ + .PPN = @intCast(address >> 12), + .MODE = .sv39 + }); +} + +pub export fn rv64BspLowerEntry(realAddress: usize) callconv(.C) noreturn { + debug.log.setWriteFn(&sbi.debugPrintByte); + + setupMmu(realAddress); + + // &bspUpperEntry will yield a pointer like: X + P, where + // * X is symbol's raw address, + // * P is the physical load base of the image (0x80200000 on rv64 usually) + // + // Relocate the address to point to Y + P, where Y is the virtual load base + // const kernelL1Offset = realAddress & ((1 << 30) - 1); + const realAddressL1Offset = vmm.L1.offset(realAddress); + const virtualEntry = @intFromPtr(&bspUpperEntry) + vmm.KERNEL_VIRTUAL_BASE - realAddress + realAddressL1Offset; + const virtualSp = @intFromPtr(&__rv64_bsp_stack_top) + vmm.KERNEL_VIRTUAL_BASE - realAddress + realAddressL1Offset; + + longJump(virtualEntry, virtualSp, realAddress, 0); + + arch.halt(); +} diff --git a/src/arch/riscv64/entry.S b/src/arch/riscv64/entry.S new file mode 100644 index 0000000..d855b4c --- /dev/null +++ b/src/arch/riscv64/entry.S @@ -0,0 +1,67 @@ +.set ENTRY_SYMBOL, rv64BspLowerEntry +.set RELOC_SYMBOL, rv64RelocateKernel + +.global __rv64_entry +.global __rv64_bsp_stack_top + +.extern ENTRY_SYMBOL +.extern RELOC_SYMBOL + +.pushsection .text.entry +.option push +.option norvc + +.type __rv64_entry, @function +__rv64_entry: + auipc s0, 0 // a0 = real PC (also a real load address/offset) + + csrw sie, zero + csrw sip, zero + csrw satp, zero + mv tp, zero + + // Zero the .bss + // NOTE: I don't trust the assembler to place a proper pair of instructions + // in place of a `la`, so do this manually +.P00: auipc t0, %pcrel_hi(__bss_start) + addi t0, t0, %pcrel_lo(.P00) +.P01: auipc t1, %pcrel_hi(__bss_end) + addi t1, t1, %pcrel_lo(.P01) + +.L01: + beq t0, t1, .L02 + sd zero, (t0) + addi t0, t0, 8 + j .L01 +.L02: + +.P02: auipc sp, %pcrel_hi(__rv64_bsp_stack_top) + addi sp, sp, %pcrel_lo(.P02) + + // Relocate the kernel +.P03: auipc a1, %pcrel_hi(__rela_start) + addi a1, a1, %pcrel_lo(.P03) +.P04: auipc a2, %pcrel_hi(__rela_end) + addi a2, a2, %pcrel_lo(.P04) +.P05: auipc t0, %pcrel_hi(RELOC_SYMBOL) + addi t0, t0, %pcrel_lo(.P05) + + mv a0, s0 + jalr t0 + +.P06: auipc t0, %pcrel_hi(ENTRY_SYMBOL) + addi t0, t0, %pcrel_lo(.P06) + + mv a0, s0 + jr t0 +.size __rv64_entry, . - __rv64_entry + +.option pop +.popsection + +.pushsection .bss +.p2align 4 +__rv64_bsp_stack_bottom: + .skip 65536 +__rv64_bsp_stack_top: +.popsection diff --git a/src/arch/riscv64/regs.zig b/src/arch/riscv64/regs.zig new file mode 100644 index 0000000..8721b9a --- /dev/null +++ b/src/arch/riscv64/regs.zig @@ -0,0 +1,35 @@ +fn makeRegister(comptime name: []const u8, comptime bits: type) type { + const repr = @typeInfo(bits).@"struct".backing_integer.?; + return enum(repr) { + pub fn set(value: repr) void { + asm volatile ("csrw " ++ name ++ ", %[value]"::[value]"r"(value)); + } + + pub fn get() repr { + return asm volatile ("csrr %[value], " ++ name:[value]"=r"(-> repr)); + } + + pub fn write(value: bits) void { + set(@bitCast(value)); + } + + pub fn read() bits { + return @bitCast(get()); + } + + pub usingnamespace bits; + }; +} + +pub const SATP = makeRegister("satp", packed struct(u64) { + // 0..44 + PPN: u44 = 0, + // 44..60 + ASID: u16 = 0, + // 60..64 + MODE: enum(u4) { + bare = 0, + sv39 = 8, + _, + } = .bare +}); diff --git a/src/arch/riscv64/sbi.zig b/src/arch/riscv64/sbi.zig new file mode 100644 index 0000000..4b2cf82 --- /dev/null +++ b/src/arch/riscv64/sbi.zig @@ -0,0 +1,56 @@ +const std = @import("std"); + +const SbiExtension = enum(u64) { + hsm = 0x48534D, + time = 0x54494D45, + dbcn = 0x4442434E, + spi = 0x735049, +}; + +const SbiError = enum(i64) { + failed = -1, + not_supported = -2, + invalid_param = -3, + denied = -4, + invalid_address = -5, + already_available = -6, + already_started = -7, + already_stopped = -8, + no_shmem = -9, + invalid_state = -10, + bad_range = -11, + timeout = -12, + io = -13, +}; + +const SbiResult = union(enum) { + ok: u64, + err: SbiError, + + fn fromSbi(a0: u64, a1: u64) SbiResult { + if (a0 == 0) { + return .{ .ok = a1 }; + } else { + return .{ .err = @enumFromInt(a0) }; + } + } +}; + +fn sbiCall1(ext: SbiExtension, func: u64, arg0: u64) SbiResult { + var a0: u64 = undefined; + var a1: u64 = undefined; + asm volatile ( + "ecall" + : [ret0] "={a0}" (a0), + [ret1] "={a1}" (a1), + : [arg0] "{a0}" (arg0), + [func] "{a6}" (func), + [extn] "{a7}" (ext) + : "a2", "a3", "a4", "a5" + ); + return SbiResult.fromSbi(a0, a1); +} + +pub fn debugPrintByte(byte: u8) void { + _ = sbiCall1(.dbcn, 0x02, @as(u64, byte)); +} diff --git a/src/arch/riscv64/vmm.zig b/src/arch/riscv64/vmm.zig new file mode 100644 index 0000000..81ea0b9 --- /dev/null +++ b/src/arch/riscv64/vmm.zig @@ -0,0 +1,127 @@ +const PhysicalAddress = @import("../../mem.zig").PhysicalAddress; + +pub const KERNEL_VIRTUAL_BASE: usize = 0xFFFFFFF000000000; +pub const KERNEL_VIRTUAL_L1I: usize = (KERNEL_VIRTUAL_BASE >> L1.SHIFT) & 511; + +fn translationLevel(comptime shift: usize) type { + return struct { + pub const SHIFT: usize = shift; + pub const SIZE: usize = 1 << shift; + + pub inline fn index(addr: usize) usize { + return (addr >> shift) & 511; + } + + pub inline fn offset(addr: usize) usize { + return addr & ((1 << shift) - 1); + } + + pub inline fn address(idx: usize) usize { + return idx << shift; + } + }; +} + +pub const L1 = translationLevel(30); +pub const L2 = translationLevel(21); +pub const L3 = translationLevel(12); + +pub const RawEntry = packed struct(u64) { + // 0: Valid + v: bool = false, + // 1 : Read + r: bool = false, + // 2: Write + w: bool = false, + // 3: Execute + x: bool = false, + // 4: U-mode access + u: bool = false, + // 5: Global bit + g: bool = false, + // 6: Access bit + a: bool = false, + // 7: dirty bit + d: bool = false, + // 8..10 Unused bits + _pad0: u2 = 0, + // 10..49: Address + address: u39 = 0, + // 49..64: Unused bits + _pad1: u15 = 0, + + pub fn makeUnion(self: @This(), other: @This()) @This() { + const lhs = @as(u64, @bitCast(self)); + const rhs = @as(u64, @bitCast(other)); + return @as(@This(), @bitCast(lhs | rhs)); + } + + pub fn clear(self: *@This(), mask: @This()) void { + const lhs = @as(*u64, @bitCast(self)); + const rhs = @as(u64, @bitCast(mask)); + lhs.* &= ~rhs; + } +}; + +pub fn TableEntry(comptime Level: type) type { + _ = Level; + return struct { + raw: RawEntry, + + pub const INVALID: @This() = .{ .raw = .{} }; + + pub fn address(self: @This()) PhysicalAddress { + if (self.raw.v) { + return .{ .raw = self.raw.address << 12 }; + } else { + return PhysicalAddress.NULL; + } + } + + pub fn bits(self: @This()) u64 { + return @as(u64, @bitCast(self.raw)); + } + + pub fn page(addr: PhysicalAddress, flags: RawEntry) @This() { + return .{ + .raw = flags.makeUnion(.{ + .address = @as(u39, @intCast(addr.raw >> 12)), + .r = true, + .v = true, + .d = true, + .a = true, + }), + }; + } + + pub fn table(addr: PhysicalAddress, flags: RawEntry) @This() { + flags.clear(.{ .r = true, .w = true, .x = true }); + return .{ + .raw = flags.makeUnion(.{ + .address = @as(u39, @intCast(addr.raw >> 12)), + .v = true, + }) + }; + } + }; +} + +pub fn Table(comptime Level: type) type { + return struct { + pub const Entry = TableEntry(Level); + + entries: [512]Entry align(4096), + + pub fn empty() @This() { + return .{ + .entries = [_]Entry{.INVALID} ** 512 + }; + } + + pub inline fn entry(self: *@This(), index: usize) *Entry { + return &self.entries[index]; + } + }; +} + +pub var fixed = Table(L1).empty(); diff --git a/src/debug.zig b/src/debug.zig new file mode 100644 index 0000000..44d16e8 --- /dev/null +++ b/src/debug.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +fn dummyWrite(_: u8) void {} + +pub const log = struct { + pub const Level = enum { + debug, + info, + warn, + err, + }; + + var writeFn: *const fn(u8) void = dummyWrite; + const writer: std.io.GenericWriter(u0, error{}, writeWrapperFn) = .{ + .context = 0 + }; + + fn writeWrapperFn(context: u0, data: []const u8) error{}!usize { + _ = context; + for (data) |byte| { + writeFn(byte); + } + return data.len; + } + + pub fn setWriteFn(f: *const fn(u8) void) void { + writeFn = f; + } + + pub fn info(comptime format: []const u8, args: anytype) void { + write(.info, format ++ "\r\n", args); + } + + pub fn writeRaw(data: []const u8) void { + writeWrapperFn(void, data); + } + + pub fn write(level: Level, comptime format: []const u8, args: anytype) void { + _ = level; + writer.print(format, args) catch return; + } +}; diff --git a/src/kernel.zig b/src/kernel.zig new file mode 100644 index 0000000..5264bac --- /dev/null +++ b/src/kernel.zig @@ -0,0 +1,7 @@ +// export const _ = @import("arch/riscv64/boot.zig").rv64BspLowerEntry; +pub const arch = @import("arch.zig").arch(); +pub const mem = @import("mem.zig"); + +export fn kernel_main() callconv(.C) void { + arch.halt(); +} diff --git a/src/mem.zig b/src/mem.zig new file mode 100644 index 0000000..ae68d29 --- /dev/null +++ b/src/mem.zig @@ -0,0 +1,9 @@ +pub const PhysicalAddress = packed struct(u64) { + raw: u64, + + pub const NULL: @This() = .{ .raw = 0 }; + + pub fn add(self: @This(), offset: usize) @This() { + return .{ .raw = self.raw + @as(u64, @intCast(offset)) }; + } +}; diff --git a/src/sync.zig b/src/sync.zig new file mode 100644 index 0000000..fddee00 --- /dev/null +++ b/src/sync.zig @@ -0,0 +1,6 @@ +pub fn IrqSafeSpinlock(comptime T: type) type { + return struct { + inner: T, + + }; +}