diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 272aef67..a3f2422d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,10 @@ name: CI -on: [push] +on: + push: + branches: "*" + pull_request: + branches: "*" jobs: test: @@ -8,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - build_mode: ["", -Drelease-fast=true, -Drelease-safe=true, -Drelease-small=true] + build_mode: ["", -Drelease-fast, -Drelease-safe, -Drelease-small] steps: - uses: actions/checkout@v1 @@ -19,15 +23,19 @@ jobs: wget $(curl -s 'https://ziglang.org/download/index.json' | python3 -c "import sys, json; print(json.load(sys.stdin)['master']['x86_64-linux']['tarball'])") sudo apt-get install mtools tar -xvf zig* - - name: Build kernel - run: zig*/zig build ${{ matrix.build_mode }} - - name: Run unit tests - run: zig*/zig build test ${{ matrix.build_mode }} - name: Install qemu run: | sudo apt-get update sudo apt-get install qemu qemu-system --fix-missing - - name: Run runtime tests - run: zig*/zig build test -Drt-test=true ${{ matrix.build_mode }} - name: Check formatting run: zig*/zig fmt --check src + - name: Build kernel + run: zig*/zig build ${{ matrix.build_mode }} + - name: Run unit tests + run: zig*/zig build test ${{ matrix.build_mode }} + - name: Run runtime test - Initialisation + run: zig*/zig build rt-test -Ddisable-display -Dtest-mode=Initialisation ${{ matrix.build_mode }} + - name: Run runtime test - Panic + run: zig*/zig build rt-test -Ddisable-display -Dtest-mode=Panic ${{ matrix.build_mode }} + - name: Run runtime test - Scheduler + run: zig*/zig build rt-test -Ddisable-display -Dtest-mode=Scheduler ${{ matrix.build_mode }} diff --git a/.gitignore b/.gitignore index 6b6d6745..e65d3845 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ # Intellij .idea/ +# VSCode +.vscode/ + # Zig zig-cache diff --git a/README.md b/README.md index b86f41e3..0e93bf09 100644 --- a/README.md +++ b/README.md @@ -28,35 +28,52 @@ zig build ```Shell zig build run ``` + or if you want to wait for a gdb connection: + ```Shell zig build debug-run ``` ## Debug -Launch a gdb instance and connect to qemu. +Launch a gdb-multiarch instance and connect to qemu. ```Shell zig build debug ``` -## Test +## Unit testing -Run the unit tests or runtime tests. +Run the unit tests. ```Shell zig build test ``` +## Runtime testing + +Run the runtime tests. + +```Shell +zig build rt-test -Dtest-mode= +``` + +Available test modes: + +* `None`: This is the default, this will run the OS normally. +* `Initialisation`: Run the OS's initialisation runtime tests to ensure the OS is properly set up. +* `Panic`: Run the panic runtime test. + ## Options -* `-Drt-test=`: Boolean (default `false`). - * **build**: Build with runtime testing enabled. Makes the kernel bigger and slower but tests important functionality. - * **test**: Run the runtime testing script instead of the unittests. Checks for the expected log statements and fails if any are missing. * `-D[build-mode]=`: Boolean (default `false`). * **build**: Build a certain build mode (*release-safe*, *release-fast*, *release-small*). Don't set in order to use the *debug* build mode. * **test**: Test a certain build mode (*release-safe*, *release-fast*, *release-small*). Don't set in order to use the *debug* build mode. +* `-Dtarget=`: String (default `i386-freestanding`). The standard target options for building with zig. Currently supported targets: + * `i386-freestanding` +* `-Ddisable-display`: Boolean (default `false`) + * This disables the display output of QEMU. ## Contribution diff --git a/build.zig b/build.zig index 9808d9d6..2ebb6c49 100644 --- a/build.zig +++ b/build.zig @@ -1,122 +1,147 @@ const std = @import("std"); const builtin = @import("builtin"); +const rt = @import("test/runtime_test.zig"); +const RuntimeStep = rt.RuntimeStep; const Builder = std.build.Builder; -const LibExeObjStep = std.build.LibExeObjStep; -const Step = std.build.Step; -const Target = std.build.Target; +const Target = std.Target; +const CrossTarget = std.zig.CrossTarget; const fs = std.fs; const Mode = builtin.Mode; +const TestMode = rt.TestMode; +const ArrayList = std.ArrayList; -pub fn build(b: *Builder) !void { - const target = Target{ - .Cross = std.build.CrossTarget{ - .arch = .i386, - .os = .freestanding, - .abi = .gnu, - }, - }; +const x86_i686 = CrossTarget{ + .cpu_arch = .i386, + .os_tag = .freestanding, + .cpu_model = .{ .explicit = &Target.x86.cpu._i686 }, +}; - const target_str = switch (target.getArch()) { +pub fn build(b: *Builder) !void { + const target = b.standardTargetOptions(.{ .whitelist = &[_]CrossTarget{x86_i686}, .default_target = x86_i686 }); + const arch = switch (target.getCpuArch()) { .i386 => "x86", else => unreachable, }; - const build_mode = b.standardReleaseOptions(); - const rt_test = b.option(bool, "rt-test", "enable/disable runtime testing") orelse false; + const fmt_step = b.addFmt(&[_][]const u8{ + "build.zig", + "src", + "test", + }); + b.default_step.dependOn(&fmt_step.step); const main_src = "src/kernel/kmain.zig"; + const arch_root = "src/kernel/arch"; + const constants_path = try fs.path.join(b.allocator, &[_][]const u8{ arch_root, arch, "constants.zig" }); + const linker_script_path = try fs.path.join(b.allocator, &[_][]const u8{ arch_root, arch, "link.ld" }); + const output_iso = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "pluto.iso" }); + const iso_dir_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso" }); + const boot_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso", "boot" }); + const modules_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso", "modules" }); + + const build_mode = b.standardReleaseOptions(); + comptime var test_mode_desc: []const u8 = "\n "; + inline for (@typeInfo(TestMode).Enum.fields) |field| { + const tm = @field(TestMode, field.name); + test_mode_desc = test_mode_desc ++ field.name ++ " (" ++ TestMode.getDescription(tm) ++ ")"; + test_mode_desc = test_mode_desc ++ "\n "; + } + + const test_mode = b.option(TestMode, "test-mode", "Run a specific runtime test. This option is for the rt-test step. Available options: " ++ test_mode_desc) orelse .None; + const disable_display = b.option(bool, "disable-display", "Disable the qemu window") orelse false; - const exec = b.addExecutable("pluto", main_src); - const constants_path = try fs.path.join(b.allocator, &[_][]const u8{ "src/kernel/arch", target_str, "constants.zig" }); + const exec = b.addExecutable("pluto.elf", main_src); exec.addPackagePath("constants", constants_path); + exec.setOutputDir(b.cache_root); + exec.addBuildOption(TestMode, "test_mode", test_mode); exec.setBuildMode(build_mode); - exec.addBuildOption(bool, "rt_test", rt_test); - exec.setLinkerScriptPath("link.ld"); - exec.setTheTarget(target); + exec.setLinkerScriptPath(linker_script_path); + exec.setTarget(target); - const iso_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "pluto.iso" }); - const grub_build_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso", "boot" }); - const iso_dir_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso" }); + const make_iso = switch (target.getCpuArch()) { + .i386 => b.addSystemCommand(&[_][]const u8{ "./makeiso.sh", boot_path, modules_path, iso_dir_path, exec.getOutputPath(), output_iso }), + else => unreachable, + }; + make_iso.step.dependOn(&exec.step); - const mkdir_cmd = b.addSystemCommand(&[_][]const u8{ "mkdir", "-p", fs.path.dirname(grub_build_path).? }); + b.default_step.dependOn(&make_iso.step); - const grub_cmd = b.addSystemCommand(&[_][]const u8{ "cp", "-r", "grub", grub_build_path }); - grub_cmd.step.dependOn(&mkdir_cmd.step); + const test_step = b.step("test", "Run tests"); + const mock_path = "\"../../test/mock/kernel/\""; + const arch_mock_path = "\"../../../../test/mock/kernel/\""; + const unit_tests = b.addTest(main_src); + unit_tests.setBuildMode(build_mode); + unit_tests.setMainPkgPath("."); + unit_tests.addPackagePath("constants", constants_path); + unit_tests.addBuildOption(TestMode, "test_mode", test_mode); + unit_tests.addBuildOption([]const u8, "mock_path", mock_path); + unit_tests.addBuildOption([]const u8, "arch_mock_path", arch_mock_path); + + if (builtin.os.tag != .windows) { + unit_tests.enable_qemu = true; + } - const cp_elf_cmd = b.addSystemCommand(&[_][]const u8{"cp"}); - const elf_path = try fs.path.join(b.allocator, &[_][]const u8{ grub_build_path, "pluto.elf" }); - cp_elf_cmd.addArtifactArg(exec); - cp_elf_cmd.addArg(elf_path); - cp_elf_cmd.step.dependOn(&grub_cmd.step); - cp_elf_cmd.step.dependOn(&exec.step); + unit_tests.setTarget(.{ .cpu_arch = target.cpu_arch }); + test_step.dependOn(&unit_tests.step); - const modules_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso", "modules" }); - const mkdir_modules_cmd = b.addSystemCommand(&[_][]const u8{ "mkdir", "-p", modules_path }); + const rt_test_step = b.step("rt-test", "Run runtime tests"); + const build_mode_str = switch (build_mode) { + .Debug => "", + .ReleaseSafe => "-Drelease-safe", + .ReleaseFast => "-Drelease-fast", + .ReleaseSmall => "-Drelease-small", + }; + + var qemu_args_al = ArrayList([]const u8).init(b.allocator); + defer qemu_args_al.deinit(); + + switch (target.getCpuArch()) { + .i386 => try qemu_args_al.append("qemu-system-i386"), + else => unreachable, + } + try qemu_args_al.append("-serial"); + try qemu_args_al.append("stdio"); + switch (target.getCpuArch()) { + .i386 => { + try qemu_args_al.append("-boot"); + try qemu_args_al.append("d"); + try qemu_args_al.append("-cdrom"); + try qemu_args_al.append(output_iso); + }, + else => unreachable, + } + if (disable_display) { + try qemu_args_al.append("-display"); + try qemu_args_al.append("none"); + } - const map_file_path = try fs.path.join(b.allocator, &[_][]const u8{ modules_path, "kernel.map" }); - const map_file_cmd = b.addSystemCommand(&[_][]const u8{ "./make_map.sh", elf_path, map_file_path }); - map_file_cmd.step.dependOn(&cp_elf_cmd.step); - map_file_cmd.step.dependOn(&mkdir_modules_cmd.step); + var qemu_args = qemu_args_al.toOwnedSlice(); - const iso_cmd = b.addSystemCommand(&[_][]const u8{ "grub-mkrescue", "-o", iso_path, iso_dir_path }); - iso_cmd.step.dependOn(&map_file_cmd.step); - b.default_step.dependOn(&iso_cmd.step); + const rt_step = RuntimeStep.create(b, test_mode, qemu_args); + rt_step.step.dependOn(&make_iso.step); + rt_test_step.dependOn(&rt_step.step); const run_step = b.step("run", "Run with qemu"); const run_debug_step = b.step("debug-run", "Run with qemu and wait for a gdb connection"); - const qemu_bin = switch (target.getArch()) { - .i386 => "qemu-system-i386", - else => unreachable, - }; - const qemu_args = &[_][]const u8{ - qemu_bin, - "-cdrom", - iso_path, - "-boot", - "d", - "-serial", - "stdio", - }; const qemu_cmd = b.addSystemCommand(qemu_args); const qemu_debug_cmd = b.addSystemCommand(qemu_args); qemu_debug_cmd.addArgs(&[_][]const u8{ "-s", "-S" }); - if (rt_test) { - const qemu_rt_test_args = &[_][]const u8{ "-display", "none" }; - qemu_cmd.addArgs(qemu_rt_test_args); - qemu_debug_cmd.addArgs(qemu_rt_test_args); - } - - qemu_cmd.step.dependOn(&iso_cmd.step); - qemu_debug_cmd.step.dependOn(&iso_cmd.step); + qemu_cmd.step.dependOn(&make_iso.step); + qemu_debug_cmd.step.dependOn(&make_iso.step); run_step.dependOn(&qemu_cmd.step); run_debug_step.dependOn(&qemu_debug_cmd.step); - const test_step = b.step("test", "Run tests"); - if (rt_test) { - const script = b.addSystemCommand(&[_][]const u8{ "python3", "test/rt-test.py", "x86", b.zig_exe }); - test_step.dependOn(&script.step); - } else { - const mock_path = "\"../../test/mock/kernel/\""; - const arch_mock_path = "\"../../../../test/mock/kernel/\""; - const unit_tests = b.addTest(main_src); - unit_tests.setBuildMode(build_mode); - unit_tests.setMainPkgPath("."); - unit_tests.addPackagePath("constants", constants_path); - unit_tests.addBuildOption(bool, "rt_test", rt_test); - unit_tests.addBuildOption([]const u8, "mock_path", mock_path); - unit_tests.addBuildOption([]const u8, "arch_mock_path", arch_mock_path); - test_step.dependOn(&unit_tests.step); - } - const debug_step = b.step("debug", "Debug with gdb and connect to a running qemu instance"); - const symbol_file_arg = try std.mem.join(b.allocator, " ", &[_][]const u8{ "symbol-file", elf_path }); + const symbol_file_arg = try std.mem.join(b.allocator, " ", &[_][]const u8{ "symbol-file", exec.getOutputPath() }); const debug_cmd = b.addSystemCommand(&[_][]const u8{ - "gdb", + "gdb-multiarch", "-ex", symbol_file_arg, + "-ex", + "set architecture auto", }); debug_cmd.addArgs(&[_][]const u8{ "-ex", diff --git a/make_map.sh b/make_map.sh deleted file mode 100755 index a7d7d930..00000000 --- a/make_map.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# Read the symbols from the binary, remove all the unnecessary columns with awk and emit to a map file -readelf -s $1 | grep -F "FUNC" | awk '{$1=$3=$4=$5=$6=$7=""; print $0}' | sort -k 1 > $2 -echo "" >> $2 diff --git a/makeiso.sh b/makeiso.sh new file mode 100755 index 00000000..c137f749 --- /dev/null +++ b/makeiso.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +BOOT_DIR=$1 +MODULES_DIR=$2 +ISO_DIR=$3 +PLUTO_ELF=$4 +OUTPUT_FILE=$5 + +MAP_FILE=$MODULES_DIR/'kernel.map' + +exit_missing() { + printf "$_ must be installed\n"; + exit 1; +} + +# Check dependencies +which xorriso > /dev/null || exit_missing +which grub-mkrescue > /dev/null || exit_missing +which readelf > /dev/null || exit_missing + +mkdir -p $BOOT_DIR +mkdir -p $MODULES_DIR + +cp -r grub $BOOT_DIR +cp $PLUTO_ELF $BOOT_DIR/"pluto.elf" + +# Read the symbols from the binary, remove all the unnecessary columns with awk and emit to a map file +readelf -s --wide $PLUTO_ELF | grep -F "FUNC" | awk '{$1=$3=$4=$5=$6=$7=""; print $0}' | sort -k 1 > $MAP_FILE +echo "" >> $MAP_FILE + +grub-mkrescue -o $OUTPUT_FILE $ISO_DIR diff --git a/src/kernel/arch.zig b/src/kernel/arch.zig index b96300ee..8cf9be08 100644 --- a/src/kernel/arch.zig +++ b/src/kernel/arch.zig @@ -1,8 +1,10 @@ +const std = @import("std"); const builtin = @import("builtin"); const is_test = builtin.is_test; const build_options = @import("build_options"); +const mock_path = build_options.mock_path; -pub const internals = if (is_test) @import(build_options.mock_path ++ "arch_mock.zig") else switch (builtin.arch) { +pub const internals = if (is_test) @import(mock_path ++ "arch_mock.zig") else switch (builtin.arch) { .i386 => @import("arch/x86/arch.zig"), else => unreachable, }; diff --git a/src/kernel/arch/x86/arch.zig b/src/kernel/arch/x86/arch.zig index 2a9300c4..498ccce1 100644 --- a/src/kernel/arch/x86/arch.zig +++ b/src/kernel/arch/x86/arch.zig @@ -1,22 +1,54 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const builtin = @import("builtin"); +const cmos = @import("cmos.zig"); const gdt = @import("gdt.zig"); const idt = @import("idt.zig"); -const pic = @import("pic.zig"); const irq = @import("irq.zig"); const isr = @import("isr.zig"); -const pit = @import("pit.zig"); const paging = @import("paging.zig"); +const pic = @import("pic.zig"); +const pit = @import("pit.zig"); +const rtc = @import("rtc.zig"); +const serial = @import("serial.zig"); const syscalls = @import("syscalls.zig"); +const tty = @import("tty.zig"); +const vga = @import("vga.zig"); const mem = @import("../../mem.zig"); -const multiboot = @import("../../multiboot.zig"); +const multiboot = @import("multiboot.zig"); +const vmm = @import("../../vmm.zig"); +const log = @import("../../log.zig"); +const Serial = @import("../../serial.zig").Serial; +const panic = @import("../../panic.zig").panic; +const TTY = @import("../../tty.zig").TTY; const MemProfile = mem.MemProfile; +/// The virtual end of the kernel code. +extern var KERNEL_VADDR_END: *u32; + +/// The virtual start of the kernel code. +extern var KERNEL_VADDR_START: *u32; + +/// The physical end of the kernel code. +extern var KERNEL_PHYSADDR_END: *u32; + +/// The physical start of the kernel code. +extern var KERNEL_PHYSADDR_START: *u32; + +/// The boot-time offset that the virtual addresses are from the physical addresses. +extern var KERNEL_ADDR_OFFSET: *u32; + +/// The virtual address of the top limit of the stack. +extern var KERNEL_STACK_START: *u32; + +/// The virtual address of the base of the stack. +extern var KERNEL_STACK_END: *u32; + /// The interrupt context that is given to a interrupt handler. It contains most of the registers /// and the interrupt number and error code (if there is one). -pub const InterruptContext = struct { +pub const CpuState = packed struct { // Extra segments + ss: u32, gs: u32, fs: u32, es: u32, @@ -43,9 +75,30 @@ pub const InterruptContext = struct { cs: u32, eflags: u32, user_esp: u32, - ss: u32, + user_ss: u32, }; +/// x86's boot payload is the multiboot info passed by grub +pub const BootPayload = *multiboot.multiboot_info_t; + +/// The type of the payload passed to a virtual memory mapper. +/// For x86 it's the page directory that should be mapped. +pub const VmmPayload = *paging.Directory; + +/// The payload used in the kernel virtual memory manager. +/// For x86 it's the kernel's page directory. +pub const KERNEL_VMM_PAYLOAD = &paging.kernel_directory; + +/// The architecture's virtual memory mapper. +/// For x86, it simply forwards the calls to the paging subsystem. +pub const VMM_MAPPER: vmm.Mapper(VmmPayload) = vmm.Mapper(VmmPayload){ .mapFn = paging.map, .unmapFn = paging.unmap }; + +/// The size of each allocatable block of memory, normally set to the page size. +pub const MEMORY_BLOCK_SIZE: usize = paging.PAGE_SIZE_4KB; + +/// The default stack size of a task. Currently this is set to a page size. +pub const STACK_SIZE: u32 = MEMORY_BLOCK_SIZE / @sizeOf(u32); + /// /// Assembly to write to a given port with a byte of data. /// @@ -77,6 +130,13 @@ pub fn inb(port: u16) u8 { ); } +/// +/// Force the CPU to wait for an I/O operation to compete. Use port 0x80 as this is unused. +/// +pub fn ioWait() void { + outb(0x80, 0); +} + /// /// Load the GDT and refreshing the code segment with the code segment offset of the kernel as we /// are still in kernel land. Also loads the kernel data segment into all the other segment @@ -189,10 +249,9 @@ pub fn halt() void { /// Wait the kernel but still can handle interrupts. /// pub fn spinWait() noreturn { + enableInterrupts(); while (true) { - enableInterrupts(); halt(); - disableInterrupts(); } } @@ -206,19 +265,200 @@ pub fn haltNoInterrupts() noreturn { } } +/// +/// Write a byte to serial port com1. Used by the serial initialiser +/// +/// Arguments: +/// IN byte: u8 - The byte to write +/// +fn writeSerialCom1(byte: u8) void { + serial.write(byte, serial.Port.COM1); +} + +/// +/// Initialise serial communication using port COM1 and construct a Serial instance +/// +/// Arguments: +/// IN boot_payload: arch.BootPayload - The payload passed at boot. Not currently used by x86 +/// +/// Return: serial.Serial +/// The Serial instance constructed with the function used to write bytes +/// +pub fn initSerial(boot_payload: BootPayload) Serial { + serial.init(serial.DEFAULT_BAUDRATE, serial.Port.COM1) catch |e| { + panic(@errorReturnTrace(), "Failed to initialise serial: {}", .{e}); + }; + return Serial{ + .write = writeSerialCom1, + }; +} + +/// +/// Initialise the TTY and construct a TTY instance +/// +/// Arguments: +/// IN boot_payload: BootPayload - The payload passed to the kernel on boot +/// +/// Return: tty.TTY +/// The TTY instance constructed with the information required by the rest of the kernel +/// +pub fn initTTY(boot_payload: BootPayload) TTY { + return .{ + .print = tty.writeString, + .setCursor = tty.setCursor, + .cols = vga.WIDTH, + .rows = vga.HEIGHT, + .clear = tty.clearScreen, + }; +} + +/// +/// Initialise the system's memory. Populates a memory profile with boot modules from grub, the amount of available memory, the reserved regions of virtual and physical memory as well as the start and end of the kernel code +/// +/// Arguments: +/// IN mb_info: *multiboot.multiboot_info_t - The multiboot info passed by grub +/// +/// Return: mem.MemProfile +/// The constructed memory profile +/// +/// Error: Allocator.Error +/// Allocator.Error.OutOfMemory - There wasn't enough memory in the allocated created to populate the memory profile, consider increasing mem.FIXED_ALLOC_SIZE +/// +pub fn initMem(mb_info: BootPayload) Allocator.Error!MemProfile { + log.logInfo("Init mem\n", .{}); + defer log.logInfo("Done mem\n", .{}); + + log.logDebug("KERNEL_ADDR_OFFSET: 0x{X}\n", .{@ptrToInt(&KERNEL_ADDR_OFFSET)}); + log.logDebug("KERNEL_STACK_START: 0x{X}\n", .{@ptrToInt(&KERNEL_STACK_START)}); + log.logDebug("KERNEL_STACK_END: 0x{X}\n", .{@ptrToInt(&KERNEL_STACK_END)}); + log.logDebug("KERNEL_VADDR_START: 0x{X}\n", .{@ptrToInt(&KERNEL_VADDR_START)}); + log.logDebug("KERNEL_VADDR_END: 0x{X}\n", .{@ptrToInt(&KERNEL_VADDR_END)}); + log.logDebug("KERNEL_PHYSADDR_START: 0x{X}\n", .{@ptrToInt(&KERNEL_PHYSADDR_START)}); + log.logDebug("KERNEL_PHYSADDR_END: 0x{X}\n", .{@ptrToInt(&KERNEL_PHYSADDR_END)}); + + const mods_count = mb_info.mods_count; + mem.ADDR_OFFSET = @ptrToInt(&KERNEL_ADDR_OFFSET); + const mmap_addr = mb_info.mmap_addr; + const num_mmap_entries = mb_info.mmap_length / @sizeOf(multiboot.multiboot_memory_map_t); + const vaddr_end = @ptrCast([*]u8, &KERNEL_VADDR_END); + + var allocator = std.heap.FixedBufferAllocator.init(vaddr_end[0..mem.FIXED_ALLOC_SIZE]); + var reserved_physical_mem = std.ArrayList(mem.Range).init(&allocator.allocator); + var reserved_virtual_mem = std.ArrayList(mem.Map).init(&allocator.allocator); + const mem_map = @intToPtr([*]multiboot.multiboot_memory_map_t, mmap_addr)[0..num_mmap_entries]; + + // Reserve the unavailable sections from the multiboot memory map + for (mem_map) |entry| { + if (entry.@"type" != multiboot.MULTIBOOT_MEMORY_AVAILABLE) { + // If addr + len is greater than maxInt(usize) just ignore whatever comes after maxInt(usize) since it can't be addressed anyway + const end: usize = if (entry.addr > std.math.maxInt(usize) - entry.len) std.math.maxInt(usize) else @intCast(usize, entry.addr + entry.len); + try reserved_physical_mem.append(.{ .start = @intCast(usize, entry.addr), .end = end }); + } + } + + // Map the multiboot info struct itself + const mb_region = mem.Range{ + .start = @ptrToInt(mb_info), + .end = @ptrToInt(mb_info) + @sizeOf(multiboot.multiboot_info_t), + }; + const mb_physical = mem.Range{ .start = mem.virtToPhys(mb_region.start), .end = mem.virtToPhys(mb_region.end) }; + try reserved_virtual_mem.append(.{ .virtual = mb_region, .physical = mb_physical }); + + // Map the tty buffer + const tty_addr = mem.virtToPhys(tty.getVideoBufferAddress()); + const tty_region = mem.Range{ + .start = tty_addr, + .end = tty_addr + 32 * 1024, + }; + try reserved_virtual_mem.append(.{ + .physical = tty_region, + .virtual = .{ + .start = mem.physToVirt(tty_region.start), + .end = mem.physToVirt(tty_region.end), + }, + }); + + // Map the boot modules + const boot_modules = @intToPtr([*]multiboot.multiboot_mod_list, mem.physToVirt(mb_info.mods_addr))[0..mods_count]; + var modules = std.ArrayList(mem.Module).init(&allocator.allocator); + for (boot_modules) |module| { + const virtual = mem.Range{ .start = mem.physToVirt(module.mod_start), .end = mem.physToVirt(module.mod_end) }; + const physical = mem.Range{ .start = module.mod_start, .end = module.mod_end }; + try modules.append(.{ .region = virtual, .name = std.mem.span(mem.physToVirt(@intToPtr([*:0]u8, module.cmdline))) }); + try reserved_virtual_mem.append(.{ .physical = physical, .virtual = virtual }); + } + + return MemProfile{ + .vaddr_end = vaddr_end, + .vaddr_start = @ptrCast([*]u8, &KERNEL_VADDR_START), + .physaddr_end = @ptrCast([*]u8, &KERNEL_PHYSADDR_END), + .physaddr_start = @ptrCast([*]u8, &KERNEL_PHYSADDR_START), + // Total memory available including the initial 1MiB that grub doesn't include + .mem_kb = mb_info.mem_upper + mb_info.mem_lower + 1024, + .modules = modules.items, + .physical_reserved = reserved_physical_mem.items, + .virtual_reserved = reserved_virtual_mem.items, + .fixed_allocator = allocator, + }; +} + +/// +/// Initialise a 32bit kernel stack used for creating a task. +/// Currently only support fn () noreturn functions for the entry point. +/// +/// Arguments: +/// IN entry_point: usize - The pointer to the entry point of the function. Functions only +/// supported is fn () noreturn +/// IN allocator: *Allocator - The allocator use for allocating a stack. +/// +/// Return: struct { stack: []u32, pointer: usize } +/// The stack and stack pointer with the stack initialised as a 32bit kernel stack. +/// +/// Error: Allocator.Error +/// OutOfMemory - Unable to allocate space for the stack. +/// +pub fn initTaskStack(entry_point: usize, allocator: *Allocator) Allocator.Error!struct { stack: []u32, pointer: usize } { + // TODO Will need to add the exit point + // Set up everything as a kernel task + var stack = try allocator.alloc(u32, STACK_SIZE); + stack[STACK_SIZE - 18] = gdt.KERNEL_DATA_OFFSET; // ss + stack[STACK_SIZE - 17] = gdt.KERNEL_DATA_OFFSET; // gs + stack[STACK_SIZE - 16] = gdt.KERNEL_DATA_OFFSET; // fs + stack[STACK_SIZE - 15] = gdt.KERNEL_DATA_OFFSET; // es + stack[STACK_SIZE - 14] = gdt.KERNEL_DATA_OFFSET; // ds + + stack[STACK_SIZE - 13] = 0; // edi + stack[STACK_SIZE - 12] = 0; // esi + // End of the stack + stack[STACK_SIZE - 11] = @ptrToInt(&stack[STACK_SIZE - 1]); // ebp + stack[STACK_SIZE - 10] = 0; // esp (temp) this won't be popped by popa bc intel is dump XD + + stack[STACK_SIZE - 9] = 0; // ebx + stack[STACK_SIZE - 8] = 0; // edx + stack[STACK_SIZE - 7] = 0; // ecx + stack[STACK_SIZE - 6] = 0; // eax + + stack[STACK_SIZE - 5] = 0; // int_num + stack[STACK_SIZE - 4] = 0; // error_code + + stack[STACK_SIZE - 3] = entry_point; // eip + stack[STACK_SIZE - 2] = gdt.KERNEL_CODE_OFFSET; // cs + stack[STACK_SIZE - 1] = 0x202; // eflags + + const ret = .{ .stack = stack, .pointer = @ptrToInt(&stack[STACK_SIZE - 18]) }; + return ret; +} + /// /// Initialise the architecture /// /// Arguments: +/// IN boot_payload: BootPayload - The multiboot information from the GRUB bootloader. /// IN mem_profile: *const MemProfile - The memory profile of the computer. Used to set up /// paging. /// IN allocator: *Allocator - The allocator use to handle memory. -/// IN comptime options: type - The build options that is passed to the kernel to be -/// used for run time testing. /// -pub fn init(mb_info: *multiboot.multiboot_info_t, mem_profile: *const MemProfile, allocator: *Allocator) void { - disableInterrupts(); - +pub fn init(boot_payload: BootPayload, mem_profile: *const MemProfile, allocator: *Allocator) void { gdt.init(); idt.init(); @@ -226,22 +466,19 @@ pub fn init(mb_info: *multiboot.multiboot_info_t, mem_profile: *const MemProfile isr.init(); irq.init(); - pit.init(); + paging.init(boot_payload, mem_profile, allocator); - paging.init(mb_info, mem_profile, allocator); + pit.init(); + rtc.init(); syscalls.init(); - enableInterrupts(); + // Initialise the VGA and TTY here since their tests belong the architecture and so should be a part of the + // arch init test messages + vga.init(); + tty.init(); } test "" { - _ = @import("gdt.zig"); - _ = @import("idt.zig"); - _ = @import("pic.zig"); - _ = @import("isr.zig"); - _ = @import("irq.zig"); - _ = @import("pit.zig"); - _ = @import("syscalls.zig"); - _ = @import("paging.zig"); + std.meta.refAllDecls(@This()); } diff --git a/src/kernel/arch/x86/boot.zig b/src/kernel/arch/x86/boot.zig index 8efc58f1..22d36a71 100644 --- a/src/kernel/arch/x86/boot.zig +++ b/src/kernel/arch/x86/boot.zig @@ -1,4 +1,5 @@ const constants = @import("constants"); +const arch = @import("arch.zig"); /// The multiboot header const MultiBoot = packed struct { @@ -63,8 +64,9 @@ export var boot_page_directory: [1024]u32 align(4096) linksection(".rodata.boot" }; export var kernel_stack: [16 * 1024]u8 align(16) linksection(".bss.stack") = undefined; +extern var KERNEL_ADDR_OFFSET: *u32; -extern fn kmain() void; +extern fn kmain(mb_info: arch.BootPayload) void; export fn _start() align(16) linksection(".text.boot") callconv(.Naked) noreturn { // Set the page directory to the boot directory @@ -99,16 +101,14 @@ export fn start_higher_half() callconv(.Naked) noreturn { asm volatile ( \\.extern KERNEL_STACK_END \\mov $KERNEL_STACK_END, %%esp - \\xor %%ebp, %%ebp + \\mov %%esp, %%ebp ); - // Push the bootloader magic number and multiboot header address with virtual offset - asm volatile ( - \\.extern KERNEL_ADDR_OFFSET - \\push %%eax - \\add $KERNEL_ADDR_OFFSET, %%ebx - \\push %%ebx - ); - kmain(); + // Get the multiboot header address and add the virtual offset + const mb_info_addr = asm ( + \\mov %%ebx, %[res] + : [res] "=r" (-> usize) + ) + @ptrToInt(&KERNEL_ADDR_OFFSET); + kmain(@intToPtr(arch.BootPayload, mb_info_addr)); while (true) {} } diff --git a/src/kernel/arch/x86/cmos.zig b/src/kernel/arch/x86/cmos.zig new file mode 100644 index 00000000..a7deece7 --- /dev/null +++ b/src/kernel/arch/x86/cmos.zig @@ -0,0 +1,463 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const is_test = builtin.is_test; +const expectEqual = std.testing.expectEqual; +const build_options = @import("build_options"); +const mock_path = build_options.arch_mock_path; +const arch = if (is_test) @import(mock_path ++ "arch_mock.zig") else @import("arch.zig"); + +/// The current year to be used for calculating the 4 digit year, as the CMOS return the last two +/// digits of the year. +const CURRENT_CENTURY: u8 = 2000; + +/// The port address for the CMOS command register. +const ADDRESS: u16 = 0x70; + +/// The port address for the CMOS data register. +const DATA: u16 = 0x71; + +/// The register location for returning the seconds, (0 - 59). +const REGISTER_SECOND: u8 = 0x00; + +/// The register location for returning the minute, (0 - 59). +const REGISTER_MINUTE: u8 = 0x02; + +/// The register location for returning the hours, (0 - 23 or 0 - 12 depending if a 12hr or 24hr +/// clock). +const REGISTER_HOUR: u8 = 0x04; + +/// The register location for returning the weekday, (0 - 6). Very unreliable, so will calculate +/// the day of the week instead. +const REGISTER_WEEKDAY: u8 = 0x06; + +/// The register location for returning the day, (0 - 31). +const REGISTER_DAY: u8 = 0x07; + +/// The register location for returning the month, (0 - 11). +const REGISTER_MONTH: u8 = 0x08; + +/// The register location for returning the year, (0 - 99). +const REGISTER_YEAR: u8 = 0x09; + +/// The register location for returning the century. +const REGISTER_CENTURY: u8 = 0x32; + +/// The register location for return the status A register. +const STATUS_REGISTER_A: u8 = 0x0A; + +/// The register location for return the status B register. +const STATUS_REGISTER_B: u8 = 0x0B; + +/// The register location for return the status C register. +const STATUS_REGISTER_C: u8 = 0x0C; + +/// The non-mockable interrupts are on the 8th bit of port 0x70 which is the register select port +/// for the CMOS. This will need to be disabled when selecting CMOS registers. +const NMI_BIT: u8 = 0x80; + +/// The enum for selecting the status register to read from +pub const StatusRegister = enum { + /// Status register A + A, + + /// Status register B + B, + + /// Status register C + C, + + /// + /// Get the register index for the status registers + /// + /// Arguments: + /// IN reg: StatusRegister - The enum that represents one of the 3 status registers. + /// + /// Return: u8 + /// The register index for one of the 3 status registers. + /// + pub fn getRegister(reg: StatusRegister) u8 { + return switch (reg) { + .A => STATUS_REGISTER_A, + .B => STATUS_REGISTER_B, + .C => STATUS_REGISTER_C, + }; + } +}; + +/// The enum for selecting the real time clock registers. +pub const RtcRegister = enum { + /// The seconds register + SECOND, + + /// The minutes register + MINUTE, + + /// The hours register + HOUR, + + /// The days register + DAY, + + /// The months register + MONTH, + + /// The year register + YEAR, + + /// The century register + CENTURY, + + /// + /// Get the register index for the RTC registers + /// + /// Arguments: + /// IN reg: RtcRegister - The enum that represents one of the RTC registers. + /// + /// Return: u8 + /// The register index for one of the RTC registers. + /// + pub fn getRegister(reg: RtcRegister) u8 { + return switch (reg) { + .SECOND => REGISTER_SECOND, + .MINUTE => REGISTER_MINUTE, + .HOUR => REGISTER_HOUR, + .DAY => REGISTER_DAY, + .MONTH => REGISTER_MONTH, + .YEAR => REGISTER_YEAR, + .CENTURY => REGISTER_CENTURY, + }; + } +}; + +/// +/// Tell the CMOS chip to select the given register ready for read or writing to. This also +/// disables the NMI when disable_nmi is true. +/// +/// Arguments: +/// IN reg: u8 - The register index to select in the CMOS chip. +/// IN comptime disable_nmi: bool - Whether to disable NMI when selecting a register. +/// +inline fn selectRegister(reg: u8, comptime disable_nmi: bool) void { + if (disable_nmi) { + arch.outb(ADDRESS, reg | NMI_BIT); + } else { + arch.outb(ADDRESS, reg); + } +} + +/// +/// Write to the selected register to the CMOS chip. +/// +/// Arguments: +/// IN data: u8 - The data to write to the selected register. +/// +inline fn writeRegister(data: u8) void { + arch.outb(DATA, data); +} + +/// +/// Read the selected register from the CMOS chip. +/// +/// Return: u8 +/// The value in the selected register. +/// +inline fn readRegister() u8 { + return arch.inb(DATA); +} + +/// +/// Select then read a register from the CMOS chip. This include a I/O wait to ensure the CMOS chip +/// has time to select the register. +/// +/// Arguments: +/// IN reg: u8 - The register index to select in the CMOS chip. +/// IN comptime disable_nmi: bool - Whether to disable NMI when selecting a register. +/// +/// Return: u8 +/// The value in the selected register. +/// +inline fn selectAndReadRegister(reg: u8, comptime disable_nmi: bool) u8 { + selectRegister(reg, disable_nmi); + arch.ioWait(); + return readRegister(); +} + +/// +/// Select then write to a register to the CMOS chip. This include a I/O wait to ensure the CMOS chip +/// has time to select the register. +/// +/// Arguments: +/// IN reg: u8 - The register index to select in the CMOS chip. +/// IN data: u8 - The data to write to the selected register. +/// IN comptime disable_nmi: bool - Whether to disable NMI when selecting a register. +/// +inline fn selectAndWriteRegister(reg: u8, data: u8, comptime disable_nmi: bool) void { + selectRegister(reg, disable_nmi); + arch.ioWait(); + writeRegister(data); +} + +/// +/// Read a register that corresponds to a real time clock register. +/// +/// Arguments: +/// IN reg: RtcRegister - A RTC register to select in the CMOS chip. +/// +/// Return: u8 +/// The value in the selected register. +/// +pub fn readRtcRegister(reg: RtcRegister) u8 { + return selectAndReadRegister(reg.getRegister(), false); +} + +/// +/// Read a status register in the CMOS chip. +/// +/// Arguments: +/// IN reg: StatusRegister - The status register to select. +/// IN comptime disable_nmi: bool - Whether to disable NMI when selecting a register. +/// +/// Return: u8 +/// The value in the selected register. +/// +pub fn readStatusRegister(reg: StatusRegister, comptime disable_nmi: bool) u8 { + return selectAndReadRegister(reg.getRegister(), disable_nmi); +} + +/// +/// Write to a status register in the CMOS chip. +/// +/// Arguments: +/// IN reg: StatusRegister - The status register to select. +/// IN data: u8 - The data to write to the selected register. +/// IN comptime disable_nmi: bool - Whether to disable NMI when selecting a register. +/// +pub fn writeStatusRegister(reg: StatusRegister, data: u8, comptime disable_nmi: bool) void { + selectAndWriteRegister(reg.getRegister(), data, disable_nmi); +} + +test "selectRegister" { + arch.initTest(); + defer arch.freeTest(); + + arch.addTestParams("outb", .{ ADDRESS, STATUS_REGISTER_A }); + + const reg = STATUS_REGISTER_A; + + selectRegister(reg, false); +} + +test "selectRegister no NMI" { + arch.initTest(); + defer arch.freeTest(); + + arch.addTestParams("outb", .{ ADDRESS, STATUS_REGISTER_A | NMI_BIT }); + + const reg = STATUS_REGISTER_A; + + selectRegister(reg, true); +} + +test "writeRegister" { + arch.initTest(); + defer arch.freeTest(); + + arch.addTestParams("outb", .{ DATA, @as(u8, 0xAA) }); + + const data = @as(u8, 0xAA); + + writeRegister(data); +} + +test "readRegister" { + arch.initTest(); + defer arch.freeTest(); + + arch.addTestParams("inb", .{ DATA, @as(u8, 0x55) }); + + const expected = @as(u8, 0x55); + const actual = readRegister(); + + expectEqual(expected, actual); +} + +test "selectAndReadRegister NMI" { + arch.initTest(); + defer arch.freeTest(); + + arch.addTestParams("outb", .{ ADDRESS, STATUS_REGISTER_C }); + arch.addTestParams("inb", .{ DATA, @as(u8, 0x44) }); + arch.addConsumeFunction("ioWait", arch.mock_ioWait); + + const reg = STATUS_REGISTER_C; + + const expected = @as(u8, 0x44); + const actual = selectAndReadRegister(reg, false); + + expectEqual(expected, actual); +} + +test "selectAndReadRegister no NMI" { + arch.initTest(); + defer arch.freeTest(); + + arch.addTestParams("outb", .{ ADDRESS, STATUS_REGISTER_C | NMI_BIT }); + arch.addTestParams("inb", .{ DATA, @as(u8, 0x44) }); + arch.addConsumeFunction("ioWait", arch.mock_ioWait); + + const reg = STATUS_REGISTER_C; + + const expected = @as(u8, 0x44); + const actual = selectAndReadRegister(reg, true); + + expectEqual(expected, actual); +} + +test "selectAndWriteRegister NMI" { + arch.initTest(); + defer arch.freeTest(); + + arch.addTestParams("outb", .{ ADDRESS, STATUS_REGISTER_C, DATA, @as(u8, 0x88) }); + arch.addConsumeFunction("ioWait", arch.mock_ioWait); + + const reg = STATUS_REGISTER_C; + const data = @as(u8, 0x88); + + selectAndWriteRegister(reg, data, false); +} + +test "selectAndWriteRegister no NMI" { + arch.initTest(); + defer arch.freeTest(); + + arch.addTestParams("outb", .{ ADDRESS, STATUS_REGISTER_C | NMI_BIT, DATA, @as(u8, 0x88) }); + arch.addConsumeFunction("ioWait", arch.mock_ioWait); + + const reg = STATUS_REGISTER_C; + const data = @as(u8, 0x88); + + selectAndWriteRegister(reg, data, true); +} + +test "readRtcRegister" { + arch.initTest(); + defer arch.freeTest(); + + arch.addRepeatFunction("ioWait", arch.mock_ioWait); + + const rtc_regs = [_]RtcRegister{ RtcRegister.SECOND, RtcRegister.MINUTE, RtcRegister.HOUR, RtcRegister.DAY, RtcRegister.MONTH, RtcRegister.YEAR, RtcRegister.CENTURY }; + + for (rtc_regs) |reg| { + const r = switch (reg) { + .SECOND => REGISTER_SECOND, + .MINUTE => REGISTER_MINUTE, + .HOUR => REGISTER_HOUR, + .DAY => REGISTER_DAY, + .MONTH => REGISTER_MONTH, + .YEAR => REGISTER_YEAR, + .CENTURY => REGISTER_CENTURY, + }; + + arch.addTestParams("outb", .{ ADDRESS, r }); + arch.addTestParams("inb", .{ DATA, @as(u8, 0x44) }); + + const expected = @as(u8, 0x44); + const actual = readRtcRegister(reg); + + expectEqual(expected, actual); + } +} + +test "readStatusRegister NMI" { + arch.initTest(); + defer arch.freeTest(); + + arch.addRepeatFunction("ioWait", arch.mock_ioWait); + + const status_regs = [_]StatusRegister{ StatusRegister.A, StatusRegister.B, StatusRegister.C }; + + for (status_regs) |reg| { + const r = switch (reg) { + .A => STATUS_REGISTER_A, + .B => STATUS_REGISTER_B, + .C => STATUS_REGISTER_C, + }; + + arch.addTestParams("outb", .{ ADDRESS, r }); + arch.addTestParams("inb", .{ DATA, @as(u8, 0x78) }); + + const expected = @as(u8, 0x78); + const actual = readStatusRegister(reg, false); + + expectEqual(expected, actual); + } +} + +test "readStatusRegister no NMI" { + arch.initTest(); + defer arch.freeTest(); + + arch.addRepeatFunction("ioWait", arch.mock_ioWait); + + const status_regs = [_]StatusRegister{ StatusRegister.A, StatusRegister.B, StatusRegister.C }; + + for (status_regs) |reg| { + const r = switch (reg) { + .A => STATUS_REGISTER_A, + .B => STATUS_REGISTER_B, + .C => STATUS_REGISTER_C, + }; + + arch.addTestParams("outb", .{ ADDRESS, r | NMI_BIT }); + arch.addTestParams("inb", .{ DATA, @as(u8, 0x78) }); + + const expected = @as(u8, 0x78); + const actual = readStatusRegister(reg, true); + + expectEqual(expected, actual); + } +} + +test "writeStatusRegister NMI" { + arch.initTest(); + defer arch.freeTest(); + + arch.addRepeatFunction("ioWait", arch.mock_ioWait); + + const status_regs = [_]StatusRegister{ StatusRegister.A, StatusRegister.B, StatusRegister.C }; + + for (status_regs) |reg| { + const r = switch (reg) { + .A => STATUS_REGISTER_A, + .B => STATUS_REGISTER_B, + .C => STATUS_REGISTER_C, + }; + + arch.addTestParams("outb", .{ ADDRESS, r, DATA, @as(u8, 0x43) }); + + const data = @as(u8, 0x43); + writeStatusRegister(reg, data, false); + } +} + +test "writeStatusRegister no NMI" { + arch.initTest(); + defer arch.freeTest(); + + arch.addRepeatFunction("ioWait", arch.mock_ioWait); + + const status_regs = [_]StatusRegister{ StatusRegister.A, StatusRegister.B, StatusRegister.C }; + + for (status_regs) |reg| { + const r = switch (reg) { + .A => STATUS_REGISTER_A, + .B => STATUS_REGISTER_B, + .C => STATUS_REGISTER_C, + }; + + arch.addTestParams("outb", .{ ADDRESS, r | NMI_BIT, DATA, @as(u8, 0x43) }); + + const data = @as(u8, 0x43); + writeStatusRegister(reg, data, true); + } +} diff --git a/src/kernel/arch/x86/gdt.zig b/src/kernel/arch/x86/gdt.zig index 76b54522..4429ee55 100644 --- a/src/kernel/arch/x86/gdt.zig +++ b/src/kernel/arch/x86/gdt.zig @@ -3,7 +3,7 @@ const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; const builtin = @import("builtin"); const is_test = builtin.is_test; - +const panic = @import("../../panic.zig").panic; const build_options = @import("build_options"); const mock_path = build_options.arch_mock_path; const arch = if (is_test) @import(mock_path ++ "arch_mock.zig") else @import("arch.zig"); @@ -84,27 +84,31 @@ const GdtEntry = packed struct { }; /// The TSS entry structure -const TtsEntry = packed struct { +const Tss = packed struct { /// Pointer to the previous TSS entry - prev_tss: u32, + prev_tss: u16, + reserved1: u16, /// Ring 0 32 bit stack pointer. esp0: u32, /// Ring 0 32 bit stack pointer. - ss0: u32, + ss0: u16, + reserved2: u16, /// Ring 1 32 bit stack pointer. esp1: u32, /// Ring 1 32 bit stack pointer. - ss1: u32, + ss1: u16, + reserved3: u16, /// Ring 2 32 bit stack pointer. esp2: u32, /// Ring 2 32 bit stack pointer. - ss2: u32, + ss2: u16, + reserved4: u16, /// The CR3 control register 3. cr3: u32, @@ -140,25 +144,32 @@ const TtsEntry = packed struct { edi: u32, /// The extra segment. - es: u32, + es: u16, + reserved5: u16, /// The code segment. - cs: u32, + cs: u16, + reserved6: u16, /// The stack segment. - ss: u32, + ss: u16, + reserved7: u16, /// The data segment. - ds: u32, + ds: u16, + reserved8: u16, /// A extra segment FS. - fs: u32, + fs: u16, + reserved9: u16, /// A extra segment GS. - gs: u32, + gs: u16, + reserved10: u16, /// The local descriptor table register. - ldtr: u32, + ldtr: u16, + reserved11: u16, /// ? trap: u16, @@ -177,8 +188,8 @@ pub const GdtPtr = packed struct { base: u32, }; -/// The total number of entries in the GTD: null, kernel code, kernel data, user code, user data -/// and TSS +/// The total number of entries in the GDT including: null, kernel code, kernel data, user code, +/// user data and the TSS. const NUMBER_OF_ENTRIES: u16 = 0x06; /// The size of the GTD in bytes (minus 1). @@ -315,24 +326,28 @@ pub const USER_DATA_OFFSET: u16 = 0x20; pub const TSS_OFFSET: u16 = 0x28; /// The GDT entry table of NUMBER_OF_ENTRIES entries. -var gdt_entries: [NUMBER_OF_ENTRIES]GdtEntry = [_]GdtEntry{ +var gdt_entries: [NUMBER_OF_ENTRIES]GdtEntry = init: { + var gdt_entries_temp: [NUMBER_OF_ENTRIES]GdtEntry = undefined; + // Null descriptor - makeEntry(0, 0, NULL_SEGMENT, NULL_FLAGS), + gdt_entries_temp[0] = makeGdtEntry(0, 0, NULL_SEGMENT, NULL_FLAGS); - // Kernel Code - makeEntry(0, 0xFFFFF, KERNEL_SEGMENT_CODE, PAGING_32_BIT), + // Kernel code descriptor + gdt_entries_temp[1] = makeGdtEntry(0, 0xFFFFF, KERNEL_SEGMENT_CODE, PAGING_32_BIT); - // Kernel Data - makeEntry(0, 0xFFFFF, KERNEL_SEGMENT_DATA, PAGING_32_BIT), + // Kernel data descriptor + gdt_entries_temp[2] = makeGdtEntry(0, 0xFFFFF, KERNEL_SEGMENT_DATA, PAGING_32_BIT); - // User Code - makeEntry(0, 0xFFFFF, USER_SEGMENT_CODE, PAGING_32_BIT), + // User code descriptor + gdt_entries_temp[3] = makeGdtEntry(0, 0xFFFFF, USER_SEGMENT_CODE, PAGING_32_BIT); - // User Data - makeEntry(0, 0xFFFFF, USER_SEGMENT_DATA, PAGING_32_BIT), + // User data descriptor + gdt_entries_temp[4] = makeGdtEntry(0, 0xFFFFF, USER_SEGMENT_DATA, PAGING_32_BIT); - // Fill in TSS at runtime - makeEntry(0, 0, NULL_SEGMENT, NULL_FLAGS), + // TSS descriptor, one each for each processor + // Will initialise the TSS at runtime + gdt_entries_temp[5] = makeGdtEntry(0, 0, NULL_SEGMENT, NULL_FLAGS); + break :init gdt_entries_temp; }; /// The GDT pointer that the CPU is loaded with that contains the base address of the GDT and the @@ -342,35 +357,12 @@ var gdt_ptr: GdtPtr = GdtPtr{ .base = undefined, }; -/// The task state segment entry. -var tss: TtsEntry = TtsEntry{ - .prev_tss = 0, - .esp0 = 0, - .ss0 = KERNEL_DATA_OFFSET, - .esp1 = 0, - .ss1 = 0, - .esp2 = 0, - .ss2 = 0, - .cr3 = 0, - .eip = 0, - .eflags = 0, - .eax = 0, - .ecx = 0, - .edx = 0, - .ebx = 0, - .esp = 0, - .ebp = 0, - .esi = 0, - .edi = 0, - .es = 0, - .cs = 0, - .ss = 0, - .ds = 0, - .fs = 0, - .gs = 0, - .ldtr = 0, - .trap = 0, - .io_permissions_base_offset = @sizeOf(TtsEntry), +/// The main task state segment entry. +var main_tss_entry: Tss = init: { + var tss_temp = std.mem.zeroes(Tss); + tss_temp.ss0 = KERNEL_DATA_OFFSET; + tss_temp.io_permissions_base_offset = @sizeOf(Tss); + break :init tss_temp; }; /// @@ -386,11 +378,11 @@ var tss: TtsEntry = TtsEntry{ /// A new GDT entry with the give access and flag bits set with the base at 0x00000000 and /// limit at 0xFFFFF. /// -fn makeEntry(base: u32, limit: u20, access: AccessBits, flags: FlagBits) GdtEntry { - return GdtEntry{ +fn makeGdtEntry(base: u32, limit: u20, access: AccessBits, flags: FlagBits) GdtEntry { + return .{ .limit_low = @truncate(u16, limit), .base_low = @truncate(u24, base), - .access = AccessBits{ + .access = .{ .accessed = access.accessed, .read_write = access.read_write, .direction_conforming = access.direction_conforming, @@ -400,7 +392,7 @@ fn makeEntry(base: u32, limit: u20, access: AccessBits, flags: FlagBits) GdtEntr .present = access.present, }, .limit_high = @truncate(u4, limit >> 16), - .flags = FlagBits{ + .flags = .{ .reserved_zero = flags.reserved_zero, .is_64_bit = flags.is_64_bit, .is_32_bit = flags.is_32_bit, @@ -410,26 +402,17 @@ fn makeEntry(base: u32, limit: u20, access: AccessBits, flags: FlagBits) GdtEntr }; } -/// -/// Set the stack pointer in the TSS entry. -/// -/// Arguments: -/// IN esp0: u32 - The stack pointer. -/// -pub fn setTssStack(esp0: u32) void { - tss.esp0 = esp0; -} - /// /// Initialise the Global Descriptor table. /// pub fn init() void { log.logInfo("Init gdt\n", .{}); + defer log.logInfo("Done gdt\n", .{}); // Initiate TSS - gdt_entries[TSS_INDEX] = makeEntry(@intCast(u32, @ptrToInt(&tss)), @sizeOf(TtsEntry) - 1, TSS_SEGMENT, NULL_FLAGS); + gdt_entries[TSS_INDEX] = makeGdtEntry(@ptrToInt(&main_tss_entry), @sizeOf(Tss) - 1, TSS_SEGMENT, NULL_FLAGS); // Set the base address where all the GDT entries are. - gdt_ptr.base = @intCast(u32, @ptrToInt(&gdt_entries[0])); + gdt_ptr.base = @ptrToInt(&gdt_entries[0]); // Load the GDT arch.lgdt(&gdt_ptr); @@ -437,21 +420,22 @@ pub fn init() void { // Load the TSS arch.ltr(TSS_OFFSET); - log.logInfo("Done\n", .{}); - - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } fn mock_lgdt(ptr: *const GdtPtr) void { expectEqual(TABLE_SIZE, ptr.limit); - expectEqual(@intCast(u32, @ptrToInt(&gdt_entries[0])), ptr.base); + expectEqual(@ptrToInt(&gdt_entries[0]), ptr.base); } test "GDT entries" { expectEqual(@as(u32, 1), @sizeOf(AccessBits)); expectEqual(@as(u32, 1), @sizeOf(FlagBits)); expectEqual(@as(u32, 8), @sizeOf(GdtEntry)); - expectEqual(@as(u32, 104), @sizeOf(TtsEntry)); + expectEqual(@as(u32, 104), @sizeOf(Tss)); expectEqual(@as(u32, 6), @sizeOf(GdtPtr)); const null_entry = gdt_entries[NULL_INDEX]; @@ -474,45 +458,45 @@ test "GDT entries" { expectEqual(TABLE_SIZE, gdt_ptr.limit); - expectEqual(@as(u32, 0), tss.prev_tss); - expectEqual(@as(u32, 0), tss.esp0); - expectEqual(@as(u32, KERNEL_DATA_OFFSET), tss.ss0); - expectEqual(@as(u32, 0), tss.esp1); - expectEqual(@as(u32, 0), tss.ss1); - expectEqual(@as(u32, 0), tss.esp2); - expectEqual(@as(u32, 0), tss.ss2); - expectEqual(@as(u32, 0), tss.cr3); - expectEqual(@as(u32, 0), tss.eip); - expectEqual(@as(u32, 0), tss.eflags); - expectEqual(@as(u32, 0), tss.eax); - expectEqual(@as(u32, 0), tss.ecx); - expectEqual(@as(u32, 0), tss.edx); - expectEqual(@as(u32, 0), tss.ebx); - expectEqual(@as(u32, 0), tss.esp); - expectEqual(@as(u32, 0), tss.ebp); - expectEqual(@as(u32, 0), tss.esi); - expectEqual(@as(u32, 0), tss.edi); - expectEqual(@as(u32, 0), tss.es); - expectEqual(@as(u32, 0), tss.cs); - expectEqual(@as(u32, 0), tss.ss); - expectEqual(@as(u32, 0), tss.ds); - expectEqual(@as(u32, 0), tss.fs); - expectEqual(@as(u32, 0), tss.gs); - expectEqual(@as(u32, 0), tss.ldtr); - expectEqual(@as(u16, 0), tss.trap); - - // Size of TtsEntry will fit in a u16 as 104 < 65535 (2^16) - expectEqual(@as(u16, @sizeOf(TtsEntry)), tss.io_permissions_base_offset); + expectEqual(@as(u32, 0), main_tss_entry.prev_tss); + expectEqual(@as(u32, 0), main_tss_entry.esp0); + expectEqual(@as(u32, KERNEL_DATA_OFFSET), main_tss_entry.ss0); + expectEqual(@as(u32, 0), main_tss_entry.esp1); + expectEqual(@as(u32, 0), main_tss_entry.ss1); + expectEqual(@as(u32, 0), main_tss_entry.esp2); + expectEqual(@as(u32, 0), main_tss_entry.ss2); + expectEqual(@as(u32, 0), main_tss_entry.cr3); + expectEqual(@as(u32, 0), main_tss_entry.eip); + expectEqual(@as(u32, 0), main_tss_entry.eflags); + expectEqual(@as(u32, 0), main_tss_entry.eax); + expectEqual(@as(u32, 0), main_tss_entry.ecx); + expectEqual(@as(u32, 0), main_tss_entry.edx); + expectEqual(@as(u32, 0), main_tss_entry.ebx); + expectEqual(@as(u32, 0), main_tss_entry.esp); + expectEqual(@as(u32, 0), main_tss_entry.ebp); + expectEqual(@as(u32, 0), main_tss_entry.esi); + expectEqual(@as(u32, 0), main_tss_entry.edi); + expectEqual(@as(u32, 0), main_tss_entry.es); + expectEqual(@as(u32, 0), main_tss_entry.cs); + expectEqual(@as(u32, 0), main_tss_entry.ss); + expectEqual(@as(u32, 0), main_tss_entry.ds); + expectEqual(@as(u32, 0), main_tss_entry.fs); + expectEqual(@as(u32, 0), main_tss_entry.gs); + expectEqual(@as(u32, 0), main_tss_entry.ldtr); + expectEqual(@as(u16, 0), main_tss_entry.trap); + + // Size of Tss will fit in a u16 as 104 < 65535 (2^16) + expectEqual(@as(u16, @sizeOf(Tss)), main_tss_entry.io_permissions_base_offset); } -test "makeEntry NULL" { - const actual = makeEntry(0, 0, NULL_SEGMENT, NULL_FLAGS); +test "makeGdtEntry NULL" { + const actual = makeGdtEntry(0, 0, NULL_SEGMENT, NULL_FLAGS); const expected: u64 = 0; expectEqual(expected, @bitCast(u64, actual)); } -test "makeEntry alternating bit pattern" { +test "makeGdtEntry alternating bit pattern" { const alt_access = AccessBits{ .accessed = 1, .read_write = 0, @@ -534,106 +518,12 @@ test "makeEntry alternating bit pattern" { expectEqual(@as(u4, 0b0101), @bitCast(u4, alt_flag)); - const actual = makeEntry(0b01010101010101010101010101010101, 0b01010101010101010101, alt_access, alt_flag); + const actual = makeGdtEntry(0b01010101010101010101010101010101, 0b01010101010101010101, alt_access, alt_flag); const expected: u64 = 0b0101010101010101010101010101010101010101010101010101010101010101; expectEqual(expected, @bitCast(u64, actual)); } -test "setTssStack" { - // Pre-testing - expectEqual(@as(u32, 0), tss.prev_tss); - expectEqual(@as(u32, 0), tss.esp0); - expectEqual(@as(u32, KERNEL_DATA_OFFSET), tss.ss0); - expectEqual(@as(u32, 0), tss.esp1); - expectEqual(@as(u32, 0), tss.ss1); - expectEqual(@as(u32, 0), tss.esp2); - expectEqual(@as(u32, 0), tss.ss2); - expectEqual(@as(u32, 0), tss.cr3); - expectEqual(@as(u32, 0), tss.eip); - expectEqual(@as(u32, 0), tss.eflags); - expectEqual(@as(u32, 0), tss.eax); - expectEqual(@as(u32, 0), tss.ecx); - expectEqual(@as(u32, 0), tss.edx); - expectEqual(@as(u32, 0), tss.ebx); - expectEqual(@as(u32, 0), tss.esp); - expectEqual(@as(u32, 0), tss.ebp); - expectEqual(@as(u32, 0), tss.esi); - expectEqual(@as(u32, 0), tss.edi); - expectEqual(@as(u32, 0), tss.es); - expectEqual(@as(u32, 0), tss.cs); - expectEqual(@as(u32, 0), tss.ss); - expectEqual(@as(u32, 0), tss.ds); - expectEqual(@as(u32, 0), tss.fs); - expectEqual(@as(u32, 0), tss.gs); - expectEqual(@as(u32, 0), tss.ldtr); - expectEqual(@as(u16, 0), tss.trap); - expectEqual(@as(u16, @sizeOf(TtsEntry)), tss.io_permissions_base_offset); - - // Call function - setTssStack(100); - - // Post-testing - expectEqual(@as(u32, 0), tss.prev_tss); - expectEqual(@as(u32, 100), tss.esp0); - expectEqual(@as(u32, KERNEL_DATA_OFFSET), tss.ss0); - expectEqual(@as(u32, 0), tss.esp1); - expectEqual(@as(u32, 0), tss.ss1); - expectEqual(@as(u32, 0), tss.esp2); - expectEqual(@as(u32, 0), tss.ss2); - expectEqual(@as(u32, 0), tss.cr3); - expectEqual(@as(u32, 0), tss.eip); - expectEqual(@as(u32, 0), tss.eflags); - expectEqual(@as(u32, 0), tss.eax); - expectEqual(@as(u32, 0), tss.ecx); - expectEqual(@as(u32, 0), tss.edx); - expectEqual(@as(u32, 0), tss.ebx); - expectEqual(@as(u32, 0), tss.esp); - expectEqual(@as(u32, 0), tss.ebp); - expectEqual(@as(u32, 0), tss.esi); - expectEqual(@as(u32, 0), tss.edi); - expectEqual(@as(u32, 0), tss.es); - expectEqual(@as(u32, 0), tss.cs); - expectEqual(@as(u32, 0), tss.ss); - expectEqual(@as(u32, 0), tss.ds); - expectEqual(@as(u32, 0), tss.fs); - expectEqual(@as(u32, 0), tss.gs); - expectEqual(@as(u32, 0), tss.ldtr); - expectEqual(@as(u16, 0), tss.trap); - expectEqual(@as(u16, @sizeOf(TtsEntry)), tss.io_permissions_base_offset); - - // Clean up - setTssStack(0); - - expectEqual(@as(u32, 0), tss.prev_tss); - expectEqual(@as(u32, 0), tss.esp0); - expectEqual(@as(u32, KERNEL_DATA_OFFSET), tss.ss0); - expectEqual(@as(u32, 0), tss.esp1); - expectEqual(@as(u32, 0), tss.ss1); - expectEqual(@as(u32, 0), tss.esp2); - expectEqual(@as(u32, 0), tss.ss2); - expectEqual(@as(u32, 0), tss.cr3); - expectEqual(@as(u32, 0), tss.eip); - expectEqual(@as(u32, 0), tss.eflags); - expectEqual(@as(u32, 0), tss.eax); - expectEqual(@as(u32, 0), tss.ecx); - expectEqual(@as(u32, 0), tss.edx); - expectEqual(@as(u32, 0), tss.ebx); - expectEqual(@as(u32, 0), tss.esp); - expectEqual(@as(u32, 0), tss.ebp); - expectEqual(@as(u32, 0), tss.esi); - expectEqual(@as(u32, 0), tss.edi); - expectEqual(@as(u32, 0), tss.es); - expectEqual(@as(u32, 0), tss.cs); - expectEqual(@as(u32, 0), tss.ss); - expectEqual(@as(u32, 0), tss.ds); - expectEqual(@as(u32, 0), tss.fs); - expectEqual(@as(u32, 0), tss.gs); - expectEqual(@as(u32, 0), tss.ldtr); - expectEqual(@as(u16, 0), tss.trap); - expectEqual(@as(u16, @sizeOf(TtsEntry)), tss.io_permissions_base_offset); -} - test "init" { // Set up arch.initTest(); @@ -648,8 +538,8 @@ test "init" { // Post testing const tss_entry = gdt_entries[TSS_INDEX]; - const tss_limit = @sizeOf(TtsEntry) - 1; - const tss_addr = @ptrToInt(&tss); + const tss_limit = @sizeOf(Tss) - 1; + const tss_addr = @ptrToInt(&main_tss_entry); var expected: u64 = 0; expected |= @as(u64, @truncate(u16, tss_limit)); @@ -663,7 +553,7 @@ test "init" { // Reset gdt_ptr.base = 0; - gdt_entries[TSS_INDEX] = makeEntry(0, 0, NULL_SEGMENT, NULL_FLAGS); + gdt_entries[TSS_INDEX] = makeGdtEntry(0, 0, NULL_SEGMENT, NULL_FLAGS); } /// @@ -672,14 +562,18 @@ test "init" { /// fn rt_loadedGDTSuccess() void { const loaded_gdt = arch.sgdt(); - expect(gdt_ptr.limit == loaded_gdt.limit); - expect(gdt_ptr.base == loaded_gdt.base); + if (gdt_ptr.limit != loaded_gdt.limit) { + panic(@errorReturnTrace(), "FAILURE: GDT not loaded properly: 0x{X} != 0x{X}\n", .{ gdt_ptr.limit, loaded_gdt.limit }); + } + if (gdt_ptr.base != loaded_gdt.base) { + panic(@errorReturnTrace(), "FAILURE: GDT not loaded properly: 0x{X} != {X}\n", .{ gdt_ptr.base, loaded_gdt.base }); + } + log.logInfo("GDT: Tested loading GDT\n", .{}); } /// /// Run all the runtime tests. /// -fn runtimeTests() void { +pub fn runtimeTests() void { rt_loadedGDTSuccess(); - log.logInfo("GDT: Tested loading GDT\n", .{}); } diff --git a/src/kernel/arch/x86/idt.zig b/src/kernel/arch/x86/idt.zig index fbd16a20..9758d0fa 100644 --- a/src/kernel/arch/x86/idt.zig +++ b/src/kernel/arch/x86/idt.zig @@ -4,7 +4,7 @@ const expectEqual = std.testing.expectEqual; const expectError = std.testing.expectError; const builtin = @import("builtin"); const is_test = builtin.is_test; - +const panic = @import("../../panic.zig").panic; const build_options = @import("build_options"); const mock_path = build_options.arch_mock_path; const gdt = if (is_test) @import(mock_path ++ "gdt_mock.zig") else @import("gdt.zig"); @@ -101,7 +101,7 @@ var idt_ptr: IdtPtr = IdtPtr{ .base = 0, }; -/// The IDT entry table of NUMBER_OF_ENTRIES entries. Initially all zero'ed. +/// The IDT entry table of NUMBER_OF_ENTRIES entries. Initially all zeroed. var idt_entries: [NUMBER_OF_ENTRIES]IdtEntry = [_]IdtEntry{IdtEntry{ .base_low = 0, .selector = 0, @@ -172,7 +172,7 @@ pub fn openInterruptGate(index: u8, handler: InterruptHandler) IdtError!void { return IdtError.IdtEntryExists; } - idt_entries[index] = makeEntry(@intCast(u32, @ptrToInt(handler)), gdt.KERNEL_CODE_OFFSET, INTERRUPT_GATE, PRIVILEGE_RING_0); + idt_entries[index] = makeEntry(@ptrToInt(handler), gdt.KERNEL_CODE_OFFSET, INTERRUPT_GATE, PRIVILEGE_RING_0); } /// @@ -180,13 +180,16 @@ pub fn openInterruptGate(index: u8, handler: InterruptHandler) IdtError!void { /// pub fn init() void { log.logInfo("Init idt\n", .{}); + defer log.logInfo("Done idt\n", .{}); - idt_ptr.base = @intCast(u32, @ptrToInt(&idt_entries)); + idt_ptr.base = @ptrToInt(&idt_entries); arch.lidt(&idt_ptr); - log.logInfo("Done\n", .{}); - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } fn testHandler0() callconv(.Naked) void {} @@ -194,7 +197,7 @@ fn testHandler1() callconv(.Naked) void {} fn mock_lidt(ptr: *const IdtPtr) void { expectEqual(TABLE_SIZE, ptr.limit); - expectEqual(@intCast(u32, @ptrToInt(&idt_entries[0])), ptr.base); + expectEqual(@ptrToInt(&idt_entries[0]), ptr.base); } test "IDT entries" { @@ -244,8 +247,8 @@ test "openInterruptGate" { openInterruptGate(index, testHandler0) catch unreachable; expectError(IdtError.IdtEntryExists, openInterruptGate(index, testHandler0)); - const test_fn_0_addr = @intCast(u32, @ptrToInt(testHandler0)); - const test_fn_1_addr = @intCast(u32, @ptrToInt(testHandler1)); + const test_fn_0_addr = @ptrToInt(testHandler0); + const test_fn_1_addr = @ptrToInt(testHandler1); const expected_entry0 = IdtEntry{ .base_low = @truncate(u16, test_fn_0_addr), @@ -313,7 +316,7 @@ test "init" { init(); // Post testing - expectEqual(@intCast(u32, @ptrToInt(&idt_entries)), idt_ptr.base); + expectEqual(@ptrToInt(&idt_entries), idt_ptr.base); // Reset idt_ptr.base = 0; @@ -325,14 +328,18 @@ test "init" { /// fn rt_loadedIDTSuccess() void { const loaded_idt = arch.sidt(); - expect(idt_ptr.limit == loaded_idt.limit); - expect(idt_ptr.base == loaded_idt.base); + if (idt_ptr.limit != loaded_idt.limit) { + panic(@errorReturnTrace(), "FAILURE: IDT not loaded properly: 0x{X} != 0x{X}\n", .{ idt_ptr.limit, loaded_idt.limit }); + } + if (idt_ptr.base != loaded_idt.base) { + panic(@errorReturnTrace(), "FAILURE: IDT not loaded properly: 0x{X} != {X}\n", .{ idt_ptr.base, loaded_idt.base }); + } log.logInfo("IDT: Tested loading IDT\n", .{}); } /// /// Run all the runtime tests. /// -fn runtimeTests() void { +pub fn runtimeTests() void { rt_loadedIDTSuccess(); } diff --git a/src/kernel/arch/x86/interrupts.zig b/src/kernel/arch/x86/interrupts.zig index d02741c5..8cb2c92b 100644 --- a/src/kernel/arch/x86/interrupts.zig +++ b/src/kernel/arch/x86/interrupts.zig @@ -3,22 +3,22 @@ const syscalls = @import("syscalls.zig"); const irq = @import("irq.zig"); const idt = @import("idt.zig"); -extern fn irqHandler(ctx: *arch.InterruptContext) void; -extern fn isrHandler(ctx: *arch.InterruptContext) void; +extern fn irqHandler(ctx: *arch.CpuState) usize; +extern fn isrHandler(ctx: *arch.CpuState) usize; /// /// The main handler for all exceptions and interrupts. This will then go and call the correct /// handler for an ISR or IRQ. /// /// Arguments: -/// IN ctx: *arch.InterruptContext - Pointer to the exception context containing the contents -/// of the registers at the time of a exception. +/// IN ctx: *arch.CpuState - Pointer to the exception context containing the contents +/// of the registers at the time of a exception. /// -export fn handler(ctx: *arch.InterruptContext) void { +export fn handler(ctx: *arch.CpuState) usize { if (ctx.int_num < irq.IRQ_OFFSET or ctx.int_num == syscalls.INTERRUPT) { - isrHandler(ctx); + return isrHandler(ctx); } else { - irqHandler(ctx); + return irqHandler(ctx); } } @@ -32,6 +32,7 @@ export fn commonStub() callconv(.Naked) void { \\push %%es \\push %%fs \\push %%gs + \\push %%ss \\mov $0x10, %%ax \\mov %%ax, %%ds \\mov %%ax, %%es @@ -40,7 +41,8 @@ export fn commonStub() callconv(.Naked) void { \\mov %%esp, %%eax \\push %%eax \\call handler - \\pop %%eax + \\mov %%eax, %%esp + \\pop %%ss \\pop %%gs \\pop %%fs \\pop %%es diff --git a/src/kernel/arch/x86/irq.zig b/src/kernel/arch/x86/irq.zig index 9087df51..76dbaaf2 100644 --- a/src/kernel/arch/x86/irq.zig +++ b/src/kernel/arch/x86/irq.zig @@ -26,7 +26,7 @@ pub const IrqError = error{ const NUMBER_OF_ENTRIES: u16 = 16; /// The type of a IRQ handler. A function that takes a interrupt context and returns void. -const IrqHandler = fn (*arch.InterruptContext) void; +const IrqHandler = fn (*arch.CpuState) usize; // The offset from the interrupt number where the IRQs are. pub const IRQ_OFFSET: u16 = 32; @@ -38,15 +38,17 @@ var irq_handlers: [NUMBER_OF_ENTRIES]?IrqHandler = [_]?IrqHandler{null} ** NUMBE /// The IRQ handler that each of the IRQs will call when a interrupt happens. /// /// Arguments: -/// IN ctx: *arch.InterruptContext - Pointer to the interrupt context containing the contents +/// IN ctx: *arch.CpuState - Pointer to the interrupt context containing the contents /// of the register at the time of the interrupt. /// -export fn irqHandler(ctx: *arch.InterruptContext) void { +export fn irqHandler(ctx: *arch.CpuState) usize { // Get the IRQ index, by getting the interrupt number and subtracting the offset. if (ctx.int_num < IRQ_OFFSET) { panic(@errorReturnTrace(), "Not an IRQ number: {}\n", .{ctx.int_num}); } + var ret_esp = @ptrToInt(ctx); + const irq_offset = ctx.int_num - IRQ_OFFSET; if (isValidIrq(irq_offset)) { // IRQ index is valid so can truncate @@ -54,7 +56,7 @@ export fn irqHandler(ctx: *arch.InterruptContext) void { if (irq_handlers[irq_num]) |handler| { // Make sure it isn't a spurious irq if (!pic.spuriousIrq(irq_num)) { - handler(ctx); + ret_esp = handler(ctx); // Send the end of interrupt command pic.sendEndOfInterrupt(irq_num); } @@ -64,6 +66,7 @@ export fn irqHandler(ctx: *arch.InterruptContext) void { } else { panic(@errorReturnTrace(), "Invalid IRQ index: {}", .{irq_offset}); } + return ret_esp; } /// @@ -88,7 +91,7 @@ fn openIrq(index: u8, handler: idt.InterruptHandler) void { /// IN irq_num: u8 - The IRQ index to test. /// /// Return: bool -/// Whether the IRQ index if valid. +/// Whether the IRQ index is valid. /// pub fn isValidIrq(irq_num: u32) bool { return irq_num < NUMBER_OF_ENTRIES; @@ -129,20 +132,26 @@ pub fn registerIrq(irq_num: u8, handler: IrqHandler) IrqError!void { /// pub fn init() void { log.logInfo("Init irq\n", .{}); + defer log.logInfo("Done irq\n", .{}); comptime var i = IRQ_OFFSET; inline while (i < IRQ_OFFSET + 16) : (i += 1) { openIrq(i, interrupts.getInterruptStub(i)); } - log.logInfo("Done\n", .{}); - - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } fn testFunction0() callconv(.Naked) void {} -fn testFunction1(ctx: *arch.InterruptContext) void {} -fn testFunction2(ctx: *arch.InterruptContext) void {} +fn testFunction1(ctx: *arch.CpuState) u32 { + return 0; +} +fn testFunction2(ctx: *arch.CpuState) u32 { + return 0; +} test "openIrq" { idt.initTest(); @@ -228,13 +237,13 @@ test "registerIrq invalid irq index" { } /// -/// Test that all handers are null at initialisation. +/// Test that all handlers are null at initialisation. /// fn rt_unregisteredHandlers() void { // Ensure all ISR are not registered yet for (irq_handlers) |h, i| { if (h) |_| { - panic(@errorReturnTrace(), "Handler found for IRQ: {}-{}\n", .{ i, h }); + panic(@errorReturnTrace(), "FAILURE: Handler found for IRQ: {}-{}\n", .{ i, h }); } } @@ -251,7 +260,7 @@ fn rt_openedIdtEntries() void { for (idt_entries) |entry, i| { if (i >= IRQ_OFFSET and isValidIrq(i - IRQ_OFFSET)) { if (!idt.isIdtOpen(entry)) { - panic(@errorReturnTrace(), "IDT entry for {} is not open\n", .{i}); + panic(@errorReturnTrace(), "FAILURE: IDT entry for {} is not open\n", .{i}); } } } @@ -262,7 +271,7 @@ fn rt_openedIdtEntries() void { /// /// Run all the runtime tests. /// -fn runtimeTests() void { +pub fn runtimeTests() void { rt_unregisteredHandlers(); rt_openedIdtEntries(); } diff --git a/src/kernel/arch/x86/isr.zig b/src/kernel/arch/x86/isr.zig index 4b5342d5..414a4904 100644 --- a/src/kernel/arch/x86/isr.zig +++ b/src/kernel/arch/x86/isr.zig @@ -23,7 +23,7 @@ pub const IsrError = error{ }; /// The type of a ISR handler. A function that takes a interrupt context and returns void. -const IsrHandler = fn (*arch.InterruptContext) void; +const IsrHandler = fn (*arch.CpuState) usize; /// The number of ISR entries. const NUMBER_OF_ENTRIES: u8 = 32; @@ -130,39 +130,43 @@ pub const SECURITY: u8 = 30; /// The of exception handlers initialised to null. Need to open a ISR for these to be valid. var isr_handlers: [NUMBER_OF_ENTRIES]?IsrHandler = [_]?IsrHandler{null} ** NUMBER_OF_ENTRIES; -/// The syscall hander. +/// The syscall handler. var syscall_handler: ?IsrHandler = null; /// /// The exception handler that each of the exceptions will call when a exception happens. /// /// Arguments: -/// IN ctx: *arch.InterruptContext - Pointer to the exception context containing the contents +/// IN ctx: *arch.CpuState - Pointer to the exception context containing the contents /// of the register at the time of the exception. /// -export fn isrHandler(ctx: *arch.InterruptContext) void { +export fn isrHandler(ctx: *arch.CpuState) usize { // Get the interrupt number const isr_num = ctx.int_num; + var ret_esp = @ptrToInt(ctx); + if (isValidIsr(isr_num)) { if (isr_num == syscalls.INTERRUPT) { // A syscall, so use the syscall handler if (syscall_handler) |handler| { - handler(ctx); + ret_esp = handler(ctx); } else { panic(@errorReturnTrace(), "Syscall handler not registered\n", .{}); } } else { if (isr_handlers[isr_num]) |handler| { // Regular ISR exception, if there is one registered. - handler(ctx); + ret_esp = handler(ctx); } else { - panic(@errorReturnTrace(), "ISR not registered to: {}-{}\n", .{ isr_num, exception_msg[isr_num] }); + log.logInfo("State: {X}\n", .{ctx}); + panic(@errorReturnTrace(), "ISR {} ({}) triggered with error code 0x{X} but not registered\n", .{ exception_msg[isr_num], isr_num, ctx.error_code }); } } } else { panic(@errorReturnTrace(), "Invalid ISR index: {}\n", .{isr_num}); } + return ret_esp; } /// @@ -188,7 +192,7 @@ fn openIsr(index: u8, handler: idt.InterruptHandler) void { /// IN isr_num: u16 - The isr number to check /// /// Return: bool -/// Whether a ISR hander index if valid. +/// Whether a ISR handler index is valid. /// pub fn isValidIsr(isr_num: u32) bool { return isr_num < NUMBER_OF_ENTRIES or isr_num == syscalls.INTERRUPT; @@ -235,6 +239,7 @@ pub fn registerIsr(isr_num: u16, handler: IsrHandler) IsrError!void { /// pub fn init() void { log.logInfo("Init isr\n", .{}); + defer log.logInfo("Done isr\n", .{}); comptime var i = 0; inline while (i < 32) : (i += 1) { @@ -243,16 +248,25 @@ pub fn init() void { openIsr(syscalls.INTERRUPT, interrupts.getInterruptStub(syscalls.INTERRUPT)); - log.logInfo("Done\n", .{}); - - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } fn testFunction0() callconv(.Naked) void {} -fn testFunction1(ctx: *arch.InterruptContext) void {} -fn testFunction2(ctx: *arch.InterruptContext) void {} -fn testFunction3(ctx: *arch.InterruptContext) void {} -fn testFunction4(ctx: *arch.InterruptContext) void {} +fn testFunction1(ctx: *arch.CpuState) u32 { + return 0; +} +fn testFunction2(ctx: *arch.CpuState) u32 { + return 0; +} +fn testFunction3(ctx: *arch.CpuState) u32 { + return 0; +} +fn testFunction4(ctx: *arch.CpuState) u32 { + return 0; +} test "openIsr" { idt.initTest(); @@ -357,18 +371,18 @@ test "registerIsr invalid isr index" { } /// -/// Test that all handers are null at initialisation. +/// Test that all handlers are null at initialisation. /// fn rt_unregisteredHandlers() void { // Ensure all ISR are not registered yet for (isr_handlers) |h, i| { if (h) |_| { - panic(@errorReturnTrace(), "Handler found for ISR: {}-{}\n", .{ i, h }); + panic(@errorReturnTrace(), "FAILURE: Handler found for ISR: {}-{}\n", .{ i, h }); } } if (syscall_handler) |h| { - panic(@errorReturnTrace(), "Pre-testing failed for syscall: {}\n", .{h}); + panic(@errorReturnTrace(), "FAILURE: Pre-testing failed for syscall: {}\n", .{h}); } log.logInfo("ISR: Tested registered handlers\n", .{}); @@ -384,7 +398,7 @@ fn rt_openedIdtEntries() void { for (idt_entries) |entry, i| { if (isValidIsr(i)) { if (!idt.isIdtOpen(entry)) { - panic(@errorReturnTrace(), "IDT entry for {} is not open\n", .{i}); + panic(@errorReturnTrace(), "FAILURE: IDT entry for {} is not open\n", .{i}); } } } @@ -395,7 +409,7 @@ fn rt_openedIdtEntries() void { /// /// Run all the runtime tests. /// -fn runtimeTests() void { +pub fn runtimeTests() void { rt_unregisteredHandlers(); rt_openedIdtEntries(); } diff --git a/link.ld b/src/kernel/arch/x86/link.ld similarity index 90% rename from link.ld rename to src/kernel/arch/x86/link.ld index 8bce8471..3729ebd8 100644 --- a/link.ld +++ b/src/kernel/arch/x86/link.ld @@ -35,9 +35,10 @@ SECTIONS { } .bss.stack ALIGN(4K) : AT (ADDR(.bss.stack) - KERNEL_ADDR_OFFSET) { - *(.bss.stack) - KERNEL_STACK_END = .; - } + KERNEL_STACK_START = .; + KEEP(*(.bss.stack)) + KERNEL_STACK_END = .; + } KERNEL_VADDR_END = .; KERNEL_PHYSADDR_END = . - KERNEL_ADDR_OFFSET; diff --git a/src/kernel/multiboot.zig b/src/kernel/arch/x86/multiboot.zig similarity index 100% rename from src/kernel/multiboot.zig rename to src/kernel/arch/x86/multiboot.zig diff --git a/src/kernel/arch/x86/paging.zig b/src/kernel/arch/x86/paging.zig index c5fb89b6..27020e8d 100644 --- a/src/kernel/arch/x86/paging.zig +++ b/src/kernel/arch/x86/paging.zig @@ -1,20 +1,23 @@ const std = @import("std"); -const expectEqual = std.testing.expectEqual; -const expect = std.testing.expect; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; const builtin = @import("builtin"); +const is_test = builtin.is_test; const panic = @import("../../panic.zig").panic; -const arch = @import("arch.zig"); +const build_options = @import("build_options"); +const mock_path = build_options.arch_mock_path; +const arch = if (is_test) @import(mock_path ++ "arch_mock.zig") else @import("arch.zig"); const isr = @import("isr.zig"); const MemProfile = @import("../../mem.zig").MemProfile; const tty = @import("../../tty.zig"); const log = @import("../../log.zig"); const mem = @import("../../mem.zig"); -const multiboot = @import("../../multiboot.zig"); -const options = @import("build_options"); -const testing = std.testing; +const vmm = @import("../../vmm.zig"); +const multiboot = @import("multiboot.zig"); /// An array of directory entries and page tables. Forms the first level of paging and covers the entire 4GB memory space. -const Directory = packed struct { +pub const Directory = packed struct { /// The directory entries. entries: [ENTRIES_PER_DIRECTORY]DirectoryEntry, @@ -28,24 +31,6 @@ const Table = packed struct { entries: [ENTRIES_PER_TABLE]TableEntry, }; -/// All errors that can be thrown by paging functions. -const PagingError = error{ - /// Physical addresses are invalid (definition is up to the function). - InvalidPhysAddresses, - - /// Virtual addresses are invalid (definition is up to the function). - InvalidVirtAddresses, - - /// Physical and virtual addresses don't cover spaces of the same size. - PhysicalVirtualMismatch, - - /// Physical addresses aren't aligned by page size. - UnalignedPhysAddresses, - - /// Virtual addresses aren't aligned by page size. - UnalignedVirtAddresses, -}; - /// An entry within a directory. References a single page table. /// Bit 0: Present. Set if present in physical memory. /// When not set, all remaining 31 bits are ignored and available for use. @@ -83,12 +68,6 @@ const ENTRIES_PER_DIRECTORY: u32 = 1024; /// Each table has 1024 entries const ENTRIES_PER_TABLE: u32 = 1024; -/// The number of bytes in 4MB -const PAGE_SIZE_4MB: u32 = 0x400000; - -/// The number of bytes in 4KB -const PAGE_SIZE_4KB: u32 = PAGE_SIZE_4MB / 1024; - /// There are 1024 entries per directory with each one covering 4KB const PAGES_PER_DIR_ENTRY: u32 = 1024; @@ -121,6 +100,15 @@ const TENTRY_GLOBAL: u32 = 0x100; const TENTRY_AVAILABLE: u32 = 0xE00; const TENTRY_PAGE_ADDR: u32 = 0xFFFFF000; +/// The number of bytes in 4MB +pub const PAGE_SIZE_4MB: usize = 0x400000; + +/// The number of bytes in 4KB +pub const PAGE_SIZE_4KB: usize = PAGE_SIZE_4MB / 1024; + +/// The kernel's page directory. Should only be used to map kernel-owned code and data +pub var kernel_directory: Directory align(@truncate(u29, PAGE_SIZE_4KB)) = Directory{ .entries = [_]DirectoryEntry{0} ** ENTRIES_PER_DIRECTORY, .tables = [_]?*Table{null} ** ENTRIES_PER_DIRECTORY }; + /// /// Convert a virtual address to an index within an array of directory entries. /// @@ -147,53 +135,92 @@ inline fn virtToTableEntryIdx(virt: usize) usize { return (virt / PAGE_SIZE_4KB) % ENTRIES_PER_TABLE; } +/// +/// Set the bit(s) associated with an attribute of a table or directory entry. +/// +/// Arguments: +/// val: *align(1) u32 - The entry to modify +/// attr: u32 - The bits corresponding to the attribute to set +/// +inline fn setAttribute(val: *align(1) u32, attr: u32) void { + val.* |= attr; +} + +/// +/// Clear the bit(s) associated with an attribute of a table or directory entry. +/// +/// Arguments: +/// val: *align(1) u32 - The entry to modify +/// attr: u32 - The bits corresponding to the attribute to clear +/// +inline fn clearAttribute(val: *align(1) u32, attr: u32) void { + val.* &= ~attr; +} + /// /// Map a page directory entry, setting the present, size, writable, write-through and physical address bits. -/// Clears the user and cache disabled bits. Entry should be zero'ed. +/// Clears the user and cache disabled bits. Entry should be zeroed. /// /// Arguments: -/// OUT dir: *Directory - The directory that this entry is in /// IN virt_addr: usize - The start of the virtual space to map /// IN virt_end: usize - The end of the virtual space to map /// IN phys_addr: usize - The start of the physical space to map /// IN phys_end: usize - The end of the physical space to map +/// IN attrs: vmm.Attributes - The attributes to apply to this mapping /// IN allocator: *Allocator - The allocator to use to map any tables needed +/// OUT dir: *Directory - The directory that this entry is in /// -/// Error: PagingError || std.mem.Allocator.Error -/// PagingError.InvalidPhysAddresses - The physical start address is greater than the end. -/// PagingError.InvalidVirtAddresses - The virtual start address is greater than the end or is larger than 4GB. -/// PagingError.PhysicalVirtualMismatch - The differences between the virtual addresses and the physical addresses aren't the same. -/// PagingError.UnalignedPhysAddresses - One or both of the physical addresses aren't page size aligned. -/// PagingError.UnalignedVirtAddresses - One or both of the virtual addresses aren't page size aligned. -/// std.mem.Allocator.Error.* - See std.mem.Allocator.alignedAlloc. +/// Error: vmm.MapperError || std.mem.Allocator.Error +/// vmm.MapperError.InvalidPhysicalAddress - The physical start address is greater than the end +/// vmm.MapperError.InvalidVirtualAddress - The virtual start address is greater than the end or is larger than 4GB +/// vmm.MapperError.AddressMismatch - The differences between the virtual addresses and the physical addresses aren't the same +/// vmm.MapperError.MisalignedPhysicalAddress - One or both of the physical addresses aren't page size aligned +/// vmm.MapperError.MisalignedVirtualAddress - One or both of the virtual addresses aren't page size aligned +/// std.mem.Allocator.Error.* - See std.mem.Allocator.alignedAlloc /// -fn mapDirEntry(dir: *Directory, virt_start: usize, virt_end: usize, phys_start: usize, phys_end: usize, allocator: *std.mem.Allocator) (PagingError || std.mem.Allocator.Error)!void { +fn mapDirEntry(dir: *Directory, virt_start: usize, virt_end: usize, phys_start: usize, phys_end: usize, attrs: vmm.Attributes, allocator: *std.mem.Allocator) (vmm.MapperError || std.mem.Allocator.Error)!void { if (phys_start > phys_end) { - return PagingError.InvalidPhysAddresses; + return vmm.MapperError.InvalidPhysicalAddress; } if (virt_start > virt_end) { - return PagingError.InvalidVirtAddresses; + return vmm.MapperError.InvalidVirtualAddress; } if (phys_end - phys_start != virt_end - virt_start) { - return PagingError.PhysicalVirtualMismatch; + return vmm.MapperError.AddressMismatch; } if (!std.mem.isAligned(phys_start, PAGE_SIZE_4KB) or !std.mem.isAligned(phys_end, PAGE_SIZE_4KB)) { - return PagingError.UnalignedPhysAddresses; + return vmm.MapperError.MisalignedPhysicalAddress; } if (!std.mem.isAligned(virt_start, PAGE_SIZE_4KB) or !std.mem.isAligned(virt_end, PAGE_SIZE_4KB)) { - return PagingError.UnalignedVirtAddresses; + return vmm.MapperError.MisalignedVirtualAddress; } const entry = virt_start / PAGE_SIZE_4MB; if (entry >= ENTRIES_PER_DIRECTORY) - return PagingError.InvalidVirtAddresses; + return vmm.MapperError.InvalidVirtualAddress; var dir_entry = &dir.entries[entry]; - dir_entry.* |= DENTRY_PRESENT; - dir_entry.* |= DENTRY_WRITABLE; - dir_entry.* &= ~DENTRY_USER; - dir_entry.* |= DENTRY_WRITE_THROUGH; - dir_entry.* &= ~DENTRY_CACHE_DISABLED; - dir_entry.* &= ~DENTRY_4MB_PAGES; + + setAttribute(dir_entry, DENTRY_PRESENT); + setAttribute(dir_entry, DENTRY_WRITE_THROUGH); + clearAttribute(dir_entry, DENTRY_4MB_PAGES); + + if (attrs.writable) { + setAttribute(dir_entry, DENTRY_WRITABLE); + } else { + clearAttribute(dir_entry, DENTRY_WRITABLE); + } + + if (attrs.kernel) { + clearAttribute(dir_entry, DENTRY_USER); + } else { + setAttribute(dir_entry, DENTRY_USER); + } + + if (attrs.cachable) { + clearAttribute(dir_entry, DENTRY_CACHE_DISABLED); + } else { + setAttribute(dir_entry, DENTRY_CACHE_DISABLED); + } // Only create a new table if one hasn't already been created for this dir entry. // Prevents us from overriding previous mappings. @@ -205,7 +232,7 @@ fn mapDirEntry(dir: *Directory, virt_start: usize, virt_end: usize, phys_start: table = &(try allocator.alignedAlloc(Table, @truncate(u29, PAGE_SIZE_4KB), 1))[0]; @memset(@ptrCast([*]u8, table), 0, @sizeOf(Table)); const table_phys_addr = @ptrToInt(mem.virtToPhys(table)); - dir_entry.* |= @intCast(u32, DENTRY_PAGE_ADDR & table_phys_addr); + dir_entry.* |= DENTRY_PAGE_ADDR & table_phys_addr; dir.tables[entry] = table; } @@ -218,7 +245,7 @@ fn mapDirEntry(dir: *Directory, virt_start: usize, virt_end: usize, phys_start: phys += PAGE_SIZE_4KB; tentry += 1; }) { - try mapTableEntry(&table.entries[tentry], phys); + try mapTableEntry(&table.entries[tentry], phys, attrs); } } @@ -233,34 +260,55 @@ fn mapDirEntry(dir: *Directory, virt_start: usize, virt_end: usize, phys_start: /// Error: PagingError /// PagingError.UnalignedPhysAddresses - If the physical address isn't page size aligned. /// -fn mapTableEntry(entry: *align(1) TableEntry, phys_addr: usize) PagingError!void { +fn mapTableEntry(entry: *align(1) TableEntry, phys_addr: usize, attrs: vmm.Attributes) vmm.MapperError!void { if (!std.mem.isAligned(phys_addr, PAGE_SIZE_4KB)) { - return PagingError.UnalignedPhysAddresses; + return vmm.MapperError.MisalignedPhysicalAddress; + } + setAttribute(entry, TENTRY_PRESENT); + if (attrs.writable) { + setAttribute(entry, TENTRY_WRITABLE); + } else { + clearAttribute(entry, TENTRY_WRITABLE); + } + if (attrs.kernel) { + clearAttribute(entry, TENTRY_USER); + } else { + setAttribute(entry, TENTRY_USER); + } + if (attrs.writable) { + setAttribute(entry, TENTRY_WRITE_THROUGH); + } else { + clearAttribute(entry, TENTRY_WRITE_THROUGH); + } + if (attrs.cachable) { + clearAttribute(entry, TENTRY_CACHE_DISABLED); + } else { + setAttribute(entry, TENTRY_CACHE_DISABLED); } - entry.* |= TENTRY_PRESENT; - entry.* |= TENTRY_WRITABLE; - entry.* &= ~TENTRY_USER; - entry.* |= TENTRY_WRITE_THROUGH; - entry.* &= ~TENTRY_CACHE_DISABLED; - entry.* &= ~TENTRY_GLOBAL; - entry.* |= TENTRY_PAGE_ADDR & @intCast(u32, phys_addr); + clearAttribute(entry, TENTRY_GLOBAL); + setAttribute(entry, TENTRY_PAGE_ADDR & phys_addr); } /// -/// Map a page directory. The addresses passed must be page size aligned and be the same distance apart. +/// Map a virtual region of memory to a physical region with a set of attributes within a directory. +/// If this call is made to a directory that has been loaded by the CPU, the virtual memory will immediately be accessible (given the proper attributes) +/// and will be mirrored to the physical region given. Otherwise it will be accessible once the given directory is loaded by the CPU. +/// +/// This call will panic if mapDir returns an error when called with any of the arguments given. /// /// Arguments: -/// OUT entry: *Directory - The directory to map -/// IN virt_start: usize - The virtual address at which to start mapping -/// IN virt_end: usize - The virtual address at which to stop mapping -/// IN phys_start: usize - The physical address at which to start mapping -/// IN phys_end: usize - The physical address at which to stop mapping -/// IN allocator: *Allocator - The allocator to use to map any tables needed +/// IN virtual_start: usize - The start of the virtual region to map +/// IN virtual_end: usize - The end (exclusive) of the virtual region to map +/// IN physical_start: usize - The start of the physical region to map to +/// IN physical_end: usize - The end (exclusive) of the physical region to map to +/// IN attrs: vmm.Attributes - The attributes to apply to this mapping +/// IN/OUT allocator: *std.mem.Allocator - The allocator to use to allocate any intermediate data structures required to map this region +/// IN/OUT dir: *Directory - The page directory to map within /// -/// Error: std.mem.allocator.Error || PagingError -/// * - See mapDirEntry. +/// Error: vmm.MapperError || std.mem.Allocator.Error +/// * - See mapDirEntry /// -fn mapDir(dir: *Directory, virt_start: usize, virt_end: usize, phys_start: usize, phys_end: usize, allocator: *std.mem.Allocator) (std.mem.Allocator.Error || PagingError)!void { +pub fn map(virt_start: usize, virt_end: usize, phys_start: usize, phys_end: usize, attrs: vmm.Attributes, allocator: *std.mem.Allocator, dir: *Directory) (std.mem.Allocator.Error || vmm.MapperError)!void { var virt_addr = virt_start; var phys_addr = phys_start; var page = virt_addr / PAGE_SIZE_4KB; @@ -270,17 +318,68 @@ fn mapDir(dir: *Directory, virt_start: usize, virt_end: usize, phys_start: usize virt_addr += PAGE_SIZE_4MB; entry_idx += 1; }) { - try mapDirEntry(dir, virt_addr, std.math.min(virt_end, virt_addr + PAGE_SIZE_4MB), phys_addr, std.math.min(phys_end, phys_addr + PAGE_SIZE_4MB), allocator); + try mapDirEntry(dir, virt_addr, std.math.min(virt_end, virt_addr + PAGE_SIZE_4MB), phys_addr, std.math.min(phys_end, phys_addr + PAGE_SIZE_4MB), attrs, allocator); } } /// -/// Called when a page fault occurs. +/// Unmap a virtual region of memory within a directory so that it is no longer accessible. /// /// Arguments: -/// IN state: *arch.InterruptContext - The CPU's state when the fault occurred. +/// IN virtual_start: usize - The start of the virtual region to unmap +/// IN virtual_end: usize - The end (exclusive) of the virtual region to unmap +/// IN/OUT dir: *Directory - The page directory to unmap within +/// +/// Error: std.mem.Allocator.Error || vmm.MapperError +/// vmm.MapperError.NotMapped - If the region being unmapped wasn't mapped in the first place +/// +pub fn unmap(virtual_start: usize, virtual_end: usize, dir: *Directory) (std.mem.Allocator.Error || vmm.MapperError)!void { + var virt_addr = virtual_start; + var page = virt_addr / PAGE_SIZE_4KB; + var entry_idx = virt_addr / PAGE_SIZE_4MB; + while (entry_idx < ENTRIES_PER_DIRECTORY and virt_addr < virtual_end) : ({ + virt_addr += PAGE_SIZE_4MB; + entry_idx += 1; + }) { + var dir_entry = &dir.entries[entry_idx]; + const table = dir.tables[entry_idx] orelse return vmm.MapperError.NotMapped; + const end = std.math.min(virtual_end, virt_addr + PAGE_SIZE_4MB); + var addr = virt_addr; + while (addr < end) : (addr += PAGE_SIZE_4KB) { + var table_entry = &table.entries[virtToTableEntryIdx(addr)]; + if (table_entry.* & TENTRY_PRESENT != 0) { + clearAttribute(table_entry, TENTRY_PRESENT); + } else { + return vmm.MapperError.NotMapped; + } + } + // If the region to be mapped covers all of this directory entry, set the whole thing as not present + if (virtual_end - virt_addr >= PAGE_SIZE_4MB) + clearAttribute(dir_entry, DENTRY_PRESENT); + } +} + +/// +/// Called when a page fault occurs. This will log the CPU state and control registers. /// -fn pageFault(state: *arch.InterruptContext) void { +/// Arguments: +/// IN state: *arch.CpuState - The CPU's state when the fault occurred. +/// +fn pageFault(state: *arch.CpuState) u32 { + log.logInfo("State: {X}\n", .{state}); + var cr0 = asm volatile ("mov %%cr0, %[cr0]" + : [cr0] "=r" (-> u32) + ); + var cr2 = asm volatile ("mov %%cr2, %[cr2]" + : [cr2] "=r" (-> u32) + ); + var cr3 = asm volatile ("mov %%cr3, %[cr3]" + : [cr3] "=r" (-> u32) + ); + var cr4 = asm volatile ("mov %%cr4, %[cr4]" + : [cr4] "=r" (-> u32) + ); + log.logInfo("CR0: 0x{X}, CR2: 0x{X}, CR3: 0x{X}, CR4: 0x{X}\n", .{ cr0, cr2, cr3, cr4 }); @panic("Page fault"); } @@ -293,75 +392,29 @@ fn pageFault(state: *arch.InterruptContext) void { /// pub fn init(mb_info: *multiboot.multiboot_info_t, mem_profile: *const MemProfile, allocator: *std.mem.Allocator) void { log.logInfo("Init paging\n", .{}); - // Calculate start and end of mapping - const v_start = std.mem.alignBackward(@ptrToInt(mem_profile.vaddr_start), PAGE_SIZE_4KB); - const v_end = std.mem.alignForward(@ptrToInt(mem_profile.vaddr_end) + mem_profile.fixed_alloc_size, PAGE_SIZE_4KB); - const p_start = std.mem.alignBackward(@ptrToInt(mem_profile.physaddr_start), PAGE_SIZE_4KB); - const p_end = std.mem.alignForward(@ptrToInt(mem_profile.physaddr_end) + mem_profile.fixed_alloc_size, PAGE_SIZE_4KB); - - var tmp = allocator.alignedAlloc(Directory, @truncate(u29, PAGE_SIZE_4KB), 1) catch |e| { - panic(@errorReturnTrace(), "Failed to allocate page directory: {}\n", .{e}); - }; - var kernel_directory = @ptrCast(*Directory, tmp.ptr); - @memset(@ptrCast([*]u8, kernel_directory), 0, @sizeOf(Directory)); + defer log.logInfo("Done paging\n", .{}); - // Map in kernel - mapDir(kernel_directory, v_start, v_end, p_start, p_end, allocator) catch |e| { - panic(@errorReturnTrace(), "Failed to map kernel directory: {}\n", .{e}); + isr.registerIsr(isr.PAGE_FAULT, if (build_options.test_mode == .Initialisation) rt_pageFault else pageFault) catch |e| { + panic(@errorReturnTrace(), "Failed to register page fault ISR: {}\n", .{e}); }; - const tty_addr = tty.getVideoBufferAddress(); - // If the previous mapping space didn't cover the tty buffer, do so now - if (v_start > tty_addr or v_end <= tty_addr) { - const tty_phys = mem.virtToPhys(tty_addr); - const tty_buff_size = 32 * 1024; - mapDir(kernel_directory, tty_addr, tty_addr + tty_buff_size, tty_phys, tty_phys + tty_buff_size, allocator) catch |e| { - panic(@errorReturnTrace(), "Failed to map vga buffer in kernel directory: {}\n", .{e}); - }; - } - - // If the kernel mapping didn't cover the multiboot info, map it so it can be accessed by code later on - // There's no way to know the size, so an estimated size of 2MB is used. This will need increasing as the kernel gets bigger. - const mb_info_addr = std.mem.alignBackward(@ptrToInt(mb_info), PAGE_SIZE_4KB); - if (v_start > mb_info_addr) { - const mb_info_end = mb_info_addr + PAGE_SIZE_4MB / 2; - mapDir(kernel_directory, mb_info_addr, mb_info_end, mem.virtToPhys(mb_info_addr), mem.virtToPhys(mb_info_end), allocator) catch |e| { - panic(@errorReturnTrace(), "Failed to map mb_info in kernel directory: {}\n", .{e}); - }; - } - - // Map in each boot module - for (mem_profile.boot_modules) |*module| { - const mod_v_struct_start = std.mem.alignBackward(@ptrToInt(module), PAGE_SIZE_4KB); - const mod_v_struct_end = std.mem.alignForward(mod_v_struct_start + @sizeOf(multiboot.multiboot_module_t), PAGE_SIZE_4KB); - mapDir(kernel_directory, mod_v_struct_start, mod_v_struct_end, mem.virtToPhys(mod_v_struct_start), mem.virtToPhys(mod_v_struct_end), allocator) catch |e| { - panic(@errorReturnTrace(), "Failed to map module struct: {}\n", .{e}); - }; - const mod_p_start = std.mem.alignBackward(module.mod_start, PAGE_SIZE_4KB); - const mod_p_end = std.mem.alignForward(module.mod_end, PAGE_SIZE_4KB); - mapDir(kernel_directory, mem.physToVirt(mod_p_start), mem.physToVirt(mod_p_end), mod_p_start, mod_p_end, allocator) catch |e| { - panic(@errorReturnTrace(), "Failed to map boot module in kernel directory: {}\n", .{e}); - }; - } - - const dir_physaddr = @ptrToInt(mem.virtToPhys(kernel_directory)); + const dir_physaddr = @ptrToInt(mem.virtToPhys(&kernel_directory)); asm volatile ("mov %[addr], %%cr3" : : [addr] "{eax}" (dir_physaddr) ); - isr.registerIsr(isr.PAGE_FAULT, if (options.rt_test) rt_pageFault else pageFault) catch |e| { - panic(@errorReturnTrace(), "Failed to register page fault ISR: {}\n", .{e}); - }; - log.logInfo("Done\n", .{}); - - if (options.rt_test) runtimeTests(v_end); + const v_end = std.mem.alignForward(@ptrToInt(mem_profile.vaddr_end) + mem.FIXED_ALLOC_SIZE, PAGE_SIZE_4KB); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(v_end), + else => {}, + } } -fn checkDirEntry(entry: DirectoryEntry, virt_start: usize, virt_end: usize, phys_start: usize, table: *Table) void { - expect(entry & DENTRY_PRESENT != 0); - expect(entry & DENTRY_WRITABLE != 0); - expectEqual(entry & DENTRY_USER, 0); - expect(entry & DENTRY_WRITE_THROUGH != 0); - expectEqual(entry & DENTRY_CACHE_DISABLED, 0); +fn checkDirEntry(entry: DirectoryEntry, virt_start: usize, virt_end: usize, phys_start: usize, attrs: vmm.Attributes, table: *Table, present: bool) void { + expectEqual(entry & DENTRY_PRESENT, if (present) DENTRY_PRESENT else 0); + expectEqual(entry & DENTRY_WRITABLE, if (attrs.writable) DENTRY_WRITABLE else 0); + expectEqual(entry & DENTRY_USER, if (attrs.kernel) 0 else DENTRY_USER); + expectEqual(entry & DENTRY_WRITE_THROUGH, DENTRY_WRITE_THROUGH); + expectEqual(entry & DENTRY_CACHE_DISABLED, if (attrs.cachable) 0 else DENTRY_CACHE_DISABLED); expectEqual(entry & DENTRY_4MB_PAGES, 0); expectEqual(entry & DENTRY_ZERO, 0); @@ -373,19 +426,36 @@ fn checkDirEntry(entry: DirectoryEntry, virt_start: usize, virt_end: usize, phys phys += PAGE_SIZE_4KB; }) { const tentry = table.entries[tentry_idx]; - checkTableEntry(tentry, phys); + checkTableEntry(tentry, phys, attrs, present); } } -fn checkTableEntry(entry: TableEntry, page_phys: usize) void { - expect(entry & TENTRY_PRESENT != 0); - expect(entry & TENTRY_WRITABLE != 0); - expectEqual(entry & TENTRY_USER, 0); - expect(entry & TENTRY_WRITE_THROUGH != 0); - expectEqual(entry & TENTRY_CACHE_DISABLED, 0); +fn checkTableEntry(entry: TableEntry, page_phys: usize, attrs: vmm.Attributes, present: bool) void { + expectEqual(entry & TENTRY_PRESENT, if (present) TENTRY_PRESENT else 0); + expectEqual(entry & TENTRY_WRITABLE, if (attrs.writable) TENTRY_WRITABLE else 0); + expectEqual(entry & TENTRY_USER, if (attrs.kernel) 0 else TENTRY_USER); + expectEqual(entry & TENTRY_WRITE_THROUGH, TENTRY_WRITE_THROUGH); + expectEqual(entry & TENTRY_CACHE_DISABLED, if (attrs.cachable) 0 else TENTRY_CACHE_DISABLED); expectEqual(entry & TENTRY_ZERO, 0); expectEqual(entry & TENTRY_GLOBAL, 0); - expectEqual(entry & TENTRY_PAGE_ADDR, @intCast(u32, page_phys)); + expectEqual(entry & TENTRY_PAGE_ADDR, page_phys); +} + +test "setAttribute and clearAttribute" { + var val: u32 = 0; + const attrs = [_]u32{ DENTRY_PRESENT, DENTRY_WRITABLE, DENTRY_USER, DENTRY_WRITE_THROUGH, DENTRY_CACHE_DISABLED, DENTRY_ACCESSED, DENTRY_ZERO, DENTRY_4MB_PAGES, DENTRY_IGNORED, DENTRY_AVAILABLE, DENTRY_PAGE_ADDR }; + + for (attrs) |attr| { + const old_val = val; + setAttribute(&val, attr); + std.testing.expectEqual(val, old_val | attr); + } + + for (attrs) |attr| { + const old_val = val; + clearAttribute(&val, attr); + std.testing.expectEqual(val, old_val & ~attr); + } } test "virtToDirEntryIdx" { @@ -410,38 +480,40 @@ test "virtToTableEntryIdx" { } test "mapDirEntry" { - var allocator = std.heap.direct_allocator; + var allocator = std.heap.page_allocator; var dir: Directory = Directory{ .entries = [_]DirectoryEntry{0} ** ENTRIES_PER_DIRECTORY, .tables = [_]?*Table{null} ** ENTRIES_PER_DIRECTORY }; var phys: usize = 0 * PAGE_SIZE_4MB; const phys_end: usize = phys + PAGE_SIZE_4MB; const virt: usize = 1 * PAGE_SIZE_4MB; const virt_end: usize = virt + PAGE_SIZE_4MB; - try mapDirEntry(&dir, virt, virt_end, phys, phys_end, allocator); + try mapDirEntry(&dir, virt, virt_end, phys, phys_end, .{ .kernel = true, .writable = true, .cachable = true }, allocator); const entry_idx = virtToDirEntryIdx(virt); const entry = dir.entries[entry_idx]; const table = dir.tables[entry_idx] orelse unreachable; - checkDirEntry(entry, virt, virt_end, phys, table); + checkDirEntry(entry, virt, virt_end, phys, .{ .kernel = true, .writable = true, .cachable = true }, table, true); } test "mapDirEntry returns errors correctly" { - var allocator = std.heap.direct_allocator; + var allocator = std.heap.page_allocator; var dir = Directory{ .entries = [_]DirectoryEntry{0} ** ENTRIES_PER_DIRECTORY, .tables = undefined }; - testing.expectError(PagingError.UnalignedVirtAddresses, mapDirEntry(&dir, 1, PAGE_SIZE_4KB + 1, 0, PAGE_SIZE_4KB, allocator)); - testing.expectError(PagingError.UnalignedPhysAddresses, mapDirEntry(&dir, 0, PAGE_SIZE_4KB, 1, PAGE_SIZE_4KB + 1, allocator)); - testing.expectError(PagingError.PhysicalVirtualMismatch, mapDirEntry(&dir, 0, PAGE_SIZE_4KB, 1, PAGE_SIZE_4KB, allocator)); - testing.expectError(PagingError.InvalidVirtAddresses, mapDirEntry(&dir, 1, 0, 0, PAGE_SIZE_4KB, allocator)); - testing.expectError(PagingError.InvalidPhysAddresses, mapDirEntry(&dir, 0, PAGE_SIZE_4KB, 1, 0, allocator)); + const attrs = vmm.Attributes{ .kernel = true, .writable = true, .cachable = true }; + testing.expectError(vmm.MapperError.MisalignedVirtualAddress, mapDirEntry(&dir, 1, PAGE_SIZE_4KB + 1, 0, PAGE_SIZE_4KB, attrs, allocator)); + testing.expectError(vmm.MapperError.MisalignedPhysicalAddress, mapDirEntry(&dir, 0, PAGE_SIZE_4KB, 1, PAGE_SIZE_4KB + 1, attrs, allocator)); + testing.expectError(vmm.MapperError.AddressMismatch, mapDirEntry(&dir, 0, PAGE_SIZE_4KB, 1, PAGE_SIZE_4KB, attrs, allocator)); + testing.expectError(vmm.MapperError.InvalidVirtualAddress, mapDirEntry(&dir, 1, 0, 0, PAGE_SIZE_4KB, attrs, allocator)); + testing.expectError(vmm.MapperError.InvalidPhysicalAddress, mapDirEntry(&dir, 0, PAGE_SIZE_4KB, 1, 0, attrs, allocator)); } -test "mapDir" { - var allocator = std.heap.direct_allocator; +test "map and unmap" { + var allocator = std.heap.page_allocator; var dir = Directory{ .entries = [_]DirectoryEntry{0} ** ENTRIES_PER_DIRECTORY, .tables = [_]?*Table{null} ** ENTRIES_PER_DIRECTORY }; const phys_start: usize = PAGE_SIZE_4MB * 2; const virt_start: usize = PAGE_SIZE_4MB * 4; const phys_end: usize = PAGE_SIZE_4MB * 4; const virt_end: usize = PAGE_SIZE_4MB * 6; - mapDir(&dir, virt_start, virt_end, phys_start, phys_end, allocator) catch unreachable; + const attrs = vmm.Attributes{ .kernel = true, .writable = true, .cachable = true }; + map(virt_start, virt_end, phys_start, phys_end, attrs, allocator, &dir) catch unreachable; var virt = virt_start; var phys = phys_start; @@ -452,22 +524,37 @@ test "mapDir" { const entry_idx = virtToDirEntryIdx(virt); const entry = dir.entries[entry_idx]; const table = dir.tables[entry_idx] orelse unreachable; - checkDirEntry(entry, virt, virt + PAGE_SIZE_4MB, phys, table); + checkDirEntry(entry, virt, virt + PAGE_SIZE_4MB, phys, attrs, table, true); + } + + unmap(virt_start, virt_end, &dir) catch unreachable; + virt = virt_start; + phys = phys_start; + while (virt < virt_end) : ({ + virt += PAGE_SIZE_4MB; + phys += PAGE_SIZE_4MB; + }) { + const entry_idx = virtToDirEntryIdx(virt); + const entry = dir.entries[entry_idx]; + const table = dir.tables[entry_idx] orelse unreachable; + checkDirEntry(entry, virt, virt + PAGE_SIZE_4MB, phys, attrs, table, false); } } // The labels to jump to after attempting to cause a page fault. This is needed as we don't want to cause an -// infinite loop by jummping to the same instruction that caused the fault. +// infinite loop by jumping to the same instruction that caused the fault. extern var rt_fault_callback: *u32; extern var rt_fault_callback2: *u32; var faulted = false; var use_callback2 = false; -fn rt_pageFault(ctx: *arch.InterruptContext) void { +fn rt_pageFault(ctx: *arch.CpuState) u32 { faulted = true; // Return to the fault callback ctx.eip = @ptrToInt(&if (use_callback2) rt_fault_callback2 else rt_fault_callback); + + return @ptrToInt(ctx); } fn rt_accessUnmappedMem(v_end: u32) void { @@ -476,30 +563,36 @@ fn rt_accessUnmappedMem(v_end: u32) void { // Accessing unmapped mem causes a page fault var ptr = @intToPtr(*u8, v_end); var value = ptr.*; + // Need this as in release builds the above is optimised out so it needs to be use + log.logError("FAILURE: Value: {}\n", .{value}); // This is the label that we return to after processing the page fault asm volatile ( \\.global rt_fault_callback \\rt_fault_callback: ); - testing.expect(faulted); + if (!faulted) { + panic(@errorReturnTrace(), "FAILURE: Paging should have faulted\n", .{}); + } log.logInfo("Paging: Tested accessing unmapped memory\n", .{}); } fn rt_accessMappedMem(v_end: u32) void { use_callback2 = true; faulted = false; - // Accessing mapped memory does't cause a page fault + // Accessing mapped memory doesn't cause a page fault var ptr = @intToPtr(*u8, v_end - PAGE_SIZE_4KB); var value = ptr.*; asm volatile ( \\.global rt_fault_callback2 \\rt_fault_callback2: ); - testing.expect(!faulted); + if (faulted) { + panic(@errorReturnTrace(), "FAILURE: Paging shouldn't have faulted\n", .{}); + } log.logInfo("Paging: Tested accessing mapped memory\n", .{}); } -fn runtimeTests(v_end: u32) void { +pub fn runtimeTests(v_end: u32) void { rt_accessUnmappedMem(v_end); rt_accessMappedMem(v_end); } diff --git a/src/kernel/arch/x86/pic.zig b/src/kernel/arch/x86/pic.zig index a1e152b6..6d5d93ef 100644 --- a/src/kernel/arch/x86/pic.zig +++ b/src/kernel/arch/x86/pic.zig @@ -433,30 +433,45 @@ pub fn clearMask(irq_num: u8) void { /// pub fn init() void { log.logInfo("Init pic\n", .{}); + defer log.logInfo("Done pic\n", .{}); // Initiate sendCommandMaster(ICW1_INITIALISATION | ICW1_EXPECT_ICW4); + arch.ioWait(); sendCommandSlave(ICW1_INITIALISATION | ICW1_EXPECT_ICW4); + arch.ioWait(); // Offsets sendDataMaster(ICW2_MASTER_REMAP_OFFSET); + arch.ioWait(); sendDataSlave(ICW2_SLAVE_REMAP_OFFSET); + arch.ioWait(); // IRQ lines sendDataMaster(ICW3_MASTER_IRQ_MAP_FROM_SLAVE); + arch.ioWait(); sendDataSlave(ICW3_SLAVE_IRQ_MAP_TO_MASTER); + arch.ioWait(); // 80x86 mode sendDataMaster(ICW4_80x86_MODE); + arch.ioWait(); sendDataSlave(ICW4_80x86_MODE); + arch.ioWait(); // Mask all interrupts sendDataMaster(0xFF); + arch.ioWait(); sendDataSlave(0xFF); + arch.ioWait(); - log.logInfo("Done\n", .{}); + // Clear the IRQ for the slave + clearMask(IRQ_CASCADE_FOR_SLAVE); - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } test "sendCommandMaster" { @@ -763,6 +778,8 @@ test "init" { arch.initTest(); defer arch.freeTest(); + arch.addRepeatFunction("ioWait", arch.mock_ioWait); + // Just a long list of OUT instructions setting up the PIC arch.addTestParams("outb", .{ MASTER_COMMAND_REG, @@ -785,8 +802,12 @@ test "init" { @as(u8, 0xFF), SLAVE_DATA_REG, @as(u8, 0xFF), + MASTER_DATA_REG, + @as(u8, 0xFB), }); + arch.addTestParams("inb", .{ MASTER_DATA_REG, @as(u8, 0xFF) }); + init(); } @@ -794,12 +815,13 @@ test "init" { /// Test that all the PIC masks are set so no interrupts can fire. /// fn rt_picAllMasked() void { - if (readDataMaster() != 0xFF) { - panic(@errorReturnTrace(), "Master masks are not set, found: {}\n", .{readDataMaster()}); + // The master will have interrupt 2 clear because this is the link to the slave (third bit) + if (readDataMaster() != 0xFB) { + panic(@errorReturnTrace(), "FAILURE: Master masks are not set, found: {}\n", .{readDataMaster()}); } if (readDataSlave() != 0xFF) { - panic(@errorReturnTrace(), "Slave masks are not set, found: {}\n", .{readDataSlave()}); + panic(@errorReturnTrace(), "FAILURE: Slave masks are not set, found: {}\n", .{readDataSlave()}); } log.logInfo("PIC: Tested masking\n", .{}); @@ -808,6 +830,6 @@ fn rt_picAllMasked() void { /// /// Run all the runtime tests. /// -fn runtimeTests() void { +pub fn runtimeTests() void { rt_picAllMasked(); } diff --git a/src/kernel/arch/x86/pit.zig b/src/kernel/arch/x86/pit.zig index ebd31f77..48efd42f 100644 --- a/src/kernel/arch/x86/pit.zig +++ b/src/kernel/arch/x86/pit.zig @@ -231,11 +231,12 @@ inline fn sendDataToCounter(counter: CounterSelect, data: u8) void { /// The interrupt handler for the PIT. This will increment a counter for now. /// /// Arguments: -/// IN ctx: *arch.InterruptContext - Pointer to the interrupt context containing the contents +/// IN ctx: *arch.CpuState - Pointer to the interrupt context containing the contents /// of the register at the time of the interrupt. /// -fn pitHandler(ctx: *arch.InterruptContext) void { +fn pitHandler(ctx: *arch.CpuState) usize { ticks +%= 1; + return @ptrToInt(ctx); } /// @@ -324,25 +325,17 @@ pub fn waitTicks(ticks_to_wait: u32) void { const wait_ticks2 = ticks_to_wait - wait_ticks1; while (ticks > wait_ticks1) { - arch.enableInterrupts(); arch.halt(); - arch.disableInterrupts(); } while (ticks < wait_ticks2) { - arch.enableInterrupts(); arch.halt(); - arch.disableInterrupts(); } - arch.enableInterrupts(); } else { const wait_ticks = ticks + ticks_to_wait; while (ticks < wait_ticks) { - arch.enableInterrupts(); arch.halt(); - arch.disableInterrupts(); } - arch.enableInterrupts(); } } @@ -371,6 +364,8 @@ pub fn getFrequency() u32 { /// pub fn init() void { log.logInfo("Init pit\n", .{}); + defer log.logInfo("Done pit\n", .{}); + // Set up counter 0 at 10000hz in a square wave mode counting in binary const freq: u32 = 10000; setupCounter(CounterSelect.Counter0, freq, OCW_MODE_SQUARE_WAVE_GENERATOR | OCW_BINARY_COUNT_BINARY) catch |e| { @@ -389,9 +384,10 @@ pub fn init() void { }, }; - log.logInfo("Done\n", .{}); - - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } test "sendCommand" { @@ -551,7 +547,7 @@ fn rt_waitTicks() void { const difference = getTicks() - waiting; if (previous_count + epsilon < difference or previous_count > difference + epsilon) { - panic(@errorReturnTrace(), "Waiting failed. difference: {}, previous_count: {}. Epsilon: {}\n", .{ difference, previous_count, epsilon }); + panic(@errorReturnTrace(), "FAILURE: Waiting failed. difference: {}, previous_count: {}. Epsilon: {}\n", .{ difference, previous_count, epsilon }); } log.logInfo("PIT: Tested wait ticks\n", .{}); @@ -575,13 +571,13 @@ fn rt_waitTicks2() void { const difference = getTicks() + 15 - waiting; if (previous_count + epsilon < difference or previous_count > difference + epsilon) { - panic(@errorReturnTrace(), "Waiting failed. difference: {}, previous_count: {}. Epsilon: {}\n", .{ difference, previous_count, epsilon }); + panic(@errorReturnTrace(), "FAILURE: Waiting failed. difference: {}, previous_count: {}. Epsilon: {}\n", .{ difference, previous_count, epsilon }); } - log.logInfo("PIT: Tested wait ticks 2\n", .{}); - // Reset ticks ticks = 0; + + log.logInfo("PIT: Tested wait ticks 2\n", .{}); } /// @@ -593,7 +589,7 @@ fn rt_initCounter_0() void { const expected_hz: u32 = 10027; if (time_ns != expected_ns or time_under_1_ns != expected_ps or getFrequency() != expected_hz) { - panic(@errorReturnTrace(), "Frequency not set properly. Hz: {}!={}, ns: {}!={}, ps: {}!= {}\n", .{ + panic(@errorReturnTrace(), "FAILURE: Frequency not set properly. Hz: {}!={}, ns: {}!={}, ps: {}!= {}\n", .{ getFrequency(), expected_hz, time_ns, @@ -611,19 +607,19 @@ fn rt_initCounter_0() void { irq_exists = true; }, error.InvalidIrq => { - panic(@errorReturnTrace(), "IRQ for PIT, IRQ number: {} is invalid", .{pic.IRQ_PIT}); + panic(@errorReturnTrace(), "FAILURE: IRQ for PIT, IRQ number: {} is invalid", .{pic.IRQ_PIT}); }, }; if (!irq_exists) { - panic(@errorReturnTrace(), "IRQ for PIT doesn't exists\n", .{}); + panic(@errorReturnTrace(), "FAILURE: IRQ for PIT doesn't exists\n", .{}); } const expected_mode = OCW_READ_LOAD_DATA | OCW_MODE_SQUARE_WAVE_GENERATOR | OCW_SELECT_COUNTER_0 | OCW_BINARY_COUNT_BINARY; const actual_mode = readBackCommand(CounterSelect.Counter0); if (expected_mode != actual_mode) { - panic(@errorReturnTrace(), "Operating mode don't not set properly. Found: {}, expecting: {}\n", .{ actual_mode, expected_mode }); + panic(@errorReturnTrace(), "FAILURE: Operating mode don't not set properly. Found: {}, expecting: {}\n", .{ actual_mode, expected_mode }); } log.logInfo("PIT: Tested init\n", .{}); @@ -632,7 +628,11 @@ fn rt_initCounter_0() void { /// /// Run all the runtime tests. /// -fn runtimeTests() void { +pub fn runtimeTests() void { + // Interrupts aren't enabled yet, so for the runtime tests, enable it temporary + arch.enableInterrupts(); + defer arch.disableInterrupts(); + rt_initCounter_0(); rt_waitTicks(); rt_waitTicks2(); diff --git a/src/kernel/arch/x86/rtc.zig b/src/kernel/arch/x86/rtc.zig new file mode 100644 index 00000000..a82c65db --- /dev/null +++ b/src/kernel/arch/x86/rtc.zig @@ -0,0 +1,762 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const is_test = builtin.is_test; +const expect = std.testing.expect; +const expectEqual = std.testing.expectEqual; +const expectError = std.testing.expectError; +const build_options = @import("build_options"); +const mock_path = build_options.arch_mock_path; +const arch = if (is_test) @import(mock_path ++ "arch_mock.zig") else @import("arch.zig"); +const log = @import("../../log.zig"); +const pic = @import("pic.zig"); +const pit = @import("pit.zig"); +const irq = @import("irq.zig"); +const cmos = if (is_test) @import(mock_path ++ "cmos_mock.zig") else @import("cmos.zig"); +const panic = if (is_test) @import(mock_path ++ "panic_mock.zig").panic else @import("../../panic.zig").panic; +const scheduler = @import("../../scheduler.zig"); + +/// The Century register is unreliable. We need a APIC interface to infer if we have a century +/// register. So this is a current TODO. +const CURRENT_CENTURY: u32 = 2000; + +/// TODO: To do with the unreliable century register. Once have APIC, can use this as a sanity check +/// if the the century register gives a wild answer then the other RTC values maybe wild. So then +/// could report that the CMOS chip is faulty or the battery is dyeing. +const CENTURY_REGISTER: bool = false; + +/// A structure to hold all the date and time information in the RTC. +const DateTime = struct { + second: u32, + minute: u32, + hour: u32, + day: u32, + month: u32, + year: u32, + century: u32, + day_of_week: u32, +}; + +/// The error set that can be returned from some RTC functions. +const RtcError = error{ + /// If setting the rate for interrupts is less than 3 or greater than 15. + RateError, +}; + +/// The number of ticks that has passed when RTC was initially set up. +var ticks: u32 = 0; + +var schedule: bool = true; + +/// +/// Checks if the CMOS chip isn't updating the RTC registers. Call this before reading any RTC +/// registers so don't get inconsistent values. +/// +/// Return: bool +/// Whether the CMOS chip is busy and a update is in progress. +/// +fn isBusy() bool { + return (cmos.readStatusRegister(cmos.StatusRegister.A, false) & 0x80) != 0; +} + +/// +/// Calculate the day of the week from the given day, month and year. +/// +/// Arguments: +/// IN date_time: DateTime - The DateTime structure that holds the current day, month and year. +/// +/// Return: u8 +/// A number that represents the day of the week. 1 = Sunday, 2 = Monday, ... +/// +fn calcDayOfWeek(date_time: DateTime) u32 { + const t = [_]u8{ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 }; + const year = date_time.year - @boolToInt(date_time.month < 3); + const month = date_time.month; + const day = date_time.day; + + return (year + (year / 4) - (year / 100) + (year / 400) + t[month - 1] + day) % 7; +} + +/// +/// Check if the RTC is in binary coded decimal mode. If the RTC ic counting in BCD, then the 3rd +/// bit in the status register B will be set. +/// +/// Return: bool +/// When the RTC is counting in BCD. +/// +fn isBcd() bool { + const reg_b = cmos.readStatusRegister(cmos.StatusRegister.B, false); + return reg_b & 0x04 != 0; +} + +/// +/// Check if the RTC in 12 hour mode. If the RTC is in 12 hour mode, then the 2nd bit in the status +/// register B is set and the most significant bit on the hour field is set. +/// +/// Arguments: +/// IN date_time: DateTime - The DateTime structure containing at least the hour field set. +/// +/// Return: bool +/// Whether the RTC is in 12 hour mode. +/// +fn is12Hr(date_time: DateTime) bool { + const reg_b = cmos.readStatusRegister(cmos.StatusRegister.B, false); + return reg_b & 0x02 != 0 and date_time.hour & 0x80 != 0; +} + +/// +/// Convert BCD to binary. +/// +/// Arguments: +/// IN bcd: u32 - The binary coded decimal value to convert +/// +/// Return: u32 +/// The converted BCD value. +/// +fn bcdToBinary(bcd: u32) u32 { + return ((bcd & 0xF0) >> 1) + ((bcd & 0xF0) >> 3) + (bcd & 0xf); +} + +/// +/// Read the real time clock registers and return them in the DateTime structure. This will read +/// the seconds, minutes, hours, days, months, years, century and day of the week. +/// +/// Return: DateTime +/// The data from the CMOS RTC registers. +/// +fn readRtcRegisters() DateTime { + // Make sure there isn't a update in progress + while (isBusy()) {} + + var date_time = DateTime{ + .second = cmos.readRtcRegister(cmos.RtcRegister.SECOND), + .minute = cmos.readRtcRegister(cmos.RtcRegister.MINUTE), + .hour = cmos.readRtcRegister(cmos.RtcRegister.HOUR), + .day = cmos.readRtcRegister(cmos.RtcRegister.DAY), + .month = cmos.readRtcRegister(cmos.RtcRegister.MONTH), + .year = cmos.readRtcRegister(cmos.RtcRegister.YEAR), + .century = if (CENTURY_REGISTER) cmos.readRtcRegister(cmos.RtcRegister.CENTURY) else CURRENT_CENTURY, + // This will be filled in later + .day_of_week = 0, + }; + + // The day of the week register is also very unreliable, so is better to calculate it + date_time.day_of_week = calcDayOfWeek(date_time); + + return date_time; +} + +/// +/// Read a stable time from the real time clock registers on the CMOS chip and return a BCD and +/// 12 hour converted date and time. +/// +/// Return: DateTime +/// The data from the CMOS RTC registers with correct BCD conversions, 12 hour conversions and +/// the century added to the year. +/// +fn readRtc() DateTime { + var date_time1 = readRtcRegisters(); + var date_time2 = readRtcRegisters(); + + // Use the method: Read the registers twice and check if they are the same so to avoid + // inconsistent values due to RTC updates + + var compare = false; + + inline for (@typeInfo(DateTime).Struct.fields) |field| { + compare = compare or @field(date_time1, field.name) != @field(date_time2, field.name); + } + + while (compare) { + date_time1 = readRtcRegisters(); + date_time2 = readRtcRegisters(); + + compare = false; + inline for (@typeInfo(DateTime).Struct.fields) |field| { + compare = compare or @field(date_time1, field.name) != @field(date_time2, field.name); + } + } + + // Convert BCD to binary if necessary + if (isBcd()) { + date_time1.second = bcdToBinary(date_time1.second); + date_time1.minute = bcdToBinary(date_time1.minute); + // Needs a special calculation because the upper bit is set + date_time1.hour = ((date_time1.hour & 0x0F) + (((date_time1.hour & 0x70) / 16) * 10)) | (date_time1.hour & 0x80); + date_time1.day = bcdToBinary(date_time1.day); + date_time1.month = bcdToBinary(date_time1.month); + date_time1.year = bcdToBinary(date_time1.year); + if (CENTURY_REGISTER) { + date_time1.century = bcdToBinary(date_time1.century); + } + } + + // Need to add on the century to the year + if (CENTURY_REGISTER) { + date_time1.year += date_time1.century * 100; + } else { + date_time1.year += CURRENT_CENTURY; + } + + // Convert to 24hr time + if (is12Hr(date_time1)) { + date_time1.hour = ((date_time1.hour & 0x7F) + 12) % 24; + } + + return date_time1; +} + +/// +/// The interrupt handler for the RTC. +/// +/// Arguments: +/// IN ctx: *arch.CpuState - Pointer to the interrupt context containing the contents +/// of the register at the time of the interrupt. +/// +fn rtcHandler(ctx: *arch.CpuState) usize { + ticks +%= 1; + + var ret_esp: usize = undefined; + + // Call the scheduler + if (schedule) { + ret_esp = scheduler.pickNextTask(ctx); + } else { + ret_esp = @ptrToInt(ctx); + } + + // Need to read status register C + // Might need to disable the NMI bit, set to true + const reg_c = cmos.readStatusRegister(cmos.StatusRegister.C, false); + + return ret_esp; +} + +/// +/// Set the rate at which the interrupts will fire. Ranges from 0x3 to 0xF. Where the frequency +/// is determined by: frequency = 32768 >> (rate-1); This will assume the interrupts are disabled. +/// +/// Arguments: +/// IN rate: u4 - The rate value to set the frequency to. +/// +/// Error: RtcError +/// RtcError.RateError - If the rate is less than 3. +/// +fn setRate(rate: u8) RtcError!void { + if (rate < 3 or rate > 0xF) { + return RtcError.RateError; + } + + // Need to disable the NMI for this process + const status_a = cmos.readStatusRegister(cmos.StatusRegister.A, true); + cmos.writeStatusRegister(cmos.StatusRegister.A, (status_a & 0xF0) | rate, true); +} + +/// +/// Enable interrupts for the RTC. This will assume the interrupts have been disabled before hand. +/// +fn enableInterrupts() void { + // Need to disable the NMI for this process + const status_b = cmos.readStatusRegister(cmos.StatusRegister.B, true); + + // Set the 7th bit to enable interrupt + cmos.writeStatusRegister(cmos.StatusRegister.B, status_b | 0x40, true); +} + +/// +/// Initialise the RTC. +/// +pub fn init() void { + log.logInfo("Init rtc\n", .{}); + defer log.logInfo("Done rtc\n", .{}); + + // Register the interrupt handler + irq.registerIrq(pic.IRQ_REAL_TIME_CLOCK, rtcHandler) catch |err| switch (err) { + error.IrqExists => { + panic(@errorReturnTrace(), "IRQ for RTC, IRQ number: {} exists", .{pic.IRQ_REAL_TIME_CLOCK}); + }, + error.InvalidIrq => { + panic(@errorReturnTrace(), "IRQ for RTC, IRQ number: {} is invalid", .{pic.IRQ_REAL_TIME_CLOCK}); + }, + }; + + // Set the interrupt rate to 512Hz + setRate(7) catch |err| switch (err) { + error.RateError => { + panic(@errorReturnTrace(), "Setting rate error", .{}); + }, + }; + + // Enable RTC interrupts + enableInterrupts(); + + // Read status register C to clear any interrupts that may have happened during set up + const reg_c = cmos.readStatusRegister(cmos.StatusRegister.C, false); + + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } +} + +test "isBusy not busy" { + cmos.initTest(); + defer cmos.freeTest(); + + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.A, false, @as(u8, 0x60) }, + ); + + expect(!isBusy()); +} + +test "isBusy busy" { + cmos.initTest(); + defer cmos.freeTest(); + + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.A, false, @as(u8, 0x80) }, + ); + + expect(isBusy()); +} + +test "calcDayOfWeek" { + var date_time = DateTime{ + .second = 0, + .minute = 0, + .hour = 0, + .day = 10, + .month = 1, + .year = 2020, + .century = 0, + .day_of_week = 0, + }; + + var actual = calcDayOfWeek(date_time); + var expected = @as(u32, 5); + + expectEqual(expected, actual); + + date_time.day = 20; + date_time.month = 7; + date_time.year = 1940; + + actual = calcDayOfWeek(date_time); + expected = @as(u32, 6); + + expectEqual(expected, actual); + + date_time.day = 9; + date_time.month = 11; + date_time.year = 2043; + + actual = calcDayOfWeek(date_time); + expected = @as(u32, 1); + + expectEqual(expected, actual); + + date_time.day = 1; + date_time.month = 1; + date_time.year = 2000; + + actual = calcDayOfWeek(date_time); + expected = @as(u32, 6); + + expectEqual(expected, actual); +} + +test "isBcd not BCD" { + cmos.initTest(); + defer cmos.freeTest(); + + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.B, false, @as(u8, 0x00) }, + ); + + expect(!isBcd()); +} + +test "isBcd BCD" { + cmos.initTest(); + defer cmos.freeTest(); + + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.B, false, @as(u8, 0x04) }, + ); + + expect(isBcd()); +} + +test "is12Hr not 12Hr" { + const date_time = DateTime{ + .second = 0, + .minute = 0, + .hour = 0, + .day = 0, + .month = 0, + .year = 0, + .century = 0, + .day_of_week = 0, + }; + + cmos.initTest(); + defer cmos.freeTest(); + + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.B, false, @as(u8, 0x00) }, + ); + + expect(!is12Hr(date_time)); +} + +test "is12Hr 12Hr" { + const date_time = DateTime{ + .second = 0, + .minute = 0, + .hour = 0x80, + .day = 0, + .month = 0, + .year = 0, + .century = 0, + .day_of_week = 0, + }; + + cmos.initTest(); + defer cmos.freeTest(); + + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.B, false, @as(u8, 0x02) }, + ); + + expect(is12Hr(date_time)); +} + +test "bcdToBinary" { + var expected = @as(u32, 59); + var actual = bcdToBinary(0x59); + + expectEqual(expected, actual); + + expected = @as(u32, 48); + actual = bcdToBinary(0x48); + + expectEqual(expected, actual); + + expected = @as(u32, 1); + actual = bcdToBinary(0x01); + + expectEqual(expected, actual); +} + +test "readRtcRegisters" { + cmos.initTest(); + defer cmos.freeTest(); + + // Have 2 busy loops + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.A, false, @as(u8, 0x80), cmos.StatusRegister.A, false, @as(u8, 0x80), cmos.StatusRegister.A, false, @as(u8, 0x00) }, + ); + + // All the RTC registers without century as it isn't supported yet + cmos.addTestParams("readRtcRegister", .{ + cmos.RtcRegister.SECOND, @as(u8, 1), + cmos.RtcRegister.MINUTE, @as(u8, 2), + cmos.RtcRegister.HOUR, @as(u8, 3), + cmos.RtcRegister.DAY, @as(u8, 10), + cmos.RtcRegister.MONTH, @as(u8, 1), + cmos.RtcRegister.YEAR, @as(u8, 20), + }); + + const expected = DateTime{ + .second = 1, + .minute = 2, + .hour = 3, + .day = 10, + .month = 1, + .year = 20, + .century = 2000, + .day_of_week = 5, + }; + const actual = readRtcRegisters(); + + expectEqual(expected, actual); +} + +test "readRtc unstable read" { + cmos.initTest(); + defer cmos.freeTest(); + + // No busy loop + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.A, false, @as(u8, 0x00), cmos.StatusRegister.A, false, @as(u8, 0x00) }, + ); + + // Reading the RTC registers twice, second time is one second ahead + cmos.addTestParams("readRtcRegister", .{ + cmos.RtcRegister.SECOND, @as(u8, 1), + cmos.RtcRegister.MINUTE, @as(u8, 2), + cmos.RtcRegister.HOUR, @as(u8, 3), + cmos.RtcRegister.DAY, @as(u8, 10), + cmos.RtcRegister.MONTH, @as(u8, 1), + cmos.RtcRegister.YEAR, @as(u8, 20), + cmos.RtcRegister.SECOND, @as(u8, 2), + cmos.RtcRegister.MINUTE, @as(u8, 2), + cmos.RtcRegister.HOUR, @as(u8, 3), + cmos.RtcRegister.DAY, @as(u8, 10), + cmos.RtcRegister.MONTH, @as(u8, 1), + cmos.RtcRegister.YEAR, @as(u8, 20), + }); + + // Will try again, and now stable + // No busy loop + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.A, false, @as(u8, 0x00), cmos.StatusRegister.A, false, @as(u8, 0x00) }, + ); + cmos.addTestParams("readRtcRegister", .{ + cmos.RtcRegister.SECOND, @as(u8, 2), + cmos.RtcRegister.MINUTE, @as(u8, 2), + cmos.RtcRegister.HOUR, @as(u8, 3), + cmos.RtcRegister.DAY, @as(u8, 10), + cmos.RtcRegister.MONTH, @as(u8, 1), + cmos.RtcRegister.YEAR, @as(u8, 20), + cmos.RtcRegister.SECOND, @as(u8, 2), + cmos.RtcRegister.MINUTE, @as(u8, 2), + cmos.RtcRegister.HOUR, @as(u8, 3), + cmos.RtcRegister.DAY, @as(u8, 10), + cmos.RtcRegister.MONTH, @as(u8, 1), + cmos.RtcRegister.YEAR, @as(u8, 20), + }); + + // Not BCD + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.B, false, @as(u8, 0x00) }, + ); + + // Not 12hr + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.B, false, @as(u8, 0x00) }, + ); + + const expected = DateTime{ + .second = 2, + .minute = 2, + .hour = 3, + .day = 10, + .month = 1, + .year = 2020, + .century = 2000, + .day_of_week = 5, + }; + const actual = readRtc(); + + expectEqual(expected, actual); +} + +test "readRtc is BCD" { + cmos.initTest(); + defer cmos.freeTest(); + + // No busy loop + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.A, false, @as(u8, 0x00), cmos.StatusRegister.A, false, @as(u8, 0x00) }, + ); + + // Reading the RTC registers once + cmos.addTestParams("readRtcRegister", .{ + cmos.RtcRegister.SECOND, @as(u8, 0x30), + cmos.RtcRegister.MINUTE, @as(u8, 0x59), + cmos.RtcRegister.HOUR, @as(u8, 0x11), + cmos.RtcRegister.DAY, @as(u8, 0x10), + cmos.RtcRegister.MONTH, @as(u8, 0x1), + cmos.RtcRegister.YEAR, @as(u8, 0x20), + cmos.RtcRegister.SECOND, @as(u8, 0x30), + cmos.RtcRegister.MINUTE, @as(u8, 0x59), + cmos.RtcRegister.HOUR, @as(u8, 0x11), + cmos.RtcRegister.DAY, @as(u8, 0x10), + cmos.RtcRegister.MONTH, @as(u8, 0x1), + cmos.RtcRegister.YEAR, @as(u8, 0x20), + }); + + // BCD + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.B, false, @as(u8, 0x04) }, + ); + + // Not 12hr + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.B, false, @as(u8, 0x00) }, + ); + + const expected = DateTime{ + .second = 30, + .minute = 59, + .hour = 11, + .day = 10, + .month = 1, + .year = 2020, + .century = 2000, + .day_of_week = 5, + }; + const actual = readRtc(); + + expectEqual(expected, actual); +} + +test "readRtc is 12 hours" { + cmos.initTest(); + defer cmos.freeTest(); + + // No busy loop + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.A, false, @as(u8, 0x00), cmos.StatusRegister.A, false, @as(u8, 0x00) }, + ); + + // Reading the RTC registers once + cmos.addTestParams("readRtcRegister", .{ + cmos.RtcRegister.SECOND, @as(u8, 1), + cmos.RtcRegister.MINUTE, @as(u8, 2), + cmos.RtcRegister.HOUR, @as(u8, 0x83), + cmos.RtcRegister.DAY, @as(u8, 10), + cmos.RtcRegister.MONTH, @as(u8, 1), + cmos.RtcRegister.YEAR, @as(u8, 20), + cmos.RtcRegister.SECOND, @as(u8, 1), + cmos.RtcRegister.MINUTE, @as(u8, 2), + cmos.RtcRegister.HOUR, @as(u8, 0x83), + cmos.RtcRegister.DAY, @as(u8, 10), + cmos.RtcRegister.MONTH, @as(u8, 1), + cmos.RtcRegister.YEAR, @as(u8, 20), + }); + + // Not BCD + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.B, false, @as(u8, 0x00) }, + ); + + // 12hr + cmos.addTestParams( + "readStatusRegister", + .{ cmos.StatusRegister.B, false, @as(u8, 0x02) }, + ); + + const expected = DateTime{ + .second = 1, + .minute = 2, + .hour = 15, + .day = 10, + .month = 1, + .year = 2020, + .century = 2000, + .day_of_week = 5, + }; + const actual = readRtc(); + + expectEqual(expected, actual); +} + +test "setRate below 3" { + expectError(RtcError.RateError, setRate(0)); + expectError(RtcError.RateError, setRate(1)); + expectError(RtcError.RateError, setRate(2)); +} + +test "setRate" { + cmos.initTest(); + defer cmos.freeTest(); + + cmos.addTestParams("readStatusRegister", .{ cmos.StatusRegister.A, true, @as(u8, 0x10) }); + cmos.addTestParams("writeStatusRegister", .{ cmos.StatusRegister.A, @as(u8, 0x17), true }); + + const rate = @as(u8, 7); + try setRate(rate); +} + +test "enableInterrupts" { + cmos.initTest(); + defer cmos.freeTest(); + + cmos.addTestParams("readStatusRegister", .{ cmos.StatusRegister.B, true, @as(u8, 0x20) }); + cmos.addTestParams("writeStatusRegister", .{ cmos.StatusRegister.B, @as(u8, 0x60), true }); + + enableInterrupts(); +} + +/// +/// Check that the IRQ is registered correctly +/// +fn rt_init() void { + var irq_exists = false; + irq.registerIrq(pic.IRQ_REAL_TIME_CLOCK, rtcHandler) catch |err| switch (err) { + error.IrqExists => { + // We should get this error + irq_exists = true; + }, + error.InvalidIrq => { + panic(@errorReturnTrace(), "FAILURE: IRQ for RTC, IRQ number: {} is invalid\n", .{pic.IRQ_REAL_TIME_CLOCK}); + }, + }; + + if (!irq_exists) { + panic(@errorReturnTrace(), "FAILURE: IRQ for RTC doesn't exists\n", .{}); + } + + // Check the rate + const status_a = cmos.readStatusRegister(cmos.StatusRegister.A, false); + if (status_a & @as(u8, 0x0F) != 7) { + panic(@errorReturnTrace(), "FAILURE: Rate not set properly, got: {}\n", .{status_a & @as(u8, 0x0F)}); + } + + // Check if interrupts are enabled + const status_b = cmos.readStatusRegister(cmos.StatusRegister.B, true); + if (status_b & ~@as(u8, 0x40) == 0) { + panic(@errorReturnTrace(), "FAILURE: Interrupts not enabled\n", .{}); + } + + log.logInfo("RTC: Tested init\n", .{}); +} + +/// +/// Check if the interrupt handler is called after a sleep so check that the RTC interrupts fire. +/// +fn rt_interrupts() void { + const prev_ticks = ticks; + + pit.waitTicks(100); + + if (prev_ticks == ticks) { + panic(@errorReturnTrace(), "FAILURE: No interrupt happened\n", .{}); + } + + log.logInfo("RTC: Tested interrupts\n", .{}); +} + +/// +/// Run all the runtime tests. +/// +pub fn runtimeTests() void { + rt_init(); + + // Disable the scheduler temporary + schedule = false; + // Interrupts aren't enabled yet, so for the runtime tests, enable it temporary + arch.enableInterrupts(); + rt_interrupts(); + arch.disableInterrupts(); + // Can enable it back + schedule = true; +} diff --git a/src/kernel/arch/x86/serial.zig b/src/kernel/arch/x86/serial.zig new file mode 100644 index 00000000..5163571e --- /dev/null +++ b/src/kernel/arch/x86/serial.zig @@ -0,0 +1,178 @@ +const arch = @import("arch.zig"); +const panic = @import("../../panic.zig").panic; +const testing = @import("std").testing; + +/// The I/O port numbers associated with each serial port +pub const Port = enum(u16) { + COM1 = 0x3F8, + COM2 = 0x2F8, + COM3 = 0x3E8, + COM4 = 0x2E8, +}; + +/// Errors thrown by serial functions +pub const SerialError = error{ + /// The given baudrate is outside of the allowed range + InvalidBaudRate, + + /// The given char len is outside the allowed range. + InvalidCharacterLength, +}; + +/// The LCR is the line control register +const LCR: u16 = 3; + +/// Maximum baudrate +const BAUD_MAX: u32 = 115200; + +/// 8 bits per serial character +const CHAR_LEN: u8 = 8; + +/// One stop bit per transmission +const SINGLE_STOP_BIT: bool = true; + +/// No parity bit +const PARITY_BIT: bool = false; + +/// Default baudrate +pub const DEFAULT_BAUDRATE = 38400; + +/// +/// Compute a value that encodes the serial properties +/// Used by the line control register +/// +/// Arguments: +/// IN char_len: u8 - The number of bits in each individual byte. Must be 0 or between 5 and 8 (inclusive). +/// IN stop_bit: bool - If a stop bit should included in each transmission. +/// IN parity_bit: bool - If a parity bit should be included in each transmission. +/// IN msb: u1 - The most significant bit to use. +/// +/// Return: u8 +/// The computed lcr value. +/// +/// Error: SerialError +/// InvalidCharacterLength - If the char_len is less than 5 or greater than 8. +/// +fn lcrValue(char_len: u8, stop_bit: bool, parity_bit: bool, msb: u1) SerialError!u8 { + if (char_len != 0 and (char_len < 5 or char_len > 8)) + return SerialError.InvalidCharacterLength; + // Set the msb and OR in all arguments passed + const val = char_len & 0x3 | + @intCast(u8, @boolToInt(stop_bit)) << 2 | + @intCast(u8, @boolToInt(parity_bit)) << 3 | + @intCast(u8, msb) << 7; + return val; +} + +/// +/// The serial controller accepts a divisor rather than a raw baudrate, as that is more space efficient. +/// This function computes the divisor for a desired baudrate. Note that multiple baudrates can have the same divisor. +/// +/// Arguments: +/// baud: u32 - The desired baudrate. Must be greater than 0 and less than BAUD_MAX. +/// +/// Return: u16 +/// The computed divisor. +/// +/// Error: SerialError +/// InvalidBaudRate - If baudrate is 0 or greater than BAUD_MAX. +/// +fn baudDivisor(baud: u32) SerialError!u16 { + if (baud > BAUD_MAX or baud == 0) + return SerialError.InvalidBaudRate; + return @truncate(u16, BAUD_MAX / baud); +} + +/// +/// Checks if the transmission buffer is empty, which means data can be sent. +/// +/// Arguments: +/// port: Port - The port to check. +/// +/// Return: bool +/// If the transmission buffer is empty. +/// +fn transmitIsEmpty(port: Port) bool { + return arch.inb(@enumToInt(port) + 5) & 0x20 > 0; +} + +/// +/// Write a byte to a serial port. Waits until the transmission queue is empty. +/// +/// Arguments: +/// char: u8 - The byte to send. +/// port: Port - The port to send the byte to. +/// +pub fn write(char: u8, port: Port) void { + while (!transmitIsEmpty(port)) { + arch.halt(); + } + arch.outb(@enumToInt(port), char); +} + +/// +/// Initialise a serial port to a certain baudrate +/// +/// Arguments +/// IN baud: u32 - The baudrate to use. Cannot be more than MAX_BAUDRATE +/// IN port: Port - The port to initialise +/// +/// Error: SerialError +/// InvalidBaudRate - The baudrate is 0 or greater than BAUD_MAX. +/// +pub fn init(baud: u32, port: Port) SerialError!void { + // The baudrate is sent as a divisor of the max baud rate + const divisor: u16 = try baudDivisor(baud); + const port_int = @enumToInt(port); + // Send a byte to start setting the baudrate + arch.outb(port_int + LCR, lcrValue(0, false, false, 1) catch |e| { + panic(@errorReturnTrace(), "Failed to initialise serial output setup: {}", .{e}); + }); + // Send the divisor's lsb + arch.outb(port_int, @truncate(u8, divisor)); + // Send the divisor's msb + arch.outb(port_int + 1, @truncate(u8, divisor >> 8)); + // Send the properties to use + arch.outb(port_int + LCR, lcrValue(CHAR_LEN, SINGLE_STOP_BIT, PARITY_BIT, 0) catch |e| { + panic(@errorReturnTrace(), "Failed to setup serial properties: {}", .{e}); + }); + // Stop initialisation + arch.outb(port_int + 1, 0); +} + +test "lcrValue computes the correct value" { + // Check valid combinations + inline for ([_]u8{ 0, 5, 6, 7, 8 }) |char_len| { + inline for ([_]bool{ true, false }) |stop_bit| { + inline for ([_]bool{ true, false }) |parity_bit| { + inline for ([_]u1{ 0, 1 }) |msb| { + const val = try lcrValue(char_len, stop_bit, parity_bit, msb); + const expected = char_len & 0x3 | + @boolToInt(stop_bit) << 2 | + @boolToInt(parity_bit) << 3 | + @intCast(u8, msb) << 7; + testing.expectEqual(val, expected); + } + } + } + } + + // Check invalid char lengths + testing.expectError(SerialError.InvalidCharacterLength, lcrValue(4, false, false, 0)); + testing.expectError(SerialError.InvalidCharacterLength, lcrValue(9, false, false, 0)); +} + +test "baudDivisor" { + // Check invalid baudrates + inline for ([_]u32{ 0, BAUD_MAX + 1 }) |baud| { + testing.expectError(SerialError.InvalidBaudRate, baudDivisor(baud)); + } + + // Check valid baudrates + var baud: u32 = 1; + while (baud <= BAUD_MAX) : (baud += 1) { + const val = try baudDivisor(baud); + const expected = @truncate(u16, BAUD_MAX / baud); + testing.expectEqual(val, expected); + } +} diff --git a/src/kernel/arch/x86/syscalls.zig b/src/kernel/arch/x86/syscalls.zig index 6d503d5b..5c777209 100644 --- a/src/kernel/arch/x86/syscalls.zig +++ b/src/kernel/arch/x86/syscalls.zig @@ -1,9 +1,14 @@ -const arch = @import("arch.zig"); -const testing = @import("std").testing; -const assert = @import("std").debug.assert; +const std = @import("std"); +const builtin = @import("builtin"); +const is_test = builtin.is_test; +const build_options = @import("build_options"); +const mock_path = build_options.arch_mock_path; +const arch = if (is_test) @import(mock_path ++ "arch_mock.zig") else @import("arch.zig"); +const testing = std.testing; +const expect = std.testing.expect; const isr = @import("isr.zig"); const log = @import("../../log.zig"); -const options = @import("build_options"); +const panic = @import("../../panic.zig").panic; /// The isr number associated with syscalls pub const INTERRUPT: u16 = 0x80; @@ -12,7 +17,7 @@ pub const INTERRUPT: u16 = 0x80; pub const NUM_HANDLERS: u16 = 256; /// A syscall handler -pub const SyscallHandler = fn (ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32; +pub const SyscallHandler = fn (ctx: *arch.CpuState, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32; /// Errors that syscall utility functions can throw pub const SyscallError = error{ @@ -43,10 +48,10 @@ pub fn isValidSyscall(syscall: u32) bool { /// warning is logged. /// /// Arguments: -/// IN ctx: *arch.InterruptContext - The cpu context when the syscall was triggered. The +/// IN ctx: *arch.CpuState - The cpu context when the syscall was triggered. The /// syscall number is stored in eax. /// -fn handle(ctx: *arch.InterruptContext) void { +fn handle(ctx: *arch.CpuState) u32 { // The syscall number is put in eax const syscall = ctx.eax; if (isValidSyscall(syscall)) { @@ -58,6 +63,7 @@ fn handle(ctx: *arch.InterruptContext) void { } else { log.logWarning("Syscall {} is invalid\n", .{syscall}); } + return @ptrToInt(ctx); } /// @@ -216,13 +222,13 @@ inline fn syscall5(syscall: u32, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg /// 3 => esi and 4 => edi. /// /// Arguments: -/// IN ctx: *arch.InterruptContext - The interrupt context from which to get the argument +/// IN ctx: *arch.CpuState - The interrupt context from which to get the argument /// IN arg_idx: comptime u32 - The argument index to get. Between 0 and 4. /// /// Return: u32 /// The syscall argument from the given index. /// -inline fn syscallArg(ctx: *arch.InterruptContext, comptime arg_idx: u32) u32 { +inline fn syscallArg(ctx: *arch.CpuState, comptime arg_idx: u32) u32 { return switch (arg_idx) { 0 => ctx.ebx, 1 => ctx.ecx, @@ -238,41 +244,46 @@ inline fn syscallArg(ctx: *arch.InterruptContext, comptime arg_idx: u32) u32 { /// pub fn init() void { log.logInfo("Init syscalls\n", .{}); + defer log.logInfo("Done syscalls\n", .{}); + isr.registerIsr(INTERRUPT, handle) catch unreachable; - log.logInfo("Done\n", .{}); - if (options.rt_test) runtimeTests(); + + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } /// Tests -var testInt: u32 = 0; +var test_int: u32 = 0; -fn testHandler0(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += 1; +fn testHandler0(ctx: *arch.CpuState, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { + test_int += 1; return 0; } -fn testHandler1(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += arg1; +fn testHandler1(ctx: *arch.CpuState, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { + test_int += arg1; return 1; } -fn testHandler2(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += arg1 + arg2; +fn testHandler2(ctx: *arch.CpuState, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { + test_int += arg1 + arg2; return 2; } -fn testHandler3(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += arg1 + arg2 + arg3; +fn testHandler3(ctx: *arch.CpuState, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { + test_int += arg1 + arg2 + arg3; return 3; } -fn testHandler4(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += arg1 + arg2 + arg3 + arg4; +fn testHandler4(ctx: *arch.CpuState, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { + test_int += arg1 + arg2 + arg3 + arg4; return 4; } -fn testHandler5(ctx: *arch.InterruptContext, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { - testInt += arg1 + arg2 + arg3 + arg4 + arg5; +fn testHandler5(ctx: *arch.CpuState, arg1: u32, arg2: u32, arg3: u32, arg4: u32, arg5: u32) u32 { + test_int += arg1 + arg2 + arg3 + arg4 + arg5; return 5; } @@ -281,33 +292,44 @@ test "registerSyscall returns SyscallExists" { registerSyscall(123, testHandler0) catch |err| { return; }; - assert(false); + expect(false); } fn runtimeTests() void { - registerSyscall(123, testHandler0) catch unreachable; - registerSyscall(124, testHandler1) catch unreachable; - registerSyscall(125, testHandler2) catch unreachable; - registerSyscall(126, testHandler3) catch unreachable; - registerSyscall(127, testHandler4) catch unreachable; - registerSyscall(128, testHandler5) catch unreachable; - assert(testInt == 0); + registerSyscall(123, testHandler0) catch panic(@errorReturnTrace(), "FAILURE registering handler 0\n", .{}); + registerSyscall(124, testHandler1) catch panic(@errorReturnTrace(), "FAILURE registering handler 1\n", .{}); + registerSyscall(125, testHandler2) catch panic(@errorReturnTrace(), "FAILURE registering handler 2\n", .{}); + registerSyscall(126, testHandler3) catch panic(@errorReturnTrace(), "FAILURE registering handler 3\n", .{}); + registerSyscall(127, testHandler4) catch panic(@errorReturnTrace(), "FAILURE registering handler 4\n", .{}); + registerSyscall(128, testHandler5) catch panic(@errorReturnTrace(), "FAILURE registering handler 5\n", .{}); - if (syscall0(123) == 0 and testInt == 1) - log.logInfo("Syscalls: Tested no args\n", .{}); + if (test_int != 0) { + panic(@errorReturnTrace(), "FAILURE initial test_int not 0: {}\n", .{test_int}); + } - if (syscall1(124, 2) == 1 and testInt == 3) - log.logInfo("Syscalls: Tested 1 arg\n", .{}); + if (syscall0(123) != 0 or test_int != 1) { + panic(@errorReturnTrace(), "FAILURE syscall0\n", .{}); + } - if (syscall2(125, 2, 3) == 2 and testInt == 8) - log.logInfo("Syscalls: Tested 2 args\n", .{}); + if (syscall1(124, 2) != 1 or test_int != 3) { + panic(@errorReturnTrace(), "FAILURE syscall2\n", .{}); + } - if (syscall3(126, 2, 3, 4) == 3 and testInt == 17) - log.logInfo("Syscalls: Tested 3 args\n", .{}); + if (syscall2(125, 2, 3) != 2 or test_int != 8) { + panic(@errorReturnTrace(), "FAILURE syscall2\n", .{}); + } + + if (syscall3(126, 2, 3, 4) != 3 or test_int != 17) { + panic(@errorReturnTrace(), "FAILURE syscall3\n", .{}); + } - if (syscall4(127, 2, 3, 4, 5) == 4 and testInt == 31) - log.logInfo("Syscalls: Tested 4 args\n", .{}); + if (syscall4(127, 2, 3, 4, 5) != 4 or test_int != 31) { + panic(@errorReturnTrace(), "FAILURE syscall4\n", .{}); + } + + if (syscall5(128, 2, 3, 4, 5, 6) != 5 or test_int != 51) { + panic(@errorReturnTrace(), "FAILURE syscall5\n", .{}); + } - if (syscall5(128, 2, 3, 4, 5, 6) == 5 and testInt == 51) - log.logInfo("Syscalls: Tested 5 args\n", .{}); + log.logInfo("Syscall: Tested all args\n", .{}); } diff --git a/src/kernel/arch/x86/tty.zig b/src/kernel/arch/x86/tty.zig new file mode 100644 index 00000000..436a3049 --- /dev/null +++ b/src/kernel/arch/x86/tty.zig @@ -0,0 +1,2109 @@ +const std = @import("std"); +const fmt = std.fmt; +const builtin = @import("builtin"); +const is_test = builtin.is_test; +const expect = std.testing.expect; +const expectEqual = std.testing.expectEqual; +const expectError = std.testing.expectError; +const build_options = @import("build_options"); +const mock_path = build_options.mock_path; +const vga = if (is_test) @import("../../" ++ mock_path ++ "vga_mock.zig") else @import("vga.zig"); +const log = if (is_test) @import("../../" ++ mock_path ++ "log_mock.zig") else @import("../../log.zig"); +const panic = if (is_test) @import("../../" ++ mock_path ++ "panic_mock.zig").panic else @import("../../panic.zig").panic; + +/// The error set for if there is an error whiles printing. +const TtyError = error{ + /// If the printing tries to print outside the video buffer. + OutOfBounds, +}; + +/// The number of rows down from the top (row 0) where the displayable region starts. Above is +/// where the logo and time is printed +const ROW_MIN: u16 = 7; + +/// The total number of rows in the displayable region +const ROW_TOTAL: u16 = vga.HEIGHT - ROW_MIN; + +/// The total number of pages (static) that the terminal will remember. In the future, this can +/// move to a more dynamic allocation when a kheap is implemented. +const TOTAL_NUM_PAGES: u16 = 5; + +/// The total number of VGA (or characters) elements are on a page +const TOTAL_CHAR_ON_PAGE: u16 = vga.WIDTH * ROW_TOTAL; + +/// The start of the displayable region in the video buffer memory +const START_OF_DISPLAYABLE_REGION: u16 = vga.WIDTH * ROW_MIN; + +/// The total number of VGA elements (or characters) the video buffer can display +const VIDEO_BUFFER_SIZE: u16 = vga.WIDTH * vga.HEIGHT; + +/// The location of the kernel in virtual memory so can calculate the address of the VGA buffer +extern var KERNEL_ADDR_OFFSET: *u32; + +/// The current x position of the cursor. +var column: u8 = 0; + +/// The current y position of the cursor. +var row: u8 = 0; + +/// The current colour of the display with foreground and background colour. +var colour: u8 = undefined; + +/// The buffer starting from the beginning of the video memory location that contains all data +/// written to the display. +var video_buffer: []volatile u16 = undefined; + +/// The blank VGA entry to be used to clear the screen. +var blank: u16 = undefined; + +/// A total of TOTAL_NUM_PAGES pages that can be saved and restored to from and to the video buffer +var pages: [TOTAL_NUM_PAGES][TOTAL_CHAR_ON_PAGE]u16 = init: { + var p: [TOTAL_NUM_PAGES][TOTAL_CHAR_ON_PAGE]u16 = undefined; + + for (p) |*page| { + page.* = [_]u16{0} ** TOTAL_CHAR_ON_PAGE; + } + + break :init p; +}; + +/// The current page index. +var page_index: u8 = 0; + +/// +/// Copies data into the video buffer. This is used for copying a page into the video buffer. +/// +/// Arguments: +/// IN video_buf_offset: u16 - The offset into the video buffer to start copying to. +/// IN data: []const u16 - The data to copy into the video buffer. +/// IN size: u16 - The amount to copy. +/// +/// Errors: TtyError +/// TtyError.OutOfBounds - If offset or the size to copy is greater than the size of the +/// video buffer or data to copy. +/// +fn videoCopy(video_buf_offset: u16, data: []const u16, size: u16) TtyError!void { + // Secure programming ;) + if (video_buf_offset >= video_buffer.len and + size > video_buffer.len - video_buf_offset and + size > data.len) + { + return TtyError.OutOfBounds; + } + + var i: u32 = 0; + while (i < size) : (i += 1) { + video_buffer[video_buf_offset + i] = data[i]; + } +} + +/// +/// Moves data with a page without overriding itself. +/// +/// Arguments: +/// IN dest: []u16 - The destination position to copy into. +/// IN src: []u16 - The source position to copy from. +/// IN size: u16 - The amount to copy. +/// +/// Errors: +/// TtyError.OutOfBounds - If the size to copy is greater than the size of the pages. +/// +fn pageMove(dest: []u16, src: []u16, size: u16) TtyError!void { + if (dest.len < size or src.len < size) { + return TtyError.OutOfBounds; + } + + // Not an error if size is zero, nothing will be copied + if (size == 0) return; + + // Make sure we don't override the values we want to copy + if (@ptrToInt(&dest[0]) < @ptrToInt(&src[0])) { + var i: u16 = 0; + while (i != size) : (i += 1) { + dest[i] = src[i]; + } + } else { + var i = size; + while (i != 0) { + i -= 1; + dest[i] = src[i]; + } + } +} + +/// +/// Clears a region of the video buffer to a VGA entry from the beginning. +/// +/// Arguments: +/// IN c: u16 - VGA entry to set the video buffer to. +/// IN size: u16 - The number to VGA entries to set from the beginning of the video buffer. +/// +/// Errors: +/// TtyError.OutOfBounds - If the size to copy is greater than the size of the video buffer. +/// +fn setVideoBuffer(c: u16, size: u16) TtyError!void { + if (size > VIDEO_BUFFER_SIZE) { + return TtyError.OutOfBounds; + } + + for (video_buffer[0..size]) |*b| { + b.* = c; + } +} + +/// +/// Updated the hardware cursor to the current column and row (x, y). +/// +fn updateCursor() void { + vga.updateCursor(column, row); +} + +/// +/// Get the hardware cursor and set the current column and row (x, y). +/// +fn getCursor() void { + const cursor = vga.getCursor(); + + row = @truncate(u8, cursor / vga.WIDTH); + column = @truncate(u8, cursor % vga.WIDTH); +} + +/// +/// Put a character at a specific column and row position on the screen. This will use the current +/// colour. +/// +/// Arguments: +/// IN char: u8 - The character to print. This will be combined with the current colour. +/// IN x: u8 - The x position (column) to put the character at. +/// IN y: u8 - The y position (row) to put the character at. +/// +/// Errors: +/// TtyError.OutOfBounds - If trying to print outside the video buffer. +/// +fn putEntryAt(char: u8, x: u8, y: u8) TtyError!void { + const index = y * vga.WIDTH + x; + + // Bounds check + if (index >= VIDEO_BUFFER_SIZE) { + return TtyError.OutOfBounds; + } + + const char_entry = vga.entry(char, colour); + + if (index >= START_OF_DISPLAYABLE_REGION) { + // If not at page zero, (bottom of page), then display that page + // The user has move up a number of pages and then typed a letter, so need to move to the + // 0'th page + if (page_index != 0) { + // This isn't out of bounds + page_index = 0; + try videoCopy(START_OF_DISPLAYABLE_REGION, pages[page_index][0..TOTAL_CHAR_ON_PAGE], TOTAL_CHAR_ON_PAGE); + + // If not on page 0, then the cursor would have been disabled + vga.enableCursor(); + updateCursor(); + } + pages[page_index][index - START_OF_DISPLAYABLE_REGION] = char_entry; + } + + video_buffer[index] = char_entry; +} + +/// +/// Move rows up pages across multiple pages leaving the last rows blank. +/// +/// Arguments: +/// IN rows: u16 - The number of rows to move up. +/// +/// Errors: +/// TtyError.OutOfBounds - If trying to move up more rows on a page. +/// +fn pagesMoveRowsUp(rows: u16) TtyError!void { + // Out of bounds check + if (rows > ROW_TOTAL) { + return TtyError.OutOfBounds; + } + + // Not an error to move 0 rows, but is pointless + if (rows == 0) return; + + // Move up rows in last page up by "rows" + const row_length = rows * vga.WIDTH; + const chars_to_move = (ROW_TOTAL - rows) * vga.WIDTH; + try pageMove(pages[TOTAL_NUM_PAGES - 1][0..chars_to_move], pages[TOTAL_NUM_PAGES - 1][row_length..], chars_to_move); + + // Loop for the other pages + var i = TOTAL_NUM_PAGES - 1; + while (i > 0) : (i -= 1) { + try pageMove(pages[i][chars_to_move..], pages[i - 1][0..row_length], row_length); + try pageMove(pages[i - 1][0..chars_to_move], pages[i - 1][row_length..], chars_to_move); + } + + // Clear the last lines + for (pages[0][chars_to_move..]) |*p| { + p.* = blank; + } +} + +/// +/// When the text/terminal gets to the bottom of the screen, then move all line up by the amount +/// that are below the bottom of the screen. Usually moves up by one line. +/// +/// Errors: +/// TtyError.OutOfBounds - If trying to move up more rows on a page. This shouldn't happen +/// as bounds checks have been done. +/// +fn scroll() void { + // Added the condition in the if from pagesMoveRowsUp as don't need to move all rows + if (row >= vga.HEIGHT and (row - vga.HEIGHT + 1) <= ROW_TOTAL) { + const rows_to_move = row - vga.HEIGHT + 1; + + // Move rows up pages by temp, will usually be one. + // TODO: Maybe panic here as we have the check above, so if this fails, then is a big problem + pagesMoveRowsUp(rows_to_move) catch |e| { + panic(@errorReturnTrace(), "Can't move {} rows up. Must be less than {}\n", .{ rows_to_move, ROW_TOTAL }); + }; + + // Move all rows up by rows_to_move + var i: u32 = 0; + while (i < (ROW_TOTAL - rows_to_move) * vga.WIDTH) : (i += 1) { + video_buffer[START_OF_DISPLAYABLE_REGION + i] = video_buffer[(rows_to_move * vga.WIDTH) + START_OF_DISPLAYABLE_REGION + i]; + } + + // Set the last rows to blanks + i = 0; + while (i < vga.WIDTH * rows_to_move) : (i += 1) { + video_buffer[(vga.HEIGHT - rows_to_move) * vga.WIDTH + i] = blank; + } + + row = vga.HEIGHT - 1; + } +} + +/// +/// Print a character without updating the cursor. For speed when printing a string as only need to +/// update the cursor once. This will also print the special characters: \n, \r, \t and \b. (\b is +/// not a valid character so use \x08 which is the hex value). +/// +/// Arguments: +/// IN char: u8 - The character to print. +/// +/// Errors: +/// TtyError.OutOfBounds - If trying to scroll more rows on a page/displayable region or +/// print beyond the video buffer. +/// +fn putChar(char: u8) TtyError!void { + const column_temp = column; + const row_temp = row; + + // If there was an error, then set the row and column back to where is was + // Like nothing happened + errdefer column = column_temp; + errdefer row = row_temp; + + switch (char) { + '\n' => { + column = 0; + row += 1; + scroll(); + }, + '\t' => { + column += 4; + if (column >= vga.WIDTH) { + column -= @truncate(u8, vga.WIDTH); + row += 1; + scroll(); + } + }, + '\r' => { + column = 0; + }, + // \b + '\x08' => { + if (column == 0) { + if (row != 0) { + column = vga.WIDTH - 1; + row -= 1; + } + } else { + column -= 1; + } + }, + else => { + try putEntryAt(char, column, row); + column += 1; + if (column == vga.WIDTH) { + column = 0; + row += 1; + scroll(); + } + }, + } +} + +/// +/// Set the TTY cursor position to a row and column +/// +/// Arguments: +/// IN r: u8 - The row to set it to +/// IN col: u8 - The column to set it to +/// +pub fn setCursor(r: u8, col: u8) void { + column = col; + row = r; + updateCursor(); +} + +/// +/// Print a string to the TTY. This also updates to hardware cursor. +/// +/// Arguments: +/// IN str: []const u8 - The string to print. +/// +/// Errors: +/// TtyError.OutOfBounds - If trying to print beyond the video buffer. +/// +pub fn writeString(str: []const u8) TtyError!void { + // Make sure we update the cursor to the last character + defer updateCursor(); + for (str) |char| { + try putChar(char); + } +} + +/// +/// Move up a page. This will copy the page above to the video buffer. Will keep trace of which +/// page is being displayed. +/// +pub fn pageUp() void { + if (page_index < TOTAL_NUM_PAGES - 1) { + // Copy page to display + page_index += 1; + // Bounds have been checked, so shouldn't error + videoCopy(START_OF_DISPLAYABLE_REGION, pages[page_index][0..TOTAL_CHAR_ON_PAGE], TOTAL_CHAR_ON_PAGE) catch |e| { + log.logError("TTY: Error moving page up. Error: {}\n", .{e}); + }; + vga.disableCursor(); + } +} + +/// +/// Move down a page. This will copy the page bellow to the video buffer. Will keep trace of which +/// page is being displayed. +/// +pub fn pageDown() void { + if (page_index > 0) { + // Copy page to display + page_index -= 1; + // Bounds have been checked, so shouldn't error + videoCopy(START_OF_DISPLAYABLE_REGION, pages[page_index][0..TOTAL_CHAR_ON_PAGE], TOTAL_CHAR_ON_PAGE) catch |e| { + log.logError("TTY: Error moving page down. Error: {}\n", .{e}); + }; + + if (page_index == 0) { + vga.enableCursor(); + updateCursor(); + } else { + vga.disableCursor(); + } + } +} + +/// +/// This clears the entire screen with blanks using the current colour. This will also save the +/// screen to the pages so can scroll back down. +/// +pub fn clearScreen() void { + // Move all the rows up + // This is within bounds, so shouldn't error + pagesMoveRowsUp(ROW_TOTAL) catch |e| { + log.logError("TTY: Error moving all pages up. Error: {}\n", .{e}); + }; + + // Clear the screen + var i: u16 = START_OF_DISPLAYABLE_REGION; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + video_buffer[i] = blank; + } + + // Set the cursor to below the logo + column = 0; + row = ROW_MIN; + updateCursor(); +} + +/// +/// This moves the software and hardware cursor to the left by one. +/// +pub fn moveCursorLeft() void { + if (column == 0) { + if (row != 0) { + column = vga.WIDTH - 1; + row -= 1; + } + } else { + column -= 1; + } + + updateCursor(); +} + +/// +/// This moves the software and hardware cursor to the right by one. +/// +pub fn moveCursorRight() void { + if (column == (vga.WIDTH - 1)) { + if (row != (vga.HEIGHT - 1)) { + column = 0; + row += 1; + } + } else { + column += 1; + } + + updateCursor(); +} + +/// +/// This will set a new colour for the screen. It will only become effective when printing new +/// characters. Use vga.colourEntry and the colour enums to set the colour. +/// +/// Arguments: +/// IN new_colour: u8 - The new foreground and background colour of the screen. +/// +pub fn setColour(new_colour: u8) void { + colour = new_colour; + blank = vga.entry(0, colour); +} + +/// +/// Gets the video buffer's virtual address. +/// +/// Return: usize +/// The virtual address of the video buffer +/// +pub fn getVideoBufferAddress() usize { + return @ptrToInt(&KERNEL_ADDR_OFFSET) + 0xB8000; +} + +/// +/// Initialise the tty. This will keep the bootloaders output and set the software cursor to where +/// the bootloader left it. Will copy the current screen to the pages, set the colour and blank +/// entry, print the logo and display the 0'th page. +/// +pub fn init() void { + // Video buffer in higher half + if (is_test) { + video_buffer = @intToPtr([*]volatile u16, mock_getVideoBufferAddress())[0..VIDEO_BUFFER_SIZE]; + } else { + video_buffer = @intToPtr([*]volatile u16, getVideoBufferAddress())[0..VIDEO_BUFFER_SIZE]; + } + + setColour(vga.entryColour(vga.COLOUR_LIGHT_GREY, vga.COLOUR_BLACK)); + + // Enable and get the hardware cursor to set the software cursor + vga.enableCursor(); + getCursor(); + + if (row != 0 or column != 0) { + // Copy rows 7 down to make room for logo + // If there isn't enough room, only take the bottom rows + var row_offset: u16 = 0; + if (vga.HEIGHT - 1 - row < ROW_MIN) { + row_offset = ROW_MIN - (vga.HEIGHT - 1 - row); + } + + // Make a copy into the pages + // Assuming that there is only one page + var i: u16 = 0; + while (i < row * vga.WIDTH) : (i += 1) { + pages[0][i] = video_buffer[i]; + } + + // Move 7 rows down + i = 0; + if (@ptrToInt(&video_buffer[ROW_MIN * vga.WIDTH]) < @ptrToInt(&video_buffer[row_offset * vga.WIDTH])) { + while (i != row * vga.WIDTH) : (i += 1) { + video_buffer[i + (ROW_MIN * vga.WIDTH)] = video_buffer[i + (row_offset * vga.WIDTH)]; + } + } else { + i = row * vga.WIDTH; + while (i != 0) { + i -= 1; + video_buffer[i + (ROW_MIN * vga.WIDTH)] = video_buffer[i + (row_offset * vga.WIDTH)]; + } + } + + // Set the top 7 rows blank + setVideoBuffer(blank, START_OF_DISPLAYABLE_REGION) catch |e| { + log.logError("TTY: Error clearing the top 7 rows. Error: {}\n", .{e}); + }; + row += @truncate(u8, row_offset + ROW_MIN); + } else { + // Clear the screen + setVideoBuffer(blank, VIDEO_BUFFER_SIZE) catch |e| { + log.logError("TTY: Error clearing the screen. Error: {}\n", .{e}); + }; + // Set the row to below the logo + row = ROW_MIN; + } + + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } +} + +const test_colour: u8 = vga.orig_entryColour(vga.COLOUR_LIGHT_GREY, vga.COLOUR_BLACK); +var test_video_buffer: [VIDEO_BUFFER_SIZE]u16 = [_]u16{0} ** VIDEO_BUFFER_SIZE; + +fn mock_getVideoBufferAddress() usize { + return @ptrToInt(&test_video_buffer); +} + +fn resetGlobals() void { + column = 0; + row = 0; + page_index = 0; + colour = undefined; + video_buffer = undefined; + blank = undefined; + + pages = init: { + var p: [TOTAL_NUM_PAGES][TOTAL_CHAR_ON_PAGE]u16 = undefined; + + for (p) |*page| { + page.* = [_]u16{0} ** TOTAL_CHAR_ON_PAGE; + } + + break :init p; + }; +} + +fn setUpVideoBuffer() void { + // Change to a stack location + video_buffer = test_video_buffer[0..VIDEO_BUFFER_SIZE]; + + expectEqual(@ptrToInt(video_buffer.ptr), @ptrToInt(&test_video_buffer[0])); + + colour = test_colour; + blank = vga.orig_entry(0, test_colour); +} + +fn setVideoBufferBlankPages() void { + setUpVideoBuffer(); + for (video_buffer) |*b| { + b.* = blank; + } + + setPagesBlank(); +} + +fn setVideoBufferIncrementingBlankPages() void { + setUpVideoBuffer(); + for (video_buffer) |*b, i| { + b.* = @intCast(u16, i); + } + + setPagesBlank(); +} + +fn setPagesBlank() void { + for (pages) |*p_i| { + for (p_i) |*p_j| { + p_j.* = blank; + } + } +} + +fn setPagesIncrementing() void { + for (pages) |*p_i, i| { + for (p_i) |*p_j, j| { + p_j.* = @intCast(u16, i) * TOTAL_CHAR_ON_PAGE + @intCast(u16, j); + } + } +} + +fn defaultVariablesTesting(p_i: u8, r: u8, c: u8) void { + expectEqual(test_colour, colour); + expectEqual(@as(u16, test_colour) << 8, blank); + expectEqual(p_i, page_index); + expectEqual(r, row); + expectEqual(c, column); +} + +fn incrementingPagesTesting() void { + for (pages) |p_i, i| { + for (p_i) |p_j, j| { + expectEqual(i * TOTAL_CHAR_ON_PAGE + j, p_j); + } + } +} + +fn blankPagesTesting() void { + for (pages) |p_i| { + for (p_i) |p_j| { + expectEqual(blank, p_j); + } + } +} + +fn incrementingVideoBufferTesting() void { + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const b = video_buffer[i]; + expectEqual(i, b); + } +} + +fn defaultVideoBufferTesting() void { + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const b = video_buffer[i]; + expectEqual(vga.orig_entry(0, test_colour), b); + } +} + +fn defaultAllTesting(p_i: u8, r: u8, c: u8) void { + defaultVariablesTesting(p_i, r, c); + blankPagesTesting(); + defaultVideoBufferTesting(); +} + +test "updateCursor" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga.updateCursor call for updating the hardware cursor + vga.initTest(); + defer vga.freeTest(); + + vga.addTestParams("updateCursor", .{ @as(u16, 0), @as(u16, 0) }); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + updateCursor(); + + // Post test + defaultAllTesting(0, 0, 0); + + // Tear down + resetGlobals(); +} + +test "getCursor zero" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga.getCursor call for getting the hardware cursor + vga.initTest(); + defer vga.freeTest(); + + vga.addTestParams("getCursor", .{@as(u16, 0)}); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + getCursor(); + + // Post test + defaultAllTesting(0, 0, 0); + + // Tear down + resetGlobals(); +} + +test "getCursor EEF" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga.getCursor call for getting the hardware cursor + vga.initTest(); + defer vga.freeTest(); + + vga.addTestParams("getCursor", .{@as(u16, 0x0EEF)}); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + getCursor(); + + // Post test + defaultAllTesting(0, 47, 63); + + // Tear down + resetGlobals(); +} + +test "putEntryAt out of bounds" { + // Set up + setVideoBufferBlankPages(); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + expectError(TtyError.OutOfBounds, putEntryAt('A', 100, 100)); + + // Post test + defaultAllTesting(0, 0, 0); + + // Tear down + resetGlobals(); +} + +test "putEntryAt not in displayable region" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("entry", vga.orig_entry); + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + // Enable and update cursor is only called once, can can use the consume function call + //vga.addConsumeFunction("enableCursor", vga.mock_enableCursor); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + const x = 0; + const y = 0; + const char = 'A'; + try putEntryAt(char, x, y); + + // Post test + defaultVariablesTesting(0, 0, 0); + blankPagesTesting(); + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const b = video_buffer[i]; + if (i == y * vga.WIDTH + x) { + expectEqual(vga.orig_entry(char, test_colour), b); + } else { + expectEqual(vga.orig_entry(0, test_colour), b); + } + } + + // Tear down + resetGlobals(); +} + +test "putEntryAt in displayable region page_index is 0" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("entry", vga.orig_entry); + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + const x = 0; + const y = ROW_MIN; + const char = 'A'; + try putEntryAt(char, x, y); + + // Post test + defaultVariablesTesting(0, 0, 0); + for (pages) |page, i| { + for (page) |c, j| { + if (i == page_index and (j == (y * vga.WIDTH + x) - START_OF_DISPLAYABLE_REGION)) { + expectEqual(vga.orig_entry(char, test_colour), c); + } else { + expectEqual(blank, c); + } + } + } + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const b = video_buffer[i]; + if (i == y * vga.WIDTH + x) { + expectEqual(vga.orig_entry(char, test_colour), b); + } else { + expectEqual(vga.orig_entry(0, test_colour), b); + } + } + + // Tear down + resetGlobals(); +} + +test "putEntryAt in displayable region page_index is not 0" { + // Set up + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("entry", vga.orig_entry); + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + // Enable and update cursor is only called once, can can use the consume function call + vga.addConsumeFunction("enableCursor", vga.mock_enableCursor); + + setVideoBufferBlankPages(); + + // Fill the 1'nd page (index 1) will all 1's + const ones = vga.orig_entry('1', test_colour); + for (pages) |*page, i| { + for (page) |*char| { + if (i == 0) { + char.* = ones; + } else { + char.* = 0; + } + } + } + + page_index = 1; + + // Pre testing + defaultVariablesTesting(1, 0, 0); + defaultVideoBufferTesting(); + + for (pages) |page, i| { + for (page) |char| { + if (i == 0) { + expectEqual(ones, char); + } else { + expectEqual(@as(u16, 0), char); + } + } + } + + // Call function + const x = 0; + const y = ROW_MIN; + const char = 'A'; + try putEntryAt(char, x, y); + + // Post test + defaultVariablesTesting(0, 0, 0); + + // Print page number + const text = "Page 0 of 4"; + const column_temp = column; + const row_temp = row; + column = @truncate(u8, vga.WIDTH) - @truncate(u8, text.len); + row = ROW_MIN - 1; + writeString(text) catch |e| { + log.logError("TTY: Unable to print page number, printing out of bounds. Error: {}\n", .{e}); + }; + column = column_temp; + row = row_temp; + + for (pages) |page, i| { + for (page) |c, j| { + if (i == 0 and j == 0) { + expectEqual(vga.orig_entry(char, test_colour), c); + } else if (i == 0) { + expectEqual(ones, c); + } else { + expectEqual(@as(u16, 0), c); + } + } + } + + // The top 7 rows won't be copied + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const b = video_buffer[i]; + if (i < START_OF_DISPLAYABLE_REGION - 11) { + expectEqual(blank, b); + } else if (i < START_OF_DISPLAYABLE_REGION) { + expectEqual(vga.orig_entry(text[i + 11 - START_OF_DISPLAYABLE_REGION], colour), b); + } else if (i == y * vga.WIDTH + x) { + expectEqual(vga.orig_entry(char, test_colour), b); + } else { + expectEqual(ones, b); + } + } + + // Tear down + resetGlobals(); +} + +test "pagesMoveRowsUp out of bounds" { + // Set up + setVideoBufferBlankPages(); + setPagesIncrementing(); + + // Pre testing + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + incrementingPagesTesting(); + + // Call function + const rows_to_move = ROW_TOTAL + 1; + expectError(TtyError.OutOfBounds, pagesMoveRowsUp(rows_to_move)); + + // Post test + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + incrementingPagesTesting(); + + // Tear down + resetGlobals(); +} + +test "pagesMoveRowsUp 0 rows" { + // Set up + setVideoBufferBlankPages(); + setPagesIncrementing(); + + // Pre testing + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + incrementingPagesTesting(); + + // Call function + const rows_to_move = 0; + try pagesMoveRowsUp(rows_to_move); + + // Post test + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + incrementingPagesTesting(); + + // Tear down + resetGlobals(); +} + +test "pagesMoveRowsUp 1 rows" { + // Set up + setVideoBufferBlankPages(); + setPagesIncrementing(); + + // Pre testing + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + incrementingPagesTesting(); + + // Call function + const rows_to_move = 1; + try pagesMoveRowsUp(rows_to_move); + + // Post test + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + + const to_add = rows_to_move * vga.WIDTH; + for (pages) |page, i| { + for (page) |c, j| { + if (j >= TOTAL_CHAR_ON_PAGE - to_add) { + if (i == 0) { + // The last rows will be blanks + expectEqual(blank, c); + } else { + expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + (j + to_add - TOTAL_CHAR_ON_PAGE), c); + } + } else { + // All rows moved up one, so add vga.WIDTH + expectEqual(i * TOTAL_CHAR_ON_PAGE + j + to_add, c); + } + } + } + + // Tear down + resetGlobals(); +} + +test "pagesMoveRowsUp ROW_TOTAL - 1 rows" { + // Set up + setVideoBufferBlankPages(); + setPagesIncrementing(); + + // Pre testing + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + incrementingPagesTesting(); + + // Call function + const rows_to_move = ROW_TOTAL - 1; + try pagesMoveRowsUp(rows_to_move); + + // Post test + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + + const to_add = rows_to_move * vga.WIDTH; + for (pages) |page, i| { + for (page) |c, j| { + if (j >= TOTAL_CHAR_ON_PAGE - to_add) { + if (i == 0) { + // The last rows will be blanks + expectEqual(blank, c); + } else { + expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + (j + to_add - TOTAL_CHAR_ON_PAGE), c); + } + } else { + // All rows moved up one, so add vga.WIDTH + expectEqual(i * TOTAL_CHAR_ON_PAGE + j + to_add, c); + } + } + } + + // Tear down + resetGlobals(); +} + +test "pagesMoveRowsUp ROW_TOTAL rows" { + // Set up + setVideoBufferBlankPages(); + setPagesIncrementing(); + + // Pre testing + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + incrementingPagesTesting(); + + // Call function + const rows_to_move = ROW_TOTAL; + try pagesMoveRowsUp(rows_to_move); + + // Post test + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + + for (pages) |page, i| { + for (page) |c, j| { + if (i == 0) { + // The last rows will be blanks + expectEqual(blank, c); + } else { + expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + j, c); + } + } + } + + // Tear down + resetGlobals(); +} + +test "scroll row is less then max height" { + // Set up + setVideoBufferBlankPages(); + setPagesIncrementing(); + + // Pre testing + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + incrementingPagesTesting(); + + // Call function + scroll(); + + // Post test + defaultVariablesTesting(0, 0, 0); + defaultVideoBufferTesting(); + incrementingPagesTesting(); + + // Tear down + resetGlobals(); +} + +test "scroll row is equal to height" { + // Set up + setVideoBufferIncrementingBlankPages(); + setPagesIncrementing(); + + const row_test = vga.HEIGHT; + row = row_test; + + // Pre testing + defaultVariablesTesting(0, row_test, 0); + incrementingPagesTesting(); + incrementingVideoBufferTesting(); + + // Call function + // Rows move up one + scroll(); + + // Post test + defaultVariablesTesting(0, vga.HEIGHT - 1, 0); + + const to_add = (row_test - vga.HEIGHT + 1) * vga.WIDTH; + for (pages) |page, i| { + for (page) |c, j| { + if (j >= TOTAL_CHAR_ON_PAGE - to_add) { + if (i == 0) { + // The last rows will be blanks + expectEqual(blank, c); + } else { + expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + (j + to_add - TOTAL_CHAR_ON_PAGE), c); + } + } else { + // All rows moved up one, so add vga.WIDTH + expectEqual(i * TOTAL_CHAR_ON_PAGE + j + to_add, c); + } + } + } + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (i < START_OF_DISPLAYABLE_REGION) { + expectEqual(i, buf); + } else if (i >= VIDEO_BUFFER_SIZE - to_add) { + expectEqual(blank, buf); + } else { + expectEqual(i + to_add, buf); + } + } + + // Tear down + resetGlobals(); +} + +test "scroll row is more than height" { + // Set up + setVideoBufferIncrementingBlankPages(); + setPagesIncrementing(); + + const row_test = vga.HEIGHT + 5; + row = row_test; + + // Pre testing + defaultVariablesTesting(0, row_test, 0); + incrementingPagesTesting(); + incrementingVideoBufferTesting(); + + // Call function + // Rows move up 5 + scroll(); + + // Post test + defaultVariablesTesting(0, vga.HEIGHT - 1, 0); + + const to_add = (row_test - vga.HEIGHT + 1) * vga.WIDTH; + for (pages) |page, i| { + for (page) |c, j| { + if (j >= TOTAL_CHAR_ON_PAGE - to_add) { + if (i == 0) { + // The last rows will be blanks + expectEqual(blank, c); + } else { + expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + (j + to_add - TOTAL_CHAR_ON_PAGE), c); + } + } else { + // All rows moved up one, so add vga.WIDTH + expectEqual(i * TOTAL_CHAR_ON_PAGE + j + to_add, c); + } + } + } + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (i < START_OF_DISPLAYABLE_REGION) { + expectEqual(i, buf); + } else if (i >= VIDEO_BUFFER_SIZE - to_add) { + expectEqual(blank, buf); + } else { + expectEqual(i + to_add, buf); + } + } + + // Tear down + resetGlobals(); +} + +test "putChar new line within screen" { + // Set up + setVideoBufferBlankPages(); + + // Pre testing + column = 5; + row = 5; + defaultAllTesting(0, 5, 5); + + // Call function + try putChar('\n'); + + // Post test + defaultAllTesting(0, 6, 0); + + // Tear down + resetGlobals(); +} + +test "putChar new line outside screen" { + // Set up + setVideoBufferBlankPages(); + + // Pre testing + column = 5; + row = vga.HEIGHT - 1; + defaultAllTesting(0, vga.HEIGHT - 1, 5); + + // Call function + try putChar('\n'); + + // Post test + defaultAllTesting(0, vga.HEIGHT - 1, 0); + + // Tear down + resetGlobals(); +} + +test "putChar tab within line" { + // Set up + setVideoBufferBlankPages(); + + // Pre testing + column = 5; + row = 6; + defaultAllTesting(0, 6, 5); + + // Call function + try putChar('\t'); + + // Post test + defaultAllTesting(0, 6, 9); + + // Tear down + resetGlobals(); +} + +test "putChar tab end of line" { + // Set up + setVideoBufferBlankPages(); + + // Pre testing + column = vga.WIDTH - 1; + row = 6; + defaultAllTesting(0, 6, vga.WIDTH - 1); + + // Call function + try putChar('\t'); + + // Post test + defaultAllTesting(0, 7, 3); + + // Tear down + resetGlobals(); +} + +test "putChar tab end of screen" { + // Set up + setVideoBufferBlankPages(); + + // Pre testing + column = vga.WIDTH - 1; + row = vga.HEIGHT - 1; + defaultAllTesting(0, vga.HEIGHT - 1, vga.WIDTH - 1); + + // Call function + try putChar('\t'); + + // Post test + defaultAllTesting(0, vga.HEIGHT - 1, 3); + + // Tear down + resetGlobals(); +} + +test "putChar line feed" { + // Set up + setVideoBufferBlankPages(); + + // Pre testing + column = vga.WIDTH - 1; + row = vga.HEIGHT - 1; + defaultAllTesting(0, vga.HEIGHT - 1, vga.WIDTH - 1); + + // Call function + try putChar('\r'); + + // Post test + defaultAllTesting(0, vga.HEIGHT - 1, 0); + + // Tear down + resetGlobals(); +} + +test "putChar back char top left of screen" { + // Set up + setVideoBufferBlankPages(); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + try putChar('\x08'); + + // Post test + defaultAllTesting(0, 0, 0); + + // Tear down + resetGlobals(); +} + +test "putChar back char top row" { + // Set up + setVideoBufferBlankPages(); + + // Pre testing + column = 8; + defaultAllTesting(0, 0, 8); + + // Call function + try putChar('\x08'); + + // Post test + defaultAllTesting(0, 0, 7); + + // Tear down + resetGlobals(); +} + +test "putChar back char beginning of row" { + // Set up + setVideoBufferBlankPages(); + + // Pre testing + row = 1; + defaultAllTesting(0, 1, 0); + + // Call function + try putChar('\x08'); + + // Post test + defaultAllTesting(0, 0, vga.WIDTH - 1); + + // Tear down + resetGlobals(); +} + +test "putChar any char in row" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("entry", vga.orig_entry); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + try putChar('A'); + + // Post test + defaultVariablesTesting(0, 0, 1); + blankPagesTesting(); + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (i == 0) { + expectEqual(vga.orig_entry('A', colour), buf); + } else { + expectEqual(blank, buf); + } + } + + // Tear down + resetGlobals(); +} + +test "putChar any char end of row" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("entry", vga.orig_entry); + + // Pre testing + column = vga.WIDTH - 1; + defaultAllTesting(0, 0, vga.WIDTH - 1); + + // Call function + try putChar('A'); + + // Post test + defaultVariablesTesting(0, 1, 0); + blankPagesTesting(); + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (i == vga.WIDTH - 1) { + expectEqual(vga.orig_entry('A', colour), buf); + } else { + expectEqual(blank, buf); + } + } + + // Tear down + resetGlobals(); +} + +test "putChar any char end of screen" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("entry", vga.orig_entry); + + // Pre testing + row = vga.HEIGHT - 1; + column = vga.WIDTH - 1; + defaultAllTesting(0, vga.HEIGHT - 1, vga.WIDTH - 1); + + // Call function + try putChar('A'); + + // Post test + defaultVariablesTesting(0, vga.HEIGHT - 1, 0); + for (pages) |page, i| { + for (page) |c, j| { + if ((i == 0) and (j == TOTAL_CHAR_ON_PAGE - vga.WIDTH - 1)) { + expectEqual(vga.orig_entry('A', colour), c); + } else { + expectEqual(blank, c); + } + } + } + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (i == VIDEO_BUFFER_SIZE - vga.WIDTH - 1) { + expectEqual(vga.orig_entry('A', colour), buf); + } else { + expectEqual(blank, buf); + } + } + + // Tear down + resetGlobals(); +} + +test "pageUp top page" { + // Set up + setVideoBufferBlankPages(); + setPagesIncrementing(); + + // Pre testing + page_index = TOTAL_NUM_PAGES - 1; + + defaultVariablesTesting(TOTAL_NUM_PAGES - 1, 0, 0); + incrementingPagesTesting(); + defaultVideoBufferTesting(); + + // Call function + pageUp(); + + // Post test + defaultVariablesTesting(TOTAL_NUM_PAGES - 1, 0, 0); + incrementingPagesTesting(); + defaultVideoBufferTesting(); + + // Tear down + resetGlobals(); +} + +test "pageUp bottom page" { + // Set up + setVideoBufferBlankPages(); + setPagesIncrementing(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("entry", vga.orig_entry); + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + vga.addConsumeFunction("disableCursor", vga.mock_disableCursor); + + // Pre testing + defaultVariablesTesting(0, 0, 0); + incrementingPagesTesting(); + defaultVideoBufferTesting(); + + // Call function + pageUp(); + + // Post test + defaultVariablesTesting(1, 0, 0); + incrementingPagesTesting(); + + // Print page number + const text = "Page 1 of 4"; + const column_temp = column; + const row_temp = row; + column = @truncate(u8, vga.WIDTH) - @truncate(u8, text.len); + row = ROW_MIN - 1; + writeString(text) catch |e| { + log.logError("TTY: Unable to print page number, printing out of bounds. Error: {}\n", .{e}); + }; + column = column_temp; + row = row_temp; + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const b = video_buffer[i]; + // Ignore the ROW_MIN row as this is where the page number is printed and is already + // tested, page number is printed 11 from the end + if (i < START_OF_DISPLAYABLE_REGION - 11) { + expectEqual(blank, b); + } else if (i < START_OF_DISPLAYABLE_REGION) { + expectEqual(vga.orig_entry(text[i + 11 - START_OF_DISPLAYABLE_REGION], colour), b); + } else { + expectEqual(i - START_OF_DISPLAYABLE_REGION + TOTAL_CHAR_ON_PAGE, b); + } + } + + // Tear down + resetGlobals(); +} + +test "pageDown bottom page" { + // Set up + setVideoBufferBlankPages(); + setPagesIncrementing(); + + // Pre testing + defaultVariablesTesting(0, 0, 0); + incrementingPagesTesting(); + defaultVideoBufferTesting(); + + // Call function + pageDown(); + + // Post test + defaultVariablesTesting(0, 0, 0); + incrementingPagesTesting(); + defaultVideoBufferTesting(); + + // Tear down + resetGlobals(); +} + +test "pageDown top page" { + // Set up + setVideoBufferBlankPages(); + setPagesIncrementing(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("entry", vga.orig_entry); + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + vga.addConsumeFunction("disableCursor", vga.mock_disableCursor); + + // Pre testing + page_index = TOTAL_NUM_PAGES - 1; + + defaultVariablesTesting(TOTAL_NUM_PAGES - 1, 0, 0); + incrementingPagesTesting(); + defaultVideoBufferTesting(); + + // Call function + pageDown(); + + // Post test + defaultVariablesTesting(TOTAL_NUM_PAGES - 2, 0, 0); + incrementingPagesTesting(); + + // Print page number + const text = "Page 3 of 4"; + const column_temp = column; + const row_temp = row; + column = @truncate(u8, vga.WIDTH) - @truncate(u8, text.len); + row = ROW_MIN - 1; + writeString(text) catch |e| { + log.logError("TTY: Unable to print page number, printing out of bounds. Error: {}\n", .{e}); + }; + column = column_temp; + row = row_temp; + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const b = video_buffer[i]; + // Ignore the ROW_MIN row as this is where the page number is printed and is already + // tested, page number is printed 11 from the end + if (i < START_OF_DISPLAYABLE_REGION - 11) { + expectEqual(blank, b); + } else if (i < START_OF_DISPLAYABLE_REGION) { + expectEqual(vga.orig_entry(text[i + 11 - START_OF_DISPLAYABLE_REGION], colour), b); + } else { + expectEqual((i - START_OF_DISPLAYABLE_REGION) + (TOTAL_CHAR_ON_PAGE * page_index), b); + } + } + + // Tear down + resetGlobals(); +} + +test "clearScreen" { + // Set up + setVideoBufferIncrementingBlankPages(); + setPagesIncrementing(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + // Pre testing + defaultVariablesTesting(0, 0, 0); + incrementingVideoBufferTesting(); + incrementingPagesTesting(); + + // Call function + clearScreen(); + + // Post test + defaultVariablesTesting(0, ROW_MIN, 0); + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (i < START_OF_DISPLAYABLE_REGION) { + expectEqual(i, buf); + } else { + expectEqual(blank, buf); + } + } + + for (pages) |page, j| { + for (page) |c, k| { + if (j == 0) { + // The last rows will be blanks + expectEqual(blank, c); + } else { + expectEqual((j - 1) * TOTAL_CHAR_ON_PAGE + k, c); + } + } + } + + // Tear down + resetGlobals(); +} + +test "moveCursorLeft top left of screen" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + moveCursorLeft(); + + // Post test + defaultAllTesting(0, 0, 0); + + // Tear down + resetGlobals(); +} + +test "moveCursorLeft top screen" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + // Pre testing + column = 5; + defaultAllTesting(0, 0, 5); + + // Call function + moveCursorLeft(); + + // Post test + defaultAllTesting(0, 0, 4); + + // Tear down + resetGlobals(); +} + +test "moveCursorLeft start of row" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + // Pre testing + row = 5; + defaultAllTesting(0, 5, 0); + + // Call function + moveCursorLeft(); + + // Post test + defaultAllTesting(0, 4, vga.WIDTH - 1); + + // Tear down + resetGlobals(); +} + +test "moveCursorRight bottom right of screen" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + // Pre testing + row = vga.HEIGHT - 1; + column = vga.WIDTH - 1; + defaultAllTesting(0, vga.HEIGHT - 1, vga.WIDTH - 1); + + // Call function + moveCursorRight(); + + // Post test + defaultAllTesting(0, vga.HEIGHT - 1, vga.WIDTH - 1); + + // Tear down + resetGlobals(); +} + +test "moveCursorRight top screen" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + // Pre testing + column = 5; + defaultAllTesting(0, 0, 5); + + // Call function + moveCursorRight(); + + // Post test + defaultAllTesting(0, 0, 6); + + // Tear down + resetGlobals(); +} + +test "moveCursorRight end of row" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + // Pre testing + row = 5; + column = vga.WIDTH - 1; + defaultAllTesting(0, 5, vga.WIDTH - 1); + + // Call function + moveCursorRight(); + + // Post test + defaultAllTesting(0, 6, 0); + + // Tear down + resetGlobals(); +} + +test "setColour" { + // Set up + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addConsumeFunction("entry", vga.orig_entry); + + // Pre testing + + // Call function + const new_colour = vga.orig_entryColour(vga.COLOUR_WHITE, vga.COLOUR_WHITE); + setColour(new_colour); + + // Post test + expectEqual(new_colour, colour); + expectEqual(vga.orig_entry(0, new_colour), blank); + + // Tear down + resetGlobals(); +} + +test "writeString" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga calls + vga.initTest(); + defer vga.freeTest(); + + vga.addRepeatFunction("entry", vga.orig_entry); + + vga.addConsumeFunction("updateCursor", vga.mock_updateCursor); + + // Pre testing + row = ROW_MIN; + defaultAllTesting(0, ROW_MIN, 0); + + // Call function + try writeString("ABC"); + + // Post test + defaultVariablesTesting(0, ROW_MIN, 3); + for (pages) |page, i| { + for (page) |c, j| { + if ((i == 0) and (j == 0)) { + expectEqual(vga.orig_entry('A', colour), c); + } else if ((i == 0) and (j == 1)) { + expectEqual(vga.orig_entry('B', colour), c); + } else if ((i == 0) and (j == 2)) { + expectEqual(vga.orig_entry('C', colour), c); + } else { + expectEqual(blank, c); + } + } + } + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (i == START_OF_DISPLAYABLE_REGION) { + expectEqual(vga.orig_entry('A', colour), buf); + } else if (i == START_OF_DISPLAYABLE_REGION + 1) { + expectEqual(vga.orig_entry('B', colour), buf); + } else if (i == START_OF_DISPLAYABLE_REGION + 2) { + expectEqual(vga.orig_entry('C', colour), buf); + } else { + expectEqual(blank, buf); + } + } + + // Tear down + resetGlobals(); +} + +test "init 0,0" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga.updateCursor call for updating the hardware cursor + vga.initTest(); + defer vga.freeTest(); + + vga.addTestParams("getCursor", .{@as(u16, 0)}); + + vga.addRepeatFunction("entryColour", vga.orig_entryColour); + vga.addRepeatFunction("entry", vga.orig_entry); + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + vga.addConsumeFunction("enableCursor", vga.mock_enableCursor); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + init(); + + // Post test + defaultVariablesTesting(0, ROW_MIN, 0); + blankPagesTesting(); + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (i < START_OF_DISPLAYABLE_REGION) { + // This is where the logo will be, but is a complex string so no testing + // Just take my word it works :P + } else { + expectEqual(blank, buf); + } + } + + // Tear down + resetGlobals(); +} + +test "init not 0,0" { + // Set up + setVideoBufferBlankPages(); + + // Mocking out the vga.updateCursor call for updating the hardware cursor + vga.initTest(); + defer vga.freeTest(); + + vga.addTestParams("getCursor", .{vga.WIDTH}); + + vga.addRepeatFunction("entryColour", vga.orig_entryColour); + vga.addRepeatFunction("entry", vga.orig_entry); + vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); + + vga.addConsumeFunction("enableCursor", vga.mock_enableCursor); + + // Pre testing + defaultAllTesting(0, 0, 0); + + // Call function + init(); + + // Post test + defaultVariablesTesting(0, ROW_MIN + 1, 0); + blankPagesTesting(); + + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (i < START_OF_DISPLAYABLE_REGION) { + // This is where the logo will be, but is a complex string so no testing + // Just take my word it works :P + } else { + expectEqual(blank, buf); + } + } + + // Tear down + resetGlobals(); +} + +/// +/// Test the init function set up everything properly. +/// +fn rt_initialisedGlobals() void { + if (@ptrToInt(video_buffer.ptr) != @ptrToInt(&KERNEL_ADDR_OFFSET) + 0xB8000) { + panic(@errorReturnTrace(), "Video buffer not at correct virtual address, found: {}\n", .{@ptrToInt(video_buffer.ptr)}); + } + + if (page_index != 0) { + panic(@errorReturnTrace(), "Page index not at zero, found: {}\n", .{page_index}); + } + + if (colour != vga.entryColour(vga.COLOUR_LIGHT_GREY, vga.COLOUR_BLACK)) { + panic(@errorReturnTrace(), "Colour not set up properly, found: {}\n", .{colour}); + } + + if (blank != vga.entry(0, colour)) { + panic(@errorReturnTrace(), "Blank not set up properly, found: {}\n", .{blank}); + } + + // Make sure the screen isn't all blank + var all_blank = true; + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (buf != blank and buf != 0) { + all_blank = false; + break; + } + } + + if (all_blank) { + panic(@errorReturnTrace(), "Screen all blank, should have logo and page number\n", .{}); + } + + log.logInfo("TTY: Tested globals\n", .{}); +} + +/// +/// Test printing a string will output to the screen. This will check both the video memory and +/// the pages. +/// +fn rt_printString() void { + const text = "abcdefg"; + const clear_text = "\x08" ** text.len; + + writeString(text) catch |e| panic(@errorReturnTrace(), "Failed to print string to tty: {}\n", .{e}); + + // Check the video memory + var counter: u32 = 0; + var i: u32 = 0; + while (i < VIDEO_BUFFER_SIZE) : (i += 1) { + const buf = video_buffer[i]; + if (counter < text.len and buf == vga.entry(text[counter], colour)) { + counter += 1; + } else if (counter == text.len) { + // Found all the text + break; + } else { + counter = 0; + } + } + + if (counter != text.len) { + panic(@errorReturnTrace(), "Didn't find the printed text in video memory\n", .{}); + } + + // Check the pages + counter = 0; + for (pages[0]) |c| { + if (counter < text.len and c == vga.entry(text[counter], colour)) { + counter += 1; + } else if (counter == text.len) { + // Found all the text + break; + } else { + counter = 0; + } + } + + if (counter != text.len) { + panic(@errorReturnTrace(), "Didn't find the printed text in pages\n", .{}); + } + + // Clear the text + writeString(clear_text) catch |e| panic(@errorReturnTrace(), "Failed to print string to tty: {}\n", .{e}); + + log.logInfo("TTY: Tested printing\n", .{}); +} + +/// +/// Run all the runtime tests. +/// +fn runtimeTests() void { + rt_initialisedGlobals(); + rt_printString(); +} diff --git a/src/kernel/vga.zig b/src/kernel/arch/x86/vga.zig similarity index 94% rename from src/kernel/vga.zig rename to src/kernel/arch/x86/vga.zig index 4c31ae83..002d7565 100644 --- a/src/kernel/vga.zig +++ b/src/kernel/arch/x86/vga.zig @@ -3,10 +3,9 @@ const builtin = @import("builtin"); const is_test = builtin.is_test; const expectEqual = std.testing.expectEqual; const build_options = @import("build_options"); -const mock_path = build_options.mock_path; -const arch = @import("arch.zig").internals; -const log = if (is_test) @import(mock_path ++ "log_mock.zig") else @import("log.zig"); -const panic = @import("panic.zig").panic; +const arch = if (is_test) @import(build_options.arch_mock_path ++ "arch_mock.zig") else @import("arch.zig"); +const log = if (is_test) @import(build_options.arch_mock_path ++ "log_mock.zig") else @import("../../log.zig"); +const panic = @import("../../panic.zig").panic; /// The port address for the VGA register selection. const PORT_ADDRESS: u16 = 0x03D4; @@ -145,7 +144,6 @@ inline fn sendData(data: u8) void { inline fn getData() u8 { return arch.inb(PORT_DATA); } - /// /// Set the VGA register port to write to and sending data to that VGA register port. /// @@ -287,6 +285,7 @@ pub fn setCursorShape(shape: CursorShape) void { /// pub fn init() void { log.logInfo("Init vga\n", .{}); + defer log.logInfo("Done vga\n", .{}); // Set the maximum scan line to 0x0F sendPortData(REG_MAXIMUM_SCAN_LINE, CURSOR_SCANLINE_END); @@ -294,9 +293,10 @@ pub fn init() void { // Set by default the underline cursor setCursorShape(CursorShape.UNDERLINE); - log.logInfo("Done\n", .{}); - - if (build_options.rt_test) runtimeTests(); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } } test "entryColour" { @@ -534,7 +534,7 @@ fn rt_correctMaxScanLine() void { const max_scan_line = getPortData(REG_MAXIMUM_SCAN_LINE); if (max_scan_line != CURSOR_SCANLINE_END) { - panic(@errorReturnTrace(), "Max scan line not {}, found {}\n", .{ CURSOR_SCANLINE_END, max_scan_line }); + panic(@errorReturnTrace(), "FAILURE: Max scan line not {}, found {}\n", .{ CURSOR_SCANLINE_END, max_scan_line }); } log.logInfo("VGA: Tested max scan line\n", .{}); @@ -546,14 +546,14 @@ fn rt_correctMaxScanLine() void { fn rt_correctCursorShape() void { // Check the global variables are correct if (cursor_scanline_start != CURSOR_SCANLINE_MIDDLE or cursor_scanline_end != CURSOR_SCANLINE_END) { - panic(@errorReturnTrace(), "Global cursor scanline incorrect. Start: {}, end: {}\n", .{ cursor_scanline_start, cursor_scanline_end }); + panic(@errorReturnTrace(), "FAILURE: Global cursor scanline incorrect. Start: {}, end: {}\n", .{ cursor_scanline_start, cursor_scanline_end }); } const cursor_start = getPortData(REG_CURSOR_START); const cursor_end = getPortData(REG_CURSOR_END); if (cursor_start != CURSOR_SCANLINE_MIDDLE or cursor_end != CURSOR_SCANLINE_END) { - panic(@errorReturnTrace(), "Cursor scanline are incorrect. Start: {}, end: {}\n", .{ cursor_start, cursor_end }); + panic(@errorReturnTrace(), "FAILURE: Cursor scanline are incorrect. Start: {}, end: {}\n", .{ cursor_start, cursor_end }); } log.logInfo("VGA: Tested cursor shape\n", .{}); @@ -582,7 +582,7 @@ fn rt_setCursorGetCursor() void { const actual_y_loc = @truncate(u8, actual_linear_loc / WIDTH); if (x != actual_x_loc or y != actual_y_loc) { - panic(@errorReturnTrace(), "VGA cursor not the same: a_x: {}, a_y: {}, e_x: {}, e_y: {}\n", .{ x, y, actual_x_loc, actual_y_loc }); + panic(@errorReturnTrace(), "FAILURE: VGA cursor not the same: a_x: {}, a_y: {}, e_x: {}, e_y: {}\n", .{ x, y, actual_x_loc, actual_y_loc }); } // Restore the previous x and y diff --git a/src/kernel/bitmap.zig b/src/kernel/bitmap.zig new file mode 100644 index 00000000..b5c4ab72 --- /dev/null +++ b/src/kernel/bitmap.zig @@ -0,0 +1,705 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const testing = std.testing; +const Allocator = std.mem.Allocator; + +/// +/// A comptime bitmap that uses a specific type to store the entries. No allocators needed. +/// +/// Arguments: +/// IN BitmapType: type - The integer type to use to store entries. +/// +/// Return: type. +/// The bitmap type created. +/// +pub fn ComptimeBitmap(comptime BitmapType: type) type { + return struct { + const Self = @This(); + + /// The number of entries that one bitmap type can hold. Evaluates to the number of bits the type has + pub const NUM_ENTRIES: usize = std.meta.bitCount(BitmapType); + + /// The value that a full bitmap will have + pub const BITMAP_FULL = std.math.maxInt(BitmapType); + + /// The type of an index into a bitmap entry. The smallest integer needed to represent all bit positions in the bitmap entry type + pub const IndexType = std.meta.IntType(false, std.math.log2(std.math.ceilPowerOfTwo(u16, std.meta.bitCount(BitmapType)) catch unreachable)); + + bitmap: BitmapType, + num_free_entries: BitmapType, + + /// + /// Create an instance of this bitmap type. + /// + /// Return: Self. + /// The bitmap instance. + /// + pub fn init() Self { + return .{ + .bitmap = 0, + .num_free_entries = NUM_ENTRIES, + }; + } + + /// + /// Set an entry within a bitmap as occupied. + /// + /// Arguments: + /// IN/OUT self: *Self - The bitmap to modify. + /// IN idx: IndexType - The index within the bitmap to set. + /// + pub fn setEntry(self: *Self, idx: IndexType) void { + if (!self.isSet(idx)) { + self.bitmap |= self.indexToBit(idx); + self.num_free_entries -= 1; + } + } + + /// + /// Set an entry within a bitmap as unoccupied. + /// + /// Arguments: + /// IN/OUT self: *Self - The bitmap to modify. + /// IN idx: IndexType - The index within the bitmap to clear. + /// + pub fn clearEntry(self: *Self, idx: IndexType) void { + if (self.isSet(idx)) { + self.bitmap &= ~self.indexToBit(idx); + self.num_free_entries += 1; + } + } + + /// + /// Convert a global bitmap index into the bit corresponding to an entry within a single BitmapType. + /// + /// Arguments: + /// IN self: *const Self - The bitmap to use. + /// IN idx: IndexType - The index into all of the bitmaps entries. + /// + /// Return: BitmapType. + /// The bit corresponding to that index but within a single BitmapType. + /// + fn indexToBit(self: *const Self, idx: IndexType) BitmapType { + return @as(BitmapType, 1) << idx; + } + + /// + /// Find a number of contiguous free entries and set them. + /// + /// Arguments: + /// IN/OUT self: *Self - The bitmap to modify. + /// IN num: usize - The number of entries to set. + /// + /// Return: ?IndexType + /// The first entry set or null if there weren't enough contiguous entries. + /// + pub fn setContiguous(self: *Self, num: usize) ?IndexType { + if (num > self.num_free_entries) { + return null; + } + + var count: usize = 0; + var start: ?IndexType = null; + + var bit: IndexType = 0; + while (true) { + const entry = bit; + if (entry >= NUM_ENTRIES) { + return null; + } + if ((self.bitmap & @as(BitmapType, 1) << bit) != 0) { + // This is a one so clear the progress + count = 0; + start = null; + } else { + // It's a zero so increment the count + count += 1; + if (start == null) { + // Start of the contiguous zeroes + start = entry; + } + if (count == num) { + // Reached the desired number + break; + } + } + // Avoiding overflow by checking if bit is less than the max - 1 + if (bit < NUM_ENTRIES - 1) { + bit += 1; + } else { + break; + } + } + + if (count == num) { + if (start) |start_entry| { + var i: IndexType = 0; + while (i < num) : (i += 1) { + self.setEntry(start_entry + i); + } + return start_entry; + } + } + return null; + } + + /// + /// Set the first free entry within the bitmaps as occupied. + /// + /// Return: ?IndexType. + /// The index within all bitmaps that was set or null if there wasn't one free. + /// 0 .. NUM_ENTRIES - 1 if in the first bitmap, NUM_ENTRIES .. NUM_ENTRIES * 2 - 1 if in the second etc. + /// + pub fn setFirstFree(self: *Self) ?IndexType { + if (self.num_free_entries == 0 or self.bitmap == BITMAP_FULL) { + std.debug.assert(self.num_free_entries == 0 and self.bitmap == BITMAP_FULL); + return null; + } + const bit = @truncate(IndexType, @ctz(BitmapType, ~self.bitmap)); + self.setEntry(bit); + return bit; + } + + /// + /// Check if an entry is set. + /// + /// Arguments: + /// IN self: *const Self - The bitmap to check. + /// IN idx: usize - The entry to check. + /// + /// Return: bool. + /// True if the entry is set, else false. + /// + pub fn isSet(self: *const Self, idx: IndexType) bool { + return (self.bitmap & self.indexToBit(idx)) != 0; + } + }; +} + +/// +/// A bitmap that uses a specific type to store the entries. +/// +/// Arguments: +/// IN BitmapType: type - The integer type to use to store entries. +/// +/// Return: type. +/// The bitmap type created. +/// +pub fn Bitmap(comptime BitmapType: type) type { + return struct { + /// The possible errors thrown by bitmap functions + pub const BitmapError = error{ + /// The address given was outside the region covered by a bitmap + OutOfBounds, + }; + + const Self = @This(); + + /// The number of entries that one bitmap type can hold. Evaluates to the number of bits the type has + pub const ENTRIES_PER_BITMAP: usize = std.meta.bitCount(BitmapType); + + /// The value that a full bitmap will have + pub const BITMAP_FULL = std.math.maxInt(BitmapType); + + /// The type of an index into a bitmap entry. The smallest integer needed to represent all bit positions in the bitmap entry type + pub const IndexType = std.meta.IntType(false, std.math.log2(std.math.ceilPowerOfTwo(u16, std.meta.bitCount(BitmapType)) catch unreachable)); + + num_bitmaps: usize, + num_entries: usize, + bitmaps: []BitmapType, + num_free_entries: usize, + + /// + /// Create an instance of this bitmap type. + /// + /// Arguments: + /// IN num_entries: usize - The number of entries that the bitmap created will have. + /// The number of BitmapType required to store this many entries will be allocated and each will be zeroed. + /// IN allocator: *std.mem.Allocator - The allocator to use when allocating the BitmapTypes required. + /// + /// Return: Self. + /// The bitmap instance. + /// + /// Error: std.mem.Allocator.Error + /// OutOfMemory: There isn't enough memory available to allocate the required number of BitmapType. + /// + pub fn init(num_entries: usize, allocator: *std.mem.Allocator) !Self { + const num = std.mem.alignForward(num_entries, ENTRIES_PER_BITMAP) / ENTRIES_PER_BITMAP; + const self = Self{ + .num_bitmaps = num, + .num_entries = num_entries, + .bitmaps = try allocator.alloc(BitmapType, num), + .num_free_entries = num_entries, + }; + for (self.bitmaps) |*bmp| { + bmp.* = 0; + } + return self; + } + + /// + /// Set an entry within a bitmap as occupied. + /// + /// Arguments: + /// IN/OUT self: *Self - The bitmap to modify. + /// IN idx: usize - The index within the bitmap to set. + /// + /// Error: BitmapError. + /// OutOfBounds: The index given is out of bounds. + /// + pub fn setEntry(self: *Self, idx: usize) BitmapError!void { + if (idx >= self.num_entries) { + return BitmapError.OutOfBounds; + } + if (!try self.isSet(idx)) { + const bit = self.indexToBit(idx); + self.bitmaps[idx / ENTRIES_PER_BITMAP] |= bit; + self.num_free_entries -= 1; + } + } + + /// + /// Set an entry within a bitmap as unoccupied. + /// + /// Arguments: + /// IN/OUT self: *Self - The bitmap to modify. + /// IN idx: usize - The index within the bitmap to clear. + /// + /// Error: BitmapError. + /// OutOfBounds: The index given is out of bounds. + /// + pub fn clearEntry(self: *Self, idx: usize) BitmapError!void { + if (idx >= self.num_entries) { + return BitmapError.OutOfBounds; + } + if (try self.isSet(idx)) { + const bit = self.indexToBit(idx); + self.bitmaps[idx / ENTRIES_PER_BITMAP] &= ~bit; + self.num_free_entries += 1; + } + } + + /// + /// Convert a global bitmap index into the bit corresponding to an entry within a single BitmapType. + /// + /// Arguments: + /// IN self: *const Self - The bitmap to use. + /// IN idx: usize - The index into all of the bitmaps entries. + /// + /// Return: BitmapType. + /// The bit corresponding to that index but within a single BitmapType. + /// + fn indexToBit(self: *const Self, idx: usize) BitmapType { + return @as(BitmapType, 1) << @intCast(IndexType, idx % ENTRIES_PER_BITMAP); + } + + /// + /// Find a number of contiguous free entries and set them. + /// + /// Arguments: + /// IN/OUT self: *Self - The bitmap to modify. + /// IN num: usize - The number of entries to set. + /// + /// Return: ?usize + /// The first entry set or null if there weren't enough contiguous entries. + /// + pub fn setContiguous(self: *Self, num: usize) ?usize { + if (num > self.num_free_entries) { + return null; + } + + var count: usize = 0; + var start: ?usize = null; + for (self.bitmaps) |bmp, i| { + var bit: IndexType = 0; + while (true) { + const entry = bit + i * ENTRIES_PER_BITMAP; + if (entry >= self.num_entries) { + return null; + } + if ((bmp & @as(BitmapType, 1) << bit) != 0) { + // This is a one so clear the progress + count = 0; + start = null; + } else { + // It's a zero so increment the count + count += 1; + if (start == null) { + // Start of the contiguous zeroes + start = entry; + } + if (count == num) { + // Reached the desired number + break; + } + } + // Avoiding overflow by checking if bit is less than the max - 1 + if (bit < ENTRIES_PER_BITMAP - 1) { + bit += 1; + } else { + // Reached the end of the bitmap + break; + } + } + if (count == num) { + break; + } + } + + if (count == num) { + if (start) |start_entry| { + var i: usize = 0; + while (i < num) : (i += 1) { + // Can't fail as the entry was found to be free + self.setEntry(start_entry + i) catch unreachable; + } + return start_entry; + } + } + return null; + } + + /// + /// Set the first free entry within the bitmaps as occupied. + /// + /// Return: ?usize. + /// The index within all bitmaps that was set or null if there wasn't one free. + /// 0 .. ENTRIES_PER_BITMAP - 1 if in the first bitmap, ENTRIES_PER_BITMAP .. ENTRIES_PER_BITMAP * 2 - 1 if in the second etc. + /// + pub fn setFirstFree(self: *Self) ?usize { + if (self.num_free_entries == 0) { + return null; + } + for (self.bitmaps) |*bmp, i| { + if (bmp.* == BITMAP_FULL) { + continue; + } + const bit = @truncate(IndexType, @ctz(BitmapType, ~bmp.*)); + const idx = bit + i * ENTRIES_PER_BITMAP; + // Failing here means that the index is outside of the bitmap, so there are no free entries + self.setEntry(idx) catch return null; + return idx; + } + return null; + } + + /// + /// Check if an entry is set. + /// + /// Arguments: + /// IN self: *const Self - The bitmap to check. + /// IN idx: usize - The entry to check. + /// + /// Return: bool. + /// True if the entry is set, else false. + /// + /// Error: BitmapError. + /// OutOfBounds: The index given is out of bounds. + /// + pub fn isSet(self: *const Self, idx: usize) BitmapError!bool { + if (idx >= self.num_entries) { + return BitmapError.OutOfBounds; + } + return (self.bitmaps[idx / ENTRIES_PER_BITMAP] & self.indexToBit(idx)) != 0; + } + }; +} + +test "Comptime setEntry" { + var bmp = ComptimeBitmap(u32).init(); + testing.expectEqual(@as(u32, 32), bmp.num_free_entries); + + bmp.setEntry(0); + testing.expectEqual(@as(u32, 1), bmp.bitmap); + testing.expectEqual(@as(u32, 31), bmp.num_free_entries); + + bmp.setEntry(1); + testing.expectEqual(@as(u32, 3), bmp.bitmap); + testing.expectEqual(@as(u32, 30), bmp.num_free_entries); + + // Repeat setting entry 1 to make sure state doesn't change + bmp.setEntry(1); + testing.expectEqual(@as(u32, 3), bmp.bitmap); + testing.expectEqual(@as(u32, 30), bmp.num_free_entries); +} + +test "Comptime clearEntry" { + var bmp = ComptimeBitmap(u32).init(); + testing.expectEqual(@as(u32, 32), bmp.num_free_entries); + + bmp.setEntry(0); + testing.expectEqual(@as(u32, 31), bmp.num_free_entries); + bmp.setEntry(1); + testing.expectEqual(@as(u32, 30), bmp.num_free_entries); + testing.expectEqual(@as(u32, 3), bmp.bitmap); + bmp.clearEntry(0); + testing.expectEqual(@as(u32, 31), bmp.num_free_entries); + testing.expectEqual(@as(u32, 2), bmp.bitmap); + + // Repeat to make sure state doesn't change + bmp.clearEntry(0); + testing.expectEqual(@as(u32, 31), bmp.num_free_entries); + testing.expectEqual(@as(u32, 2), bmp.bitmap); + + // Try clearing an unset entry to make sure state doesn't change + bmp.clearEntry(2); + testing.expectEqual(@as(u32, 31), bmp.num_free_entries); + testing.expectEqual(@as(u32, 2), bmp.bitmap); +} + +test "Comptime setFirstFree" { + var bmp = ComptimeBitmap(u32).init(); + + // Allocate the first entry + testing.expectEqual(bmp.setFirstFree() orelse unreachable, 0); + testing.expectEqual(bmp.bitmap, 1); + + // Allocate the second entry + testing.expectEqual(bmp.setFirstFree() orelse unreachable, 1); + testing.expectEqual(bmp.bitmap, 3); + + // Make all but the MSB occupied and try to allocate it + bmp.bitmap = ComptimeBitmap(u32).BITMAP_FULL & ~@as(u32, 1 << (ComptimeBitmap(u32).NUM_ENTRIES - 1)); + bmp.num_free_entries = 1; + testing.expectEqual(bmp.setFirstFree() orelse unreachable, ComptimeBitmap(u32).NUM_ENTRIES - 1); + testing.expectEqual(bmp.bitmap, ComptimeBitmap(u32).BITMAP_FULL); + + // We should no longer be able to allocate any entries + testing.expectEqual(bmp.setFirstFree(), null); + testing.expectEqual(bmp.bitmap, ComptimeBitmap(u32).BITMAP_FULL); +} + +test "Comptime isSet" { + var bmp = ComptimeBitmap(u32).init(); + + bmp.bitmap = 1; + // Make sure that only the set entry is considered set + testing.expect(bmp.isSet(0)); + var i: usize = 1; + while (i < ComptimeBitmap(u32).NUM_ENTRIES) : (i += 1) { + testing.expect(!bmp.isSet(@truncate(ComptimeBitmap(u32).IndexType, i))); + } + + bmp.bitmap = 3; + testing.expect(bmp.isSet(0)); + testing.expect(bmp.isSet(1)); + i = 2; + while (i < ComptimeBitmap(u32).NUM_ENTRIES) : (i += 1) { + testing.expect(!bmp.isSet(@truncate(ComptimeBitmap(u32).IndexType, i))); + } + + bmp.bitmap = 11; + testing.expect(bmp.isSet(0)); + testing.expect(bmp.isSet(1)); + testing.expect(!bmp.isSet(2)); + testing.expect(bmp.isSet(3)); + i = 4; + while (i < ComptimeBitmap(u32).NUM_ENTRIES) : (i += 1) { + testing.expect(!bmp.isSet(@truncate(ComptimeBitmap(u32).IndexType, i))); + } +} + +test "Comptime indexToBit" { + var bmp = ComptimeBitmap(u8).init(); + testing.expectEqual(bmp.indexToBit(0), 1); + testing.expectEqual(bmp.indexToBit(1), 2); + testing.expectEqual(bmp.indexToBit(2), 4); + testing.expectEqual(bmp.indexToBit(3), 8); + testing.expectEqual(bmp.indexToBit(4), 16); + testing.expectEqual(bmp.indexToBit(5), 32); + testing.expectEqual(bmp.indexToBit(6), 64); + testing.expectEqual(bmp.indexToBit(7), 128); +} + +test "Comptime setContiguous" { + var bmp = ComptimeBitmap(u15).init(); + // Test trying to set more entries than the bitmap has + testing.expectEqual(bmp.setContiguous(ComptimeBitmap(u15).NUM_ENTRIES + 1), null); + // All entries should still be free + testing.expectEqual(bmp.num_free_entries, ComptimeBitmap(u15).NUM_ENTRIES); + testing.expectEqual(bmp.setContiguous(3) orelse unreachable, 0); + testing.expectEqual(bmp.setContiguous(4) orelse unreachable, 3); + // 0b0000.0000.0111.1111 + bmp.bitmap |= 0x200; + // 0b0000.0010.0111.1111 + testing.expectEqual(bmp.setContiguous(3) orelse unreachable, 10); + // 0b0001.1110.0111.1111 + testing.expectEqual(bmp.setContiguous(5), null); + testing.expectEqual(bmp.setContiguous(2), 7); + // 0b001.1111.1111.1111 + // Test trying to set beyond the end of the bitmaps + testing.expectEqual(bmp.setContiguous(3), null); + testing.expectEqual(bmp.setContiguous(2), 13); +} + +test "setEntry" { + var bmp = try Bitmap(u32).init(31, std.heap.page_allocator); + testing.expectEqual(@as(u32, 31), bmp.num_free_entries); + + try bmp.setEntry(0); + testing.expectEqual(@as(u32, 1), bmp.bitmaps[0]); + testing.expectEqual(@as(u32, 30), bmp.num_free_entries); + + try bmp.setEntry(1); + testing.expectEqual(@as(u32, 3), bmp.bitmaps[0]); + testing.expectEqual(@as(u32, 29), bmp.num_free_entries); + + // Repeat setting entry 1 to make sure state doesn't change + try bmp.setEntry(1); + testing.expectEqual(@as(u32, 3), bmp.bitmaps[0]); + testing.expectEqual(@as(u32, 29), bmp.num_free_entries); + + testing.expectError(Bitmap(u32).BitmapError.OutOfBounds, bmp.setEntry(31)); + testing.expectEqual(@as(u32, 29), bmp.num_free_entries); +} + +test "clearEntry" { + var bmp = try Bitmap(u32).init(32, std.heap.page_allocator); + testing.expectEqual(@as(u32, 32), bmp.num_free_entries); + + try bmp.setEntry(0); + testing.expectEqual(@as(u32, 31), bmp.num_free_entries); + try bmp.setEntry(1); + testing.expectEqual(@as(u32, 30), bmp.num_free_entries); + testing.expectEqual(@as(u32, 3), bmp.bitmaps[0]); + try bmp.clearEntry(0); + testing.expectEqual(@as(u32, 31), bmp.num_free_entries); + testing.expectEqual(@as(u32, 2), bmp.bitmaps[0]); + + // Repeat to make sure state doesn't change + try bmp.clearEntry(0); + testing.expectEqual(@as(u32, 31), bmp.num_free_entries); + testing.expectEqual(@as(u32, 2), bmp.bitmaps[0]); + + // Try clearing an unset entry to make sure state doesn't change + try bmp.clearEntry(2); + testing.expectEqual(@as(u32, 31), bmp.num_free_entries); + testing.expectEqual(@as(u32, 2), bmp.bitmaps[0]); + + testing.expectError(Bitmap(u32).BitmapError.OutOfBounds, bmp.clearEntry(32)); +} + +test "setFirstFree multiple bitmaps" { + var bmp = try Bitmap(u8).init(9, std.heap.page_allocator); + + // Allocate the first entry + testing.expectEqual(bmp.setFirstFree() orelse unreachable, 0); + testing.expectEqual(bmp.bitmaps[0], 1); + + // Allocate the second entry + testing.expectEqual(bmp.setFirstFree() orelse unreachable, 1); + testing.expectEqual(bmp.bitmaps[0], 3); + + // Allocate the entirety of the first bitmap + var entry: u32 = 2; + var expected: u8 = 7; + while (entry < Bitmap(u8).ENTRIES_PER_BITMAP) { + testing.expectEqual(bmp.setFirstFree() orelse unreachable, entry); + testing.expectEqual(bmp.bitmaps[0], expected); + if (entry + 1 < Bitmap(u8).ENTRIES_PER_BITMAP) { + entry += 1; + expected = expected * 2 + 1; + } else { + break; + } + } + + // Try allocating an entry in the next bitmap + testing.expectEqual(bmp.setFirstFree() orelse unreachable, Bitmap(u8).ENTRIES_PER_BITMAP); + testing.expectEqual(bmp.bitmaps[0], Bitmap(u8).BITMAP_FULL); + testing.expectEqual(bmp.bitmaps[1], 1); + + // We should no longer be able to allocate any entries + testing.expectEqual(bmp.setFirstFree(), null); + testing.expectEqual(bmp.bitmaps[0], Bitmap(u8).BITMAP_FULL); + testing.expectEqual(bmp.bitmaps[1], 1); +} + +test "setFirstFree" { + var bmp = try Bitmap(u32).init(32, std.heap.page_allocator); + + // Allocate the first entry + testing.expectEqual(bmp.setFirstFree() orelse unreachable, 0); + testing.expectEqual(bmp.bitmaps[0], 1); + + // Allocate the second entry + testing.expectEqual(bmp.setFirstFree() orelse unreachable, 1); + testing.expectEqual(bmp.bitmaps[0], 3); + + // Make all but the MSB occupied and try to allocate it + bmp.bitmaps[0] = Bitmap(u32).BITMAP_FULL & ~@as(u32, 1 << (Bitmap(u32).ENTRIES_PER_BITMAP - 1)); + testing.expectEqual(bmp.setFirstFree() orelse unreachable, Bitmap(u32).ENTRIES_PER_BITMAP - 1); + testing.expectEqual(bmp.bitmaps[0], Bitmap(u32).BITMAP_FULL); + + // We should no longer be able to allocate any entries + testing.expectEqual(bmp.setFirstFree(), null); + testing.expectEqual(bmp.bitmaps[0], Bitmap(u32).BITMAP_FULL); +} + +test "isSet" { + var bmp = try Bitmap(u32).init(32, std.heap.page_allocator); + + bmp.bitmaps[0] = 1; + // Make sure that only the set entry is considered set + testing.expect(try bmp.isSet(0)); + var i: u32 = 1; + while (i < bmp.num_entries) : (i += 1) { + testing.expect(!try bmp.isSet(i)); + } + + bmp.bitmaps[0] = 3; + testing.expect(try bmp.isSet(0)); + testing.expect(try bmp.isSet(1)); + i = 2; + while (i < bmp.num_entries) : (i += 1) { + testing.expect(!try bmp.isSet(i)); + } + + bmp.bitmaps[0] = 11; + testing.expect(try bmp.isSet(0)); + testing.expect(try bmp.isSet(1)); + testing.expect(!try bmp.isSet(2)); + testing.expect(try bmp.isSet(3)); + i = 4; + while (i < bmp.num_entries) : (i += 1) { + testing.expect(!try bmp.isSet(i)); + } + + testing.expectError(Bitmap(u32).BitmapError.OutOfBounds, bmp.isSet(33)); +} + +test "indexToBit" { + var bmp = try Bitmap(u8).init(10, std.heap.page_allocator); + testing.expectEqual(bmp.indexToBit(0), 1); + testing.expectEqual(bmp.indexToBit(1), 2); + testing.expectEqual(bmp.indexToBit(2), 4); + testing.expectEqual(bmp.indexToBit(3), 8); + testing.expectEqual(bmp.indexToBit(4), 16); + testing.expectEqual(bmp.indexToBit(5), 32); + testing.expectEqual(bmp.indexToBit(6), 64); + testing.expectEqual(bmp.indexToBit(7), 128); + testing.expectEqual(bmp.indexToBit(8), 1); + testing.expectEqual(bmp.indexToBit(9), 2); +} + +test "setContiguous" { + var bmp = try Bitmap(u4).init(15, std.heap.page_allocator); + // Test trying to set more entries than the bitmap has + testing.expectEqual(bmp.setContiguous(bmp.num_entries + 1), null); + // All entries should still be free + testing.expectEqual(bmp.num_free_entries, bmp.num_entries); + + testing.expectEqual(bmp.setContiguous(3) orelse unreachable, 0); + testing.expectEqual(bmp.setContiguous(4) orelse unreachable, 3); + // 0b0000.0000.0111.1111 + bmp.bitmaps[2] |= 2; + // 0b0000.0010.0111.1111 + testing.expectEqual(bmp.setContiguous(3) orelse unreachable, 10); + // 0b0001.1110.0111.1111 + testing.expectEqual(bmp.setContiguous(5), null); + testing.expectEqual(bmp.setContiguous(2), 7); + // 0b001.1111.1111.1111 + // Test trying to set beyond the end of the bitmaps + testing.expectEqual(bmp.setContiguous(3), null); + testing.expectEqual(bmp.setContiguous(2), 13); +} diff --git a/src/kernel/heap.zig b/src/kernel/heap.zig new file mode 100644 index 00000000..ce402037 --- /dev/null +++ b/src/kernel/heap.zig @@ -0,0 +1,571 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const testing = std.testing; +const builtin = @import("builtin"); +const is_test = builtin.is_test; +const build_options = @import("build_options"); +const mock_path = build_options.mock_path; +const vmm = if (is_test) @import(mock_path ++ "vmm_mock.zig") else @import("vmm.zig"); +const log = if (is_test) @import(mock_path ++ "log_mock.zig") else @import("log.zig"); +const panic = @import("panic.zig").panic; + +const FreeListAllocator = struct { + const Error = error{TooSmall}; + const Header = struct { + size: usize, + next_free: ?*Header, + + const Self = @Self(); + + /// + /// Intitialise the header for a free allocation node + /// + /// Arguments: + /// IN size: usize - The node's size, not including the size of the header itself + /// IN next_free: ?*Header - A pointer to the next free node + /// + /// Return: Header + /// The header constructed + fn init(size: usize, next_free: ?*Header) Header { + return .{ + .size = size, + .next_free = next_free, + }; + } + }; + + first_free: ?*Header, + allocator: Allocator, + + /// + /// Initialise an empty and free FreeListAllocator + /// + /// Arguments: + /// IN start: usize - The starting address for all alloctions + /// IN size: usize - The size of the region of memory to allocate within. Must be greater than @sizeOf(Header) + /// + /// Return: FreeListAllocator + /// The FreeListAllocator constructed + /// + /// Error: Error + /// Error.TooSmall - If size <= @sizeOf(Header) + /// + pub fn init(start: usize, size: usize) Error!FreeListAllocator { + if (size <= @sizeOf(Header)) return Error.TooSmall; + return FreeListAllocator{ + .first_free = insertFreeHeader(start, size - @sizeOf(Header), null), + .allocator = .{ + .allocFn = alloc, + .resizeFn = resize, + }, + }; + } + + /// + /// Create a free header at a specific location + /// + /// Arguments: + /// IN at: usize - The address to create it at + /// IN size: usize - The node's size, excluding the size of the header itself + /// IN next_free: ?*Header - The next free header in the allocator, or null if there isn't one + /// + /// Return *Header + /// The pointer to the header created + /// + fn insertFreeHeader(at: usize, size: usize, next_free: ?*Header) *Header { + var node = @intToPtr(*Header, at); + node.* = Header.init(size, next_free); + return node; + } + + /// + /// Update the free header pointers that should point to the provided header + /// + /// Arguments: + /// IN self: *FreeListAllocator - The FreeListAllocator to modify + /// IN previos: ?*Header - The previous free node or null if there wasn't one. If null, self.first_free will be set to header, else previous.next_free will be set to header + /// IN header: ?*Header - The header being pointed to. This will be the new value of self.first_free or previous.next_free + /// + fn registerFreeHeader(self: *FreeListAllocator, previous: ?*Header, header: ?*Header) void { + if (previous) |p| { + p.next_free = header; + } else { + self.first_free = header; + } + } + + /// + /// Free an allocation + /// + /// Arguments: + /// IN self: *FreeListAllocator - The allocator being freed within + /// IN mem: []u8 - The memory to free + /// + fn free(self: *FreeListAllocator, mem: []u8) void { + const size = std.math.max(mem.len, @sizeOf(Header)); + const addr = @ptrToInt(mem.ptr); + var header = insertFreeHeader(addr, size - @sizeOf(Header), null); + if (self.first_free) |first| { + var prev: ?*Header = null; + // Find the previous free node + if (@ptrToInt(first) < addr) { + prev = first; + while (prev.?.next_free) |next| { + if (@ptrToInt(next) > addr) break; + prev = next; + } + } + // Make the freed header point to the next one, which is the one after the previous or the first if there was no previous + header.next_free = if (prev) |p| p.next_free else first; + + self.registerFreeHeader(prev, header); + + // Join with the next one until the next isn't a neighbour + if (header.next_free) |next| { + if (@ptrToInt(next) == @ptrToInt(header) + header.size + @sizeOf(Header)) { + header.size += next.size + @sizeOf(Header); + header.next_free = next.next_free; + } + } + + // Try joining with the previous one + if (prev) |p| { + p.size += header.size + @sizeOf(Header); + p.next_free = header.next_free; + } + } else { + self.first_free = header; + } + } + + /// + /// Attempt to resize an allocation. This should only be called via the Allocator interface. + /// + /// When the new size requested is 0, a free happens. See the free function for details. + /// + /// When the new size is greater than the old buffer's size, we attempt to steal some space from the neighbouring node. + /// This can only be done if the neighbouring node is free and the remaining space after taking what is needed to resize is enough to create a new Header. This is because we don't want to leave any dangling memory that isn't tracked by a header. + /// + /// | <----- new_size -----> + /// |---------|--------\----------------| + /// | | \ | + /// | old_mem | header \ header's space | + /// | | \ | + /// |---------|--------\----------------| + /// + /// After expanding to new_size, it will look like + /// |-----------------------|--------\--| + /// | | \ | + /// | old_mem | header \ | + /// | | \ | + /// |-----------------------|--------\--| + /// The free node before old_mem needs to then point to the new header rather than the old one and the new header needs to point to the free node after the old one. If there was no previous free node then the new one becomes the first free node. + /// + /// When the new size is smaller than the old_buffer's size, we attempt to shrink it and create a new header to the right. + /// This can only be done if the space left by the shrinking is enough to create a new header, since we don't want to leave any dangling untracked memory. + /// | <--- new_size ---> + /// |-----------------------------------| + /// | | + /// | old_mem | + /// | | + /// |-----------------------------------| + /// + /// After shrinking to new_size, it will look like + /// | <--- new_size ---> + /// |-------------------|--------\-- ---| + /// | | \ | + /// | old_mem | header \ | + /// | | \ | + /// |-------------------|--------\------| + /// We then attempt to join with neighbouring free nodes. + /// The node before old_mem needs to then point to the new header and the new header needs to point to the next free node. + /// + /// Arguments: + /// IN allocator: *std.Allocator - The allocator to resize within. + /// IN old_mem: []u8 - The buffer to resize. + /// IN new_size: usize - What to resize to. + /// IN size_alignment: u29 - The alignment that the size should have. + /// + /// Return: usize + /// The new size of the buffer, which will be new_size if the operation was successful. + /// + /// Error: std.Allocator.Error + /// std.Allocator.Error.OutOfMemory - If there wasn't enough free memory to expand into + /// + fn resize(allocator: *Allocator, old_mem: []u8, new_size: usize, size_alignment: u29) Allocator.Error!usize { + var self = @fieldParentPtr(FreeListAllocator, "allocator", allocator); + if (new_size == 0) { + self.free(old_mem); + return 0; + } + if (new_size == old_mem.len) return new_size; + + const end = @ptrToInt(old_mem.ptr) + old_mem.len; + const real_size = if (size_alignment > 1) std.mem.alignForward(new_size, size_alignment) else new_size; + + // Try to find the buffer's neighbour (if it's free) and the previous free node + // We'll be stealing some of the free neighbour's space when expanding or joining up with it when shrinking + var free_node = self.first_free; + var next: ?*Header = null; + var prev: ?*Header = null; + while (free_node) |f| { + if (@ptrToInt(f) == end) { + // This free node is right next to the node being freed so is its neighbour + next = f; + break; + } else if (@ptrToInt(f) > end) { + // We've found a node past the node being freed so end early + break; + } + prev = f; + free_node = f.next_free; + } + + // If we're expanding the buffer + if (real_size > old_mem.len) { + if (next) |n| { + // If the free neighbour isn't big enough then fail + if (old_mem.len + n.size + @sizeOf(Header) < real_size) return Allocator.Error.OutOfMemory; + + const size_diff = real_size - old_mem.len; + const consumes_whole_neighbour = size_diff == n.size + @sizeOf(Header); + // If the space left over in the free neighbour from the resize isn't enough to fit a new node, then fail + if (!consumes_whole_neighbour and n.size + @sizeOf(Header) - size_diff < @sizeOf(Header)) return Allocator.Error.OutOfMemory; + var new_next: ?*Header = n.next_free; + // We don't do any splitting when consuming the whole neighbour + if (!consumes_whole_neighbour) { + // Create the new header. It starts at the end of the buffer plus the stolen space + // The size will be the previous size minus what we stole + new_next = insertFreeHeader(end + size_diff, n.size - size_diff, n.next_free); + } + self.registerFreeHeader(prev, new_next); + return real_size; + } + // The neighbour isn't free so we can't expand into it + return Allocator.Error.OutOfMemory; + } else { + // Shrinking + const size_diff = old_mem.len - real_size; + // If shrinking would leave less space than required for a new header, + // or if shrinking would make the buffer too small, don't shrink + if (size_diff < @sizeOf(Header) or real_size < @sizeOf(Header)) return Allocator.Error.OutOfMemory; + + // Create a new header for the space gained from shrinking + var new_next = insertFreeHeader(@ptrToInt(old_mem.ptr) + real_size, size_diff - @sizeOf(Header), if (prev) |p| p.next_free else self.first_free); + self.registerFreeHeader(prev, new_next); + + // Join with the neighbour + if (next) |n| { + new_next.size += n.size + @sizeOf(Header); + new_next.next_free = n.next_free; + } + + return real_size; + } + } + + /// + /// Allocate a portion of memory. This should only be called via the Allocator interface. + /// + /// This will find the first free node within the heap that can fit the size requested. If the size of the node is larger than the requested size but any space left over isn't enough to create a new Header, the next node is tried. If the node would require some padding to reach the desired alignment and that padding wouldn't fit a new Header, the next node is tried (however this node is kept as a backup in case no future nodes can fit the request). + /// + /// |--------------\---------------------| + /// | \ | + /// | free header \ free space | + /// | \ | + /// |--------------\---------------------| + /// + /// When the alignment padding is large enough for a new Header, the node found is split on the left, like so + /// <---- padding ----> + /// |------------\-----|-------------\---| + /// | \ | \ | + /// | new header \ | free header \ | + /// | \ | \ | + /// |------------\-----|-------------\---| + /// The previous free node should then point to the left split. The left split should point to the free node after the one that was found + /// + /// When the space left over in the free node is more than required for the allocation, it is split on the right + /// |--------------\-------|------------\--| + /// | \ | \ | + /// | free header \ space | new header \ | + /// | \ | \ | + /// |--------------\-------|------------\--| + /// The previous free node should then point to the new node on the left and the new node should point to the next free node + /// + /// Splitting on the left and right can both happen in one allocation + /// + /// Arguments: + /// IN allocator: *std.Allocator - The allocator to use + /// IN size: usize - The amount of memory requested + /// IN alignment: u29 - The alignment that the address of the allocated memory should have + /// IN size_alignment: u29 - The alignment that the length of the allocated memory should have + /// + /// Return: []u8 + /// The allocated memory + /// + /// Error: std.Allocator.Error + /// std.Allocator.Error.OutOfMemory - There wasn't enough memory left to fulfill the request + /// + pub fn alloc(allocator: *Allocator, size: usize, alignment: u29, size_alignment: u29) Allocator.Error![]u8 { + var self = @fieldParentPtr(FreeListAllocator, "allocator", allocator); + if (self.first_free == null) return Allocator.Error.OutOfMemory; + + // Get the real size being allocated, which is the aligned size or the size of a header (whichever is largest) + // The size must be at least the size of a header so that it can be freed properly + const real_size = std.math.max(if (size_alignment > 1) std.mem.alignForward(size, size_alignment) else size, @sizeOf(Header)); + + var free_header = self.first_free; + var prev: ?*Header = null; + var backup: ?*Header = null; + var backup_prev: ?*Header = null; + + // Search for the first node that can fit the request + const alloc_to = find: while (free_header) |h| : ({ + prev = h; + free_header = h.next_free; + }) { + if (h.size + @sizeOf(Header) < real_size) { + continue; + } + // The address at which to allocate. This will clobber the header. + const addr = @ptrToInt(h); + var alignment_padding: usize = 0; + + if (alignment > 1 and !std.mem.isAligned(addr, alignment)) { + alignment_padding = alignment - (addr % alignment); + // If the size can't fit the alignment padding then try the next one + if (h.size + @sizeOf(Header) < real_size + alignment_padding) continue; + // If a new node couldn't be created from the space left by alignment padding then try the next one + // This check is necessary as otherwise we'd have wasted space that could never be allocated + // We do however set the backup variable to this node so that in the unfortunate case that no other nodes can take the allocation, we allocate it here and sacrifice the wasted space + if (alignment_padding < @sizeOf(Header)) { + backup = h; + backup_prev = prev; + continue; + } + } + + // If we wouldn't be able to create a node with any unused space, try the next one + // This check is necessary as otherwise we'd have wasted space that could never be allocated + // Much like with the alignment padding, we set this node as a backup + if (@sizeOf(Header) + h.size - alignment_padding - real_size < @sizeOf(Header)) { + backup = h; + backup_prev = prev; + continue; + } + + break :find h; + } else backup; + + if (alloc_to == backup) + prev = backup_prev; + + if (alloc_to) |x| { + var header = x; + var addr = @ptrToInt(header); + // Allocate to this node + var alignment_padding: usize = 0; + if (alignment > 1 and !std.mem.isAligned(addr, alignment)) { + alignment_padding = alignment - (addr % alignment); + } + + // If we were going to use alignment padding and it's big enough to fit a new node, create a node to the left using the unused space + if (alignment_padding >= @sizeOf(Header)) { + // Since the header's address is going to be reused for the smaller one being created, backup the header to its new position + header = insertFreeHeader(addr + alignment_padding, header.size - alignment_padding, header.next_free); + + var left = insertFreeHeader(addr, alignment_padding - @sizeOf(Header), header.next_free); + // The previous should link to the new one instead + self.registerFreeHeader(prev, left); + prev = left; + alignment_padding = 0; + } + + // If there is enough unused space to the right of this node then create a smaller node + if ((@sizeOf(Header) + header.size) - alignment_padding - real_size > @sizeOf(Header)) { + header.next_free = insertFreeHeader(@ptrToInt(header) + real_size + alignment_padding, header.size + @sizeOf(Header) - real_size - alignment_padding - @sizeOf(Header), header.next_free); + } + self.registerFreeHeader(prev, header.next_free); + + return @intToPtr([*]u8, @ptrToInt(header))[0..std.mem.alignForward(size, if (size_alignment > 1) size_alignment else 1)]; + } + + return Allocator.Error.OutOfMemory; + } + + test "init" { + const size = 1024; + var region = try testing.allocator.alloc(u8, size); + defer testing.allocator.free(region); + var free_list = &(try FreeListAllocator.init(@ptrToInt(region.ptr), size)); + + var header = @intToPtr(*FreeListAllocator.Header, @ptrToInt(region.ptr)); + testing.expectEqual(header, free_list.first_free.?); + testing.expectEqual(header.next_free, null); + testing.expectEqual(header.size, size - @sizeOf(Header)); + + testing.expectError(Error.TooSmall, FreeListAllocator.init(0, @sizeOf(Header) - 1)); + } + + test "alloc" { + const size = 1024; + var region = try testing.allocator.alloc(u8, size); + defer testing.allocator.free(region); + const start = @ptrToInt(region.ptr); + var free_list = &(try FreeListAllocator.init(start, size)); + var allocator = &free_list.allocator; + + const alloc0 = try alloc(allocator, 64, 0, 0); + const alloc0_addr = @ptrToInt(alloc0.ptr); + // Should be at the start of the heap + testing.expectEqual(alloc0_addr, start); + // The allocation should have produced a node on the right of the allocation + var header = @intToPtr(*Header, start + 64); + testing.expectEqual(header.size, size - 64 - @sizeOf(Header)); + testing.expectEqual(header.next_free, null); + testing.expectEqual(free_list.first_free, header); + + // 64 bytes aligned to 4 bytes + const alloc1 = try alloc(allocator, 64, 4, 0); + const alloc1_addr = @ptrToInt(alloc1.ptr); + const alloc1_end = alloc1_addr + alloc1.len; + // Should be to the right of the first allocation, with some alignment padding in between + const alloc0_end = alloc0_addr + alloc0.len; + testing.expect(alloc0_end <= alloc1_addr); + testing.expectEqual(std.mem.alignForward(alloc0_end, 4), alloc1_addr); + // It should have produced a node on the right + header = @intToPtr(*Header, alloc1_end); + testing.expectEqual(header.size, size - (alloc1_end - start) - @sizeOf(Header)); + testing.expectEqual(header.next_free, null); + testing.expectEqual(free_list.first_free, header); + + const alloc2 = try alloc(allocator, 64, 256, 0); + const alloc2_addr = @ptrToInt(alloc2.ptr); + const alloc2_end = alloc2_addr + alloc2.len; + testing.expect(alloc1_end < alloc2_addr); + // There should be a free node to the right of alloc2 + const second_header = @intToPtr(*Header, alloc2_end); + testing.expectEqual(second_header.size, size - (alloc2_end - start) - @sizeOf(Header)); + testing.expectEqual(second_header.next_free, null); + // There should be a free node in between alloc1 and alloc2 due to the large alignment padding (depends on the allocation by the testing allocator, hence the check) + if (alloc2_addr - alloc1_end >= @sizeOf(Header)) { + header = @intToPtr(*Header, alloc1_end); + testing.expectEqual(free_list.first_free, header); + testing.expectEqual(header.next_free, second_header); + } + + // Try allocating something smaller than @sizeOf(Header). This should scale up to @sizeOf(Header) + var alloc3 = try alloc(allocator, 1, 0, 0); + const alloc3_addr = @ptrToInt(alloc3.ptr); + const alloc3_end = alloc3_addr + @sizeOf(Header); + const header2 = @intToPtr(*Header, alloc3_end); + // The new free node on the right should be the first one free + testing.expectEqual(free_list.first_free, header2); + // And it should point to the free node on the right of alloc2 + testing.expectEqual(header2.next_free, second_header); + + // Attempting to allocate more than the size of the largest free node should fail + const remaining_size = second_header.size + @sizeOf(Header); + testing.expectError(Allocator.Error.OutOfMemory, alloc(&free_list.allocator, remaining_size + 1, 0, 0)); + } + + test "free" { + const size = 1024; + var region = try testing.allocator.alloc(u8, size); + defer testing.allocator.free(region); + const start = @ptrToInt(region.ptr); + var free_list = &(try FreeListAllocator.init(start, size)); + var allocator = &free_list.allocator; + + var alloc0 = try alloc(allocator, 128, 0, 0); + var alloc1 = try alloc(allocator, 256, 0, 0); + var alloc2 = try alloc(allocator, 64, 0, 0); + + // There should be a single free node after alloc2 + const free_node3 = @intToPtr(*Header, @ptrToInt(alloc2.ptr) + alloc2.len); + testing.expectEqual(free_list.first_free, free_node3); + testing.expectEqual(free_node3.size, size - alloc0.len - alloc1.len - alloc2.len - @sizeOf(Header)); + testing.expectEqual(free_node3.next_free, null); + + free_list.free(alloc0); + // There should now be two free nodes. One where alloc0 was and another after alloc2 + const free_node0 = @intToPtr(*Header, start); + testing.expectEqual(free_list.first_free, free_node0); + testing.expectEqual(free_node0.size, alloc0.len - @sizeOf(Header)); + testing.expectEqual(free_node0.next_free, free_node3); + + // Freeing alloc1 should join it with free_node0 + free_list.free(alloc1); + testing.expectEqual(free_list.first_free, free_node0); + testing.expectEqual(free_node0.size, alloc0.len - @sizeOf(Header) + alloc1.len); + testing.expectEqual(free_node0.next_free, free_node3); + + // Freeing alloc2 should then join them all together into one big free node + free_list.free(alloc2); + testing.expectEqual(free_list.first_free, free_node0); + testing.expectEqual(free_node0.size, size - @sizeOf(Header)); + testing.expectEqual(free_node0.next_free, null); + } + + test "resize" { + const size = 1024; + var region = try testing.allocator.alloc(u8, size); + defer testing.allocator.free(region); + const start = @ptrToInt(region.ptr); + var free_list = &(try FreeListAllocator.init(start, size)); + var allocator = &free_list.allocator; + + var alloc0 = try alloc(allocator, 128, 0, 0); + var alloc1 = try alloc(allocator, 256, 0, 0); + + // Expanding alloc0 should fail as alloc1 is right next to it + testing.expectError(Allocator.Error.OutOfMemory, resize(&free_list.allocator, alloc0, 136, 0)); + + // Expanding alloc1 should succeed + testing.expectEqual(try resize(allocator, alloc1, 512, 0), 512); + alloc1 = alloc1.ptr[0..512]; + // And there should be a free node on the right of it + var header = @intToPtr(*Header, @ptrToInt(alloc1.ptr) + 512); + testing.expectEqual(header.size, size - 128 - 512 - @sizeOf(Header)); + testing.expectEqual(header.next_free, null); + testing.expectEqual(free_list.first_free, header); + + // Shrinking alloc1 should produce a big free node on the right + testing.expectEqual(try resize(allocator, alloc1, 128, 0), 128); + alloc1 = alloc1.ptr[0..128]; + header = @intToPtr(*Header, @ptrToInt(alloc1.ptr) + 128); + testing.expectEqual(header.size, size - 128 - 128 - @sizeOf(Header)); + testing.expectEqual(header.next_free, null); + testing.expectEqual(free_list.first_free, header); + + // Shrinking by less space than would allow for a new Header shouldn't work + testing.expectError(Allocator.Error.OutOfMemory, resize(allocator, alloc1, alloc1.len - @sizeOf(Header) / 2, 0)); + // Shrinking to less space than would allow for a new Header shouldn't work + testing.expectError(Allocator.Error.OutOfMemory, resize(allocator, alloc1, @sizeOf(Header) / 2, 0)); + } +}; + +/// +/// Initialise the kernel heap with a chosen allocator +/// +/// Arguments: +/// IN vmm_payload: type - The payload passed around by the VMM. Decided by the architecture +/// IN heap_vmm: *vmm.VirtualMemoryManager - The VMM associated with the kernel +/// IN attributes: vmm.Attributes - The attributes to associate with the memory allocated for the heap +/// IN heap_size: usize - The desired size of the heap, in bytes. Must be greater than @sizeOf(FreeListAllocator.Header) +/// +/// Return: FreeListAllocator +/// The FreeListAllocator created to keep track of the kernel heap +/// +/// Error: FreeListAllocator.Error || Allocator.Error +/// FreeListAllocator.Error.TooSmall - heap_size is too small +/// Allocator.Error.OutOfMemory - heap_vmm's allocator didn't have enough memory available to fulfill the request +/// +pub fn init(comptime vmm_payload: type, heap_vmm: *vmm.VirtualMemoryManager(vmm_payload), attributes: vmm.Attributes, heap_size: usize) (FreeListAllocator.Error || Allocator.Error)!FreeListAllocator { + log.logInfo("Init heap\n", .{}); + defer log.logInfo("Done heap\n", .{}); + var heap_start = (try heap_vmm.alloc(heap_size / vmm.BLOCK_SIZE, attributes)) orelse panic(null, "Not enough contiguous virtual memory blocks to allocate to kernel heap\n", .{}); + // This free call cannot error as it is guaranteed to have been allocated above + errdefer heap_vmm.free(heap_start) catch unreachable; + return try FreeListAllocator.init(heap_start, heap_size); +} diff --git a/src/kernel/kmain.zig b/src/kernel/kmain.zig index 1fc50464..1a9e1dfc 100644 --- a/src/kernel/kmain.zig +++ b/src/kernel/kmain.zig @@ -4,25 +4,40 @@ const is_test = builtin.is_test; const build_options = @import("build_options"); const mock_path = build_options.mock_path; const arch = @import("arch.zig").internals; -const multiboot = @import("multiboot.zig"); const tty = @import("tty.zig"); const vga = @import("vga.zig"); const log = @import("log.zig"); +const pmm = @import("pmm.zig"); const serial = @import("serial.zig"); +const vmm = if (is_test) @import(mock_path ++ "vmm_mock.zig") else @import("vmm.zig"); const mem = if (is_test) @import(mock_path ++ "mem_mock.zig") else @import("mem.zig"); const panic_root = if (is_test) @import(mock_path ++ "panic_mock.zig") else @import("panic.zig"); -const options = @import("build_options"); +const task = if (is_test) @import(mock_path ++ "task_mock.zig") else @import("task.zig"); +const heap = @import("heap.zig"); +const scheduler = @import("scheduler.zig"); comptime { - switch (builtin.arch) { - .i386 => _ = @import("arch/x86/boot.zig"), - else => {}, + if (!is_test) { + switch (builtin.arch) { + .i386 => _ = @import("arch/x86/boot.zig"), + else => {}, + } } } +/// The virtual memory manager associated with the kernel address space +var kernel_vmm: vmm.VirtualMemoryManager(arch.VmmPayload) = undefined; + // This is for unit testing as we need to export KERNEL_ADDR_OFFSET as it is no longer available // from the linker script +// These will need to be kept up to date with the debug logs in the mem init. export var KERNEL_ADDR_OFFSET: u32 = if (builtin.is_test) 0xC0000000 else undefined; +export var KERNEL_STACK_START: u32 = if (builtin.is_test) 0xC014A000 else undefined; +export var KERNEL_STACK_END: u32 = if (builtin.is_test) 0xC014E000 else undefined; +export var KERNEL_VADDR_START: u32 = if (builtin.is_test) 0xC0100000 else undefined; +export var KERNEL_VADDR_END: u32 = if (builtin.is_test) 0xC014E000 else undefined; +export var KERNEL_PHYSADDR_START: u32 = if (builtin.is_test) 0x100000 else undefined; +export var KERNEL_PHYSADDR_END: u32 = if (builtin.is_test) 0x14E000 else undefined; // Just call the panic function, as this need to be in the root source file pub fn panic(msg: []const u8, error_return_trace: ?*builtin.StackTrace) noreturn { @@ -30,30 +45,87 @@ pub fn panic(msg: []const u8, error_return_trace: ?*builtin.StackTrace) noreturn panic_root.panic(error_return_trace, "{}", .{msg}); } -export fn kmain(mb_info: *multiboot.multiboot_info_t, mb_magic: u32) void { - if (mb_magic == multiboot.MULTIBOOT_BOOTLOADER_MAGIC) { - // Booted with compatible bootloader - serial.init(serial.DEFAULT_BAUDRATE, serial.Port.COM1) catch |e| { - panic_root.panic(@errorReturnTrace(), "Failed to initialise serial: {}", .{e}); - }; - if (build_options.rt_test) - log.runtimeTests(); - const mem_profile = mem.init(mb_info); - var buffer = mem_profile.vaddr_end[0..mem_profile.fixed_alloc_size]; - var fixed_allocator = std.heap.FixedBufferAllocator.init(buffer); - - log.logInfo("Init arch " ++ @tagName(builtin.arch) ++ "\n", .{}); - arch.init(mb_info, &mem_profile, &fixed_allocator.allocator); - log.logInfo("Arch init done\n", .{}); - panic_root.init(&mem_profile, &fixed_allocator.allocator) catch |e| { - panic_root.panic(@errorReturnTrace(), "Failed to initialise panic: {}", .{e}); - }; - vga.init(); - tty.init(); - - log.logInfo("Init done\n", .{}); - tty.print("Hello Pluto from kernel :)\n", .{}); - // The panic runtime tests must run last as they never return - if (options.rt_test) panic_root.runtimeTests(); +export fn kmain(boot_payload: arch.BootPayload) void { + const serial_stream = serial.init(boot_payload); + + log.init(serial_stream); + + const mem_profile = arch.initMem(boot_payload) catch |e| panic_root.panic(@errorReturnTrace(), "Failed to initialise memory profile: {}", .{e}); + var fixed_allocator = mem_profile.fixed_allocator; + + panic_root.init(&mem_profile, &fixed_allocator.allocator) catch |e| { + panic_root.panic(@errorReturnTrace(), "Failed to initialise panic: {}", .{e}); + }; + + pmm.init(&mem_profile, &fixed_allocator.allocator); + kernel_vmm = vmm.init(&mem_profile, &fixed_allocator.allocator) catch |e| panic_root.panic(@errorReturnTrace(), "Failed to initialise kernel VMM: {}", .{e}); + + log.logInfo("Init arch " ++ @tagName(builtin.arch) ++ "\n", .{}); + arch.init(boot_payload, &mem_profile, &fixed_allocator.allocator); + log.logInfo("Arch init done\n", .{}); + + // Give the kernel heap 10% of the available memory. This can be fine-tuned as time goes on. + var heap_size = mem_profile.mem_kb / 10 * 1024; + // The heap size must be a power of two so find the power of two smaller than or equal to the heap_size + if (!std.math.isPowerOfTwo(heap_size)) { + heap_size = std.math.floorPowerOfTwo(usize, heap_size); + } + var kernel_heap = heap.init(arch.VmmPayload, &kernel_vmm, vmm.Attributes{ .kernel = true, .writable = true, .cachable = true }, heap_size) catch |e| { + panic_root.panic(@errorReturnTrace(), "Failed to initialise kernel heap: {}\n", .{e}); + }; + + tty.init(&kernel_heap.allocator, boot_payload); + + scheduler.init(&kernel_heap.allocator) catch |e| { + panic_root.panic(@errorReturnTrace(), "Failed to initialise scheduler: {}", .{e}); + }; + + // Initialisation is finished, now does other stuff + log.logInfo("Init done\n", .{}); + + // Main initialisation finished so can enable interrupts + arch.enableInterrupts(); + + log.logInfo("Creating init2\n", .{}); + + // Create a init2 task + var idle_task = task.Task.create(initStage2, &kernel_heap.allocator) catch |e| { + panic_root.panic(@errorReturnTrace(), "Failed to create init stage 2 task: {}", .{e}); + }; + scheduler.scheduleTask(idle_task, &kernel_heap.allocator) catch |e| { + panic_root.panic(@errorReturnTrace(), "Failed to schedule init stage 2 task: {}", .{e}); + }; + + // Can't return for now, later this can return maybe + // TODO: Maybe make this the idle task + arch.spinWait(); +} + +/// +/// Stage 2 initialisation. This will initialise main kernel features after the architecture +/// initialisation. +/// +fn initStage2() noreturn { + tty.clear(); + const logo = + \\ _____ _ _ _ _______ ____ + \\ | __ \ | | | | | | |__ __| / __ \ + \\ | |__) | | | | | | | | | | | | | + \\ | ___/ | | | | | | | | | | | | + \\ | | | |____ | |__| | | | | |__| | + \\ |_| |______| \____/ |_| \____/ + ; + tty.print("{}\n\n", .{logo}); + + tty.print("Hello Pluto from kernel :)\n", .{}); + + switch (build_options.test_mode) { + .Initialisation => { + log.logInfo("SUCCESS\n", .{}); + }, + else => {}, } + + // Can't return for now, later this can return maybe + arch.spinWait(); } diff --git a/src/kernel/log.zig b/src/kernel/log.zig index b46a66f4..ae187f59 100644 --- a/src/kernel/log.zig +++ b/src/kernel/log.zig @@ -1,6 +1,16 @@ -const serial = @import("serial.zig"); -const fmt = @import("std").fmt; +const build_options = @import("build_options"); +const std = @import("std"); +const fmt = std.fmt; +const Serial = @import("serial.zig").Serial; +const scheduler = @import("scheduler.zig"); +/// The errors that can occur when logging +const LoggingError = error{}; + +/// The OutStream for the format function +const OutStream = std.io.OutStream(void, LoggingError, logCallback); + +/// The different levels of logging that can be outputted. pub const Level = enum { INFO, DEBUG, @@ -8,30 +18,52 @@ pub const Level = enum { ERROR, }; -fn logCallback(context: void, str: []const u8) anyerror!void { - serial.writeBytes(str, serial.Port.COM1); +var serial: Serial = undefined; + +/// +/// The call back function for the std library format function. +/// +/// Arguments: +/// context: void - The context of the printing. There isn't a need for a context for this +/// so is void. +/// str: []const u8 - The string to print to the serial terminal. +/// +/// Return: usize +/// The number of bytes written. This will always be the length of the string to print. +/// +/// Error: LoggingError +/// {} - No error as LoggingError is empty. +/// +fn logCallback(context: void, str: []const u8) LoggingError!usize { + serial.writeBytes(str); + return str.len; } /// /// Write a message to the log output stream with a certain logging level. /// /// Arguments: -/// IN comptime level: Level - The logging level to use. Determines the message prefix and whether it is filtered. -/// IN comptime format: []const u8 - The message format. Uses the standard format specification options. -/// IN args: var - A struct of the parameters for the format string. +/// IN comptime level: Level - The logging level to use. Determines the message prefix and +/// whether it is filtered. +/// IN comptime format: []const u8 - The message format. Uses the standard format specification +/// options. +/// IN args: anytype - A struct of the parameters for the format string. /// -pub fn log(comptime level: Level, comptime format: []const u8, args: var) void { - fmt.format({}, anyerror, logCallback, "[" ++ @tagName(level) ++ "] " ++ format, args) catch unreachable; +pub fn log(comptime level: Level, comptime format: []const u8, args: anytype) void { + scheduler.taskSwitching(false); + fmt.format(OutStream{ .context = {} }, "[" ++ @tagName(level) ++ "] " ++ format, args) catch unreachable; + scheduler.taskSwitching(true); } /// /// Write a message to the log output stream with the INFO level. /// /// Arguments: -/// IN comptime format: []const u8 - The message format. Uses the standard format specification options. -/// IN args: var - A struct of the parameters for the format string. +/// IN comptime format: []const u8 - The message format. Uses the standard format specification +/// options. +/// IN args: anytype - A struct of the parameters for the format string. /// -pub fn logInfo(comptime format: []const u8, args: var) void { +pub fn logInfo(comptime format: []const u8, args: anytype) void { log(Level.INFO, format, args); } @@ -39,10 +71,11 @@ pub fn logInfo(comptime format: []const u8, args: var) void { /// Write a message to the log output stream with the DEBUG level. /// /// Arguments: -/// IN comptime format: []const u8 - The message format. Uses the standard format specification options. -/// IN args: var - A struct of the parameters for the format string. +/// IN comptime format: []const u8 - The message format. Uses the standard format specification +/// options. +/// IN args: anytype - A struct of the parameters for the format string. /// -pub fn logDebug(comptime format: []const u8, args: var) void { +pub fn logDebug(comptime format: []const u8, args: anytype) void { log(Level.DEBUG, format, args); } @@ -50,10 +83,11 @@ pub fn logDebug(comptime format: []const u8, args: var) void { /// Write a message to the log output stream with the WARNING level. /// /// Arguments: -/// IN comptime format: []const u8 - The message format. Uses the standard format specification options. -/// IN args: var - A struct of the parameters for the format string. +/// IN comptime format: []const u8 - The message format. Uses the standard format specification +/// options. +/// IN args: anytype - A struct of the parameters for the format string. /// -pub fn logWarning(comptime format: []const u8, args: var) void { +pub fn logWarning(comptime format: []const u8, args: anytype) void { log(Level.WARNING, format, args); } @@ -61,14 +95,33 @@ pub fn logWarning(comptime format: []const u8, args: var) void { /// Write a message to the log output stream with the ERROR level. /// /// Arguments: -/// IN comptime format: []const u8 - The message format. Uses the standard format specification options. -/// IN args: var - A struct of the parameters for the format string. +/// IN comptime format: []const u8 - The message format. Uses the standard format specification +/// options. +/// IN args: anytype - A struct of the parameters for the format string. /// -pub fn logError(comptime format: []const u8, args: var) void { +pub fn logError(comptime format: []const u8, args: anytype) void { log(Level.ERROR, format, args); } -pub fn runtimeTests() void { +/// +/// Initialise the logging stream using the given Serial instance. +/// +/// Arguments: +/// IN ser: Serial - The serial instance to use when logging +/// +pub fn init(ser: Serial) void { + serial = ser; + + switch (build_options.test_mode) { + .Initialisation => runtimeTests(), + else => {}, + } +} + +/// +/// The logging runtime tests that will test all logging levels. +/// +fn runtimeTests() void { inline for (@typeInfo(Level).Enum.fields) |field| { const level = @field(Level, field.name); log(level, "Test " ++ field.name ++ " level\n", .{}); diff --git a/src/kernel/mem.zig b/src/kernel/mem.zig index 43b42f53..57f9099e 100644 --- a/src/kernel/mem.zig +++ b/src/kernel/mem.zig @@ -1,80 +1,78 @@ -const multiboot = @import("multiboot.zig"); const std = @import("std"); const expectEqual = std.testing.expectEqual; const log = @import("log.zig"); +pub const Module = struct { + /// The region of memory occupied by the module + region: Range, + /// The module's name + name: []const u8, +}; + +pub const Map = struct { + /// The virtual range to reserve + virtual: Range, + /// The physical range to map to, if any + physical: ?Range, +}; + +/// A range of memory +pub const Range = struct { + /// The start of the range, inclusive + start: usize, + /// The end of the range, exclusive + end: usize, +}; + pub const MemProfile = struct { + /// The virtual end address of the kernel code. vaddr_end: [*]u8, + + /// The virtual end address of the kernel code. vaddr_start: [*]u8, + + /// The physical end address of the kernel code. physaddr_end: [*]u8, + + /// The physical start address of the kernel code. physaddr_start: [*]u8, - mem_kb: u32, - fixed_alloc_size: u32, - boot_modules: []multiboot.multiboot_module_t, -}; -/// The virtual end of the kernel code -extern var KERNEL_VADDR_END: *u32; + /// The amount of memory in the system, in kilobytes. + mem_kb: usize, -/// The virtual start of the kernel code -extern var KERNEL_VADDR_START: *u32; + /// The modules loaded into memory at boot. + modules: []Module, -/// The physical end of the kernel code -extern var KERNEL_PHYSADDR_END: *u32; + /// The virtual regions of reserved memory. Should not include what is tracked by the vaddr_* fields but should include the regions occupied by the modules. These are reserved and mapped by the VMM + virtual_reserved: []Map, -/// The physical start of the kernel code -extern var KERNEL_PHYSADDR_START: *u32; + /// The physical regions of reserved memory. Should not include what is tracked by the physaddr_* fields but should include the regions occupied by the modules. These are reserved by the PMM + physical_reserved: []Range, -/// The boot-time offset that the virtual addresses are from the physical addresses -extern var KERNEL_ADDR_OFFSET: *u32; + /// The allocator to use before a heap can be set up. + fixed_allocator: std.heap.FixedBufferAllocator, +}; /// The size of the fixed allocator used before the heap is set up. Set to 1MiB. -const FIXED_ALLOC_SIZE: usize = 1024 * 1024; +pub const FIXED_ALLOC_SIZE: usize = 1024 * 1024; /// The kernel's virtual address offset. It's assigned in the init function and this file's tests. /// We can't just use KERNEL_ADDR_OFFSET since using externs in the virtToPhys test is broken in /// release-safe. This is a workaround until that is fixed. -var ADDR_OFFSET: usize = undefined; - -/// -/// Initialise the system's memory profile based on linker symbols and the multiboot info struct. -/// -/// Arguments: -/// IN mb_info: *multiboot.multiboot_info_t - The multiboot info passed by the bootloader. -/// -/// Return: MemProfile -/// The memory profile constructed from the exported linker symbols and the relevant multiboot info. -/// -pub fn init(mb_info: *multiboot.multiboot_info_t) MemProfile { - log.logInfo("Init mem\n", .{}); - const mods_count = mb_info.mods_count; - ADDR_OFFSET = @ptrToInt(&KERNEL_ADDR_OFFSET); - const mem_profile = MemProfile{ - .vaddr_end = @ptrCast([*]u8, &KERNEL_VADDR_END), - .vaddr_start = @ptrCast([*]u8, &KERNEL_VADDR_START), - .physaddr_end = @ptrCast([*]u8, &KERNEL_PHYSADDR_END), - .physaddr_start = @ptrCast([*]u8, &KERNEL_PHYSADDR_START), - // Total memory available including the initial 1MiB that grub doesn't include - .mem_kb = mb_info.mem_upper + mb_info.mem_lower + 1024, - .fixed_alloc_size = FIXED_ALLOC_SIZE, - .boot_modules = @intToPtr([*]multiboot.multiboot_mod_list, physToVirt(mb_info.mods_addr))[0..mods_count], - }; - log.logInfo("Done\n", .{}); - return mem_profile; -} +pub var ADDR_OFFSET: usize = undefined; /// /// Convert a virtual address to its physical counterpart by subtracting the kernel virtual offset from the virtual address. /// /// Arguments: -/// IN virt: var - The virtual address to covert. Either an integer or pointer. +/// IN virt: anytype - The virtual address to covert. Either an integer or pointer. /// /// Return: @TypeOf(virt) /// The physical address. /// -pub inline fn virtToPhys(virt: var) @TypeOf(virt) { +pub fn virtToPhys(virt: anytype) @TypeOf(virt) { const T = @TypeOf(virt); - return switch (@typeId(T)) { + return switch (@typeInfo(T)) { .Pointer => @intToPtr(T, @ptrToInt(virt) - ADDR_OFFSET), .Int => virt - ADDR_OFFSET, else => @compileError("Only pointers and integers are supported"), @@ -85,14 +83,14 @@ pub inline fn virtToPhys(virt: var) @TypeOf(virt) { /// Convert a physical address to its virtual counterpart by adding the kernel virtual offset to the physical address. /// /// Arguments: -/// IN phys: var - The physical address to covert. Either an integer or pointer. +/// IN phys: anytype - The physical address to covert. Either an integer or pointer. /// /// Return: @TypeOf(virt) /// The virtual address. /// -pub inline fn physToVirt(phys: var) @TypeOf(phys) { +pub fn physToVirt(phys: anytype) @TypeOf(phys) { const T = @TypeOf(phys); - return switch (@typeId(T)) { + return switch (@typeInfo(T)) { .Pointer => @intToPtr(T, @ptrToInt(phys) + ADDR_OFFSET), .Int => phys + ADDR_OFFSET, else => @compileError("Only pointers and integers are supported"), diff --git a/src/kernel/panic.zig b/src/kernel/panic.zig index 58ba85de..84705afa 100644 --- a/src/kernel/panic.zig +++ b/src/kernel/panic.zig @@ -5,6 +5,7 @@ const arch = @import("arch.zig").internals; const log = @import("log.zig"); const multiboot = @import("multiboot.zig"); const mem = @import("mem.zig"); +const build_options = @import("build_options"); const ArrayList = std.ArrayList; const testing = std.testing; @@ -15,7 +16,7 @@ const PanicError = error{ InvalidSymbolFile, }; -/// An entry within a symbol map. Corresponds to one entry in a symbole file +/// An entry within a symbol map. Corresponds to one entry in a symbol file const MapEntry = struct { /// The address that the entry corresponds to addr: usize, @@ -59,7 +60,7 @@ const SymbolMap = struct { /// Error: std.mem.Allocator.Error /// * - See ArrayList.append /// - pub fn add(self: *SymbolMap, name: []const u8, addr: u32) !void { + pub fn add(self: *SymbolMap, name: []const u8, addr: usize) !void { try self.addEntry(MapEntry{ .addr = addr, .func_name = name }); } @@ -77,11 +78,11 @@ const SymbolMap = struct { /// The function name associated with that program address, or null if one wasn't found. /// pub fn search(self: *const SymbolMap, addr: usize) ?[]const u8 { - if (self.symbols.len == 0) + if (self.symbols.items.len == 0) return null; // Find the first element whose address is greater than addr var previous_name: ?[]const u8 = null; - for (self.symbols.toSliceConst()) |entry| { + for (self.symbols.items) |entry| { if (entry.addr > addr) return previous_name; previous_name = entry.func_name; @@ -104,7 +105,7 @@ fn logTraceAddress(addr: usize) void { log.logError("{x}: {}\n", .{ addr, str }); } -pub fn panic(trace: ?*builtin.StackTrace, comptime format: []const u8, args: var) noreturn { +pub fn panic(trace: ?*builtin.StackTrace, comptime format: []const u8, args: anytype) noreturn { @setCold(true); log.logError("Kernel panic: " ++ format ++ "\n", args); if (trace) |trc| { @@ -116,7 +117,7 @@ pub fn panic(trace: ?*builtin.StackTrace, comptime format: []const u8, args: var } else { const first_ret_addr = @returnAddress(); var last_addr: u64 = 0; - var it = std.debug.StackIterator.init(first_ret_addr); + var it = std.debug.StackIterator.init(first_ret_addr, null); while (it.next()) |ret_addr| { if (ret_addr != last_addr) logTraceAddress(ret_addr); last_addr = ret_addr; @@ -130,7 +131,7 @@ pub fn panic(trace: ?*builtin.StackTrace, comptime format: []const u8, args: var /// whitespace character. /// /// Arguments: -/// INOUT ptr: *[*]const u8 - The address at which to start looking, updated after all +/// IN/OUT ptr: *[*]const u8 - The address at which to start looking, updated after all /// characters have been consumed. /// IN end: *const u8 - The end address at which to start looking. A whitespace character must /// be found before this. @@ -166,7 +167,9 @@ fn parseAddr(ptr: *[*]const u8, end: *const u8) !usize { /// PanicError.InvalidSymbolFile: The address given is greater than or equal to the end address. /// fn parseChar(ptr: [*]const u8, end: *const u8) PanicError!u8 { - if (@ptrToInt(ptr) >= @ptrToInt(end)) return PanicError.InvalidSymbolFile; + if (@ptrToInt(ptr) >= @ptrToInt(end)) { + return PanicError.InvalidSymbolFile; + } return ptr[0]; } @@ -219,7 +222,7 @@ fn parseNonWhitespace(ptr: [*]const u8, end: *const u8) PanicError![*]const u8 { /// character. /// /// Arguments: -/// INOUT ptr: *[*]const u8 - The address at which to start looking, updated after all +/// IN/OUT ptr: *[*]const u8 - The address at which to start looking, updated after all /// characters have been consumed. /// IN end: *const u8 - The end address at which to start looking. A whitespace character must /// be found before this. @@ -242,7 +245,7 @@ fn parseName(ptr: *[*]const u8, end: *const u8) PanicError![]const u8 { /// in the format of '\d+\w+[a-zA-Z0-9]+'. Must be terminated by a whitespace character. /// /// Arguments: -/// INOUT ptr: *[*]const u8 - The address at which to start looking, updated once after the +/// IN/OUT ptr: *[*]const u8 - The address at which to start looking, updated once after the /// address has been consumed and once again after the name has been consumed. /// IN end: *const u8 - The end address at which to start looking. A whitespace character must /// be found before this. @@ -278,17 +281,18 @@ fn parseMapEntry(start: *[*]const u8, end: *const u8) !MapEntry { /// pub fn init(mem_profile: *const mem.MemProfile, allocator: *std.mem.Allocator) !void { log.logInfo("Init panic\n", .{}); - defer log.logInfo("Done\n", .{}); + defer log.logInfo("Done panic\n", .{}); + // Exit if we haven't loaded all debug modules - if (mem_profile.boot_modules.len < 1) + if (mem_profile.modules.len < 1) { return; - var kmap_start: u32 = 0; - var kmap_end: u32 = 0; - for (mem_profile.boot_modules) |module| { - const mod_start = mem.physToVirt(module.mod_start); - const mod_end = mem.physToVirt(module.mod_end) - 1; - const mod_str_ptr = mem.physToVirt(@intToPtr([*:0]u8, module.cmdline)); - if (std.mem.eql(u8, std.mem.toSlice(u8, mod_str_ptr), "kernel.map")) { + } + var kmap_start: usize = 0; + var kmap_end: usize = 0; + for (mem_profile.modules) |module| { + const mod_start = module.region.start; + const mod_end = module.region.end - 1; + if (std.mem.eql(u8, module.name, "kernel.map")) { kmap_start = mod_start; kmap_end = mod_end; break; @@ -296,18 +300,23 @@ pub fn init(mem_profile: *const mem.MemProfile, allocator: *std.mem.Allocator) ! } // Don't try to load the symbols if there was no symbol map file. This is a valid state so just // exit early - if (kmap_start == 0 or kmap_end == 0) + if (kmap_start == 0 or kmap_end == 0) { return; + } var syms = SymbolMap.init(allocator); errdefer syms.deinit(); - var file_index = kmap_start; var kmap_ptr = @intToPtr([*]u8, kmap_start); while (@ptrToInt(kmap_ptr) < kmap_end - 1) { const entry = try parseMapEntry(&kmap_ptr, @intToPtr(*const u8, kmap_end)); try syms.addEntry(entry); } symbol_map = syms; + + switch (build_options.test_mode) { + .Panic => runtimeTests(), + else => {}, + } } test "parseChar" { @@ -422,7 +431,7 @@ test "parseMapEntry fails without a name" { } test "SymbolMap" { - var allocator = std.heap.direct_allocator; + var allocator = std.heap.page_allocator; var map = SymbolMap.init(allocator); try map.add("abc"[0..], 123); try map.addEntry(MapEntry{ .func_name = "def"[0..], .addr = 456 }); @@ -442,7 +451,13 @@ test "SymbolMap" { testing.expectEqual(map.search(2345), "jkl"); } +/// +/// Runtime test for panic. This will trigger a integer overflow. +/// pub fn runtimeTests() void { + @setRuntimeSafety(true); var x: u8 = 255; x += 1; + // If we get here, then a panic was not triggered so fail + panic(@errorReturnTrace(), "FAILURE: No integer overflow\n", .{}); } diff --git a/src/kernel/pmm.zig b/src/kernel/pmm.zig new file mode 100644 index 00000000..62478c22 --- /dev/null +++ b/src/kernel/pmm.zig @@ -0,0 +1,230 @@ +const is_test = @import("builtin").is_test; +const std = @import("std"); +const build_options = @import("build_options"); +const mock_path = build_options.mock_path; +const arch = if (is_test) @import(mock_path ++ "arch_mock.zig") else @import("arch.zig").internals; +const MemProfile = (if (is_test) @import(mock_path ++ "mem_mock.zig") else @import("mem.zig")).MemProfile; +const testing = std.testing; +const panic = @import("panic.zig").panic; +const log = if (is_test) @import(mock_path ++ "log_mock.zig") else @import("log.zig"); +const Bitmap = @import("bitmap.zig").Bitmap; + +const PmmBitmap = Bitmap(u32); + +/// The possible errors thrown by bitmap functions +const PmmError = error{ + /// The address given hasn't been allocated + NotAllocated, +}; + +/// The size of memory associated with each bitmap entry +pub const BLOCK_SIZE: usize = arch.MEMORY_BLOCK_SIZE; + +var bitmap: PmmBitmap = undefined; + +/// +/// Set the bitmap entry for an address as occupied +/// +/// Arguments: +/// IN addr: usize - The address. +/// +/// Error: PmmBitmap.BitmapError. +/// *: See PmmBitmap.setEntry. Could occur if the address is out of bounds. +/// +pub fn setAddr(addr: usize) PmmBitmap.BitmapError!void { + try bitmap.setEntry(@intCast(u32, addr / BLOCK_SIZE)); +} + +/// +/// Check if an address is set as occupied. +/// +/// Arguments: +/// IN addr: usize - The address to check. +/// +/// Return: True if occupied, else false. +/// +/// Error: PmmBitmap.BitmapError. +/// *: See PmmBitmap.setEntry. Could occur if the address is out of bounds. +/// +pub fn isSet(addr: usize) PmmBitmap.BitmapError!bool { + return bitmap.isSet(@intCast(u32, addr / BLOCK_SIZE)); +} + +/// +/// Find the next free memory block, set it as occupied and return it. The region allocated will be of size BLOCK_SIZE. +/// +/// Return: The address that was allocated. +/// +pub fn alloc() ?usize { + if (bitmap.setFirstFree()) |entry| { + return entry * BLOCK_SIZE; + } + return null; +} + +/// +/// Set the address as free so it can be allocated in the future. This will free a block of size BLOCK_SIZE. +/// +/// Arguments: +/// IN addr: usize - The previously allocated address to free. Will be aligned down to the nearest multiple of BLOCK_SIZE. +/// +/// Error: PmmError || PmmBitmap.BitmapError. +/// PmmError.NotAllocated: The address wasn't allocated. +/// PmmBitmap.BitmapError.OutOfBounds: The address given was out of bounds. +/// +pub fn free(addr: usize) (PmmBitmap.BitmapError || PmmError)!void { + const idx = @intCast(u32, addr / BLOCK_SIZE); + if (try bitmap.isSet(idx)) { + try bitmap.clearEntry(idx); + } else { + return PmmError.NotAllocated; + } +} + +/// +/// Get the number of unallocated blocks of memory. +/// +/// Return: usize. +/// The number of unallocated blocks of memory +/// +pub fn blocksFree() usize { + return bitmap.num_free_entries; +} + +/// Intiialise the physical memory manager and set all unavailable regions as occupied (those from the memory map and those from the linker symbols). +/// +/// Arguments: +/// IN mem: *const MemProfile - The system's memory profile. +/// IN allocator: *std.mem.Allocator - The allocator to use to allocate the bitmaps. +/// +pub fn init(mem: *const MemProfile, allocator: *std.mem.Allocator) void { + log.logInfo("Init pmm\n", .{}); + defer log.logInfo("Done pmm\n", .{}); + + bitmap = PmmBitmap.init(mem.mem_kb * 1024 / BLOCK_SIZE, allocator) catch @panic("Bitmap allocation failed"); + + // Occupy the regions of memory that the memory map describes as reserved + for (mem.physical_reserved) |entry| { + var addr = std.mem.alignBackward(entry.start, BLOCK_SIZE); + var end = entry.end - 1; + // If the end address can be aligned without overflowing then align it + if (end <= std.math.maxInt(usize) - BLOCK_SIZE) { + end = std.mem.alignForward(end, BLOCK_SIZE); + } + while (addr < end) : (addr += BLOCK_SIZE) { + setAddr(addr) catch |e| switch (e) { + // We can ignore out of bounds errors as the memory won't be available anyway + PmmBitmap.BitmapError.OutOfBounds => break, + else => panic(@errorReturnTrace(), "Failed setting address 0x{x} from memory map as occupied: {}", .{ addr, e }), + }; + } + } + + switch (build_options.test_mode) { + .Initialisation => runtimeTests(mem, allocator), + else => {}, + } +} + +test "alloc" { + bitmap = try Bitmap(u32).init(32, std.heap.page_allocator); + comptime var addr = 0; + comptime var i = 0; + // Allocate all entries, making sure they succeed and return the correct addresses + inline while (i < 32) : ({ + i += 1; + addr += BLOCK_SIZE; + }) { + testing.expect(!(try isSet(addr))); + testing.expect(alloc().? == addr); + testing.expect(try isSet(addr)); + testing.expectEqual(blocksFree(), 31 - i); + } + // Allocation should now fail + testing.expect(alloc() == null); +} + +test "free" { + bitmap = try Bitmap(u32).init(32, std.heap.page_allocator); + comptime var i = 0; + // Allocate and free all entries + inline while (i < 32) : (i += 1) { + const addr = alloc().?; + testing.expect(try isSet(addr)); + testing.expectEqual(blocksFree(), 31); + try free(addr); + testing.expectEqual(blocksFree(), 32); + testing.expect(!(try isSet(addr))); + // Double frees should be caught + testing.expectError(PmmError.NotAllocated, free(addr)); + } +} + +test "setAddr and isSet" { + const num_entries: u32 = 32; + bitmap = try Bitmap(u32).init(num_entries, std.heap.page_allocator); + var addr: u32 = 0; + var i: u32 = 0; + while (i < num_entries) : ({ + i += 1; + addr += BLOCK_SIZE; + }) { + // Ensure all previous blocks are still set + var h: u32 = 0; + var addr2: u32 = 0; + while (h < i) : ({ + h += 1; + addr2 += BLOCK_SIZE; + }) { + testing.expect(try isSet(addr2)); + } + + testing.expectEqual(blocksFree(), num_entries - i); + // Set the current block + try setAddr(addr); + testing.expect(try isSet(addr)); + testing.expectEqual(blocksFree(), num_entries - i - 1); + + // Ensure all successive entries are not set + var j: u32 = i + 1; + var addr3: u32 = addr + BLOCK_SIZE; + while (j < num_entries) : ({ + j += 1; + addr3 += BLOCK_SIZE; + }) { + testing.expect(!try isSet(addr3)); + } + } +} + +/// +/// Allocate all blocks and make sure they don't overlap with any reserved addresses. +/// +/// Arguments: +/// IN mem: *const MemProfile - The memory profile to check for reserved memory regions. +/// IN/OUT allocator: *std.mem.Allocator - The allocator to use when needing to create intermediate structures used for testing +/// +fn runtimeTests(mem: *const MemProfile, allocator: *std.mem.Allocator) void { + // Make sure that occupied memory can't be allocated + var prev_alloc: usize = std.math.maxInt(usize); + var alloc_list = std.ArrayList(usize).init(allocator); + defer alloc_list.deinit(); + while (alloc()) |alloced| { + if (prev_alloc == alloced) { + panic(null, "FAILURE: PMM allocated the same address twice: 0x{x}", .{alloced}); + } + prev_alloc = alloced; + for (mem.physical_reserved) |entry| { + var addr = std.mem.alignBackward(@intCast(usize, entry.start), BLOCK_SIZE); + if (addr == alloced) { + panic(null, "FAILURE: PMM allocated an address that should be reserved by the memory map: 0x{x}", .{addr}); + } + } + alloc_list.append(alloced) catch |e| panic(@errorReturnTrace(), "FAILURE: Failed to add PMM allocation to list: {}", .{e}); + } + // Clean up + for (alloc_list.items) |alloced| { + free(alloced) catch |e| panic(@errorReturnTrace(), "FAILURE: Failed freeing allocation in PMM rt test: {}", .{e}); + } + log.logInfo("PMM: Tested allocation\n", .{}); +} diff --git a/src/kernel/scheduler.zig b/src/kernel/scheduler.zig new file mode 100644 index 00000000..e6961999 --- /dev/null +++ b/src/kernel/scheduler.zig @@ -0,0 +1,360 @@ +const std = @import("std"); +const expectEqual = std.testing.expectEqual; +const expectError = std.testing.expectError; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const is_test = builtin.is_test; +const build_options = @import("build_options"); +const mock_path = build_options.mock_path; +const arch = @import("arch.zig").internals; +const log = if (is_test) @import(mock_path ++ "log_mock.zig") else @import("log.zig"); +const panic = if (is_test) @import(mock_path ++ "panic_mock.zig").panic else @import("panic.zig").panic; +const task = if (is_test) @import(mock_path ++ "task_mock.zig") else @import("task.zig"); +const Task = task.Task; +const Allocator = std.mem.Allocator; +const TailQueue = std.TailQueue; + +/// The function type for the entry point. +const EntryPointFn = fn () void; + +/// The default stack size of a task. Currently this is set to a page size. +const STACK_SIZE: u32 = arch.MEMORY_BLOCK_SIZE / @sizeOf(usize); + +/// Pointer to the start of the main kernel stack +extern var KERNEL_STACK_START: []u32; + +/// The current task running +var current_task: *Task = undefined; + +/// Array list of all runnable tasks +var tasks: TailQueue(*Task) = undefined; + +/// Whether the scheduler is allowed to switch tasks. +var can_switch: bool = true; + +/// +/// The idle task that just halts the CPU but the CPU can still handle interrupts. +/// +fn idle() noreturn { + arch.spinWait(); +} + +pub fn taskSwitching(enabled: bool) void { + can_switch = enabled; +} + +/// +/// Round robin. This will first save the the current tasks stack pointer, then will pick the next +/// task to be run from the queue. It will add the current task to the end of the queue and pop the +/// next task from the front as set this as the current task. Then will return the stack pointer +/// of the next task to be loaded into the stack register to load the next task stack to pop off +/// its state. Interrupts are assumed disabled. +/// +/// Argument: +/// IN ctx: *arch.CpuState - Pointer to the exception context containing the contents +/// of the registers at the time of a exception. +/// +/// Return: usize +/// The new stack pointer to the next stack of the next task. +/// +pub fn pickNextTask(ctx: *arch.CpuState) usize { + // Save the stack pointer from old task + current_task.stack_pointer = @ptrToInt(ctx); + + // If we can't switch, then continue with the current task + if (!can_switch) { + return current_task.stack_pointer; + } + + // Pick the next task + // If there isn't one, then just return the same task + if (tasks.pop()) |new_task_node| { + // Get the next task + const next_task = new_task_node.data; + + // Move some pointers to don't need to allocate memory, speeds things up + new_task_node.data = current_task; + new_task_node.prev = null; + new_task_node.next = null; + + // Add the 'current_task' node to the end of the queue + tasks.prepend(new_task_node); + + current_task = next_task; + } + + // Context switch in the interrupt stub handler which will pop the next task state off the + // stack + return current_task.stack_pointer; +} + +/// +/// Create a new task and add it to the scheduling queue. No locking. +/// +/// Arguments: +/// IN entry_point: EntryPointFn - The entry point into the task. This must be a function. +/// +/// Error: Allocator.Error +/// OutOfMemory - If there isn't enough memory for the a task/stack. Any memory allocated will +/// be freed on return. +/// +pub fn scheduleTask(new_task: *Task, allocator: *Allocator) Allocator.Error!void { + var task_node = try tasks.createNode(new_task, allocator); + tasks.prepend(task_node); +} + +/// +/// Initialise the scheduler. This will set up the current task to the code that is currently +/// running. So if there is a task switch before kmain can finish, can continue when switched back. +/// This will set the stack to KERNEL_STACK_START from the linker stript. This will also create the +/// idle task for when there is no more tasks to run. +/// +/// Arguments: +/// IN allocator: *Allocator - The allocator to use when needing to allocate memory. +/// +/// Error: Allocator.Error +/// OutOfMemory - There is no more memory. Any memory allocated will be freed on return. +/// +pub fn init(allocator: *Allocator) Allocator.Error!void { + // TODO: Maybe move the task init here? + log.logInfo("Init scheduler\n", .{}); + defer log.logInfo("Done scheduler\n", .{}); + + // Init the task list for round robin + tasks = TailQueue(*Task).init(); + + // Set up the init task to continue execution + current_task = try allocator.create(Task); + errdefer allocator.destroy(current_task); + // PID 0 + current_task.pid = 0; + current_task.stack = @intToPtr([*]u32, @ptrToInt(&KERNEL_STACK_START))[0..4096]; + // ESP will be saved on next schedule + + // Run the runtime tests here + switch (build_options.test_mode) { + .Scheduler => runtimeTests(allocator), + else => {}, + } + + // Create the idle task when there are no more tasks left + var idle_task = try Task.create(idle, allocator); + errdefer idle_task.destroy(allocator); + + try scheduleTask(idle_task, allocator); +} + +// For testing the errdefer +const FailingAllocator = std.testing.FailingAllocator; +const testing_allocator = &std.testing.base_allocator_instance.allocator; + +fn test_fn1() void {} +fn test_fn2() void {} + +var test_pid_counter: u7 = 1; + +fn task_create(entry_point: EntryPointFn, allocator: *Allocator) Allocator.Error!*Task { + var t = try allocator.create(Task); + errdefer allocator.destroy(t); + t.pid = test_pid_counter; + // Just alloc something + t.stack = try allocator.alloc(u32, 1); + t.stack_pointer = 0; + test_pid_counter += 1; + return t; +} + +fn task_destroy(self: *Task, allocator: *Allocator) void { + if (@ptrToInt(self.stack.ptr) != @ptrToInt(&KERNEL_STACK_START)) { + allocator.free(self.stack); + } + allocator.destroy(self); +} + +test "pickNextTask" { + task.initTest(); + defer task.freeTest(); + + task.addConsumeFunction("Task.create", task_create); + task.addConsumeFunction("Task.create", task_create); + task.addRepeatFunction("Task.destroy", task_destroy); + + var ctx: arch.CpuState = std.mem.zeroes(arch.CpuState); + + var allocator = std.testing.allocator; + tasks = TailQueue(*Task).init(); + + // Set up a current task + current_task = try allocator.create(Task); + defer allocator.destroy(current_task); + current_task.pid = 0; + current_task.stack = @intToPtr([*]u32, @ptrToInt(&KERNEL_STACK_START))[0..4096]; + current_task.stack_pointer = @ptrToInt(&KERNEL_STACK_START); + + // Create two tasks and schedule them + var test_fn1_task = try Task.create(test_fn1, allocator); + defer test_fn1_task.destroy(allocator); + try scheduleTask(test_fn1_task, allocator); + + var test_fn2_task = try Task.create(test_fn2, allocator); + defer test_fn2_task.destroy(allocator); + try scheduleTask(test_fn2_task, allocator); + + // Get the stack pointers of the created tasks + const fn1_stack_pointer = tasks.first.?.data.stack_pointer; + const fn2_stack_pointer = tasks.first.?.next.?.data.stack_pointer; + + expectEqual(pickNextTask(&ctx), fn1_stack_pointer); + // The stack pointer of the re-added task should point to the context + expectEqual(tasks.first.?.data.stack_pointer, @ptrToInt(&ctx)); + + // Should be the PID of the next task + expectEqual(current_task.pid, 1); + + expectEqual(pickNextTask(&ctx), fn2_stack_pointer); + // The stack pointer of the re-added task should point to the context + expectEqual(tasks.first.?.data.stack_pointer, @ptrToInt(&ctx)); + + // Should be the PID of the next task + expectEqual(current_task.pid, 2); + + expectEqual(pickNextTask(&ctx), @ptrToInt(&ctx)); + // The stack pointer of the re-added task should point to the context + expectEqual(tasks.first.?.data.stack_pointer, @ptrToInt(&ctx)); + + // Should be back tot he beginning + expectEqual(current_task.pid, 0); + + // Reset the test pid + test_pid_counter = 1; + + // Free the queue + while (tasks.pop()) |elem| { + tasks.destroyNode(elem, allocator); + } +} + +test "createNewTask add new task" { + task.initTest(); + defer task.freeTest(); + + task.addConsumeFunction("Task.create", task_create); + task.addConsumeFunction("Task.destroy", task_destroy); + + // Set the global allocator + var allocator = std.testing.allocator; + + // Init the task list + tasks = TailQueue(*Task).init(); + + var test_fn1_task = try Task.create(test_fn1, allocator); + defer test_fn1_task.destroy(allocator); + try scheduleTask(test_fn1_task, allocator); + + expectEqual(tasks.len, 1); + + // Free the memory + tasks.destroyNode(tasks.first.?, allocator); +} + +test "init" { + task.initTest(); + defer task.freeTest(); + + task.addConsumeFunction("Task.create", task_create); + task.addRepeatFunction("Task.destroy", task_destroy); + + var allocator = std.testing.allocator; + + try init(allocator); + + expectEqual(current_task.pid, 0); + expectEqual(current_task.stack, @intToPtr([*]u32, @ptrToInt(&KERNEL_STACK_START))[0..4096]); + + expectEqual(tasks.len, 1); + + // Free the tasks created + current_task.destroy(allocator); + while (tasks.pop()) |elem| { + elem.data.destroy(allocator); + tasks.destroyNode(elem, allocator); + } +} + +/// A volatile pointer used to control a loop outside the task. This is so to ensure a task switch +/// ocurred. +var is_set: *volatile bool = undefined; + +/// +/// The test task function. +/// +fn task_function() noreturn { + log.logInfo("Switched\n", .{}); + is_set.* = false; + while (true) {} +} + +/// +/// This tests that variables in registers and on the stack are preserved when a task switch +/// occurs. Also tests that a global volatile can be test in one task and be reacted to in another. +/// +/// Arguments: +/// IN allocator: *Allocator - The allocator to use when needing to allocate memory. +/// +fn rt_variable_preserved(allocator: *Allocator) void { + // Create the memory for the boolean + is_set = allocator.create(bool) catch unreachable; + defer allocator.destroy(is_set); + is_set.* = true; + + var test_task = Task.create(task_function, allocator) catch unreachable; + scheduleTask(test_task, allocator) catch unreachable; + // TODO: Need to add the ability to remove tasks + + var w: u32 = 0; + var x: u32 = 1; + var y: u32 = 2; + var z: u32 = 3; + + while (is_set.*) { + if (w != 0) { + panic(@errorReturnTrace(), "FAILED: w not 0, but: {}\n", .{w}); + } + if (x != 1) { + panic(@errorReturnTrace(), "FAILED: x not 1, but: {}\n", .{x}); + } + if (y != 2) { + panic(@errorReturnTrace(), "FAILED: y not 2, but: {}\n", .{y}); + } + if (z != 3) { + panic(@errorReturnTrace(), "FAILED: z not 3, but: {}\n", .{z}); + } + } + // Make sure these are the same values + if (w != 0) { + panic(@errorReturnTrace(), "FAILED: w not 0, but: {}\n", .{w}); + } + if (x != 1) { + panic(@errorReturnTrace(), "FAILED: x not 1, but: {}\n", .{x}); + } + if (y != 2) { + panic(@errorReturnTrace(), "FAILED: y not 2, but: {}\n", .{y}); + } + if (z != 3) { + panic(@errorReturnTrace(), "FAILED: z not 3, but: {}\n", .{z}); + } + + log.logInfo("SUCCESS: Scheduler variables preserved\n", .{}); +} + +/// +/// The scheduler runtime tests that will test the scheduling functionality. +/// +/// Arguments: +/// IN allocator: *Allocator - The allocator to use when needing to allocate memory. +/// +fn runtimeTests(allocator: *Allocator) void { + arch.enableInterrupts(); + rt_variable_preserved(allocator); + while (true) {} +} diff --git a/src/kernel/serial.zig b/src/kernel/serial.zig index bc89f1c9..2ee6ea78 100644 --- a/src/kernel/serial.zig +++ b/src/kernel/serial.zig @@ -1,216 +1,62 @@ const arch = @import("arch.zig").internals; -const panic = @import("panic.zig").panic; -const testing = @import("std").testing; -const options = @import("build_options"); - -/// The I/O port numbers associated with each serial port -pub const Port = enum(u16) { - COM1 = 0x3F8, - COM2 = 0x2F8, - COM3 = 0x3E8, - COM4 = 0x2E8, -}; - -/// Errors thrown by serial functions -const SerialError = error{ - /// The given baudrate is outside of the allowed range - InvalidBaudRate, - - /// The given char len is outside the allowed range. - InvalidCharacterLength, -}; - -/// The LCR is the line control register -const LCR: u16 = 3; - -/// Maximum baudrate -const BAUD_MAX: u32 = 115200; - -/// 8 bits per serial character -const CHAR_LEN: u8 = 8; - -/// One stop bit per transmission -const SINGLE_STOP_BIT: bool = true; - -/// No parity bit -const PARITY_BIT: bool = false; - -/// Default baudrate -pub const DEFAULT_BAUDRATE = 38400; - -/// -/// Compute a value that encodes the serial properties -/// Used by the line control register -/// -/// Arguments: -/// IN char_len: u8 - The number of bits in each individual byte. Must be 0 or between 5 and 8 (inclusive). -/// IN stop_bit: bool - If a stop bit should included in each transmission. -/// IN parity_bit: bool - If a parity bit should be included in each transmission. -/// IN msb: u1 - The most significant bit to use. -/// -/// Return: u8 -/// The computed lcr value. -/// -/// Error: SerialError -/// InvalidCharacterLength - If the char_len is less than 5 or greater than 8. -/// -fn lcrValue(char_len: u8, stop_bit: bool, parity_bit: bool, msb: u1) SerialError!u8 { - if (char_len != 0 and (char_len < 5 or char_len > 8)) - return SerialError.InvalidCharacterLength; - // Set the msb and OR in all arguments passed - const val = char_len & 0x3 | - @intCast(u8, @boolToInt(stop_bit)) << 2 | - @intCast(u8, @boolToInt(parity_bit)) << 3 | - @intCast(u8, msb) << 7; - return val; -} - -/// -/// The serial controller accepts a divisor rather than a raw badrate, as that is more space efficient. -/// This function computes the divisor for a desired baudrate. Note that multiple baudrates can have the same divisor. -/// -/// Arguments: -/// baud: u32 - The desired baudrate. Must be greater than 0 and less than BAUD_MAX. -/// -/// Return: u16 -/// The computed divisor. -/// -/// Error: SerialError -/// InvalidBaudRate - If baudrate is 0 or greater than BAUD_MAX. -/// -fn baudDivisor(baud: u32) SerialError!u16 { - if (baud > BAUD_MAX or baud == 0) - return SerialError.InvalidBaudRate; - return @truncate(u16, BAUD_MAX / baud); -} - -/// -/// Checks if the transmission buffer is empty, which means data can be sent. -/// -/// Arguments: -/// port: Port - The port to check. -/// -/// Return: bool -/// If the transmission buffer is empty. -/// -fn transmitIsEmpty(port: Port) bool { - return arch.inb(@enumToInt(port) + 5) & 0x20 > 0; -} - -/// -/// Write a byte to a serial port. Waits until the transmission queue is empty. -/// -/// Arguments: -/// char: u8 - The byte to send. -/// port: Port - The port to send the byte to. -/// -pub fn write(char: u8, port: Port) void { - while (!transmitIsEmpty(port)) { - arch.halt(); +const build_options = @import("build_options"); + +pub const Serial = struct { + /// Function that writes a single byte to the serial stream + pub const Write = fn (byte: u8) void; + + write: Write, + + /// + /// Write a slice of bytes to the serial stream. + /// + /// Arguments: + /// str: []const u8 - The bytes to send. + /// + pub fn writeBytes(self: *const @This(), bytes: []const u8) void { + for (bytes) |byte| { + self.write(byte); + } } - arch.outb(@enumToInt(port), char); -} +}; /// -/// Write a slice of bytes to a serial port. See write for more detailed information. +/// Initialise the serial interface. The details of how this is done depends on the architecture. /// /// Arguments: -/// str: []const u8 - The bytes to send. -/// port: Port - The port to send the bytes to. +/// IN boot_payload: arch.BootPayload - The payload passed to the kernel at boot. How this is used depends on the architecture /// -pub fn writeBytes(str: []const u8, port: Port) void { - for (str) |char| { - write(char, port); - } -} - -/// -/// Initialise a serial port to a certain baudrate -/// -/// Arguments -/// IN baud: u32 - The baudrate to use. Cannot be more than MAX_BAUDRATE -/// IN port: Port - The port to initialise -/// -/// Error: SerialError -/// InvalidBaudRate - The baudrate is 0 or greater than BAUD_MAX. +/// Return: Serial +/// The serial interface constructed by the architecture /// -pub fn init(baud: u32, port: Port) SerialError!void { - // The baudrate is sent as a divisor of the max baud rate - const divisor: u16 = try baudDivisor(baud); - const port_int = @enumToInt(port); - // Send a byte to start setting the baudrate - arch.outb(port_int + LCR, lcrValue(0, false, false, 1) catch |e| { - panic(@errorReturnTrace(), "Failed to initialise serial output setup: {}", .{e}); - }); - // Send the divisor's lsb - arch.outb(port_int, @truncate(u8, divisor)); - // Send the divisor's msb - arch.outb(port_int + 1, @truncate(u8, divisor >> 8)); - // Send the properties to use - arch.outb(port_int + LCR, lcrValue(CHAR_LEN, SINGLE_STOP_BIT, PARITY_BIT, 0) catch |e| panic(@errorReturnTrace(), "Failed to setup serial properties: {}", .{e})); - // Stop initialisation - arch.outb(port_int + 1, 0); - - if (options.rt_test) - runtimeTests(); -} - -test "lcrValue computes the correct value" { - // Check valid combinations - inline for ([_]u8{ 0, 5, 6, 7, 8 }) |char_len| { - inline for ([_]bool{ true, false }) |stop_bit| { - inline for ([_]bool{ true, false }) |parity_bit| { - inline for ([_]u1{ 0, 1 }) |msb| { - const val = try lcrValue(char_len, stop_bit, parity_bit, msb); - const expected = char_len & 0x3 | - @boolToInt(stop_bit) << 2 | - @boolToInt(parity_bit) << 3 | - @intCast(u8, msb) << 7; - testing.expectEqual(val, expected); - } - } - } - } - - // Check invalid char lengths - testing.expectError(SerialError.InvalidCharacterLength, lcrValue(4, false, false, 0)); - testing.expectError(SerialError.InvalidCharacterLength, lcrValue(9, false, false, 0)); -} - -test "baudDivisor" { - // Check invalid baudrates - inline for ([_]u32{ 0, BAUD_MAX + 1 }) |baud| { - testing.expectError(SerialError.InvalidBaudRate, baudDivisor(baud)); - } - - // Check valid baudrates - var baud: u32 = 1; - while (baud <= BAUD_MAX) : (baud += 1) { - const val = try baudDivisor(baud); - const expected = @truncate(u16, BAUD_MAX / baud); - testing.expectEqual(val, expected); +pub fn init(boot_payload: arch.BootPayload) Serial { + const serial = arch.initSerial(boot_payload); + switch (build_options.test_mode) { + .Initialisation => runtimeTests(serial), + else => {}, } + return serial; } /// /// Run all the runtime tests /// -fn runtimeTests() void { - rt_writeByte(); - rt_writeBytes(); +pub fn runtimeTests(serial: Serial) void { + rt_writeByte(serial); + rt_writeBytes(serial); } /// /// Test writing a byte and a new line separately /// -fn rt_writeByte() void { - write('c', Port.COM1); - write('\n', Port.COM1); +fn rt_writeByte(serial: Serial) void { + serial.write('c'); + serial.write('\n'); } /// /// Test writing a series of bytes /// -fn rt_writeBytes() void { - writeBytes(&[_]u8{ '1', '2', '3', '\n' }, Port.COM1); +fn rt_writeBytes(serial: Serial) void { + serial.writeBytes(&[_]u8{ '1', '2', '3', '\n' }); } diff --git a/src/kernel/start.asm b/src/kernel/start.asm deleted file mode 100644 index 30dfe29f..00000000 --- a/src/kernel/start.asm +++ /dev/null @@ -1,10 +0,0 @@ - [bits 32] - [section .text] - - [extern kernel_main] -start: - call kernel_main -halt: - cli - hlt - jmp halt diff --git a/src/kernel/start.s b/src/kernel/start.s deleted file mode 100644 index 1018b7a7..00000000 --- a/src/kernel/start.s +++ /dev/null @@ -1,9 +0,0 @@ -.global _start -.type _start, @function - -_start: - call kmain -halt: - cli - hlt - jmp halt \ No newline at end of file diff --git a/src/kernel/task.zig b/src/kernel/task.zig new file mode 100644 index 00000000..68e479ab --- /dev/null +++ b/src/kernel/task.zig @@ -0,0 +1,209 @@ +const std = @import("std"); +const expectEqual = std.testing.expectEqual; +const expectError = std.testing.expectError; +const builtin = @import("builtin"); +const is_test = builtin.is_test; +const build_options = @import("build_options"); +const mock_path = build_options.mock_path; +const arch = @import("arch.zig").internals; +const log = if (is_test) @import(mock_path ++ "log_mock.zig") else @import("log.zig"); +const panic = if (is_test) @import(mock_path ++ "panic_mock.zig").panic else @import("panic.zig").panic; +const ComptimeBitmap = @import("bitmap.zig").ComptimeBitmap; +const Allocator = std.mem.Allocator; + +/// The kernels main stack start as this is used to check for if the task being destroyed is this stack +/// as we cannot deallocate this. +extern var KERNEL_STACK_START: *u32; + +/// The function type for the entry point. +const EntryPointFn = fn () void; + +/// The bitmap type for the PIDs +const PidBitmap = if (is_test) ComptimeBitmap(u128) else ComptimeBitmap(u1024); + +/// The list of PIDs that have been allocated. +var all_pids: PidBitmap = brk: { + var pids = PidBitmap.init(); + // Set the first PID as this is for the current task running, init 0 + _ = pids.setFirstFree() orelse unreachable; + break :brk pids; +}; + +/// The task control block for storing all the information needed to save and restore a task. +pub const Task = struct { + const Self = @This(); + + /// The unique task identifier + pid: PidBitmap.IndexType, + + /// Pointer to the stack for the task. This will be allocated on initialisation. + stack: []u32, + + /// The current stack pointer into the stack. + stack_pointer: usize, + + /// + /// Create a task. This will allocate a PID and the stack. The stack will be set up as a + /// kernel task. As this is a new task, the stack will need to be initialised with the CPU + /// state as described in arch.CpuState struct. + /// + /// Arguments: + /// IN entry_point: EntryPointFn - The entry point into the task. This must be a function. + /// IN allocator: *Allocator - The allocator for allocating memory for a task. + /// + /// Return: *Task + /// Pointer to an allocated task. This will then need to be added to the task queue. + /// + /// Error: Allocator.Error + /// OutOfMemory - If there is no more memory to allocate. Any memory or PID allocated will + /// be freed on return. + /// + pub fn create(entry_point: EntryPointFn, allocator: *Allocator) Allocator.Error!*Task { + var task = try allocator.create(Task); + errdefer allocator.destroy(task); + + task.pid = allocatePid(); + errdefer freePid(task.pid); + + const task_stack = try arch.initTaskStack(@ptrToInt(entry_point), allocator); + task.stack = task_stack.stack; + task.stack_pointer = task_stack.pointer; + + return task; + } + + /// + /// Destroy the task. This will release the allocated PID and free the stack and self. + /// + /// Arguments: + /// IN/OUT self: *Self - The pointer to self. + /// + pub fn destroy(self: *Self, allocator: *Allocator) void { + freePid(self.pid); + // We need to check that the the stack has been allocated as task 0 (init) won't have a + // stack allocated as this in the linker script + if (@ptrToInt(self.stack.ptr) != @ptrToInt(&KERNEL_STACK_START)) { + allocator.free(self.stack); + } + allocator.destroy(self); + } +}; + +/// +/// Allocate a process identifier. If out of PIDs, then will panic. Is this occurs, will need to +/// increase the bitmap. +/// +/// Return: u32 +/// A new PID. +/// +fn allocatePid() PidBitmap.IndexType { + return all_pids.setFirstFree() orelse panic(@errorReturnTrace(), "Out of PIDs\n", .{}); +} + +/// +/// Free an allocated PID. One must be allocated to be freed. If one wasn't allocated will panic. +/// +/// Arguments: +/// IN pid: u32 - The PID to free. +/// +fn freePid(pid: PidBitmap.IndexType) void { + if (!all_pids.isSet(pid)) { + panic(@errorReturnTrace(), "PID {} not allocated\n", .{pid}); + } + all_pids.clearEntry(pid); +} + +// For testing the errdefer +const FailingAllocator = std.testing.FailingAllocator; +const testing_allocator = &std.testing.base_allocator_instance.allocator; + +fn test_fn1() void {} + +test "create out of memory for task" { + // Set the global allocator + var fa = FailingAllocator.init(testing_allocator, 0); + + expectError(error.OutOfMemory, Task.create(test_fn1, &fa.allocator)); + + // Make sure any memory allocated is freed + expectEqual(fa.allocated_bytes, fa.freed_bytes); + + // Make sure no PIDs were allocated + expectEqual(all_pids.bitmap, 1); +} + +test "create out of memory for stack" { + // Set the global allocator + var fa = FailingAllocator.init(testing_allocator, 1); + + expectError(error.OutOfMemory, Task.create(test_fn1, &fa.allocator)); + + // Make sure any memory allocated is freed + expectEqual(fa.allocated_bytes, fa.freed_bytes); + + // Make sure no PIDs were allocated + expectEqual(all_pids.bitmap, 1); +} + +test "create expected setup" { + var task = try Task.create(test_fn1, std.testing.allocator); + defer task.destroy(std.testing.allocator); + + // Will allocate the first PID 1, 0 will always be allocated + expectEqual(task.pid, 1); +} + +test "destroy cleans up" { + // This used the leak detector allocator in testing + // So if any alloc were not freed, this will fail the test + var fa = FailingAllocator.init(testing_allocator, 2); + + var task = try Task.create(test_fn1, &fa.allocator); + + task.destroy(&fa.allocator); + + // Make sure any memory allocated is freed + expectEqual(fa.allocated_bytes, fa.freed_bytes); + + // All PIDs were freed + expectEqual(all_pids.bitmap, 1); +} + +test "Multiple create" { + var task1 = try Task.create(test_fn1, std.testing.allocator); + var task2 = try Task.create(test_fn1, std.testing.allocator); + + expectEqual(task1.pid, 1); + expectEqual(task2.pid, 2); + expectEqual(all_pids.bitmap, 7); + + task1.destroy(std.testing.allocator); + + expectEqual(all_pids.bitmap, 5); + + var task3 = try Task.create(test_fn1, std.testing.allocator); + + expectEqual(task3.pid, 1); + expectEqual(all_pids.bitmap, 7); + + task2.destroy(std.testing.allocator); + task3.destroy(std.testing.allocator); +} + +test "allocatePid and freePid" { + expectEqual(all_pids.bitmap, 1); + + var i: usize = 1; + while (i < PidBitmap.NUM_ENTRIES) : (i += 1) { + expectEqual(i, allocatePid()); + } + + expectEqual(all_pids.bitmap, PidBitmap.BITMAP_FULL); + + i = 0; + while (i < PidBitmap.NUM_ENTRIES) : (i += 1) { + freePid(@truncate(PidBitmap.IndexType, i)); + } + + expectEqual(all_pids.bitmap, 0); +} diff --git a/src/kernel/tty.zig b/src/kernel/tty.zig index e093b0f7..eb6385e8 100644 --- a/src/kernel/tty.zig +++ b/src/kernel/tty.zig @@ -1,434 +1,44 @@ -const builtin = @import("builtin"); -const is_test = builtin.is_test; const std = @import("std"); const fmt = std.fmt; -const expect = std.testing.expect; -const expectEqual = std.testing.expectEqual; -const expectError = std.testing.expectError; +const Allocator = std.mem.Allocator; const build_options = @import("build_options"); -const mock_path = build_options.mock_path; -const vga = if (is_test) @import(mock_path ++ "vga_mock.zig") else @import("vga.zig"); -const log = if (is_test) @import(mock_path ++ "log_mock.zig") else @import("log.zig"); -const panic = if (is_test) @import(mock_path ++ "panic_mock.zig").panic else @import("panic.zig").panic; - -/// The error set for if there is an error whiles printing. -const TtyError = error{ - /// If the printing tries to print outside the video buffer. - OutOfBounds, -}; - -/// The number of rows down from the top (row 0) where the displayable region starts. Above is -/// where the logo and time is printed -const ROW_MIN: u16 = 7; - -/// The total number of rows in the displayable region -const ROW_TOTAL: u16 = vga.HEIGHT - ROW_MIN; - -/// The total number of pages (static) that the terminal will remember. In the future, this can -/// move to a more dynamic allocation when a kheap is implemented. -const TOTAL_NUM_PAGES: u16 = 5; - -/// The total number of VGA (or characters) elements are on a page -const TOTAL_CHAR_ON_PAGE: u16 = vga.WIDTH * ROW_TOTAL; - -/// The start of the displayable region in the video buffer memory -const START_OF_DISPLAYABLE_REGION: u16 = vga.WIDTH * ROW_MIN; - -/// The total number of VGA elements (or characters) the video buffer can display -const VIDEO_BUFFER_SIZE: u16 = vga.WIDTH * vga.HEIGHT; - -/// The location of the kernel in virtual memory so can calculate the address of the VGA buffer -extern var KERNEL_ADDR_OFFSET: *u32; - -/// The current x position of the cursor. -var column: u8 = 0; - -/// The current y position of the cursor. -var row: u8 = 0; - -/// The current colour of the display with foreground and background colour. -var colour: u8 = undefined; - -/// The buffer starting from the beginning of the video memory location that contains all data -/// written to the display. -var video_buffer: []volatile u16 = undefined; - -/// The blank VGA entry to be used to clear the screen. -var blank: u16 = undefined; - -/// A total of TOTAL_NUM_PAGES pages that can be saved and restored to from and to the video buffer -var pages: [TOTAL_NUM_PAGES][TOTAL_CHAR_ON_PAGE]u16 = init: { - var p: [TOTAL_NUM_PAGES][TOTAL_CHAR_ON_PAGE]u16 = undefined; - - for (p) |*page| { - page.* = [_]u16{0} ** TOTAL_CHAR_ON_PAGE; - } - - break :init p; +const arch = @import("arch.zig").internals; +const log = @import("log.zig"); +const panic = @import("panic.zig").panic; + +/// The OutStream for the format function +const OutStream = std.io.OutStream(void, anyerror, printCallback); + +pub const TTY = struct { + /// Print a already-formatted string + print: fn ([]const u8) anyerror!void, + /// Set the TTY cursor position to a row and column + setCursor: fn (u8, u8) void, + /// Clear the screen and set the cursor to top left. The default implementation will be used if null + clear: ?fn () void, + /// The number of character rows supported + rows: u8, + /// The number of character columns supported + cols: u8, }; -/// The current page index. -var page_index: u8 = 0; - -/// -/// Copies data into the video buffer. This is used for copying a page into the video buffer. -/// -/// Arguments: -/// IN video_buf_offset: u16 - The offset into the video buffer to start copying to. -/// IN data: []const u16 - The data to copy into the video buffer. -/// IN size: u16 - The amount to copy. -/// -/// Errors: TtyError -/// TtyError.OutOfBounds - If offset or the size to copy is greater than the size of the -/// video buffer or data to copy. -/// -fn videoCopy(video_buf_offset: u16, data: []const u16, size: u16) TtyError!void { - // Secure programming ;) - if (video_buf_offset >= video_buffer.len and - size > video_buffer.len - video_buf_offset and - size > data.len) - { - return TtyError.OutOfBounds; - } - - var i: u32 = 0; - while (i < size) : (i += 1) { - video_buffer[video_buf_offset + i] = data[i]; - } -} - -/// -/// Moves data with a page without overriding itself. -/// -/// Arguments: -/// IN dest: []u16 - The destination position to copy into. -/// IN src: []u16 - The source position to copy from. -/// IN size: u16 - The amount to copy. -/// -/// Errors: -/// TtyError.OutOfBounds - If the size to copy is greater than the size of the pages. -/// -fn pageMove(dest: []u16, src: []u16, size: u16) TtyError!void { - if (dest.len < size or src.len < size) { - return TtyError.OutOfBounds; - } - - // Not an error if size is zero, nothing will be copied - if (size == 0) return; - - // Make sure we don't override the values we want to copy - if (@ptrToInt(&dest[0]) < @ptrToInt(&src[0])) { - var i: u16 = 0; - while (i != size) : (i += 1) { - dest[i] = src[i]; - } - } else { - var i = size; - while (i != 0) { - i -= 1; - dest[i] = src[i]; - } - } -} - -/// -/// Clears a region of the video buffer to a VGA entry from the beginning. -/// -/// Arguments: -/// IN c: u16 - VGA entry to set the video buffer to. -/// IN size: u16 - The number to VGA entries to set from the beginning of the video buffer. -/// -/// Errors: -/// TtyError.OutOfBounds - If the size to copy is greater than the size of the video buffer. -/// -fn setVideoBuffer(c: u16, size: u16) TtyError!void { - if (size > VIDEO_BUFFER_SIZE) { - return TtyError.OutOfBounds; - } - - for (video_buffer[0..size]) |*b| { - b.* = c; - } -} - -/// -/// Updated the hardware cursor to the current column and row (x, y). -/// -fn updateCursor() void { - vga.updateCursor(column, row); -} - -/// -/// Get the hardware cursor and set the current column and row (x, y). -/// -fn getCursor() void { - const cursor = vga.getCursor(); - - row = @truncate(u8, cursor / vga.WIDTH); - column = @truncate(u8, cursor % vga.WIDTH); -} - -/// -/// Display the current page number at the bottom right corner. If there was an error with this, -/// then the page number may not be printed and a error log will be emitted. -/// -fn displayPageNumber() void { - const column_temp = column; - const row_temp = row; - - defer column = column_temp; - defer row = row_temp; - - var text_buf = [_]u8{0} ** vga.WIDTH; - - // Formate the page number string so can work out the right alignment. - const fmt_text = fmt.bufPrint(text_buf[0..], "Page {} of {}", .{ page_index, TOTAL_NUM_PAGES - 1 }) catch |e| { - log.logError("TTY: Unable to print page number, buffer too small. Error: {}\n", .{e}); - return; - }; - - // TODO: #89 TTY - print string with alignment - // When print a string with alignment is available, can remove. - // But for now we can calculate the alignment. - column = @truncate(u8, vga.WIDTH) - @truncate(u8, fmt_text.len); - row = ROW_MIN - 1; - - writeString(fmt_text) catch |e| { - log.logError("TTY: Unable to print page number, printing out of bounds. Error: {}\n", .{e}); - }; -} - -/// -/// Put a character at a specific column and row position on the screen. This will use the current -/// colour. -/// -/// Arguments: -/// IN char: u8 - The character to print. This will be combined with the current colour. -/// IN x: u8 - The x position (column) to put the character at. -/// IN y: u8 - The y position (row) to put the character at. -/// -/// Errors: -/// TtyError.OutOfBounds - If trying to print outside the video buffer. -/// -fn putEntryAt(char: u8, x: u8, y: u8) TtyError!void { - const index = y * vga.WIDTH + x; - - // Bounds check - if (index >= VIDEO_BUFFER_SIZE) { - return TtyError.OutOfBounds; - } - - const char_entry = vga.entry(char, colour); - - if (index >= START_OF_DISPLAYABLE_REGION) { - // If not at page zero, (bottom of page), then display that page - // The user has move up a number of pages and then typed a letter, so need to move to the - // 0'th page - if (page_index != 0) { - // This isn't out of bounds - page_index = 0; - try videoCopy(START_OF_DISPLAYABLE_REGION, pages[page_index][0..TOTAL_CHAR_ON_PAGE], TOTAL_CHAR_ON_PAGE); - displayPageNumber(); - - // If not on page 0, then the cursor would have been disabled - vga.enableCursor(); - updateCursor(); - } - pages[page_index][index - START_OF_DISPLAYABLE_REGION] = char_entry; - } - - video_buffer[index] = char_entry; -} - -/// -/// Move rows up pages across multiple pages leaving the last rows blank. -/// -/// Arguments: -/// IN rows: u16 - The number of rows to move up. -/// -/// Errors: -/// TtyError.OutOfBounds - If trying to move up more rows on a page. -/// -fn pagesMoveRowsUp(rows: u16) TtyError!void { - // Out of bounds check - if (rows > ROW_TOTAL) { - return TtyError.OutOfBounds; - } - - // Not an error to move 0 rows, but is pointless - if (rows == 0) return; - - // Move up rows in last page up by "rows" - const row_length = rows * vga.WIDTH; - const chars_to_move = (ROW_TOTAL - rows) * vga.WIDTH; - try pageMove(pages[TOTAL_NUM_PAGES - 1][0..chars_to_move], pages[TOTAL_NUM_PAGES - 1][row_length..], chars_to_move); - - // Loop for the other pages - var i = TOTAL_NUM_PAGES - 1; - while (i > 0) : (i -= 1) { - try pageMove(pages[i][chars_to_move..], pages[i - 1][0..row_length], row_length); - try pageMove(pages[i - 1][0..chars_to_move], pages[i - 1][row_length..], chars_to_move); - } - - // Clear the last lines - for (pages[0][chars_to_move..]) |*p| { - p.* = blank; - } -} - -/// -/// When the text/terminal gets to the bottom of the screen, then move all line up by the amount -/// that are below the bottom of the screen. Usually moves up by one line. -/// -/// Errors: -/// TtyError.OutOfBounds - If trying to move up more rows on a page. This shouldn't happen -/// as bounds checks have been done. -/// -fn scroll() void { - // Added the condition in the if from pagesMoveRowsUp as don't need to move all rows - if (row >= vga.HEIGHT and (row - vga.HEIGHT + 1) <= ROW_TOTAL) { - const rows_to_move = row - vga.HEIGHT + 1; - - // Move rows up pages by temp, will usually be one. - // TODO: Maybe panic here as we have the check above, so if this fails, then is a big problem - pagesMoveRowsUp(rows_to_move) catch |e| { - panic(@errorReturnTrace(), "Can't move {} rows up. Must be less than {}\n", .{ rows_to_move, ROW_TOTAL }); - }; - - // Move all rows up by rows_to_move - var i: u32 = 0; - while (i < (ROW_TOTAL - rows_to_move) * vga.WIDTH) : (i += 1) { - video_buffer[START_OF_DISPLAYABLE_REGION + i] = video_buffer[(rows_to_move * vga.WIDTH) + START_OF_DISPLAYABLE_REGION + i]; - } - - // Set the last rows to blanks - i = 0; - while (i < vga.WIDTH * rows_to_move) : (i += 1) { - video_buffer[(vga.HEIGHT - rows_to_move) * vga.WIDTH + i] = blank; - } - - row = vga.HEIGHT - 1; - } -} - -/// -/// Print a character without updating the cursor. For speed when printing a string as only need to -/// update the cursor once. This will also print the special characters: \n, \r, \t and \b. (\b is -/// not a valid character so use \x08 which is the hex value). -/// -/// Arguments: -/// IN char: u8 - The character to print. -/// -/// Errors: -/// TtyError.OutOfBounds - If trying to scroll more rows on a page/displayable region or -/// print beyond the video buffer. -/// -fn putChar(char: u8) TtyError!void { - const column_temp = column; - const row_temp = row; - - // If there was an error, then set the row and column back to where is was - // Like nothing happened - errdefer column = column_temp; - errdefer row = row_temp; - - switch (char) { - '\n' => { - column = 0; - row += 1; - scroll(); - }, - '\t' => { - column += 4; - if (column >= vga.WIDTH) { - column -= @truncate(u8, vga.WIDTH); - row += 1; - scroll(); - } - }, - '\r' => { - column = 0; - }, - // \b - '\x08' => { - if (column == 0) { - if (row != 0) { - column = vga.WIDTH - 1; - row -= 1; - } - } else { - column -= 1; - } - }, - else => { - try putEntryAt(char, column, row); - column += 1; - if (column == vga.WIDTH) { - column = 0; - row += 1; - scroll(); - } - }, - } -} - -/// -/// Print a string to the TTY. This also updates to hardware cursor. -/// -/// Arguments: -/// IN str: []const u8 - The string to print. -/// -/// Errors: -/// TtyError.OutOfBounds - If trying to print beyond the video buffer. -/// -fn writeString(str: []const u8) TtyError!void { - // Make sure we update the cursor to the last character - defer updateCursor(); - for (str) |char| { - try putChar(char); - } -} - -/// -/// Print the pluto logo. -/// -fn printLogo() void { - const column_temp = column; - const row_temp = row; - - defer column = column_temp; - defer row = row_temp; - - const logo = - \\ _____ _ _ _ _______ ____ - \\ | __ \ | | | | | | |__ __| / __ \ - \\ | |__) | | | | | | | | | | | | | - \\ | ___/ | | | | | | | | | | | | - \\ | | | |____ | |__| | | | | |__| | - \\ |_| |______| \____/ |_| \____/ - ; - - // Print the logo at the top of the screen - column = 0; - row = 0; - - writeString(logo) catch |e| { - log.logError("TTY: Error print logo. Error {}\n", .{e}); - }; -} +/// The current tty stream +var tty: TTY = undefined; +var allocator: *Allocator = undefined; /// -/// A call back function for use in the formation of a string. This calls writeString normally. +/// A call back function for use in the formation of a string. This calls the architecture's print function. /// /// Arguments: /// IN ctx: void - The context of the printing. This will be empty. /// IN str: []const u8 - The string to print. /// -/// Errors: -/// TtyError.OutOfBounds - If trying to print beyond the video buffer. +/// Return: usize +/// The number of characters printed /// -fn printCallback(ctx: void, str: []const u8) TtyError!void { - try writeString(str); +fn printCallback(ctx: void, str: []const u8) !usize { + tty.print(str) catch |e| panic(@errorReturnTrace(), "Failed to print to tty: {}\n", .{e}); + return str.len; } /// @@ -437,1765 +47,63 @@ fn printCallback(ctx: void, str: []const u8) TtyError!void { /// /// Arguments: /// IN comptime format: []const u8 - The format string to print -/// IN args: var - The arguments to be used in the formatted string +/// IN args: anytype - The arguments to be used in the formatted string /// -pub fn print(comptime format: []const u8, args: var) void { +pub fn print(comptime format: []const u8, args: anytype) void { // Printing can't error because of the scrolling, if it does, we have a big problem - fmt.format({}, TtyError, printCallback, format, args) catch |e| { + fmt.format(OutStream{ .context = {} }, format, args) catch |e| { log.logError("TTY: Error printing. Error: {}\n", .{e}); }; } /// -/// Move up a page. This will copy the page above to the video buffer. Will keep trace of which -/// page is being displayed. +/// Clear the screen by printing a space at each cursor position. Sets the cursor to the top left (0, 0) /// -pub fn pageUp() void { - if (page_index < TOTAL_NUM_PAGES - 1) { - // Copy page to display - page_index += 1; - // Bounds have been checked, so shouldn't error - videoCopy(START_OF_DISPLAYABLE_REGION, pages[page_index][0..TOTAL_CHAR_ON_PAGE], TOTAL_CHAR_ON_PAGE) catch |e| { - log.logError("TTY: Error moving page up. Error: {}\n", .{e}); - }; - displayPageNumber(); - vga.disableCursor(); - } -} - -/// -/// Move down a page. This will copy the page bellow to the video buffer. Will keep trace of which -/// page is being displayed. -/// -pub fn pageDown() void { - if (page_index > 0) { - // Copy page to display - page_index -= 1; - // Bounds have been checked, so shouldn't error - videoCopy(START_OF_DISPLAYABLE_REGION, pages[page_index][0..TOTAL_CHAR_ON_PAGE], TOTAL_CHAR_ON_PAGE) catch |e| { - log.logError("TTY: Error moving page down. Error: {}\n", .{e}); +pub fn clear() void { + if (tty.clear) |clr| { + clr(); + } else { + // Try to allocate the number of spaces for a whole row to avoid calling print too many times + var spaces = allocator.alloc(u8, tty.cols + 1) catch |e| switch (e) { + Allocator.Error.OutOfMemory => { + var row: u8 = 0; + // If we can't allocate the spaces then try the unoptimised way instead + while (row < tty.rows) : (row += 1) { + var col: u8 = 0; + while (col < tty.cols) : (col += 1) { + print(" ", .{}); + } + print("\n", .{}); + } + tty.setCursor(0, 0); + return; + }, }; + defer allocator.free(spaces); - displayPageNumber(); - if (page_index == 0) { - vga.enableCursor(); - updateCursor(); - } else { - vga.disableCursor(); - } - } -} - -/// -/// This clears the entire screen with blanks using the current colour. This will also save the -/// screen to the pages so can scroll back down. -/// -pub fn clearScreen() void { - // Move all the rows up - // This is within bounds, so shouldn't error - pagesMoveRowsUp(ROW_TOTAL) catch |e| { - log.logError("TTY: Error moving all pages up. Error: {}\n", .{e}); - }; - - // Clear the screen - var i: u16 = START_OF_DISPLAYABLE_REGION; - while (i < VIDEO_BUFFER_SIZE) : (i += 1) { - video_buffer[i] = blank; - } - - // Set the cursor to below the logo - column = 0; - row = ROW_MIN; - updateCursor(); -} - -/// -/// This moves the software and hardware cursor to the left by one. -/// -pub fn moveCursorLeft() void { - if (column == 0) { - if (row != 0) { - column = vga.WIDTH - 1; - row -= 1; + var col: u8 = 0; + while (col < tty.cols) : (col += 1) { + spaces[col] = " "[0]; } - } else { - column -= 1; - } - - updateCursor(); -} - -/// -/// This moves the software and hardware cursor to the right by one. -/// -pub fn moveCursorRight() void { - if (column == (vga.WIDTH - 1)) { - if (row != (vga.HEIGHT - 1)) { - column = 0; - row += 1; + spaces[col] = "\n"[0]; + var row: u8 = 0; + while (row < tty.rows) : (row += 1) { + print("{}", .{spaces}); } - } else { - column += 1; + tty.setCursor(0, 0); } - - updateCursor(); } /// -/// This will set a new colour for the screen. It will only become effective when printing new -/// characters. Use vga.colourEntry and the colour enums to set the colour. +/// Initialise the TTY. The details of which are up to the architecture /// /// Arguments: -/// IN new_colour: u8 - The new foreground and background colour of the screen. -/// -pub fn setColour(new_colour: u8) void { - colour = new_colour; - blank = vga.entry(0, colour); -} - -/// -/// Gets the video buffer's virtual address. -/// -/// Return: usize -/// The virtual address of the video buffer -/// -pub fn getVideoBufferAddress() usize { - return @ptrToInt(&KERNEL_ADDR_OFFSET) + 0xB8000; -} - +/// IN alloc: *std.mem.Allocator - The allocator to use when requiring memory +/// IN boot_payload: arch.BootPayload - The payload passed to the kernel on boot /// -/// Initialise the tty. This will keep the bootloaders output and set the software cursor to where -/// the bootloader left it. Will copy the current screen to the pages, set the colour and blank -/// entry, print the logo and display the 0'th page. -/// -pub fn init() void { +pub fn init(alloc: *Allocator, boot_payload: arch.BootPayload) void { log.logInfo("Init tty\n", .{}); - - // Video buffer in higher half - if (is_test) { - video_buffer = @intToPtr([*]volatile u16, mock_getVideoBufferAddress())[0..VIDEO_BUFFER_SIZE]; - } else { - video_buffer = @intToPtr([*]volatile u16, getVideoBufferAddress())[0..VIDEO_BUFFER_SIZE]; - } - - setColour(vga.entryColour(vga.COLOUR_LIGHT_GREY, vga.COLOUR_BLACK)); - - // Enable and get the hardware cursor to set the software cursor - vga.enableCursor(); - getCursor(); - - if (row != 0 or column != 0) { - // Copy rows 7 down to make room for logo - // If there isn't enough room, only take the bottom rows - var row_offset: u16 = 0; - if (vga.HEIGHT - 1 - row < ROW_MIN) { - row_offset = ROW_MIN - (vga.HEIGHT - 1 - row); - } - - // Make a copy into the pages - // Assuming that there is only one page - var i: u16 = 0; - while (i < row * vga.WIDTH) : (i += 1) { - pages[0][i] = video_buffer[i]; - } - - // Move 7 rows down - i = 0; - if (@ptrToInt(&video_buffer[ROW_MIN * vga.WIDTH]) < @ptrToInt(&video_buffer[row_offset * vga.WIDTH])) { - while (i != row * vga.WIDTH) : (i += 1) { - video_buffer[i + (ROW_MIN * vga.WIDTH)] = video_buffer[i + (row_offset * vga.WIDTH)]; - } - } else { - i = row * vga.WIDTH; - while (i != 0) { - i -= 1; - video_buffer[i + (ROW_MIN * vga.WIDTH)] = video_buffer[i + (row_offset * vga.WIDTH)]; - } - } - - // Set the top 7 rows blank - setVideoBuffer(blank, START_OF_DISPLAYABLE_REGION) catch |e| { - log.logError("TTY: Error clearing the top 7 rows. Error: {}\n", .{e}); - }; - row += @truncate(u8, row_offset + ROW_MIN); - } else { - // Clear the screen - setVideoBuffer(blank, VIDEO_BUFFER_SIZE) catch |e| { - log.logError("TTY: Error clearing the screen. Error: {}\n", .{e}); - }; - // Set the row to below the logo - row = ROW_MIN; - } - - printLogo(); - displayPageNumber(); - updateCursor(); - - log.logInfo("Done\n", .{}); - - if (build_options.rt_test) runtimeTests(); -} - -const test_colour: u8 = vga.orig_entryColour(vga.COLOUR_LIGHT_GREY, vga.COLOUR_BLACK); -var test_video_buffer: [VIDEO_BUFFER_SIZE]u16 = [_]u16{0} ** VIDEO_BUFFER_SIZE; - -fn mock_getVideoBufferAddress() usize { - return @ptrToInt(&test_video_buffer); -} - -fn resetGlobals() void { - column = 0; - row = 0; - page_index = 0; - colour = undefined; - video_buffer = undefined; - blank = undefined; - - pages = init: { - var p: [TOTAL_NUM_PAGES][TOTAL_CHAR_ON_PAGE]u16 = undefined; - - for (p) |*page| { - page.* = [_]u16{0} ** TOTAL_CHAR_ON_PAGE; - } - - break :init p; - }; -} - -fn setUpVideoBuffer() void { - // Change to a stack location - video_buffer = test_video_buffer[0..VIDEO_BUFFER_SIZE]; - - expectEqual(@ptrToInt(video_buffer.ptr), @ptrToInt(&test_video_buffer[0])); - - colour = test_colour; - blank = vga.orig_entry(0, test_colour); -} - -fn setVideoBufferBlankPages() void { - setUpVideoBuffer(); - for (video_buffer) |*b| { - b.* = blank; - } - - setPagesBlank(); -} - -fn setVideoBufferIncrementingBlankPages() void { - setUpVideoBuffer(); - for (video_buffer) |*b, i| { - b.* = @intCast(u16, i); - } - - setPagesBlank(); -} - -fn setPagesBlank() void { - for (pages) |*p_i| { - for (p_i) |*p_j| { - p_j.* = blank; - } - } -} - -fn setPagesIncrementing() void { - for (pages) |*p_i, i| { - for (p_i) |*p_j, j| { - p_j.* = @intCast(u16, i) * TOTAL_CHAR_ON_PAGE + @intCast(u16, j); - } - } -} - -fn defaultVariablesTesting(p_i: u8, r: u8, c: u8) void { - expectEqual(test_colour, colour); - expectEqual(@as(u16, test_colour) << 8, blank); - expectEqual(p_i, page_index); - expectEqual(r, row); - expectEqual(c, column); -} - -fn incrementingPagesTesting() void { - for (pages) |p_i, i| { - for (p_i) |p_j, j| { - expectEqual(i * TOTAL_CHAR_ON_PAGE + j, p_j); - } - } -} - -fn blankPagesTesting() void { - for (pages) |p_i| { - for (p_i) |p_j| { - expectEqual(blank, p_j); - } - } -} - -fn incrementingVideoBufferTesting() void { - for (video_buffer) |b, i| { - expectEqual(i, b); - } -} - -fn defaultVideoBufferTesting() void { - for (video_buffer) |b| { - expectEqual(vga.orig_entry(0, test_colour), b); - } -} - -fn defaultAllTesting(p_i: u8, r: u8, c: u8) void { - defaultVariablesTesting(p_i, r, c); - blankPagesTesting(); - defaultVideoBufferTesting(); -} - -test "updateCursor" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga.updateCursor call for updating the hardware cursor - vga.initTest(); - defer vga.freeTest(); - - vga.addTestParams("updateCursor", .{ @as(u16, 0), @as(u16, 0) }); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - updateCursor(); - - // Post test - defaultAllTesting(0, 0, 0); - - // Tear down - resetGlobals(); -} - -test "getCursor zero" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga.getCursor call for getting the hardware cursor - vga.initTest(); - defer vga.freeTest(); - - vga.addTestParams("getCursor", .{@as(u16, 0)}); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - getCursor(); - - // Post test - defaultAllTesting(0, 0, 0); - - // Tear down - resetGlobals(); -} - -test "getCursor EEF" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga.getCursor call for getting the hardware cursor - vga.initTest(); - defer vga.freeTest(); - - vga.addTestParams("getCursor", .{@as(u16, 0x0EEF)}); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - getCursor(); - - // Post test - defaultAllTesting(0, 47, 63); - - // Tear down - resetGlobals(); -} - -test "displayPageNumber column and row is reset" { - // Set up - setVideoBufferBlankPages(); - column = 5; - row = 6; - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - defaultAllTesting(0, 6, 5); - - // Call function - displayPageNumber(); - - // Post test - defaultVariablesTesting(0, 6, 5); - - const text = "Page 0 of 4"; - - // Test both video and pages for page number 0 - for (video_buffer) |b, i| { - if (i < START_OF_DISPLAYABLE_REGION - 11) { - expectEqual(blank, b); - } else if (i < START_OF_DISPLAYABLE_REGION) { - expectEqual(vga.orig_entry(text[i + 11 - START_OF_DISPLAYABLE_REGION], colour), b); - } else { - expectEqual(blank, b); - } - } - - // Tear down - resetGlobals(); -} - -test "putEntryAt out of bounds" { - // Set up - setVideoBufferBlankPages(); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - expectError(TtyError.OutOfBounds, putEntryAt('A', 100, 100)); - - // Post test - defaultAllTesting(0, 0, 0); - - // Tear down - resetGlobals(); -} - -test "putEntryAt not in displayable region" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Enable and update cursor is only called once, can can use the consume function call - //vga.addConsumeFunction("enableCursor", vga.mock_enableCursor); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - const x = 0; - const y = 0; - const char = 'A'; - try putEntryAt(char, x, y); - - // Post test - defaultVariablesTesting(0, 0, 0); - blankPagesTesting(); - - for (video_buffer) |b, i| { - if (i == y * vga.WIDTH + x) { - expectEqual(vga.orig_entry(char, test_colour), b); - } else { - expectEqual(vga.orig_entry(0, test_colour), b); - } - } - - // Tear down - resetGlobals(); -} - -test "putEntryAt in displayable region page_index is 0" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - const x = 0; - const y = ROW_MIN; - const char = 'A'; - try putEntryAt(char, x, y); - - // Post test - defaultVariablesTesting(0, 0, 0); - for (pages) |page, i| { - for (page) |c, j| { - if (i == page_index and (j == (y * vga.WIDTH + x) - START_OF_DISPLAYABLE_REGION)) { - expectEqual(vga.orig_entry(char, test_colour), c); - } else { - expectEqual(blank, c); - } - } - } - - for (video_buffer) |b, i| { - if (i == y * vga.WIDTH + x) { - expectEqual(vga.orig_entry(char, test_colour), b); - } else { - expectEqual(vga.orig_entry(0, test_colour), b); - } - } - - // Tear down - resetGlobals(); -} - -test "putEntryAt in displayable region page_index is not 0" { - // Set up - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Enable and update cursor is only called once, can can use the consume function call - vga.addConsumeFunction("enableCursor", vga.mock_enableCursor); - - setVideoBufferBlankPages(); - - // Fill the 1'nd page (index 1) will all 1's - const ones = vga.orig_entry('1', test_colour); - for (pages) |*page, i| { - for (page) |*char| { - if (i == 0) { - char.* = ones; - } else { - char.* = 0; - } - } - } - - page_index = 1; - - // Pre testing - defaultVariablesTesting(1, 0, 0); - defaultVideoBufferTesting(); - - for (pages) |page, i| { - for (page) |char| { - if (i == 0) { - expectEqual(ones, char); - } else { - expectEqual(@as(u16, 0), char); - } - } - } - - // Call function - const x = 0; - const y = ROW_MIN; - const char = 'A'; - try putEntryAt(char, x, y); - - // Post test - defaultVariablesTesting(0, 0, 0); - - const text = "Page 0 of 4"; - - for (pages) |page, i| { - for (page) |c, j| { - if (i == 0 and j == 0) { - expectEqual(vga.orig_entry(char, test_colour), c); - } else if (i == 0) { - expectEqual(ones, c); - } else { - expectEqual(@as(u16, 0), c); - } - } - } - - // The top 7 rows won't be copied - for (video_buffer) |b, i| { - if (i < START_OF_DISPLAYABLE_REGION - 11) { - expectEqual(blank, b); - } else if (i < START_OF_DISPLAYABLE_REGION) { - expectEqual(vga.orig_entry(text[i + 11 - START_OF_DISPLAYABLE_REGION], colour), b); - } else if (i == y * vga.WIDTH + x) { - expectEqual(vga.orig_entry(char, test_colour), b); - } else { - expectEqual(ones, b); - } - } - - // Tear down - resetGlobals(); -} - -test "pagesMoveRowsUp out of bounds" { - // Set up - setVideoBufferBlankPages(); - setPagesIncrementing(); - - // Pre testing - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - incrementingPagesTesting(); - - // Call function - const rows_to_move = ROW_TOTAL + 1; - expectError(TtyError.OutOfBounds, pagesMoveRowsUp(rows_to_move)); - - // Post test - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - incrementingPagesTesting(); - - // Tear down - resetGlobals(); -} - -test "pagesMoveRowsUp 0 rows" { - // Set up - setVideoBufferBlankPages(); - setPagesIncrementing(); - - // Pre testing - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - incrementingPagesTesting(); - - // Call function - const rows_to_move = 0; - try pagesMoveRowsUp(rows_to_move); - - // Post test - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - incrementingPagesTesting(); - - // Tear down - resetGlobals(); -} - -test "pagesMoveRowsUp 1 rows" { - // Set up - setVideoBufferBlankPages(); - setPagesIncrementing(); - - // Pre testing - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - incrementingPagesTesting(); - - // Call function - const rows_to_move = 1; - try pagesMoveRowsUp(rows_to_move); - - // Post test - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - - const to_add = rows_to_move * vga.WIDTH; - for (pages) |page, i| { - for (page) |c, j| { - if (j >= TOTAL_CHAR_ON_PAGE - to_add) { - if (i == 0) { - // The last rows will be blanks - expectEqual(blank, c); - } else { - expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + (j + to_add - TOTAL_CHAR_ON_PAGE), c); - } - } else { - // All rows moved up one, so add vga.WIDTH - expectEqual(i * TOTAL_CHAR_ON_PAGE + j + to_add, c); - } - } - } - - // Tear down - resetGlobals(); -} - -test "pagesMoveRowsUp ROW_TOTAL - 1 rows" { - // Set up - setVideoBufferBlankPages(); - setPagesIncrementing(); - - // Pre testing - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - incrementingPagesTesting(); - - // Call function - const rows_to_move = ROW_TOTAL - 1; - try pagesMoveRowsUp(rows_to_move); - - // Post test - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - - const to_add = rows_to_move * vga.WIDTH; - for (pages) |page, i| { - for (page) |c, j| { - if (j >= TOTAL_CHAR_ON_PAGE - to_add) { - if (i == 0) { - // The last rows will be blanks - expectEqual(blank, c); - } else { - expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + (j + to_add - TOTAL_CHAR_ON_PAGE), c); - } - } else { - // All rows moved up one, so add vga.WIDTH - expectEqual(i * TOTAL_CHAR_ON_PAGE + j + to_add, c); - } - } - } - - // Tear down - resetGlobals(); -} - -test "pagesMoveRowsUp ROW_TOTAL rows" { - // Set up - setVideoBufferBlankPages(); - setPagesIncrementing(); - - // Pre testing - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - incrementingPagesTesting(); - - // Call function - const rows_to_move = ROW_TOTAL; - try pagesMoveRowsUp(rows_to_move); - - // Post test - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - - for (pages) |page, i| { - for (page) |c, j| { - if (i == 0) { - // The last rows will be blanks - expectEqual(blank, c); - } else { - expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + j, c); - } - } - } - - // Tear down - resetGlobals(); -} - -test "scroll row is less then max height" { - // Set up - setVideoBufferBlankPages(); - setPagesIncrementing(); - - // Pre testing - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - incrementingPagesTesting(); - - // Call function - scroll(); - - // Post test - defaultVariablesTesting(0, 0, 0); - defaultVideoBufferTesting(); - incrementingPagesTesting(); - - // Tear down - resetGlobals(); -} - -test "scroll row is equal to height" { - // Set up - setVideoBufferIncrementingBlankPages(); - setPagesIncrementing(); - - const row_test = vga.HEIGHT; - row = row_test; - - // Pre testing - defaultVariablesTesting(0, row_test, 0); - incrementingPagesTesting(); - incrementingVideoBufferTesting(); - - // Call function - // Rows move up one - scroll(); - - // Post test - defaultVariablesTesting(0, vga.HEIGHT - 1, 0); - - const to_add = (row_test - vga.HEIGHT + 1) * vga.WIDTH; - for (pages) |page, i| { - for (page) |c, j| { - if (j >= TOTAL_CHAR_ON_PAGE - to_add) { - if (i == 0) { - // The last rows will be blanks - expectEqual(blank, c); - } else { - expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + (j + to_add - TOTAL_CHAR_ON_PAGE), c); - } - } else { - // All rows moved up one, so add vga.WIDTH - expectEqual(i * TOTAL_CHAR_ON_PAGE + j + to_add, c); - } - } - } - - for (video_buffer) |buf, i| { - if (i < START_OF_DISPLAYABLE_REGION) { - expectEqual(i, buf); - } else if (i >= VIDEO_BUFFER_SIZE - to_add) { - expectEqual(blank, buf); - } else { - expectEqual(i + to_add, buf); - } - } - - // Tear down - resetGlobals(); -} - -test "scroll row is more than height" { - // Set up - setVideoBufferIncrementingBlankPages(); - setPagesIncrementing(); - - const row_test = vga.HEIGHT + 5; - row = row_test; - - // Pre testing - defaultVariablesTesting(0, row_test, 0); - incrementingPagesTesting(); - incrementingVideoBufferTesting(); - - // Call function - // Rows move up 5 - scroll(); - - // Post test - defaultVariablesTesting(0, vga.HEIGHT - 1, 0); - - const to_add = (row_test - vga.HEIGHT + 1) * vga.WIDTH; - for (pages) |page, i| { - for (page) |c, j| { - if (j >= TOTAL_CHAR_ON_PAGE - to_add) { - if (i == 0) { - // The last rows will be blanks - expectEqual(blank, c); - } else { - expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + (j + to_add - TOTAL_CHAR_ON_PAGE), c); - } - } else { - // All rows moved up one, so add vga.WIDTH - expectEqual(i * TOTAL_CHAR_ON_PAGE + j + to_add, c); - } - } - } - - for (video_buffer) |buf, i| { - if (i < START_OF_DISPLAYABLE_REGION) { - expectEqual(i, buf); - } else if (i >= VIDEO_BUFFER_SIZE - to_add) { - expectEqual(blank, buf); - } else { - expectEqual(i + to_add, buf); - } - } - - // Tear down - resetGlobals(); -} - -test "putChar new line within screen" { - // Set up - setVideoBufferBlankPages(); - - // Pre testing - column = 5; - row = 5; - defaultAllTesting(0, 5, 5); - - // Call function - try putChar('\n'); - - // Post test - defaultAllTesting(0, 6, 0); - - // Tear down - resetGlobals(); -} - -test "putChar new line outside screen" { - // Set up - setVideoBufferBlankPages(); - - // Pre testing - column = 5; - row = vga.HEIGHT - 1; - defaultAllTesting(0, vga.HEIGHT - 1, 5); - - // Call function - try putChar('\n'); - - // Post test - defaultAllTesting(0, vga.HEIGHT - 1, 0); - - // Tear down - resetGlobals(); -} - -test "putChar tab within line" { - // Set up - setVideoBufferBlankPages(); - - // Pre testing - column = 5; - row = 6; - defaultAllTesting(0, 6, 5); - - // Call function - try putChar('\t'); - - // Post test - defaultAllTesting(0, 6, 9); - - // Tear down - resetGlobals(); -} - -test "putChar tab end of line" { - // Set up - setVideoBufferBlankPages(); - - // Pre testing - column = vga.WIDTH - 1; - row = 6; - defaultAllTesting(0, 6, vga.WIDTH - 1); - - // Call function - try putChar('\t'); - - // Post test - defaultAllTesting(0, 7, 3); - - // Tear down - resetGlobals(); -} - -test "putChar tab end of screen" { - // Set up - setVideoBufferBlankPages(); - - // Pre testing - column = vga.WIDTH - 1; - row = vga.HEIGHT - 1; - defaultAllTesting(0, vga.HEIGHT - 1, vga.WIDTH - 1); - - // Call function - try putChar('\t'); - - // Post test - defaultAllTesting(0, vga.HEIGHT - 1, 3); - - // Tear down - resetGlobals(); -} - -test "putChar line feed" { - // Set up - setVideoBufferBlankPages(); - - // Pre testing - column = vga.WIDTH - 1; - row = vga.HEIGHT - 1; - defaultAllTesting(0, vga.HEIGHT - 1, vga.WIDTH - 1); - - // Call function - try putChar('\r'); - - // Post test - defaultAllTesting(0, vga.HEIGHT - 1, 0); - - // Tear down - resetGlobals(); -} - -test "putChar back char top left of screen" { - // Set up - setVideoBufferBlankPages(); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - try putChar('\x08'); - - // Post test - defaultAllTesting(0, 0, 0); - - // Tear down - resetGlobals(); -} - -test "putChar back char top row" { - // Set up - setVideoBufferBlankPages(); - - // Pre testing - column = 8; - defaultAllTesting(0, 0, 8); - - // Call function - try putChar('\x08'); - - // Post test - defaultAllTesting(0, 0, 7); - - // Tear down - resetGlobals(); -} - -test "putChar back char beginning of row" { - // Set up - setVideoBufferBlankPages(); - - // Pre testing - row = 1; - defaultAllTesting(0, 1, 0); - - // Call function - try putChar('\x08'); - - // Post test - defaultAllTesting(0, 0, vga.WIDTH - 1); - - // Tear down - resetGlobals(); -} - -test "putChar any char in row" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - try putChar('A'); - - // Post test - defaultVariablesTesting(0, 0, 1); - blankPagesTesting(); - - for (video_buffer) |buf, i| { - if (i == 0) { - expectEqual(vga.orig_entry('A', colour), buf); - } else { - expectEqual(blank, buf); - } - } - - // Tear down - resetGlobals(); -} - -test "putChar any char end of row" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - - // Pre testing - column = vga.WIDTH - 1; - defaultAllTesting(0, 0, vga.WIDTH - 1); - - // Call function - try putChar('A'); - - // Post test - defaultVariablesTesting(0, 1, 0); - blankPagesTesting(); - - for (video_buffer) |buf, i| { - if (i == vga.WIDTH - 1) { - expectEqual(vga.orig_entry('A', colour), buf); - } else { - expectEqual(blank, buf); - } - } - - // Tear down - resetGlobals(); -} - -test "putChar any char end of screen" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - - // Pre testing - row = vga.HEIGHT - 1; - column = vga.WIDTH - 1; - defaultAllTesting(0, vga.HEIGHT - 1, vga.WIDTH - 1); - - // Call function - try putChar('A'); - - // Post test - defaultVariablesTesting(0, vga.HEIGHT - 1, 0); - for (pages) |page, i| { - for (page) |c, j| { - if ((i == 0) and (j == TOTAL_CHAR_ON_PAGE - vga.WIDTH - 1)) { - expectEqual(vga.orig_entry('A', colour), c); - } else { - expectEqual(blank, c); - } - } - } - - for (video_buffer) |buf, i| { - if (i == VIDEO_BUFFER_SIZE - vga.WIDTH - 1) { - expectEqual(vga.orig_entry('A', colour), buf); - } else { - expectEqual(blank, buf); - } - } - - // Tear down - resetGlobals(); -} - -test "printLogo" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - column = 0; - row = ROW_MIN; - - defaultAllTesting(0, ROW_MIN, 0); - - // Call function - printLogo(); - - // Post test - defaultVariablesTesting(0, ROW_MIN, 0); - blankPagesTesting(); - - for (video_buffer) |buf, i| { - if (i < START_OF_DISPLAYABLE_REGION) { - // This is where the logo will be, but is a complex string so no testing - // Just take my word it works :P - } else { - expectEqual(blank, buf); - } - } - - // Tear down - resetGlobals(); -} - -test "pageUp top page" { - // Set up - setVideoBufferBlankPages(); - setPagesIncrementing(); - - // Pre testing - page_index = TOTAL_NUM_PAGES - 1; - - defaultVariablesTesting(TOTAL_NUM_PAGES - 1, 0, 0); - incrementingPagesTesting(); - defaultVideoBufferTesting(); - - // Call function - pageUp(); - - // Post test - defaultVariablesTesting(TOTAL_NUM_PAGES - 1, 0, 0); - incrementingPagesTesting(); - defaultVideoBufferTesting(); - - // Tear down - resetGlobals(); -} - -test "pageUp bottom page" { - // Set up - setVideoBufferBlankPages(); - setPagesIncrementing(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - vga.addConsumeFunction("disableCursor", vga.mock_disableCursor); - - // Pre testing - defaultVariablesTesting(0, 0, 0); - incrementingPagesTesting(); - defaultVideoBufferTesting(); - - // Call function - pageUp(); - - // Post test - defaultVariablesTesting(1, 0, 0); - incrementingPagesTesting(); - - const text = "Page 1 of 4"; - - for (video_buffer) |b, i| { - // Ignore the ROW_MIN row as this is where the page number is printed and is already - // tested, page number is printed 11 from the end - if (i < START_OF_DISPLAYABLE_REGION - 11) { - expectEqual(blank, b); - } else if (i < START_OF_DISPLAYABLE_REGION) { - expectEqual(vga.orig_entry(text[i + 11 - START_OF_DISPLAYABLE_REGION], colour), b); - } else { - expectEqual(i - START_OF_DISPLAYABLE_REGION + TOTAL_CHAR_ON_PAGE, b); - } - } - - // Tear down - resetGlobals(); -} - -test "pageDown bottom page" { - // Set up - setVideoBufferBlankPages(); - setPagesIncrementing(); - - // Pre testing - defaultVariablesTesting(0, 0, 0); - incrementingPagesTesting(); - defaultVideoBufferTesting(); - - // Call function - pageDown(); - - // Post test - defaultVariablesTesting(0, 0, 0); - incrementingPagesTesting(); - defaultVideoBufferTesting(); - - // Tear down - resetGlobals(); -} - -test "pageDown top page" { - // Set up - setVideoBufferBlankPages(); - setPagesIncrementing(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - vga.addConsumeFunction("disableCursor", vga.mock_disableCursor); - - // Pre testing - page_index = TOTAL_NUM_PAGES - 1; - - defaultVariablesTesting(TOTAL_NUM_PAGES - 1, 0, 0); - incrementingPagesTesting(); - defaultVideoBufferTesting(); - - // Call function - pageDown(); - - // Post test - defaultVariablesTesting(TOTAL_NUM_PAGES - 2, 0, 0); - incrementingPagesTesting(); - - const text = "Page 3 of 4"; - - for (video_buffer) |b, i| { - // Ignore the ROW_MIN row as this is where the page number is printed and is already - // tested, page number is printed 11 from the end - if (i < START_OF_DISPLAYABLE_REGION - 11) { - expectEqual(blank, b); - } else if (i < START_OF_DISPLAYABLE_REGION) { - expectEqual(vga.orig_entry(text[i + 11 - START_OF_DISPLAYABLE_REGION], colour), b); - } else { - expectEqual((i - START_OF_DISPLAYABLE_REGION) + (TOTAL_CHAR_ON_PAGE * page_index), b); - } - } - - // Tear down - resetGlobals(); -} - -test "clearScreen" { - // Set up - setVideoBufferIncrementingBlankPages(); - setPagesIncrementing(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - defaultVariablesTesting(0, 0, 0); - incrementingVideoBufferTesting(); - incrementingPagesTesting(); - - // Call function - clearScreen(); - - // Post test - defaultVariablesTesting(0, ROW_MIN, 0); - for (video_buffer) |buf, i| { - if (i < START_OF_DISPLAYABLE_REGION) { - expectEqual(i, buf); - } else { - expectEqual(blank, buf); - } - } - - for (pages) |page, i| { - for (page) |c, j| { - if (i == 0) { - // The last rows will be blanks - expectEqual(blank, c); - } else { - expectEqual((i - 1) * TOTAL_CHAR_ON_PAGE + j, c); - } - } - } - - // Tear down - resetGlobals(); -} - -test "moveCursorLeft top left of screen" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - moveCursorLeft(); - - // Post test - defaultAllTesting(0, 0, 0); - - // Tear down - resetGlobals(); -} - -test "moveCursorLeft top screen" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - column = 5; - defaultAllTesting(0, 0, 5); - - // Call function - moveCursorLeft(); - - // Post test - defaultAllTesting(0, 0, 4); - - // Tear down - resetGlobals(); -} - -test "moveCursorLeft start of row" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - row = 5; - defaultAllTesting(0, 5, 0); - - // Call function - moveCursorLeft(); - - // Post test - defaultAllTesting(0, 4, vga.WIDTH - 1); - - // Tear down - resetGlobals(); -} - -test "moveCursorRight bottom right of screen" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - row = vga.HEIGHT - 1; - column = vga.WIDTH - 1; - defaultAllTesting(0, vga.HEIGHT - 1, vga.WIDTH - 1); - - // Call function - moveCursorRight(); - - // Post test - defaultAllTesting(0, vga.HEIGHT - 1, vga.WIDTH - 1); - - // Tear down - resetGlobals(); -} - -test "moveCursorRight top screen" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - column = 5; - defaultAllTesting(0, 0, 5); - - // Call function - moveCursorRight(); - - // Post test - defaultAllTesting(0, 0, 6); - - // Tear down - resetGlobals(); -} - -test "moveCursorRight end of row" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - row = 5; - column = vga.WIDTH - 1; - defaultAllTesting(0, 5, vga.WIDTH - 1); - - // Call function - moveCursorRight(); - - // Post test - defaultAllTesting(0, 6, 0); - - // Tear down - resetGlobals(); -} - -test "setColour" { - // Set up - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addConsumeFunction("entry", vga.orig_entry); - - // Pre testing - - // Call function - const new_colour = vga.orig_entryColour(vga.COLOUR_WHITE, vga.COLOUR_WHITE); - setColour(new_colour); - - // Post test - expectEqual(new_colour, colour); - expectEqual(vga.orig_entry(0, new_colour), blank); - - // Tear down - resetGlobals(); -} - -test "writeString" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga calls - vga.initTest(); - defer vga.freeTest(); - - vga.addRepeatFunction("entry", vga.orig_entry); - - vga.addConsumeFunction("updateCursor", vga.mock_updateCursor); - - // Pre testing - row = ROW_MIN; - defaultAllTesting(0, ROW_MIN, 0); - - // Call function - try writeString("ABC"); - - // Post test - defaultVariablesTesting(0, ROW_MIN, 3); - for (pages) |page, i| { - for (page) |c, j| { - if ((i == 0) and (j == 0)) { - expectEqual(vga.orig_entry('A', colour), c); - } else if ((i == 0) and (j == 1)) { - expectEqual(vga.orig_entry('B', colour), c); - } else if ((i == 0) and (j == 2)) { - expectEqual(vga.orig_entry('C', colour), c); - } else { - expectEqual(blank, c); - } - } - } - - for (video_buffer) |buf, i| { - if (i == START_OF_DISPLAYABLE_REGION) { - expectEqual(vga.orig_entry('A', colour), buf); - } else if (i == START_OF_DISPLAYABLE_REGION + 1) { - expectEqual(vga.orig_entry('B', colour), buf); - } else if (i == START_OF_DISPLAYABLE_REGION + 2) { - expectEqual(vga.orig_entry('C', colour), buf); - } else { - expectEqual(blank, buf); - } - } - - // Tear down - resetGlobals(); -} - -test "init 0,0" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga.updateCursor call for updating the hardware cursor - vga.initTest(); - defer vga.freeTest(); - - vga.addTestParams("getCursor", .{@as(u16, 0)}); - - vga.addRepeatFunction("entryColour", vga.orig_entryColour); - vga.addRepeatFunction("entry", vga.orig_entry); - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - vga.addConsumeFunction("enableCursor", vga.mock_enableCursor); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - init(); - - // Post test - defaultVariablesTesting(0, ROW_MIN, 0); - blankPagesTesting(); - - for (video_buffer) |buf, i| { - if (i < START_OF_DISPLAYABLE_REGION) { - // This is where the logo will be, but is a complex string so no testing - // Just take my word it works :P - } else { - expectEqual(blank, buf); - } - } - - // Tear down - resetGlobals(); -} - -test "init not 0,0" { - // Set up - setVideoBufferBlankPages(); - - // Mocking out the vga.updateCursor call for updating the hardware cursor - vga.initTest(); - defer vga.freeTest(); - - vga.addTestParams("getCursor", .{vga.WIDTH}); - - vga.addRepeatFunction("entryColour", vga.orig_entryColour); - vga.addRepeatFunction("entry", vga.orig_entry); - vga.addRepeatFunction("updateCursor", vga.mock_updateCursor); - - vga.addConsumeFunction("enableCursor", vga.mock_enableCursor); - - // Pre testing - defaultAllTesting(0, 0, 0); - - // Call function - init(); - - // Post test - defaultVariablesTesting(0, ROW_MIN + 1, 0); - blankPagesTesting(); - - for (video_buffer) |buf, i| { - if (i < START_OF_DISPLAYABLE_REGION) { - // This is where the logo will be, but is a complex string so no testing - // Just take my word it works :P - } else { - expectEqual(blank, buf); - } - } - - // Tear down - resetGlobals(); -} - -/// -/// Test the init function set up everything properly. -/// -fn rt_initialisedGlobals() void { - if (@ptrToInt(video_buffer.ptr) != @ptrToInt(&KERNEL_ADDR_OFFSET) + 0xB8000) { - panic(@errorReturnTrace(), "Video buffer not at correct virtual address, found: {}\n", .{@ptrToInt(video_buffer.ptr)}); - } - - if (page_index != 0) { - panic(@errorReturnTrace(), "Page index not at zero, found: {}\n", .{page_index}); - } - - if (colour != vga.entryColour(vga.COLOUR_LIGHT_GREY, vga.COLOUR_BLACK)) { - panic(@errorReturnTrace(), "Colour not set up properly, found: {}\n", .{colour}); - } - - if (blank != vga.entry(0, colour)) { - panic(@errorReturnTrace(), "Blank not set up properly, found: {}\n", .{blank}); - } - - // Make sure the screen isn't all blank - var all_blank = true; - for (video_buffer) |buf| { - if (buf != blank and buf != 0) { - all_blank = false; - break; - } - } - - if (all_blank) { - panic(@errorReturnTrace(), "Screen all blank, should have logo and page number\n", .{}); - } - - log.logInfo("TTY: Tested globals\n", .{}); -} - -/// -/// Test printing a string will output to the screen. This will check both the video memory and -/// the pages. -/// -fn rt_printString() void { - const text = "abcdefg"; - const clear_text = "\x08" ** text.len; - - print(text, .{}); - - // Check the video memory - var counter: u32 = 0; - for (video_buffer) |buf| { - if (counter < text.len and buf == vga.entry(text[counter], colour)) { - counter += 1; - } else if (counter == text.len) { - // Found all the text - break; - } else { - counter = 0; - } - } - - if (counter != text.len) { - panic(@errorReturnTrace(), "Didn't find the printed text in video memory\n", .{}); - } - - // Check the pages - counter = 0; - for (pages[0]) |c| { - if (counter < text.len and c == vga.entry(text[counter], colour)) { - counter += 1; - } else if (counter == text.len) { - // Found all the text - break; - } else { - counter = 0; - } - } - - if (counter != text.len) { - panic(@errorReturnTrace(), "Didn't find the printed text in pages\n", .{}); - } - - // Clear the text - print(clear_text, .{}); - - log.logInfo("TTY: Tested printing\n", .{}); -} - -/// -/// Run all the runtime tests. -/// -fn runtimeTests() void { - rt_initialisedGlobals(); - rt_printString(); + defer log.logInfo("Done tty\n", .{}); + tty = arch.initTTY(boot_payload); + allocator = alloc; } diff --git a/src/kernel/vfs.zig b/src/kernel/vfs.zig new file mode 100644 index 00000000..e343e347 --- /dev/null +++ b/src/kernel/vfs.zig @@ -0,0 +1,736 @@ +const std = @import("std"); +const testing = std.testing; +const TailQueue = std.TailQueue; +const ArrayList = std.ArrayList; +const Allocator = std.mem.Allocator; + +/// Flags specifying what to do when opening a file or directory +const OpenFlags = enum { + /// Create a directory if it doesn't exist + CREATE_DIR, + /// Create a file if it doesn't exist + CREATE_FILE, + /// Do not create a file or directory + NO_CREATION, +}; + +/// A filesystem node that could either be a directory or a file +pub const Node = union(enum) { + /// The file node if this represents a file + File: FileNode, + /// The dir node if this represents a directory + Dir: DirNode, + const Self = @This(); + + /// + /// Check if this node is a directory + /// + /// Arguments: + /// IN self: Self - The node being checked + /// + /// Return: bool + /// True if this is a directory else false + /// + pub fn isDir(self: Self) bool { + return switch (self) { + .Dir => true, + .File => false, + }; + } + + /// + /// Check if this node is a file + /// + /// Arguments: + /// IN self: Self - The node being checked + /// + /// Return: bool + /// True if this is a file else false + /// + pub fn isFile(self: Self) bool { + return switch (self) { + .File => true, + .Dir => false, + }; + } +}; + +/// The functions of a filesystem +pub const FileSystem = struct { + const Self = @This(); + + /// + /// Close an open file, performing any last operations required to save data etc. + /// + /// Arguments: + /// IN self: *const FileSystem - The filesystem in question being operated on. + /// IN node: *const FileNode - The file being closed + /// + const Close = fn (self: *const Self, node: *const FileNode) void; + + /// + /// Read from an open file + /// + /// Arguments: + /// IN self: *const FileSystem - The filesystem in question being operated on + /// IN node: *const FileNode - The file being read from + /// IN len: usize - The number of bytes to read from the file + /// + /// Return: []u8 + /// The data read as a slice of bytes. The length will be <= len, including 0 if there was no data to read + /// + /// Error: Allocator.Error + /// Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request + /// + const Read = fn (self: *const Self, node: *const FileNode, len: usize) Allocator.Error![]u8; + + /// + /// Write to an open file + /// + /// Arguments: + /// IN self: *const FileSystem - The filesystem in question being operated on + /// IN node: *const FileNode - The file being read from + /// IN bytes: []u8 - The bytes to write to the file + /// + /// Error: Allocator.Error + /// Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request + /// + const Write = fn (self: *const Self, node: *const FileNode, bytes: []const u8) Allocator.Error!void; + + /// + /// Open a file/dir within the filesystem. The result can then be used for write, read or close operations + /// + /// Arguments: + /// IN self: *const FileSystem - The filesystem in question being operated on + /// IN node: *const DirNode - The directory under which to open the file/dir from + /// IN name: []const u8 - The name of the file to open + /// IN flags: OpenFlags - The flags to consult when opening the file + /// + /// Return: *const Node + /// The node representing the file/dir opened + /// + /// Error: Allocator.Error || Error + /// Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request + /// Error.NoSuchFileOrDir - The file/dir by that name doesn't exist and the flags didn't specify to create it + /// + const Open = fn (self: *const Self, node: *const DirNode, name: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node; + + /// + /// Get the node representing the root of the filesystem. Used when mounting to bind the mount point to the root of the mounted fs + /// + /// Arguments: + /// IN self: *const Self - The filesystem to get the root node for + /// + /// Return: *const DirNode + /// The root directory node + /// + const GetRootNode = fn (self: *const Self) *const DirNode; + + /// The close function + close: Close, + + /// The read function + read: Read, + + /// The write function + write: Write, + + /// The open function + open: Open, + + /// The function for retrieving the root node + getRootNode: GetRootNode, + + /// Points to a usize field within the underlying filesystem so that the close, read, write and open functions can access its low-level implementation using @fieldParentPtr. For example, this could point to a usize field within a FAT32 filesystem data structure, which stores all the data and state that is needed in order to interact with a physical disk + /// The value of instance is reserved for future use and so should be left as 0 + instance: *usize, +}; + +/// A node representing a file within a filesystem +pub const FileNode = struct { + /// The filesystem that handles operations on this file + fs: *const FileSystem, + + /// See the documentation for FileSystem.Read + pub fn read(self: *const FileNode, len: usize) Allocator.Error![]u8 { + return self.fs.read(self.fs, self, len); + } + + /// See the documentation for FileSystem.Close + pub fn close(self: *const FileNode) void { + return self.fs.close(self.fs, self); + } + + /// See the documentation for FileSystem.Write + pub fn write(self: *const FileNode, bytes: []const u8) Allocator.Error!void { + return self.fs.write(self.fs, self, bytes); + } +}; + +/// A node representing a directory within a filesystem +pub const DirNode = struct { + /// The filesystem that handles operations on this directory + fs: *const FileSystem, + + /// The directory that this directory is mounted to, else null + mount: ?*const DirNode, + + /// See the documentation for FileSystem.Open + pub fn open(self: *const DirNode, name: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node { + var fs = self.fs; + var node = self; + if (self.mount) |mnt| { + fs = mnt.fs; + node = mnt; + } + return fs.open(fs, node, name, flags); + } +}; + +/// Errors that can be thrown by filesystem functions +pub const Error = error{ + /// The file or directory requested doesn't exist in the filesystem + NoSuchFileOrDir, + + /// The parent of a requested file or directory isn't a directory itself + NotADirectory, + + /// The requested file is actually a directory + IsADirectory, + + /// The path provided is not absolute + NotAbsolutePath, + + /// The flags provided are invalid for the requested operation + InvalidFlags, +}; + +/// Errors that can be thrown when attempting to mount +pub const MountError = error{ + /// The directory being mounted to a filesystem is already mounted to something + DirAlreadyMounted, +}; + +/// The separator used between segments of a file path +pub const SEPARATOR: u8 = '/'; + +/// The root of the system's top-level filesystem +var root: *Node = undefined; + +/// +/// Traverse the specified path from the root and open the file/dir corresponding to that path. If the file/dir doesn't exist it can be created by specifying the open flags +/// +/// Arguments: +/// IN path: []const u8 - The path to traverse. Must be absolute (see isAbsolute) +/// IN flags: OpenFlags - The flags that specify if the file/dir should be created if it doesn't exist +/// +/// Return: *const Node +/// The node that exists at the path starting at the system root +/// +/// Error: Allocator.Error || Error +/// Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request +/// Error.NotADirectory - A segment within the path which is not at the end does not correspond to a directory +/// Error.NoSuchFileOrDir - The file/dir at the end of the path doesn't exist and the flags didn't specify to create it +/// +fn traversePath(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node { + if (!isAbsolute(path)) { + return Error.NotAbsolutePath; + } + + const TraversalParent = struct { + parent: *Node, + child: []const u8, + + const Self = @This(); + fn func(split: *std.mem.SplitIterator, node: *Node, rec_flags: OpenFlags) (Allocator.Error || Error)!Self { + // Get segment string. This will not be unreachable as we've made sure the spliterator has more segments left + const seg = split.next() orelse unreachable; + if (split.rest().len == 0) { + return Self{ + .parent = node, + .child = seg, + }; + } + return switch (node.*) { + .File => Error.NotADirectory, + .Dir => |*dir| try func(split, try dir.open(seg, rec_flags), rec_flags), + }; + } + }; + + // Split path but skip the first separator character + var split = std.mem.split(path[1..], &[_]u8{SEPARATOR}); + // Traverse directories while we're not at the last segment + const result = try TraversalParent.func(&split, root, .NO_CREATION); + + // There won't always be a second segment in the path, e.g. in "/" + if (std.mem.eql(u8, result.child, "")) { + return result.parent; + } + + // Open the final segment of the path from whatever the parent is + return switch (result.parent.*) { + .File => Error.NotADirectory, + .Dir => |*dir| try dir.open(result.child, flags), + }; +} + +/// +/// Mount the root of a filesystem to a directory. Opening files within that directory will then redirect to the target filesystem +/// +/// Arguments: +/// IN dir: *DirNode - The directory to mount to. dir.mount is modified. +/// IN fs: *const FileSystem - The filesystem to mount +/// +/// Error: MountError +/// MountError.DirAlreadyMounted - The directory is already mounted to a filesystem +/// +pub fn mount(dir: *DirNode, fs: *const FileSystem) MountError!void { + if (dir.mount) |_| { + return MountError.DirAlreadyMounted; + } + dir.mount = fs.getRootNode(fs); +} + +/// +/// Open a node at a path. +/// +/// Arguments: +/// IN path: []const u8 - The path to open. Must be absolute (see isAbsolute) +/// IN flags: OpenFlags - The flags specifying if this node should be created if it doesn't exist +/// +/// Return: *const Node +/// The node that exists at the path starting at the system root +/// +/// Error: Allocator.Error || Error +/// Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request +/// Error.NotADirectory - A segment within the path which is not at the end does not correspond to a directory +/// Error.NoSuchFileOrDir - The file/dir at the end of the path doesn't exist and the flags didn't specify to create it +/// +pub fn open(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node { + return try traversePath(path, flags); +} + +/// +/// Open a file at a path. +/// +/// Arguments: +/// IN path: []const u8 - The path to open. Must be absolute (see isAbsolute) +/// IN flags: OpenFlags - The flags specifying if this node should be created if it doesn't exist. Cannot be CREATE_DIR +/// +/// Return: *const FileNode +/// The node that exists at the path starting at the system root +/// +/// Error: Allocator.Error || Error +/// Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request +/// Error.InvalidFlags - The flags were a value invalid when opening files +/// Error.NotADirectory - A segment within the path which is not at the end does not correspond to a directory +/// Error.NoSuchFileOrDir - The file/dir at the end of the path doesn't exist and the flags didn't specify to create it +/// Error.IsADirectory - The path corresponds to a directory rather than a file +/// +pub fn openFile(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*const FileNode { + switch (flags) { + .CREATE_DIR => return Error.InvalidFlags, + .NO_CREATION, .CREATE_FILE => {}, + } + var node = try open(path, flags); + return switch (node.*) { + .File => &node.File, + .Dir => Error.IsADirectory, + }; +} + +/// +/// Open a directory at a path. +/// +/// Arguments: +/// IN path: []const u8 - The path to open. Must be absolute (see isAbsolute) +/// IN flags: OpenFlags - The flags specifying if this node should be created if it doesn't exist. Cannot be CREATE_FILE +/// +/// Return: *const DirNode +/// The node that exists at the path starting at the system root +/// +/// Error: Allocator.Error || Error +/// Allocator.Error.OutOfMemory - There wasn't enough memory to fulfill the request +/// Error.InvalidFlags - The flags were a value invalid when opening files +/// Error.NotADirectory - A segment within the path which is not at the end does not correspond to a directory +/// Error.NoSuchFileOrDir - The file/dir at the end of the path doesn't exist and the flags didn't specify to create it +/// Error.NotADirectory - The path corresponds to a file rather than a directory +/// +pub fn openDir(path: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*DirNode { + switch (flags) { + .CREATE_FILE => return Error.InvalidFlags, + .NO_CREATION, .CREATE_DIR => {}, + } + var node = try open(path, flags); + return switch (node.*) { + .File => Error.NotADirectory, + .Dir => &node.Dir, + }; +} + +// TODO: Replace this with the std lib implementation once the OS abstraction layer is up and running +/// +/// Check if a path is absolute, i.e. its length is greater than 0 and starts with the path separator character +/// +/// Arguments: +/// IN path: []const u8 - The path to check +/// +/// Return: bool +/// True if the path is absolute else false +/// +pub fn isAbsolute(path: []const u8) bool { + return path.len > 0 and path[0] == SEPARATOR; +} + +const TestFS = struct { + const TreeNode = struct { + val: *Node, + name: []u8, + data: ?[]u8, + children: *ArrayList(*@This()), + + fn deinit(self: *@This(), allocator: *Allocator) void { + allocator.destroy(self.val); + allocator.free(self.name); + if (self.data) |d| { + allocator.free(d); + } + for (self.children.items) |child| { + child.deinit(allocator); + allocator.destroy(child); + } + self.children.deinit(); + allocator.destroy(self.children); + } + }; + + tree: TreeNode, + fs: *FileSystem, + allocator: *Allocator, + instance: usize, + + const Self = @This(); + + fn deinit(self: *@This()) void { + self.tree.deinit(self.allocator); + self.allocator.destroy(self.fs); + } + + fn getTreeNode(test_fs: *Self, node: anytype) Allocator.Error!?*TreeNode { + switch (@TypeOf(node)) { + *const Node, *const FileNode, *const DirNode => {}, + else => @compileError("Node is of type " ++ @typeName(@TypeOf(node)) ++ ". Only *const Node, *const FileNode and *const DirNode are supported"), + } + // Form a list containing all directory nodes to check via a breadth-first search + // This is inefficient but good for testing as it's clear and easy to modify + var to_check = TailQueue(*TreeNode).init(); + var root_node = try to_check.createNode(&test_fs.tree, test_fs.allocator); + to_check.append(root_node); + + while (to_check.popFirst()) |queue_node| { + var tree_node = queue_node.data; + to_check.destroyNode(queue_node, test_fs.allocator); + if ((@TypeOf(node) == *const FileNode and tree_node.val.isFile() and &tree_node.val.File == node) or (@TypeOf(node) == *const DirNode and tree_node.val.isDir() and &tree_node.val.Dir == node) or (@TypeOf(node) == *const Node and &tree_node.val == node)) { + // Clean up any unused queue nodes + while (to_check.popFirst()) |t_node| { + to_check.destroyNode(t_node, test_fs.allocator); + } + return tree_node; + } + for (tree_node.children.items) |child| { + // It's not the parent so add its children to the list for checking + to_check.append(try to_check.createNode(child, test_fs.allocator)); + } + } + return null; + } + + fn getRootNode(fs: *const FileSystem) *const DirNode { + var test_fs = @fieldParentPtr(TestFS, "instance", fs.instance); + return &test_fs.tree.val.Dir; + } + + fn close(fs: *const FileSystem, node: *const FileNode) void {} + + fn read(fs: *const FileSystem, node: *const FileNode, len: usize) Allocator.Error![]u8 { + var test_fs = @fieldParentPtr(TestFS, "instance", fs.instance); + // Get the tree that corresponds to the node. Cannot error as the file is already open so it does exist + var tree = (getTreeNode(test_fs, node) catch unreachable) orelse unreachable; + const count = if (tree.data) |d| std.math.min(len, d.len) else 0; + const data = if (tree.data) |d| d[0..count] else ""; + var bytes = try test_fs.allocator.alloc(u8, count); + std.mem.copy(u8, bytes, data); + return bytes; + } + + fn write(fs: *const FileSystem, node: *const FileNode, bytes: []const u8) Allocator.Error!void { + var test_fs = @fieldParentPtr(TestFS, "instance", fs.instance); + var tree = (try getTreeNode(test_fs, node)) orelse unreachable; + if (tree.data) |_| { + test_fs.allocator.free(tree.data.?); + } + tree.data = try test_fs.allocator.alloc(u8, bytes.len); + std.mem.copy(u8, tree.data.?, bytes); + } + + fn open(fs: *const FileSystem, dir: *const DirNode, name: []const u8, flags: OpenFlags) (Allocator.Error || Error)!*Node { + var test_fs = @fieldParentPtr(TestFS, "instance", fs.instance); + const parent = (try getTreeNode(test_fs, dir)) orelse unreachable; + // Check if the children match the file wanted + for (parent.children.items) |child| { + if (std.mem.eql(u8, child.name, name)) { + return child.val; + } + } + // The file/dir doesn't exist so create it if necessary + if (flags != .NO_CREATION) { + var child: *Node = undefined; + switch (flags) { + .CREATE_DIR => { + // Create the fs node + child = try test_fs.allocator.create(Node); + child.* = .{ .Dir = .{ .fs = test_fs.fs, .mount = null } }; + }, + .CREATE_FILE => { + // Create the fs node + child = try test_fs.allocator.create(Node); + child.* = .{ .File = .{ .fs = test_fs.fs } }; + }, + .NO_CREATION => unreachable, + } + // Create the test fs tree node + var child_tree = try test_fs.allocator.create(TreeNode); + var child_name = try test_fs.allocator.alloc(u8, name.len); + std.mem.copy(u8, child_name, name); + child_tree.* = .{ + .val = child, + .name = child_name, + .children = try test_fs.allocator.create(ArrayList(*TreeNode)), + .data = null, + }; + child_tree.children.* = ArrayList(*TreeNode).init(test_fs.allocator); + // Add it to the tree + try parent.children.append(child_tree); + return child; + } + return Error.NoSuchFileOrDir; + } +}; + +fn testInitFs(allocator: *Allocator) !*TestFS { + const fs = try allocator.create(FileSystem); + var testfs = try allocator.create(TestFS); + var root_node = try allocator.create(Node); + root_node.* = .{ .Dir = .{ .fs = fs, .mount = null } }; + var name = try allocator.alloc(u8, 4); + std.mem.copy(u8, name, "root"); + testfs.* = TestFS{ + .tree = .{ + .val = root_node, + .name = name, + .children = try allocator.create(ArrayList(*TestFS.TreeNode)), + .data = null, + }, + .fs = fs, + .instance = 123, + .allocator = allocator, + }; + testfs.tree.children.* = ArrayList(*TestFS.TreeNode).init(allocator); + fs.* = .{ .open = TestFS.open, .close = TestFS.close, .read = TestFS.read, .write = TestFS.write, .instance = &testfs.instance, .getRootNode = TestFS.getRootNode }; + return testfs; +} + +test "mount" { + var allocator = testing.allocator; + + // The root fs + var testfs = try testInitFs(allocator); + defer testfs.deinit(); + defer allocator.destroy(testfs); + + testfs.instance = 1; + root = testfs.tree.val; + + // The fs that is to be mounted + var testfs2 = try testInitFs(allocator); + defer testfs2.deinit(); + defer allocator.destroy(testfs2); + + testfs2.instance = 2; + // Create the dir to mount to + var dir = try openDir("/mnt", .CREATE_DIR); + try mount(dir, testfs2.fs); + testing.expectError(MountError.DirAlreadyMounted, mount(dir, testfs2.fs)); + + // Ensure the mount worked + testing.expectEqual((dir.mount orelse unreachable), testfs2.fs.getRootNode(testfs2.fs)); + testing.expectEqual((dir.mount orelse unreachable).fs, testfs2.fs); + // Create a file within the mounted directory + var test_file = try openFile("/mnt/123.txt", .CREATE_FILE); + testing.expectEqual(@ptrCast(*const FileSystem, testfs2.fs), test_file.fs); + // This shouldn't be in the root fs + testing.expectEqual(@as(usize, 1), testfs.tree.children.items.len); + testing.expectEqual(@as(usize, 0), testfs.tree.children.items[0].children.items.len); + // It should be in the mounted fs + testing.expectEqual(@as(usize, 1), testfs2.tree.children.items.len); + testing.expectEqual(test_file, &testfs2.tree.children.items[0].val.File); +} + +test "traversePath" { + var allocator = testing.allocator; + var testfs = try testInitFs(allocator); + defer testfs.deinit(); + defer allocator.destroy(testfs); + root = testfs.tree.val; + + // Get the root + var test_root = try traversePath("/", .NO_CREATION); + testing.expectEqual(test_root, root); + // Create a file in the root and try to traverse to it + var child1 = try test_root.Dir.open("child1.txt", .CREATE_FILE); + testing.expectEqual(child1, try traversePath("/child1.txt", .NO_CREATION)); + // Same but with a directory + var child2 = try test_root.Dir.open("child2", .CREATE_DIR); + testing.expectEqual(child2, try traversePath("/child2", .NO_CREATION)); + // Again but with a file within that directory + var child3 = try child2.Dir.open("child3.txt", .CREATE_FILE); + testing.expectEqual(child3, try traversePath("/child2/child3.txt", .NO_CREATION)); + + testing.expectError(Error.NotAbsolutePath, traversePath("abc", .NO_CREATION)); + testing.expectError(Error.NotAbsolutePath, traversePath("", .NO_CREATION)); + testing.expectError(Error.NotAbsolutePath, traversePath("a/", .NO_CREATION)); + testing.expectError(Error.NoSuchFileOrDir, traversePath("/notadir/abc.txt", .NO_CREATION)); + testing.expectError(Error.NoSuchFileOrDir, traversePath("/ ", .NO_CREATION)); + testing.expectError(Error.NotADirectory, traversePath("/child1.txt/abc.txt", .NO_CREATION)); +} + +test "isAbsolute" { + testing.expect(isAbsolute("/")); + testing.expect(isAbsolute("/abc")); + testing.expect(isAbsolute("/abc/def")); + testing.expect(isAbsolute("/ a bc/de f")); + testing.expect(isAbsolute("//")); + testing.expect(!isAbsolute(" /")); + testing.expect(!isAbsolute("")); + testing.expect(!isAbsolute("abc")); + testing.expect(!isAbsolute("abc/def")); +} + +test "isDir" { + const fs: FileSystem = undefined; + const dir = Node{ .Dir = .{ .fs = &fs, .mount = null } }; + const file = Node{ .File = .{ .fs = &fs } }; + testing.expect(dir.isDir()); + testing.expect(!file.isDir()); +} + +test "isFile" { + const fs: FileSystem = undefined; + const dir = Node{ .Dir = .{ .fs = &fs, .mount = null } }; + const file = Node{ .File = .{ .fs = &fs } }; + testing.expect(!dir.isFile()); + testing.expect(file.isFile()); +} + +test "open" { + var testfs = try testInitFs(testing.allocator); + defer testfs.deinit(); + defer testing.allocator.destroy(testfs); + root = testfs.tree.val; + + // Creating a file + var test_node = try openFile("/abc.txt", .CREATE_FILE); + testing.expectEqual(testfs.tree.children.items.len, 1); + var tree = testfs.tree.children.items[0]; + testing.expect(tree.val.isFile()); + testing.expectEqual(test_node, &tree.val.File); + testing.expect(std.mem.eql(u8, tree.name, "abc.txt")); + testing.expectEqual(tree.data, null); + testing.expectEqual(tree.children.items.len, 0); + + // Creating a dir + var test_dir = try openDir("/def", .CREATE_DIR); + testing.expectEqual(testfs.tree.children.items.len, 2); + tree = testfs.tree.children.items[1]; + testing.expect(tree.val.isDir()); + testing.expectEqual(test_dir, &tree.val.Dir); + testing.expect(std.mem.eql(u8, tree.name, "def")); + testing.expectEqual(tree.data, null); + testing.expectEqual(tree.children.items.len, 0); + + // Creating a file under a new dir + test_node = try openFile("/def/ghi.zig", .CREATE_FILE); + testing.expectEqual(testfs.tree.children.items[1].children.items.len, 1); + tree = testfs.tree.children.items[1].children.items[0]; + testing.expect(tree.val.isFile()); + testing.expectEqual(test_node, &tree.val.File); + testing.expect(std.mem.eql(u8, tree.name, "ghi.zig")); + testing.expectEqual(tree.data, null); + testing.expectEqual(tree.children.items.len, 0); + + testing.expectError(Error.NoSuchFileOrDir, openDir("/jkl", .NO_CREATION)); + testing.expectError(Error.NoSuchFileOrDir, openFile("/mno.txt", .NO_CREATION)); + testing.expectError(Error.NoSuchFileOrDir, openFile("/def/pqr.txt", .NO_CREATION)); + testing.expectError(Error.NoSuchFileOrDir, openDir("/mno/stu", .NO_CREATION)); + testing.expectError(Error.NoSuchFileOrDir, openFile("/mno/stu.txt", .NO_CREATION)); + testing.expectError(Error.NotADirectory, openFile("/abc.txt/vxy.md", .NO_CREATION)); + testing.expectError(Error.IsADirectory, openFile("/def", .NO_CREATION)); + testing.expectError(Error.InvalidFlags, openFile("/abc.txt", .CREATE_DIR)); + testing.expectError(Error.InvalidFlags, openDir("/abc.txt", .CREATE_FILE)); + testing.expectError(Error.NotAbsolutePath, open("", .NO_CREATION)); + testing.expectError(Error.NotAbsolutePath, open("abc", .NO_CREATION)); +} + +test "read" { + var testfs = try testInitFs(testing.allocator); + defer testfs.deinit(); + defer testing.allocator.destroy(testfs); + root = testfs.tree.val; + + var test_file = try openFile("/foo.txt", .CREATE_FILE); + var f_data = &testfs.tree.children.items[0].data; + var str = "test123"; + f_data.* = try std.mem.dupe(testing.allocator, u8, str); + + { + var data = try test_file.read(str.len); + defer testing.allocator.free(data); + testing.expect(std.mem.eql(u8, str, data)); + } + + { + var data = try test_file.read(str.len + 1); + defer testing.allocator.free(data); + testing.expect(std.mem.eql(u8, str, data)); + } + + { + var data = try test_file.read(str.len + 3); + defer testing.allocator.free(data); + testing.expect(std.mem.eql(u8, str, data)); + } + + { + var data = try test_file.read(str.len - 1); + defer testing.allocator.free(data); + testing.expect(std.mem.eql(u8, str[0 .. str.len - 1], data)); + } + + testing.expect(std.mem.eql(u8, str[0..0], try test_file.read(0))); +} + +test "write" { + var testfs = try testInitFs(testing.allocator); + defer testfs.deinit(); + defer testing.allocator.destroy(testfs); + root = testfs.tree.val; + + var test_file = try openFile("/foo.txt", .CREATE_FILE); + var f_data = &testfs.tree.children.items[0].data; + testing.expectEqual(f_data.*, null); + + var str = "test123"; + try test_file.write(str); + testing.expect(std.mem.eql(u8, str, f_data.* orelse unreachable)); +} diff --git a/src/kernel/vmm.zig b/src/kernel/vmm.zig new file mode 100644 index 00000000..c9847afa --- /dev/null +++ b/src/kernel/vmm.zig @@ -0,0 +1,602 @@ +const build_options = @import("build_options"); +const mock_path = build_options.mock_path; +const builtin = @import("builtin"); +const is_test = builtin.is_test; +const std = @import("std"); +const bitmap = @import("bitmap.zig"); +const pmm = @import("pmm.zig"); +const mem = if (is_test) @import(mock_path ++ "mem_mock.zig") else @import("mem.zig"); +const tty = @import("tty.zig"); +const log = @import("log.zig"); +const panic = @import("panic.zig").panic; +const arch = @import("arch.zig").internals; + +/// Attributes for a virtual memory allocation +pub const Attributes = struct { + /// Whether this memory belongs to the kernel and can therefore not be accessed in user mode + kernel: bool, + + /// If this memory can be written to + writable: bool, + + /// If this memory can be cached. Memory mapped to a device shouldn't, for example + cachable: bool, +}; + +/// All data that must be remembered for a virtual memory allocation +const Allocation = struct { + /// The physical blocks of memory associated with this allocation + physical: std.ArrayList(usize), +}; + +/// The size of each allocatable block, the same as the physical memory manager's block size +pub const BLOCK_SIZE: usize = pmm.BLOCK_SIZE; + +pub const MapperError = error{ + InvalidVirtualAddress, + InvalidPhysicalAddress, + AddressMismatch, + MisalignedVirtualAddress, + MisalignedPhysicalAddress, + NotMapped, +}; + +/// +/// Returns a container that can map and unmap virtual memory to physical memory. +/// The mapper can pass some payload data when mapping an unmapping, which is of type `Payload`. This can be anything that the underlying mapper needs to carry out the mapping process. +/// For x86, it would be the page directory that is being mapped within. An architecture or other mapper can specify the data it needs when mapping by specifying this type. +/// +/// Arguments: +/// IN comptime Payload: type - The type of the VMM-specific payload to pass when mapping and unmapping +/// +/// Return: type +/// The Mapper type constructed. +/// +pub fn Mapper(comptime Payload: type) type { + return struct { + /// + /// Map a region (can span more than one block) of virtual memory to physical memory. After a call to this function, the memory should be present the next time it is accessed. + /// The attributes given must be obeyed when possible. + /// + /// Arguments: + /// IN virtual_start: usize - The start of the virtual memory to map + /// IN virtual_end: usize - The end of the virtual memory to map + /// IN physical_start: usize - The start of the physical memory to map to + /// IN physical_end: usize - The end of the physical memory to map to + /// IN attrs: Attributes - The attributes to apply to this region of memory + /// IN/OUT allocator: std.mem.Allocator - The allocator to use when mapping, if required + /// IN spec: Payload - The payload to pass to the mapper + /// + /// Error: std.mem.AllocatorError || MapperError + /// The causes depend on the mapper used + /// + mapFn: fn (virtual_start: usize, virtual_end: usize, physical_start: usize, physical_end: usize, attrs: Attributes, allocator: *std.mem.Allocator, spec: Payload) (std.mem.Allocator.Error || MapperError)!void, + + /// + /// Unmap a region (can span more than one block) of virtual memory from its physical memory. After a call to this function, the memory should not be accessible without error. + /// + /// Arguments: + /// IN virtual_start: usize - The start of the virtual region to unmap + /// IN virtual_end: usize - The end of the virtual region to unmap + /// IN spec: Payload - The payload to pass to the mapper + /// + /// Error: std.mem.AllocatorError || MapperError + /// The causes depend on the mapper used + /// + unmapFn: fn (virtual_start: usize, virtual_end: usize, spec: Payload) (std.mem.Allocator.Error || MapperError)!void, + }; +} + +/// Errors that can be returned by VMM functions +pub const VmmError = error{ + /// A memory region expected to be allocated wasn't + NotAllocated, + + /// A memory region expected to not be allocated was + AlreadyAllocated, + + /// A physical memory region expected to not be allocated was + PhysicalAlreadyAllocated, + + /// A physical region of memory isn't of the same size as a virtual region + PhysicalVirtualMismatch, + + /// Virtual addresses are invalid + InvalidVirtAddresses, + + /// Physical addresses are invalid + InvalidPhysAddresses, +}; + +/// The boot-time offset that the virtual addresses are from the physical addresses +/// This is the start of the memory owned by the kernel and so is where the kernel VMM starts +extern var KERNEL_ADDR_OFFSET: *u32; + +/// +/// Construct a virtual memory manager to keep track of allocated and free virtual memory regions within a certain space +/// +/// Arguments: +/// IN comptime Payload: type - The type of the payload to pass to the mapper +/// +/// Return: type +/// The constructed type +/// +pub fn VirtualMemoryManager(comptime Payload: type) type { + return struct { + /// The bitmap that keeps track of allocated and free regions + bmp: bitmap.Bitmap(usize), + + /// The start of the memory to be tracked + start: usize, + + /// The end of the memory to be tracked + end: usize, + + /// The allocator to use when allocating and freeing regions + allocator: *std.mem.Allocator, + + /// All allocations that have been made with this manager + allocations: std.hash_map.AutoHashMap(usize, Allocation), + + /// The mapper to use when allocating and freeing regions + mapper: Mapper(Payload), + + /// The payload to pass to the mapper functions + payload: Payload, + + const Self = @This(); + + /// + /// Initialise a virtual memory manager + /// + /// Arguments: + /// IN start: usize - The start of the memory region to manage + /// IN end: usize - The end of the memory region to manage. Must be greater than the start + /// IN/OUT allocator: *std.mem.Allocator - The allocator to use when allocating and freeing regions + /// IN mapper: Mapper - The mapper to use when allocating and freeing regions + /// IN payload: Payload - The payload data to be passed to the mapper + /// + /// Return: Self + /// The manager constructed + /// + /// Error: std.mem.Allocator.Error + /// std.mem.Allocator.Error.OutOfMemory - The allocator cannot allocate the memory required + /// + pub fn init(start: usize, end: usize, allocator: *std.mem.Allocator, mapper: Mapper(Payload), payload: Payload) std.mem.Allocator.Error!Self { + const size = end - start; + var bmp = try bitmap.Bitmap(usize).init(std.mem.alignForward(size, pmm.BLOCK_SIZE) / pmm.BLOCK_SIZE, allocator); + return Self{ + .bmp = bmp, + .start = start, + .end = end, + .allocator = allocator, + .allocations = std.hash_map.AutoHashMap(usize, Allocation).init(allocator), + .mapper = mapper, + .payload = payload, + }; + } + + /// + /// Check if a virtual memory address has been set + /// + /// Arguments: + /// IN self: *Self - The manager to check + /// IN virt: usize - The virtual memory address to check + /// + /// Return: bool + /// Whether the address is set + /// + /// Error: pmm.PmmError + /// Bitmap(u32).Error.OutOfBounds - The address given is outside of the memory managed + /// + pub fn isSet(self: *const Self, virt: usize) bitmap.Bitmap(u32).BitmapError!bool { + return try self.bmp.isSet((virt - self.start) / BLOCK_SIZE); + } + + /// + /// Map a region (can span more than one block) of virtual memory to a specific region of memory + /// + /// Arguments: + /// IN/OUT self: *Self - The manager to modify + /// IN virtual: mem.Range - The virtual region to set + /// IN physical: ?mem.Range - The physical region to map to or null if only the virtual region is to be set + /// IN attrs: Attributes - The attributes to apply to the memory regions + /// + /// Error: VmmError || Bitmap(u32).BitmapError || std.mem.Allocator.Error || MapperError + /// VmmError.AlreadyAllocated - The virtual address has already been allocated + /// VmmError.PhysicalAlreadyAllocated - The physical address has already been allocated + /// VmmError.PhysicalVirtualMismatch - The physical region and virtual region are of different sizes + /// VmmError.InvalidVirtAddresses - The start virtual address is greater than the end address + /// VmmError.InvalidPhysicalAddresses - The start physical address is greater than the end address + /// Bitmap.BitmapError.OutOfBounds - The physical or virtual addresses are out of bounds + /// std.mem.Allocator.Error.OutOfMemory - Allocating the required memory failed + /// MapperError.* - The causes depend on the mapper used + /// + pub fn set(self: *Self, virtual: mem.Range, physical: ?mem.Range, attrs: Attributes) (VmmError || bitmap.Bitmap(u32).BitmapError || std.mem.Allocator.Error || MapperError)!void { + var virt = virtual.start; + while (virt < virtual.end) : (virt += BLOCK_SIZE) { + if (try self.isSet(virt)) + return VmmError.AlreadyAllocated; + } + if (virtual.start > virtual.end) { + return VmmError.InvalidVirtAddresses; + } + + if (physical) |p| { + if (virtual.end - virtual.start != p.end - p.start) { + return VmmError.PhysicalVirtualMismatch; + } + if (p.start > p.end) { + return VmmError.InvalidPhysAddresses; + } + var phys = p.start; + while (phys < p.end) : (phys += BLOCK_SIZE) { + if (try pmm.isSet(phys)) { + return VmmError.PhysicalAlreadyAllocated; + } + } + } + + var phys_list = std.ArrayList(usize).init(self.allocator); + + virt = virtual.start; + while (virt < virtual.end) : (virt += BLOCK_SIZE) { + try self.bmp.setEntry((virt - self.start) / BLOCK_SIZE); + } + + if (physical) |p| { + try self.mapper.mapFn(virtual.start, virtual.end, p.start, p.end, attrs, self.allocator, self.payload); + + var phys = p.start; + while (phys < p.end) : (phys += BLOCK_SIZE) { + try pmm.setAddr(phys); + try phys_list.append(phys); + } + } + _ = try self.allocations.put(virtual.start, Allocation{ .physical = phys_list }); + } + + /// + /// Allocate a number of contiguous blocks of virtual memory + /// + /// Arguments: + /// IN/OUT self: *Self - The manager to allocate for + /// IN num: usize - The number of blocks to allocate + /// IN attrs: Attributes - The attributes to apply to the mapped memory + /// + /// Return: ?usize + /// The address at the start of the allocated region, or null if no region could be allocated due to a lack of contiguous blocks. + /// + /// Error: std.mem.Allocator.Error + /// std.mem.AllocatorError.OutOfMemory: The required amount of memory couldn't be allocated + /// + pub fn alloc(self: *Self, num: usize, attrs: Attributes) std.mem.Allocator.Error!?usize { + if (num == 0) + return null; + // Ensure that there is both enough physical and virtual address space free + if (pmm.blocksFree() >= num and self.bmp.num_free_entries >= num) { + // The virtual address space must be contiguous + if (self.bmp.setContiguous(num)) |entry| { + var block_list = std.ArrayList(usize).init(self.allocator); + try block_list.ensureCapacity(num); + + var i: usize = 0; + const vaddr_start = self.start + entry * BLOCK_SIZE; + var vaddr = vaddr_start; + // Map the blocks to physical memory + while (i < num) : (i += 1) { + const addr = pmm.alloc() orelse unreachable; + try block_list.append(addr); + // The map function failing isn't the caller's responsibility so panic as it shouldn't happen + self.mapper.mapFn(vaddr, vaddr + BLOCK_SIZE, addr, addr + BLOCK_SIZE, attrs, self.allocator, self.payload) catch |e| panic(@errorReturnTrace(), "Failed to map virtual memory: {}\n", .{e}); + vaddr += BLOCK_SIZE; + } + _ = try self.allocations.put(vaddr_start, Allocation{ .physical = block_list }); + return vaddr_start; + } + } + return null; + } + + /// + /// Free a previous allocation + /// + /// Arguments: + /// IN/OUT self: *Self - The manager to free within + /// IN vaddr: usize - The start of the allocation to free. This should be the address returned from a prior `alloc` call + /// + /// Error: Bitmap.BitmapError || VmmError + /// VmmError.NotAllocated - This address hasn't been allocated yet + /// Bitmap.BitmapError.OutOfBounds - The address is out of the manager's bounds + /// + pub fn free(self: *Self, vaddr: usize) (bitmap.Bitmap(u32).BitmapError || VmmError)!void { + const entry = (vaddr - self.start) / BLOCK_SIZE; + if (try self.bmp.isSet(entry)) { + // There will be an allocation associated with this virtual address + const allocation = self.allocations.get(vaddr) orelse unreachable; + const physical = allocation.physical; + defer physical.deinit(); + const num_physical_allocations = physical.items.len; + for (physical.items) |block, i| { + // Clear the address space entry and free the physical memory + try self.bmp.clearEntry(entry + i); + pmm.free(block) catch |e| panic(@errorReturnTrace(), "Failed to free PMM reserved memory at 0x{X}: {}\n", .{ block * BLOCK_SIZE, e }); + } + // Unmap the entire range + const region_start = entry * BLOCK_SIZE; + const region_end = (entry + num_physical_allocations) * BLOCK_SIZE; + self.mapper.unmapFn(region_start, region_end, self.payload) catch |e| panic(@errorReturnTrace(), "Failed to unmap VMM reserved memory from 0x{X} to 0x{X}: {}\n", .{ region_start, region_end, e }); + // The allocation is freed so remove from the map + self.allocations.removeAssertDiscard(vaddr); + } else { + return VmmError.NotAllocated; + } + } + }; +} + +/// +/// Initialise the main system virtual memory manager covering 4GB. Maps in the kernel code and reserved virtual memory +/// +/// Arguments: +/// IN mem_profile: *const mem.MemProfile - The system's memory profile. This is used to find the kernel code region and boot modules +/// IN/OUT allocator: *std.mem.Allocator - The allocator to use when needing to allocate memory +/// +/// Return: VirtualMemoryManager +/// The virtual memory manager created with all reserved virtual regions allocated +/// +/// Error: std.mem.Allocator.Error +/// std.mem.Allocator.Error.OutOfMemory - The allocator cannot allocate the memory required +/// +pub fn init(mem_profile: *const mem.MemProfile, allocator: *std.mem.Allocator) std.mem.Allocator.Error!VirtualMemoryManager(arch.VmmPayload) { + log.logInfo("Init vmm\n", .{}); + defer log.logInfo("Done vmm\n", .{}); + + var vmm = try VirtualMemoryManager(arch.VmmPayload).init(@ptrToInt(&KERNEL_ADDR_OFFSET), 0xFFFFFFFF, allocator, arch.VMM_MAPPER, arch.KERNEL_VMM_PAYLOAD); + + // Map in kernel + // Calculate start and end of mapping + const v_start = std.mem.alignBackward(@ptrToInt(mem_profile.vaddr_start), BLOCK_SIZE); + const v_end = std.mem.alignForward(@ptrToInt(mem_profile.vaddr_end) + mem.FIXED_ALLOC_SIZE, BLOCK_SIZE); + const p_start = std.mem.alignBackward(@ptrToInt(mem_profile.physaddr_start), BLOCK_SIZE); + const p_end = std.mem.alignForward(@ptrToInt(mem_profile.physaddr_end) + mem.FIXED_ALLOC_SIZE, BLOCK_SIZE); + vmm.set(.{ .start = v_start, .end = v_end }, mem.Range{ .start = p_start, .end = p_end }, .{ .kernel = true, .writable = false, .cachable = true }) catch |e| panic(@errorReturnTrace(), "Failed mapping kernel code in VMM: {}", .{e}); + + for (mem_profile.virtual_reserved) |entry| { + const virtual = mem.Range{ .start = std.mem.alignBackward(entry.virtual.start, BLOCK_SIZE), .end = std.mem.alignForward(entry.virtual.end, BLOCK_SIZE) }; + const physical: ?mem.Range = if (entry.physical) |phys| mem.Range{ .start = std.mem.alignBackward(phys.start, BLOCK_SIZE), .end = std.mem.alignForward(phys.end, BLOCK_SIZE) } else null; + vmm.set(virtual, physical, .{ .kernel = true, .writable = true, .cachable = true }) catch |e| switch (e) { + VmmError.AlreadyAllocated => {}, + else => panic(@errorReturnTrace(), "Failed mapping region in VMM {}: {}\n", .{ entry, e }), + }; + } + + switch (build_options.test_mode) { + .Initialisation => runtimeTests(arch.VmmPayload, vmm, mem_profile), + else => {}, + } + return vmm; +} + +test "alloc and free" { + const num_entries = 512; + var vmm = try testInit(num_entries); + var allocations = test_allocations orelse unreachable; + var virtual_allocations = std.ArrayList(usize).init(std.testing.allocator); + defer virtual_allocations.deinit(); + + var entry: u32 = 0; + while (entry < num_entries) { + // Test allocating various numbers of blocks all at once + // Rather than using a random number generator, just set the number of blocks to allocate based on how many entries have been done so far + var num_to_alloc: u32 = if (entry > 400) @as(u32, 8) else if (entry > 320) @as(u32, 14) else if (entry > 270) @as(u32, 9) else if (entry > 150) @as(u32, 26) else @as(u32, 1); + const result = try vmm.alloc(num_to_alloc, .{ .kernel = true, .writable = true, .cachable = true }); + + var should_be_set = true; + if (entry + num_to_alloc > num_entries) { + // If the number to allocate exceeded the number of entries, then allocation should have failed + std.testing.expectEqual(@as(?usize, null), result); + should_be_set = false; + } else { + // Else it should have succeeded and allocated the correct address + std.testing.expectEqual(@as(?usize, vmm.start + entry * BLOCK_SIZE), result); + try virtual_allocations.append(result orelse unreachable); + } + + // Make sure that the entries are set or not depending on the allocation success + var vaddr = entry * BLOCK_SIZE; + while (vaddr < (entry + num_to_alloc) * BLOCK_SIZE) : (vaddr += BLOCK_SIZE) { + if (should_be_set) { + // Allocation succeeded so this address should be set + std.testing.expect(try vmm.isSet(vaddr)); + // The test mapper should have received this address + std.testing.expect(try allocations.isSet(vaddr / BLOCK_SIZE)); + } else { + // Allocation failed as there weren't enough free entries + if (vaddr >= num_entries * BLOCK_SIZE) { + // If this address is beyond the VMM's end address, it should be out of bounds + std.testing.expectError(bitmap.Bitmap(u32).BitmapError.OutOfBounds, vmm.isSet(vaddr)); + std.testing.expectError(bitmap.Bitmap(u64).BitmapError.OutOfBounds, allocations.isSet(vaddr / BLOCK_SIZE)); + } else { + // Else it should not be set + std.testing.expect(!(try vmm.isSet(vaddr))); + // The test mapper should not have received this address + std.testing.expect(!(try allocations.isSet(vaddr / BLOCK_SIZE))); + } + } + } + entry += num_to_alloc; + + // All later entries should not be set + var later_entry = entry; + while (later_entry < num_entries) : (later_entry += 1) { + std.testing.expect(!(try vmm.isSet(vmm.start + later_entry * BLOCK_SIZE))); + std.testing.expect(!(try pmm.isSet(later_entry * BLOCK_SIZE))); + } + } + + // Try freeing all allocations + for (virtual_allocations.items) |alloc| { + const alloc_group = vmm.allocations.get(alloc); + std.testing.expect(alloc_group != null); + const physical = alloc_group.?.physical; + // We need to create a copy of the physical allocations since the free call deinits them + var physical_copy = std.ArrayList(usize).init(std.testing.allocator); + defer physical_copy.deinit(); + // Make sure they are all reserved in the PMM + for (physical.items) |phys| { + std.testing.expect(try pmm.isSet(phys)); + try physical_copy.append(phys); + } + vmm.free(alloc) catch unreachable; + // This virtual allocation should no longer be in the hashmap + std.testing.expectEqual(vmm.allocations.get(alloc), null); + std.testing.expect(!try vmm.isSet(alloc)); + // And all its physical blocks should now be free + for (physical_copy.items) |phys| { + std.testing.expect(!try pmm.isSet(phys)); + } + } +} + +test "set" { + const num_entries = 512; + var vmm = try testInit(num_entries); + + const vstart = BLOCK_SIZE * 37; + const vend = BLOCK_SIZE * 46; + const pstart = vstart + 123; + const pend = vend + 123; + const attrs = Attributes{ .kernel = true, .writable = true, .cachable = true }; + try vmm.set(.{ .start = vstart, .end = vend }, mem.Range{ .start = pstart, .end = pend }, attrs); + // Make sure it put the correct address in the map + std.testing.expect(vmm.allocations.get(vstart) != null); + + var allocations = test_allocations orelse unreachable; + // The entries before the virtual start shouldn't be set + var vaddr = vmm.start; + while (vaddr < vstart) : (vaddr += BLOCK_SIZE) { + std.testing.expect(!(try allocations.isSet(vaddr / BLOCK_SIZE))); + } + // The entries up until the virtual end should be set + while (vaddr < vend) : (vaddr += BLOCK_SIZE) { + std.testing.expect(try allocations.isSet(vaddr / BLOCK_SIZE)); + } + // The entries after the virtual end should not be set + while (vaddr < vmm.end) : (vaddr += BLOCK_SIZE) { + std.testing.expect(!(try allocations.isSet(vaddr / BLOCK_SIZE))); + } +} + +var test_allocations: ?bitmap.Bitmap(u64) = null; +var test_mapper = Mapper(u8){ .mapFn = testMap, .unmapFn = testUnmap }; + +/// +/// Initialise a virtual memory manager used for testing +/// +/// Arguments: +/// IN num_entries: u32 - The number of entries the VMM should track +/// +/// Return: VirtualMemoryManager(u8) +/// The VMM constructed +/// +/// Error: std.mem.Allocator.Error +/// OutOfMemory: The allocator couldn't allocate the structures needed +/// +fn testInit(num_entries: u32) std.mem.Allocator.Error!VirtualMemoryManager(u8) { + if (test_allocations == null) { + test_allocations = try bitmap.Bitmap(u64).init(num_entries, std.heap.page_allocator); + } else |allocations| { + var entry: u32 = 0; + while (entry < allocations.num_entries) : (entry += 1) { + allocations.clearEntry(entry) catch unreachable; + } + } + var allocations = test_allocations orelse unreachable; + const mem_profile = mem.MemProfile{ + .vaddr_end = undefined, + .vaddr_start = undefined, + .physaddr_start = undefined, + .physaddr_end = undefined, + .mem_kb = num_entries * BLOCK_SIZE / 1024, + .fixed_allocator = undefined, + .virtual_reserved = &[_]mem.Map{}, + .physical_reserved = &[_]mem.Range{}, + .modules = &[_]mem.Module{}, + }; + pmm.init(&mem_profile, std.heap.page_allocator); + return try VirtualMemoryManager(u8).init(0, num_entries * BLOCK_SIZE, std.heap.page_allocator, test_mapper, 39); +} + +/// +/// A mapping function used when doing unit tests +/// +/// Arguments: +/// IN vstart: usize - The start of the virtual region to map +/// IN vend: usize - The end of the virtual region to map +/// IN pstart: usize - The start of the physical region to map +/// IN pend: usize - The end of the physical region to map +/// IN attrs: Attributes - The attributes to map with +/// IN/OUT allocator: *std.mem.Allocator - The allocator to use. Ignored +/// IN payload: u8 - The payload value. Expected to be 39 +/// +fn testMap(vstart: usize, vend: usize, pstart: usize, pend: usize, attrs: Attributes, allocator: *std.mem.Allocator, payload: u8) (std.mem.Allocator.Error || MapperError)!void { + std.testing.expectEqual(@as(u8, 39), payload); + var vaddr = vstart; + while (vaddr < vend) : (vaddr += BLOCK_SIZE) { + (test_allocations orelse unreachable).setEntry(vaddr / BLOCK_SIZE) catch unreachable; + } +} + +/// +/// An unmapping function used when doing unit tests +/// +/// Arguments: +/// IN vstart: usize - The start of the virtual region to unmap +/// IN vend: usize - The end of the virtual region to unmap +/// IN payload: u8 - The payload value. Expected to be 39 +/// +fn testUnmap(vstart: usize, vend: usize, payload: u8) (std.mem.Allocator.Error || MapperError)!void { + std.testing.expectEqual(@as(u8, 39), payload); + var vaddr = vstart; + while (vaddr < vend) : (vaddr += BLOCK_SIZE) { + (test_allocations orelse unreachable).clearEntry(vaddr / BLOCK_SIZE) catch unreachable; + } +} + +/// +/// Run the runtime tests. +/// +/// Arguments: +/// IN comptime Payload: type - The type of the payload passed to the mapper +/// IN vmm: VirtualMemoryManager(Payload) - The virtual memory manager to test +/// IN mem_profile: *const mem.MemProfile - The mem profile with details about all the memory regions that should be reserved +/// IN mb_info: *multiboot.multiboot_info_t - The multiboot info struct that should also be reserved +/// +fn runtimeTests(comptime Payload: type, vmm: VirtualMemoryManager(Payload), mem_profile: *const mem.MemProfile) void { + const v_start = std.mem.alignBackward(@ptrToInt(mem_profile.vaddr_start), BLOCK_SIZE); + const v_end = std.mem.alignForward(@ptrToInt(mem_profile.vaddr_end) + mem.FIXED_ALLOC_SIZE, BLOCK_SIZE); + + var vaddr = vmm.start; + while (vaddr < vmm.end - BLOCK_SIZE) : (vaddr += BLOCK_SIZE) { + const set = vmm.isSet(vaddr) catch unreachable; + var should_be_set = false; + if (vaddr < v_end and vaddr >= v_start) { + should_be_set = true; + } else { + for (mem_profile.virtual_reserved) |entry| { + if (vaddr >= std.mem.alignBackward(entry.virtual.start, BLOCK_SIZE) and vaddr < std.mem.alignForward(entry.virtual.end, BLOCK_SIZE)) { + should_be_set = true; + break; + } + } + } + if (set and !should_be_set) { + panic(@errorReturnTrace(), "An address was set in the VMM when it shouldn't have been: 0x{x}\n", .{vaddr}); + } else if (!set and should_be_set) { + panic(@errorReturnTrace(), "An address was not set in the VMM when it should have been: 0x{x}\n", .{vaddr}); + } + } + + log.logInfo("VMM: Tested allocations\n", .{}); +} diff --git a/test/kernel/arch/x86/rt-test.py b/test/kernel/arch/x86/rt-test.py deleted file mode 100644 index c9a15c3b..00000000 --- a/test/kernel/arch/x86/rt-test.py +++ /dev/null @@ -1,21 +0,0 @@ -def get_test_cases(TestCase): - return [ - TestCase("GDT init", [r"Init gdt", r"Done"]), - TestCase("GDT tests", [r"GDT: Tested loading GDT"]), - TestCase("IDT init", [r"Init idt", r"Done"]), - TestCase("IDT tests", [r"IDT: Tested loading IDT"]), - TestCase("PIC init", [r"Init pic", r"Done"]), - TestCase("PIC tests", [r"PIC: Tested masking"]), - TestCase("ISR init", [r"Init isr", r"Done"]), - TestCase("ISR tests", [r"ISR: Tested registered handlers", r"ISR: Tested opened IDT entries"]), - TestCase("IRQ init", [r"Init irq", r"Done"]), - TestCase("IRQ tests", [r"IRQ: Tested registered handlers", r"IRQ: Tested opened IDT entries"]), - TestCase("PIT init", [r"Init pit"]), - TestCase("PIT init", [r".+"], r"\[DEBUG\] "), - TestCase("PIT init", [r"Done"]), - TestCase("PIT tests", [r"PIT: Tested init", r"PIT: Tested wait ticks", r"PIT: Tested wait ticks 2"]), - TestCase("Paging init", [r"Init paging", r"Done"]), - TestCase("Paging tests", [r"Paging: Tested accessing unmapped memory", r"Paging: Tested accessing mapped memory"]), - TestCase("Syscalls init", [r"Init syscalls", r"Done"]), - TestCase("Syscall tests", [r"Syscalls: Tested no args", r"Syscalls: Tested 1 arg", r"Syscalls: Tested 2 args", r"Syscalls: Tested 3 args", r"Syscalls: Tested 4 args", r"Syscalls: Tested 5 args"]) - ] diff --git a/test/mock/kernel/arch_mock.zig b/test/mock/kernel/arch_mock.zig index 1d043e8f..47e7740a 100644 --- a/test/mock/kernel/arch_mock.zig +++ b/test/mock/kernel/arch_mock.zig @@ -4,7 +4,12 @@ const mem = @import("mem_mock.zig"); const MemProfile = mem.MemProfile; const gdt = @import("gdt_mock.zig"); const idt = @import("idt_mock.zig"); -const multiboot = @import("../../../src/kernel/multiboot.zig"); +const vmm = @import("vmm_mock.zig"); +const paging = @import("paging_mock.zig"); +const Serial = @import("../../../src/kernel/serial.zig").Serial; +const TTY = @import("../../../src/kernel/tty.zig").TTY; + +pub const task = @import("task_mock.zig"); const mock_framework = @import("mock_framework.zig"); pub const initTest = mock_framework.initTest; @@ -13,7 +18,8 @@ pub const addTestParams = mock_framework.addTestParams; pub const addConsumeFunction = mock_framework.addConsumeFunction; pub const addRepeatFunction = mock_framework.addRepeatFunction; -pub const InterruptContext = struct { +pub const CpuState = struct { + ss: u32, gs: u32, fs: u32, es: u32, @@ -32,9 +38,24 @@ pub const InterruptContext = struct { cs: u32, eflags: u32, user_esp: u32, - ss: u32, + user_ss: u32, }; +pub const VmmPayload = u8; +pub const KERNEL_VMM_PAYLOAD: usize = 0; +pub const MEMORY_BLOCK_SIZE: u32 = paging.PAGE_SIZE_4KB; +pub const STACK_SIZE: u32 = MEMORY_BLOCK_SIZE / @sizeOf(u32); +pub const VMM_MAPPER: vmm.Mapper(VmmPayload) = undefined; +pub const BootPayload = u8; +pub const Task = task.Task; + +// The virtual/physical start/end of the kernel code +var KERNEL_PHYSADDR_START: u32 = 0x00100000; +var KERNEL_PHYSADDR_END: u32 = 0x01000000; +var KERNEL_VADDR_START: u32 = 0xC0100000; +var KERNEL_VADDR_END: u32 = 0xC1100000; +var KERNEL_ADDR_OFFSET: u32 = 0xC0000000; + pub fn outb(port: u16, data: u8) void { return mock_framework.performAction("outb", void, .{ port, data }); } @@ -87,8 +108,42 @@ pub fn haltNoInterrupts() noreturn { while (true) {} } -pub fn init(mb_info: *multiboot.multiboot_info_t, mem_profile: *const MemProfile, allocator: *Allocator) void { - // I'll get back to this as this doesn't effect the GDT testing. +pub fn initSerial(boot_payload: BootPayload) Serial { + return .{ .write = undefined }; +} + +pub fn initTTY(boot_payload: BootPayload) TTY { + return .{ + .print = undefined, + .setCursor = undefined, + .cols = undefined, + .rows = undefined, + .clear = null, + }; +} + +pub fn initMem(payload: BootPayload) std.mem.Allocator.Error!mem.MemProfile { + return MemProfile{ + .vaddr_end = @ptrCast([*]u8, &KERNEL_VADDR_END), + .vaddr_start = @ptrCast([*]u8, &KERNEL_VADDR_START), + .physaddr_end = @ptrCast([*]u8, &KERNEL_PHYSADDR_END), + .physaddr_start = @ptrCast([*]u8, &KERNEL_PHYSADDR_START), + // Total memory available including the initial 1MiB that grub doesn't include + .mem_kb = 0, + .fixed_allocator = undefined, + .virtual_reserved = undefined, + .physical_reserved = undefined, + .modules = undefined, + }; +} + +pub fn initTaskStack(entry_point: usize, allocator: *Allocator) Allocator.Error!struct { stack: []u32, pointer: usize } { + const ret = .{ .stack = &([_]u32{}), .pointer = 0 }; + return ret; +} + +pub fn init(payload: BootPayload, mem_profile: *const MemProfile, allocator: *Allocator) void { + // I'll get back to this as this doesn't effect the current testing. // When I come on to the mem.zig testing, I'll fix :) //return mock_framework.performAction("init", void, mem_profile, allocator); } @@ -98,3 +153,5 @@ pub fn init(mb_info: *multiboot.multiboot_info_t, mem_profile: *const MemProfile pub fn mock_disableInterrupts() void {} pub fn mock_enableInterrupts() void {} + +pub fn mock_ioWait() void {} diff --git a/test/mock/kernel/cmos_mock.zig b/test/mock/kernel/cmos_mock.zig new file mode 100644 index 00000000..735b2a46 --- /dev/null +++ b/test/mock/kernel/cmos_mock.zig @@ -0,0 +1,34 @@ +const mock_framework = @import("mock_framework.zig"); +pub const initTest = mock_framework.initTest; +pub const freeTest = mock_framework.freeTest; +pub const addTestParams = mock_framework.addTestParams; +pub const addConsumeFunction = mock_framework.addConsumeFunction; +pub const addRepeatFunction = mock_framework.addRepeatFunction; + +pub const StatusRegister = enum { + A, + B, + C, +}; + +pub const RtcRegister = enum { + SECOND, + MINUTE, + HOUR, + DAY, + MONTH, + YEAR, + CENTURY, +}; + +pub fn readRtcRegister(reg: RtcRegister) u8 { + return mock_framework.performAction("readRtcRegister", u8, .{reg}); +} + +pub fn readStatusRegister(reg: StatusRegister, comptime disable_nmi: bool) u8 { + return mock_framework.performAction("readStatusRegister", u8, .{ reg, disable_nmi }); +} + +pub fn writeStatusRegister(reg: StatusRegister, data: u8, comptime disable_nmi: bool) void { + return mock_framework.performAction("writeStatusRegister", void, .{ reg, data, disable_nmi }); +} diff --git a/test/mock/kernel/gdt_mock.zig b/test/mock/kernel/gdt_mock.zig index 031d697e..2c65f8b8 100644 --- a/test/mock/kernel/gdt_mock.zig +++ b/test/mock/kernel/gdt_mock.zig @@ -35,7 +35,7 @@ const GdtEntry = packed struct { base_high: u8, }; -const TtsEntry = packed struct { +const Tss = packed struct { prev_tss: u32, esp0: u32, ss0: u32, @@ -160,10 +160,6 @@ pub const USER_CODE_OFFSET: u16 = 0x18; pub const USER_DATA_OFFSET: u16 = 0x20; pub const TSS_OFFSET: u16 = 0x28; -pub fn setTssStack(esp0: u32) void { - return mock_framework.performAction("setTssStack", void, esp0); -} - pub fn init() void { return mock_framework.performAction("init", void); } diff --git a/test/mock/kernel/idt_mock.zig b/test/mock/kernel/idt_mock.zig index ee5e297a..ed50fd24 100644 --- a/test/mock/kernel/idt_mock.zig +++ b/test/mock/kernel/idt_mock.zig @@ -7,7 +7,7 @@ pub const addTestParams = mock_framework.addTestParams; pub const addConsumeFunction = mock_framework.addConsumeFunction; pub const addRepeatFunction = mock_framework.addRepeatFunction; -const IdtEntry = packed struct { +pub const IdtEntry = packed struct { base_low: u16, selector: u16, zero: u8, @@ -34,10 +34,14 @@ const PRIVILEGE_RING_1: u2 = 0x1; const PRIVILEGE_RING_2: u2 = 0x2; const PRIVILEGE_RING_3: u2 = 0x3; -const NUMBER_OF_ENTRIES: u16 = 256; +pub const NUMBER_OF_ENTRIES: u16 = 256; const TABLE_SIZE: u16 = @sizeOf(IdtEntry) * NUMBER_OF_ENTRIES - 1; +pub fn isIdtOpen(entry: IdtEntry) bool { + return mock_framework.performAction("isIdtOpen", bool, .{entry}); +} + pub fn openInterruptGate(index: u8, handler: InterruptHandler) IdtError!void { return mock_framework.performAction("openInterruptGate", IdtError!void, .{ index, handler }); } diff --git a/test/mock/kernel/log_mock.zig b/test/mock/kernel/log_mock.zig index 09ce7dee..4a4b93fa 100644 --- a/test/mock/kernel/log_mock.zig +++ b/test/mock/kernel/log_mock.zig @@ -12,22 +12,22 @@ pub const Level = enum { ERROR, }; -pub fn log(comptime level: Level, comptime format: []const u8, args: var) void { +pub fn log(comptime level: Level, comptime format: []const u8, args: anytype) void { //return mock_framework.performAction("log", void, level, format, args); } -pub fn logInfo(comptime format: []const u8, args: var) void { +pub fn logInfo(comptime format: []const u8, args: anytype) void { //return mock_framework.performAction("logInfo", void, format, args); } -pub fn logDebug(comptime format: []const u8, args: var) void { +pub fn logDebug(comptime format: []const u8, args: anytype) void { //return mock_framework.performAction("logDebug", void, format, args); } -pub fn logWarning(comptime format: []const u8, args: var) void { +pub fn logWarning(comptime format: []const u8, args: anytype) void { //return mock_framework.performAction("logWarning", void, format, args); } -pub fn logError(comptime format: []const u8, args: var) void { +pub fn logError(comptime format: []const u8, args: anytype) void { //return mock_framework.performAction("logError", void, format, args); } diff --git a/test/mock/kernel/mem_mock.zig b/test/mock/kernel/mem_mock.zig index 62abeff4..cfe7f364 100644 --- a/test/mock/kernel/mem_mock.zig +++ b/test/mock/kernel/mem_mock.zig @@ -1,32 +1,50 @@ +const std = @import("std"); const multiboot = @import("../../../src/kernel/multiboot.zig"); +pub const Module = struct { + region: Range, + name: []const u8, +}; + +pub const Map = struct { + virtual: Range, + physical: ?Range, +}; + +pub const Range = struct { + start: usize, + end: usize, +}; + pub const MemProfile = struct { vaddr_end: [*]u8, vaddr_start: [*]u8, physaddr_end: [*]u8, physaddr_start: [*]u8, mem_kb: u32, - fixed_alloc_size: u32, + modules: []Module, + virtual_reserved: []Map, + physical_reserved: []Range, + fixed_allocator: std.heap.FixedBufferAllocator, }; -// The virtual/physical start/end of the kernel code -var KERNEL_PHYSADDR_START: u32 = 0x00100000; -var KERNEL_PHYSADDR_END: u32 = 0x01000000; -var KERNEL_VADDR_START: u32 = 0xC0100000; -var KERNEL_VADDR_END: u32 = 0xC1100000; -var KERNEL_ADDR_OFFSET: u32 = 0xC0000000; - // The size of the fixed allocator used before the heap is set up. Set to 1MiB. const FIXED_ALLOC_SIZE = 1024 * 1024; -pub fn init(mb_info: *multiboot.multiboot_info_t) MemProfile { - return MemProfile{ - .vaddr_end = @ptrCast([*]u8, &KERNEL_VADDR_END), - .vaddr_start = @ptrCast([*]u8, &KERNEL_VADDR_START), - .physaddr_end = @ptrCast([*]u8, &KERNEL_PHYSADDR_END), - .physaddr_start = @ptrCast([*]u8, &KERNEL_PHYSADDR_START), - // Total memory available including the initial 1MiB that grub doesn't include - .mem_kb = mb_info.mem_upper + mb_info.mem_lower + 1024, - .fixed_alloc_size = FIXED_ALLOC_SIZE, +pub fn virtToPhys(virt: anytype) @TypeOf(virt) { + const T = @TypeOf(virt); + return switch (@typeInfo(T)) { + .Pointer => @intToPtr(T, @ptrToInt(virt) - KERNEL_ADDR_OFFSET), + .Int => virt - KERNEL_ADDR_OFFSET, + else => @compileError("Only pointers and integers are supported"), + }; +} + +pub fn physToVirt(phys: anytype) @TypeOf(phys) { + const T = @TypeOf(phys); + return switch (@typeInfo(T)) { + .Pointer => @intToPtr(T, @ptrToInt(phys) + KERNEL_ADDR_OFFSET), + .Int => phys + KERNEL_ADDR_OFFSET, + else => @compileError("Only pointers and integers are supported"), }; } diff --git a/test/mock/kernel/mock_framework.zig b/test/mock/kernel/mock_framework.zig index 17e8d4a3..2508674a 100644 --- a/test/mock/kernel/mock_framework.zig +++ b/test/mock/kernel/mock_framework.zig @@ -1,12 +1,15 @@ const std = @import("std"); +const builtin = @import("builtin"); const StringHashMap = std.StringHashMap; const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; -const GlobalAllocator = std.debug.global_allocator; +const GlobalAllocator = std.testing.allocator; const TailQueue = std.TailQueue; const warn = std.debug.warn; const gdt = @import("gdt_mock.zig"); const idt = @import("idt_mock.zig"); +const cmos = @import("cmos_mock.zig"); +const task = @import("task_mock.zig"); /// /// The enumeration of types that the mocking framework supports. These include basic types like u8 @@ -18,9 +21,18 @@ const DataElementType = enum { U8, U16, U32, - PTR_CONST_GdtPtr, - PTR_CONST_IdtPtr, + USIZE, + PTR_ALLOCATOR, + ECMOSSTATUSREGISTER, + ECMOSRTCREGISTER, + GDTPTR, + IDTPTR, + IDTENTRY, + PTR_CONST_GDTPTR, + PTR_CONST_IDTPTR, ERROR_IDTERROR_VOID, + ERROR_MEM_PTRTASK, + PTR_TASK, EFN_OVOID, NFN_OVOID, FN_OVOID, @@ -29,21 +41,35 @@ const DataElementType = enum { FN_IU8_OBOOL, FN_IU8_OVOID, FN_IU16_OVOID, + FN_IUSIZE_OVOID, FN_IU16_OU8, FN_IU4_IU4_OU8, FN_IU8_IU8_OU16, FN_IU16_IU8_OVOID, FN_IU16_IU16_OVOID, + FN_IECMOSSTATUSREGISTER_IBOOL_OU8, + FN_IECMOSSTATUSREGISTER_IU8_IBOOL_OVOID, + FN_IECMOSRTCREGISTER_OU8, FN_IU8_IEFNOVOID_OERRORIDTERRORVOID, FN_IU8_INFNOVOID_OERRORIDTERRORVOID, FN_IPTRCONSTGDTPTR_OVOID, FN_IPTRCONSTIDTPTR_OVOID, + FN_OGDTPTR, + FN_OIDTPTR, + FN_IIDTENTRY_OBOOL, + FN_IPTRTask_IUSIZE_OVOID, + FN_IPTRTASK_IPTRALLOCATOR_OVOID, + FN_IFNOVOID_OMEMERRORPTRTASK, + FN_IFNOVOID_IPTRALLOCATOR_OMEMERRORPTRTASK, }; /// /// A tagged union of all the data elements that the mocking framework can work with. This can be /// expanded to add new types. This is needed as need a list of data that all have different types, /// so this wraps the data into a union, (which is of one type) so can have a list of them. +/// When https://github.com/ziglang/zig/issues/383 anf https://github.com/ziglang/zig/issues/2907 +/// is done, can programitaclly create types for this. Can use a compile time block that loops +/// through the available basic types and create function types so don't have a long list. /// const DataElement = union(DataElementType) { BOOL: bool, @@ -51,26 +77,46 @@ const DataElement = union(DataElementType) { U8: u8, U16: u16, U32: u32, - PTR_CONST_GdtPtr: *const gdt.GdtPtr, - PTR_CONST_IdtPtr: *const idt.IdtPtr, + USIZE: usize, + PTR_ALLOCATOR: *std.mem.Allocator, + ECMOSSTATUSREGISTER: cmos.StatusRegister, + ECMOSRTCREGISTER: cmos.RtcRegister, + GDTPTR: gdt.GdtPtr, + IDTPTR: idt.IdtPtr, + IDTENTRY: idt.IdtEntry, + PTR_CONST_GDTPTR: *const gdt.GdtPtr, + PTR_CONST_IDTPTR: *const idt.IdtPtr, ERROR_IDTERROR_VOID: idt.IdtError!void, - EFN_OVOID: extern fn () void, + ERROR_MEM_PTRTASK: std.mem.Allocator.Error!*task.Task, + PTR_TASK: *task.Task, + EFN_OVOID: fn () callconv(.C) void, NFN_OVOID: fn () callconv(.Naked) void, FN_OVOID: fn () void, FN_OUSIZE: fn () usize, FN_OU16: fn () u16, FN_IU8_OBOOL: fn (u8) bool, FN_IU8_OVOID: fn (u8) void, + FN_IUSIZE_OVOID: fn (usize) void, FN_IU16_OVOID: fn (u16) void, FN_IU16_OU8: fn (u16) u8, FN_IU4_IU4_OU8: fn (u4, u4) u8, FN_IU8_IU8_OU16: fn (u8, u8) u16, FN_IU16_IU8_OVOID: fn (u16, u8) void, FN_IU16_IU16_OVOID: fn (u16, u16) void, - FN_IU8_IEFNOVOID_OERRORIDTERRORVOID: fn (u8, extern fn () void) idt.IdtError!void, + FN_IECMOSSTATUSREGISTER_IBOOL_OU8: fn (cmos.StatusRegister, bool) u8, + FN_IECMOSSTATUSREGISTER_IU8_IBOOL_OVOID: fn (cmos.StatusRegister, u8, bool) void, + FN_IECMOSRTCREGISTER_OU8: fn (cmos.RtcRegister) u8, + FN_IU8_IEFNOVOID_OERRORIDTERRORVOID: fn (u8, fn () callconv(.C) void) idt.IdtError!void, FN_IU8_INFNOVOID_OERRORIDTERRORVOID: fn (u8, fn () callconv(.Naked) void) idt.IdtError!void, FN_IPTRCONSTGDTPTR_OVOID: fn (*const gdt.GdtPtr) void, FN_IPTRCONSTIDTPTR_OVOID: fn (*const idt.IdtPtr) void, + FN_OGDTPTR: fn () gdt.GdtPtr, + FN_OIDTPTR: fn () idt.IdtPtr, + FN_IIDTENTRY_OBOOL: fn (idt.IdtEntry) bool, + FN_IPTRTask_IUSIZE_OVOID: fn (*task.Task, usize) void, + FN_IPTRTASK_IPTRALLOCATOR_OVOID: fn (*task.Task, *std.mem.Allocator) void, + FN_IFNOVOID_OMEMERRORPTRTASK: fn (fn () void) std.mem.Allocator.Error!*task.Task, + FN_IFNOVOID_IPTRALLOCATOR_OMEMERRORPTRTASK: fn (fn () void, *std.mem.Allocator) std.mem.Allocator.Error!*task.Task, }; /// @@ -139,38 +185,58 @@ fn Mock() type { /// to have a list of different types. /// /// Arguments: - /// IN arg: var - The data, this can be a function or basic type value. + /// IN arg: anytype - The data, this can be a function or basic type value. /// /// Return: DataElement /// A DataElement with the data wrapped. /// - fn createDataElement(arg: var) DataElement { + fn createDataElement(arg: anytype) DataElement { return switch (@TypeOf(arg)) { bool => DataElement{ .BOOL = arg }, u4 => DataElement{ .U4 = arg }, u8 => DataElement{ .U8 = arg }, u16 => DataElement{ .U16 = arg }, u32 => DataElement{ .U32 = arg }, - *const gdt.GdtPtr => DataElement{ .PTR_CONST_GdtPtr = arg }, - *const idt.IdtPtr => DataElement{ .PTR_CONST_IdtPtr = arg }, + usize => DataElement{ .USIZE = arg }, + *std.mem.Allocator => DataElement{ .PTR_ALLOCATOR = arg }, + cmos.StatusRegister => DataElement{ .ECMOSSTATUSREGISTER = arg }, + cmos.RtcRegister => DataElement{ .ECMOSRTCREGISTER = arg }, + gdt.GdtPtr => DataElement{ .GDTPTR = arg }, + idt.IdtPtr => DataElement{ .IDTPTR = arg }, + idt.IdtEntry => DataElement{ .IDTENTRY = arg }, + *const gdt.GdtPtr => DataElement{ .PTR_CONST_GDTPTR = arg }, + *const idt.IdtPtr => DataElement{ .PTR_CONST_IDTPTR = arg }, idt.IdtError!void => DataElement{ .ERROR_IDTERROR_VOID = arg }, - extern fn () void => DataElement{ .EFN_OVOID = arg }, + std.mem.Allocator.Error!*task.Task => DataElement{ .ERROR_MEM_PTRTASK = arg }, + *task.Task => DataElement{ .PTR_TASK = arg }, + fn () callconv(.C) void => DataElement{ .EFN_OVOID = arg }, fn () callconv(.Naked) void => DataElement{ .NFN_OVOID = arg }, fn () void => DataElement{ .FN_OVOID = arg }, fn () usize => DataElement{ .FN_OUSIZE = arg }, fn () u16 => DataElement{ .FN_OU16 = arg }, fn (u8) bool => DataElement{ .FN_IU8_OBOOL = arg }, fn (u8) void => DataElement{ .FN_IU8_OVOID = arg }, + fn (usize) void => DataElement{ .FN_IUSIZE_OVOID = arg }, fn (u16) void => DataElement{ .FN_IU16_OVOID = arg }, fn (u16) u8 => DataElement{ .FN_IU16_OU8 = arg }, fn (u4, u4) u8 => DataElement{ .FN_IU4_IU4_OU8 = arg }, fn (u8, u8) u16 => DataElement{ .FN_IU8_IU8_OU16 = arg }, fn (u16, u8) void => DataElement{ .FN_IU16_IU8_OVOID = arg }, fn (u16, u16) void => DataElement{ .FN_IU16_IU16_OVOID = arg }, + fn (cmos.StatusRegister, bool) u8 => DataElement{ .FN_IECMOSSTATUSREGISTER_IBOOL_OU8 = arg }, + fn (cmos.StatusRegister, u8, bool) void => DataElement{ .FN_IECMOSSTATUSREGISTER_IU8_IBOOL_OVOID = arg }, + fn (cmos.RtcRegister) u8 => DataElement{ .FN_IECMOSRTCREGISTER_OU8 = arg }, fn (*const gdt.GdtPtr) void => DataElement{ .FN_IPTRCONSTGDTPTR_OVOID = arg }, + fn () gdt.GdtPtr => DataElement{ .FN_OGDTPTR = arg }, fn (*const idt.IdtPtr) void => DataElement{ .FN_IPTRCONSTIDTPTR_OVOID = arg }, - fn (u8, extern fn () void) idt.IdtError!void => DataElement{ .FN_IU8_IEFNOVOID_OERRORIDTERRORVOID = arg }, + fn () idt.IdtPtr => DataElement{ .FN_OIDTPTR = arg }, + fn (u8, fn () callconv(.C) void) idt.IdtError!void => DataElement{ .FN_IU8_IEFNOVOID_OERRORIDTERRORVOID = arg }, fn (u8, fn () callconv(.Naked) void) idt.IdtError!void => DataElement{ .FN_IU8_INFNOVOID_OERRORIDTERRORVOID = arg }, + fn (idt.IdtEntry) bool => DataElement{ .FN_IIDTENTRY_OBOOL = arg }, + fn (*task.Task, usize) void => DataElement{ .FN_IPTRTask_IUSIZE_OVOID = arg }, + fn (*task.Task, *std.mem.Allocator) void => DataElement{ .FN_IPTRTASK_IPTRALLOCATOR_OVOID = arg }, + fn (fn () void) std.mem.Allocator.Error!*task.Task => DataElement{ .FN_IFNOVOID_OMEMERRORPTRTASK = arg }, + fn (fn () void, *std.mem.Allocator) std.mem.Allocator.Error!*task.Task => DataElement{ .FN_IFNOVOID_IPTRALLOCATOR_OMEMERRORPTRTASK = arg }, else => @compileError("Type not supported: " ++ @typeName(@TypeOf(arg))), }; } @@ -191,25 +257,46 @@ fn Mock() type { u8 => DataElementType.U8, u16 => DataElementType.U16, u32 => DataElementType.U32, - *const gdt.GdtPtr => DataElement.PTR_CONST_GdtPtr, - *const idt.IdtPtr => DataElement.PTR_CONST_IdtPtr, - idt.IdtError!void => DataElement.ERROR_IDTERROR_VOID, - extern fn () void => DataElementType.EFN_OVOID, + usize => DataElementType.USIZE, + *std.mem.Allocator => DataElementType.PTR_ALLOCATOR, + cmos.StatusRegister => DataElementType.ECMOSSTATUSREGISTER, + cmos.RtcRegister => DataElementType.ECMOSRTCREGISTER, + gdt.GdtPtr => DataElementType.GDTPTR, + idt.IdtPtr => DataElementType.IDTPTR, + idt.IdtEntry => DataElementType.IDTENTRY, + *const gdt.GdtPtr => DataElementType.PTR_CONST_GDTPTR, + *const idt.IdtPtr => DataElementType.PTR_CONST_IDTPTR, + idt.IdtError!void => DataElementType.ERROR_IDTERROR_VOID, + std.mem.Allocator.Error!*task.Task => DataElementType.ERROR_MEM_PTRTASK, + *task.Task => DataElementType.PTR_TASK, + fn () callconv(.C) void => DataElementType.EFN_OVOID, fn () callconv(.Naked) void => DataElementType.NFN_OVOID, fn () void => DataElementType.FN_OVOID, + fn () usize => DataElementType.FN_OUSIZE, fn () u16 => DataElementType.FN_OU16, fn (u8) bool => DataElementType.FN_IU8_OBOOL, fn (u8) void => DataElementType.FN_IU8_OVOID, fn (u16) void => DataElementType.FN_IU16_OVOID, + fn (usize) void => DataElementType.FN_IUSIZE_OVOID, fn (u16) u8 => DataElementType.FN_IU16_OU8, fn (u4, u4) u8 => DataElementType.FN_IU4_IU4_OU8, fn (u8, u8) u16 => DataElementType.FN_IU8_IU8_OU16, fn (u16, u8) void => DataElementType.FN_IU16_IU8_OVOID, fn (u16, u16) void => DataElementType.FN_IU16_IU16_OVOID, + fn (cmos.StatusRegister, bool) u8 => DataElementType.FN_IECMOSSTATUSREGISTER_IBOOL_OU8, + fn (cmos.StatusRegister, u8, bool) void => DataElementType.FN_IECMOSSTATUSREGISTER_IU8_IBOOL_OVOID, + fn (cmos.RtcRegister) u8 => DataElementType.FN_IECMOSRTCREGISTER_OU8, fn (*const gdt.GdtPtr) void => DataElementType.FN_IPTRCONSTGDTPTR_OVOID, fn (*const idt.IdtPtr) void => DataElementType.FN_IPTRCONSTIDTPTR_OVOID, - fn (u8, extern fn () void) idt.IdtError!void => DataElementType.FN_IU8_IEFNOVOID_OERRORIDTERRORVOID, + fn () gdt.GdtPtr => DataElementType.FN_OGDTPTR, + fn () idt.IdtPtr => DataElementType.FN_OIDTPTR, + fn (u8, fn () callconv(.C) void) idt.IdtError!void => DataElementType.FN_IU8_IEFNOVOID_OERRORIDTERRORVOID, fn (u8, fn () callconv(.Naked) void) idt.IdtError!void => DataElementType.FN_IU8_INFNOVOID_OERRORIDTERRORVOID, + fn (idt.IdtEntry) bool => DataElementType.FN_IIDTENTRY_OBOOL, + fn (*task.Task, usize) void => DataElementType.FN_IPTRTask_IUSIZE_OVOID, + fn (*task.Task, *std.mem.Allocator) void => DataElementType.FN_IPTRTASK_IPTRALLOCATOR_OVOID, + fn (fn () void) std.mem.Allocator.Error!*task.Task => DataElementType.FN_IFNOVOID_OMEMERRORPTRTASK, + fn (fn () void, *std.mem.Allocator) std.mem.Allocator.Error!*task.Task => DataElementType.FN_IFNOVOID_IPTRALLOCATOR_OMEMERRORPTRTASK, else => @compileError("Type not supported: " ++ @typeName(T)), }; } @@ -232,25 +319,46 @@ fn Mock() type { u8 => element.U8, u16 => element.U16, u32 => element.U32, - *const gdt.GdtPtr => element.PTR_CONST_GdtPtr, - *const idt.IdtPtr => element.PTR_CONST_IdtPtr, + usize => element.USIZE, + *std.mem.Allocator => element.PTR_ALLOCATOR, + cmos.StatusRegister => element.ECMOSSTATUSREGISTER, + gdt.GdtPtr => element.GDTPTR, + idt.IdtPtr => element.IDTPTR, + idt.IdtEntry => element.IDTENTRY, + cmos.RtcRegister => element.ECMOSRTCREGISTER, + *const gdt.GdtPtr => element.PTR_CONST_GDTPTR, + *const idt.IdtPtr => element.PTR_CONST_IDTPTR, idt.IdtError!void => element.ERROR_IDTERROR_VOID, - extern fn () void => element.EFN_OVOID, + std.mem.Allocator.Error!*task.Task => element.ERROR_MEM_PTRTASK, + *task.Task => element.PTR_TASK, + fn () callconv(.C) void => element.EFN_OVOID, fn () callconv(.Naked) void => element.NFN_OVOID, fn () void => element.FN_OVOID, + fn () usize => element.FN_OUSIZE, fn () u16 => element.FN_OU16, fn (u8) bool => element.FN_IU8_OBOOL, fn (u8) void => element.FN_IU8_OVOID, fn (u16) void => element.FN_IU16_OVOID, + fn (usize) void => element.FN_IUSIZE_OVOID, fn (u16) u8 => element.FN_IU16_OU8, fn (u4, u4) u8 => element.FN_IU4_IU4_OU8, fn (u8, u8) u16 => element.FN_IU8_IU8_OU16, fn (u16, u8) void => element.FN_IU16_IU8_OVOID, fn (u16, u16) void => element.FN_IU16_IU16_OVOID, + fn (cmos.StatusRegister, bool) u8 => element.FN_IECMOSSTATUSREGISTER_IBOOL_OU8, + fn (cmos.StatusRegister, u8, bool) void => element.FN_IECMOSSTATUSREGISTER_IU8_IBOOL_OVOID, + fn (cmos.RtcRegister) u8 => element.FN_IECMOSRTCREGISTER_OU8, fn (*const gdt.GdtPtr) void => element.FN_IPTRCONSTGDTPTR_OVOID, fn (*const idt.IdtPtr) void => element.FN_IPTRCONSTIDTPTR_OVOID, - fn (u8, extern fn () void) idt.IdtError!void => element.FN_IU8_IEFNOVOID_OERRORIDTERRORVOID, + fn (u8, fn () callconv(.C) void) idt.IdtError!void => element.FN_IU8_IEFNOVOID_OERRORIDTERRORVOID, fn (u8, fn () callconv(.Naked) void) idt.IdtError!void => element.FN_IU8_INFNOVOID_OERRORIDTERRORVOID, + fn () gdt.GdtPtr => element.FN_OGDTPTR, + fn () idt.IdtPtr => element.FN_OIDTPTR, + fn (idt.IdtEntry) bool => element.FN_IIDTENTRY_OBOOL, + fn (*task.Task, usize) void => element.FN_IPTRTask_IUSIZE_OVOID, + fn (*task.Task, *std.mem.Allocator) void => element.FN_IPTRTASK_IPTRALLOCATOR_OVOID, + fn (fn () void) std.mem.Allocator.Error!*task.Task => element.FN_IFNOVOID_OMEMERRORPTRTASK, + fn (fn () void, *std.mem.Allocator) std.mem.Allocator.Error!*task.Task => element.FN_IFNOVOID_IPTRALLOCATOR_OMEMERRORPTRTASK, else => @compileError("Type not supported: " ++ @typeName(T)), }; } @@ -261,17 +369,17 @@ fn Mock() type { /// /// Arguments: /// IN RetType: type - The return type of the function. - /// IN params: arglist - The argument list for the function. + /// IN params: anytype - The argument list for the function. /// /// Return: type /// A function type that represents the return type and its arguments. /// - fn getFunctionType(comptime RetType: type, params: var) type { + fn getFunctionType(comptime RetType: type, params: anytype) type { return switch (params.len) { 0 => fn () RetType, 1 => fn (@TypeOf(params[0])) RetType, 2 => fn (@TypeOf(params[0]), @TypeOf(params[1])) RetType, - else => @compileError("Couldn't generate function type for " ++ params.len ++ "parameters\n"), + else => @compileError("Couldn't generate function type for " ++ len ++ " parameters\n"), }; } @@ -342,17 +450,17 @@ fn Mock() type { /// IN/OUT self: *Self - Self. This is the mocking object to be modified to add /// the test data. /// IN fun_name: []const u8 - The function name to add the test parameters to. - /// IN data: var - The data to add. + /// IN data: anytype - The data to add. /// IN action_type: ActionType - The action type to add. /// - pub fn addAction(self: *Self, comptime fun_name: []const u8, data: var, action_type: ActionType) void { + pub fn addAction(self: *Self, comptime fun_name: []const u8, data: anytype, action_type: ActionType) void { // Add a new mapping if one doesn't exist. if (!self.named_actions.contains(fun_name)) { - expect(self.named_actions.put(fun_name, TailQueue(Action).init()) catch unreachable == null); + self.named_actions.put(fun_name, TailQueue(Action).init()) catch unreachable; } // Get the function mapping to add the parameter to. - if (self.named_actions.get(fun_name)) |actions_kv| { + if (self.named_actions.getEntry(fun_name)) |actions_kv| { var action_list = actions_kv.value; const action = Action{ .action = action_type, @@ -377,13 +485,13 @@ fn Mock() type { /// perform a action. /// IN fun_name: []const u8 - The function name to act on. /// IN RetType: type - The return type of the function being mocked. - /// IN params: arglist - The list of parameters of the mocked function. + /// IN params: anytype - The list of parameters of the mocked function. /// /// Return: RetType /// The return value of the mocked function. This can be void. /// - pub fn performAction(self: *Self, comptime fun_name: []const u8, comptime RetType: type, params: var) RetType { - if (self.named_actions.get(fun_name)) |kv_actions_list| { + pub fn performAction(self: *Self, comptime fun_name: []const u8, comptime RetType: type, params: anytype) RetType { + if (self.named_actions.getEntry(fun_name)) |kv_actions_list| { var action_list = kv_actions_list.value; // Peak the first action to test the action type if (action_list.first) |action_node| { @@ -418,11 +526,18 @@ fn Mock() type { // Waiting for this: // error: compiler bug: unable to call var args function at compile time. https://github.com/ziglang/zig/issues/313 // to be resolved + + comptime const param_len = [1]u8{switch (params.len) { + 0...9 => params.len + @as(u8, '0'), + else => unreachable, + }}; + const expected_function = switch (params.len) { 0 => fn () RetType, 1 => fn (@TypeOf(params[0])) RetType, 2 => fn (@TypeOf(params[0]), @TypeOf(params[1])) RetType, - else => @compileError("Couldn't generate function type for " ++ params.len ++ "parameters\n"), + 3 => fn (@TypeOf(params[0]), @TypeOf(params[1]), @TypeOf(params[2])) RetType, + else => @compileError("Couldn't generate function type for " ++ param_len ++ " parameters\n"), }; // Get the corresponding DataElementType @@ -438,11 +553,12 @@ fn Mock() type { action_list.destroyNode(test_node, GlobalAllocator); // The data element will contain the function to call - var r = switch (params.len) { + const r = switch (params.len) { 0 => actual_function(), 1 => actual_function(params[0]), 2 => actual_function(params[0], params[1]), - else => @compileError(params.len ++ " or more parameters not supported"), + 3 => actual_function(params[0], params[1], params[2]), + else => @compileError(param_len ++ " or more parameters not supported"), }; break :ret r; @@ -451,11 +567,18 @@ fn Mock() type { // Do the same for ActionType.ConsumeFunctionCall but instead of // popping the function, just peak const test_element = action.data; + + comptime const param_len = [1]u8{switch (params.len) { + 0...9 => params.len + @as(u8, '0'), + else => unreachable, + }}; + const expected_function = switch (params.len) { 0 => fn () RetType, 1 => fn (@TypeOf(params[0])) RetType, 2 => fn (@TypeOf(params[0]), @TypeOf(params[1])) RetType, - else => @compileError("Couldn't generate function type for " ++ params.len ++ "parameters\n"), + 3 => fn (@TypeOf(params[0]), @TypeOf(params[1]), @TypeOf(params[2])) RetType, + else => @compileError("Couldn't generate function type for " ++ param_len ++ " parameters\n"), }; // Get the corresponding DataElementType @@ -472,7 +595,8 @@ fn Mock() type { 0 => actual_function(), 1 => actual_function(params[0]), 2 => actual_function(params[0], params[1]), - else => @compileError(params.len ++ " or more parameters not supported"), + 3 => actual_function(params[0], params[1], params[2]), + else => @compileError(param_len ++ " or more parameters not supported"), }; break :ret r; @@ -553,7 +677,7 @@ fn getMockObject() *Mock() { if (mock) |*m| { return m; } else { - warn("MOCK object doesn't exists, please initiate this test\n", .{}); + warn("MOCK object doesn't exists, please initialise this test\n", .{}); expect(false); unreachable; } @@ -594,9 +718,9 @@ pub fn freeTest() void { /// IN/OUT self: *Self - Self. This is the mocking object to be modified to add /// the test parameters. /// IN fun_name: []const u8 - The function name to add the test parameters to. -/// IN params: arglist - The parameters to add. +/// IN params: anytype - The parameters to add. /// -pub fn addTestParams(comptime fun_name: []const u8, params: var) void { +pub fn addTestParams(comptime fun_name: []const u8, params: anytype) void { var mock_obj = getMockObject(); comptime var i = 0; inline while (i < params.len) : (i += 1) { @@ -610,9 +734,9 @@ pub fn addTestParams(comptime fun_name: []const u8, params: var) void { /// /// Arguments: /// IN fun_name: []const u8 - The function name to add the function to. -/// IN function: var - The function to add. +/// IN function: anytype - The function to add. /// -pub fn addConsumeFunction(comptime fun_name: []const u8, function: var) void { +pub fn addConsumeFunction(comptime fun_name: []const u8, function: anytype) void { getMockObject().addAction(fun_name, function, ActionType.ConsumeFunctionCall); } @@ -622,9 +746,9 @@ pub fn addConsumeFunction(comptime fun_name: []const u8, function: var) void { /// /// Arguments: /// IN fun_name: []const u8 - The function name to add the function to. -/// IN function: var - The function to add. +/// IN function: anytype - The function to add. /// -pub fn addRepeatFunction(comptime fun_name: []const u8, function: var) void { +pub fn addRepeatFunction(comptime fun_name: []const u8, function: anytype) void { getMockObject().addAction(fun_name, function, ActionType.RepeatFunctionCall); } @@ -634,11 +758,11 @@ pub fn addRepeatFunction(comptime fun_name: []const u8, function: var) void { /// Arguments: /// IN fun_name: []const u8 - The function name to act on. /// IN RetType: type - The return type of the function being mocked. -/// IN params: arglist - The list of parameters of the mocked function. +/// IN params: anytype - The list of parameters of the mocked function. /// /// Return: RetType /// The return value of the mocked function. This can be void. /// -pub fn performAction(comptime fun_name: []const u8, comptime RetType: type, params: var) RetType { +pub fn performAction(comptime fun_name: []const u8, comptime RetType: type, params: anytype) RetType { return getMockObject().performAction(fun_name, RetType, params); } diff --git a/test/mock/kernel/paging_mock.zig b/test/mock/kernel/paging_mock.zig new file mode 100644 index 00000000..002f9a89 --- /dev/null +++ b/test/mock/kernel/paging_mock.zig @@ -0,0 +1 @@ +pub const PAGE_SIZE_4KB = 4096; diff --git a/test/mock/kernel/panic_mock.zig b/test/mock/kernel/panic_mock.zig index 632ec1f2..9f2c1105 100644 --- a/test/mock/kernel/panic_mock.zig +++ b/test/mock/kernel/panic_mock.zig @@ -2,7 +2,7 @@ const builtin = @import("builtin"); const std = @import("std"); const MemProfile = @import("mem_mock.zig").MemProfile; -pub fn panic(trace: ?*builtin.StackTrace, comptime format: []const u8, args: var) noreturn { +pub fn panic(trace: ?*builtin.StackTrace, comptime format: []const u8, args: anytype) noreturn { @setCold(true); std.debug.panic(format, args); } diff --git a/test/mock/kernel/task_mock.zig b/test/mock/kernel/task_mock.zig new file mode 100644 index 00000000..390bfad0 --- /dev/null +++ b/test/mock/kernel/task_mock.zig @@ -0,0 +1,27 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const mock_framework = @import("mock_framework.zig"); +pub const initTest = mock_framework.initTest; +pub const freeTest = mock_framework.freeTest; +pub const addTestParams = mock_framework.addTestParams; +pub const addConsumeFunction = mock_framework.addConsumeFunction; +pub const addRepeatFunction = mock_framework.addRepeatFunction; + +const EntryPointFn = fn () void; + +pub const Task = struct { + const Self = @This(); + + pid: u32, + stack: []u32, + stack_pointer: usize, + + pub fn create(entry_point: EntryPointFn, allocator: *Allocator) Allocator.Error!*Task { + return mock_framework.performAction("Task.create", Allocator.Error!*Task, .{ entry_point, allocator }); + } + + pub fn destroy(self: *Self, allocator: *Allocator) void { + return mock_framework.performAction("Task.destroy", void, .{ self, allocator }); + } +}; diff --git a/test/mock/kernel/vmm_mock.zig b/test/mock/kernel/vmm_mock.zig new file mode 100644 index 00000000..8ef4e0d0 --- /dev/null +++ b/test/mock/kernel/vmm_mock.zig @@ -0,0 +1,38 @@ +const mem = @import("mem_mock.zig"); +const bitmap = @import("../../../src/kernel/bitmap.zig"); +const arch = @import("arch_mock.zig"); +const std = @import("std"); + +pub const VmmError = error{ + /// A memory region expected to be allocated wasn't + NotAllocated, +}; + +pub const Attributes = struct { + kernel: bool, + writable: bool, + cachable: bool, +}; +pub const BLOCK_SIZE: u32 = 1024; + +pub fn Mapper(comptime Payload: type) type { + return struct {}; +} + +pub fn VirtualMemoryManager(comptime Payload: type) type { + return struct { + const Self = @This(); + + pub fn alloc(self: *Self, num: u32, attrs: Attributes) std.mem.Allocator.Error!?usize { + return std.mem.Allocator.Error.OutOfMemory; + } + + pub fn free(self: *Self, vaddr: usize) (bitmap.Bitmap(u32).BitmapError || VmmError)!void { + return VmmError.NotAllocated; + } + }; +} + +pub fn init(mem_profile: *const mem.MemProfile, allocator: *std.mem.Allocator) std.mem.Allocator.Error!VirtualMemoryManager(arch.VmmPayload) { + return std.mem.Allocator.Error.OutOfMemory; +} diff --git a/test/rt-test.py b/test/rt-test.py deleted file mode 100644 index 30aea247..00000000 --- a/test/rt-test.py +++ /dev/null @@ -1,102 +0,0 @@ -import atexit -import queue -import threading -import subprocess -import signal -import re -import sys -import datetime -import os -import importlib.util - -msg_queue = queue.Queue(-1) -proc = None - -class TestCase: - def __init__(self, name, expected, prefix=r"\[INFO\] "): - self.name = name - self.expected = expected - self.prefix = prefix - -def failure(msg): - print("FAILURE: %s" %(msg)) - sys.exit(1) - -def test_failure(case, exp, expected_idx, found): - failure("%s #%d, expected '%s', found '%s'" %(case.name, expected_idx + 1, exp, found)) - -def test_pass(case, exp, expected_idx, found): - print("PASS: %s #%d, expected '%s', found '%s'" %(case.name, expected_idx + 1, exp, found)) - -def get_pre_archinit_cases(): - return [ - TestCase("Serial tests", [r"c", r"123"], ""), - TestCase("Log info tests", [r"Test INFO level", r"Test INFO level with args a, 1", r"Test INFO function", r"Test INFO function with args a, 1"], "\[INFO\] "), - TestCase("Log debug tests", [r"Test DEBUG level", r"Test DEBUG level with args a, 1", r"Test DEBUG function", r"Test DEBUG function with args a, 1"], "\[DEBUG\] "), - TestCase("Log warning tests", [r"Test WARNING level", r"Test WARNING level with args a, 1", r"Test WARNING function", r"Test WARNING function with args a, 1"], "\[WARNING\] "), - TestCase("Log error tests", [r"Test ERROR level", r"Test ERROR level with args a, 1", r"Test ERROR function", r"Test ERROR function with args a, 1"], "\[ERROR\] "), - TestCase("Mem init", [r"Init mem", r"Done"]), - TestCase("Arch init starts", [r"Init arch \w+"]) - ] - -def get_post_archinit_cases(): - return [ - TestCase("Arch init finishes", [r"Arch init done"]), - TestCase("Panic init", [r"Init panic", r"Done"]), - TestCase("VGA init", [r"Init vga", r"Done"]), - TestCase("VGA tests", [r"VGA: Tested max scan line", r"VGA: Tested cursor shape", r"VGA: Tested updating cursor"]), - TestCase("TTY init", [r"Init tty", r"Done"]), - TestCase("TTY tests", [r"TTY: Tested globals", r"TTY: Tested printing"]), - TestCase("Init finishes", [r"Init done"]), - TestCase("Panic tests", [r"Kernel panic: integer overflow", r"c[a-z\d]+: panic", r"c[a-z\d]+: panic.runtimeTests", r"c[a-z\d]+: kmain", r"c[a-z\d]+: start_higher_half"], "\[ERROR\] ") - ] - -def read_messages(proc): - while True: - line = proc.stdout.readline().decode("utf-8") - msg_queue.put(line) - -def cleanup(): - global proc - os.killpg(os.getpgid(proc.pid), signal.SIGTERM) - -if __name__ == "__main__": - arch = sys.argv[1] - zig_path = sys.argv[2] - spec = importlib.util.spec_from_file_location("arch", "test/kernel/arch/" + arch + "/rt-test.py") - arch_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(arch_module) - - # The list of log statements to look for before arch init is called + - # All log statements to look for, including the arch-specific ones + - # The list of log statements to look for after arch init is called - cases = get_pre_archinit_cases() + arch_module.get_test_cases(TestCase) + get_post_archinit_cases() - - if len(cases) > 0: - proc = subprocess.Popen(zig_path + " build run -Drt-test=true", stdout=subprocess.PIPE, shell=True, preexec_fn=os.setsid) - atexit.register(cleanup) - case_idx = 0 - read_thread = threading.Thread(target=read_messages, args=(proc,)) - read_thread.daemon = True - read_thread.start() - # Go through the cases - while case_idx < len(cases): - case = cases[case_idx] - expected_idx = 0 - # Go through the expected log messages - while expected_idx < len(case.expected): - e = case.prefix + case.expected[expected_idx] - try: - line = msg_queue.get(block=True, timeout=5) - except queue.Empty: - failure("Timed out waiting for '%s'" %(e)) - line = line.strip() - pattern = re.compile(e) - # Pass if the line matches the expected pattern, else fail - if pattern.fullmatch(line): - test_pass(case, e, expected_idx, line) - else: - test_failure(case, e, expected_idx, line) - expected_idx += 1 - case_idx += 1 - sys.exit(0) diff --git a/test/runtime_test.zig b/test/runtime_test.zig new file mode 100644 index 00000000..38536f4f --- /dev/null +++ b/test/runtime_test.zig @@ -0,0 +1,314 @@ +const std = @import("std"); +const ChildProcess = std.ChildProcess; +const Thread = std.Thread; +const Allocator = std.mem.Allocator; +const Builder = std.build.Builder; +const Step = std.build.Step; +const Queue = std.atomic.Queue([]const u8); +const Node = std.TailQueue([]const u8).Node; + +// Creating a new runtime test: +// 1. Add a enum to `TestMode`. The name should try to describe the test in one word :P +// 2. Add a description for the new runtime test to explain to the use what this will test. +// 3. Create a function with in the RuntimeStep struct that will perform the test. At least this +// should use `self.get_msg()` which will get the serial log lines from the OS. Look at +// test_init or test_panic for examples. +// 4. In the create function, add your test mode and test function to the switch. +// 5. Celebrate if it works lel + +/// The enumeration of tests with all the runtime tests. +pub const TestMode = enum { + /// This is for the default test mode. This will just run the OS normally. + None, + + /// Run the OS's initialisation runtime tests to ensure the OS is properly set up. + Initialisation, + + /// Run the panic runtime test. + Panic, + + /// Run the scheduler runtime test. + Scheduler, + + /// + /// Return a string description for the test mode provided. + /// + /// Argument: + /// IN mode: TestMode - The test mode. + /// + /// Return: []const u8 + /// The string description for the test mode. + /// + pub fn getDescription(mode: TestMode) []const u8 { + return switch (mode) { + .None => "Runs the OS normally (Default)", + .Initialisation => "Initialisation runtime tests", + .Panic => "Panic runtime tests", + .Scheduler => "Scheduler runtime tests", + }; + } +}; + +/// The runtime step for running the runtime tests for the OS. +pub const RuntimeStep = struct { + /// The Step, that is all you need to know + step: Step, + + /// The builder pointer, also all you need to know + builder: *Builder, + + /// The message queue that stores the log lines + msg_queue: Queue, + + /// The qemu process, this is needed for the `read_logs` thread. + os_proc: *ChildProcess, + + /// The argv of the qemu process so can create the qemu process + argv: [][]const u8, + + /// The test function that will be run for the current runtime test. + test_func: TestFn, + + /// The error set for the RuntimeStep + const Error = error{ + /// The error for if a test fails. If the test function returns false, this will be thrown + /// at the wnd of the make function as we need to clean up first. This will ensure the + /// build fails. + TestFailed, + + /// This is used for `self.get_msg()` when the queue is empty after a timeout. + QueueEmpty, + }; + + /// The type of the test function. + const TestFn = fn (self: *RuntimeStep) bool; + + /// The time used for getting message from the message queue. This is in milliseconds. + const queue_timeout: usize = 5000; + + /// + /// This will just print all the serial logs. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + /// Return: bool + /// This will always return true + /// + fn print_logs(self: *RuntimeStep) bool { + while (true) { + const msg = self.get_msg() catch return true; + defer self.builder.allocator.free(msg); + std.debug.warn("{}\n", .{msg}); + } + } + + /// + /// This tests the OS is initialised correctly by checking that we get a `SUCCESS` at the end. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + /// Return: bool + /// Whether the test has passed or failed. + /// + fn test_init(self: *RuntimeStep) bool { + while (true) { + const msg = self.get_msg() catch return false; + defer self.builder.allocator.free(msg); + // Print the line to see what is going on + std.debug.warn("{}\n", .{msg}); + if (std.mem.startsWith(u8, msg, "[ERROR] FAILURE")) { + return false; + } else if (std.mem.eql(u8, msg, "[INFO] SUCCESS")) { + return true; + } + } + } + + /// + /// This tests the OS's panic by checking that we get a kernel panic for integer overflow. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + /// Return: bool + /// Whether the test has passed or failed. + /// + fn test_panic(self: *RuntimeStep) bool { + while (true) { + const msg = self.get_msg() catch return false; + defer self.builder.allocator.free(msg); + // Print the line to see what is going on + std.debug.warn("{}\n", .{msg}); + if (std.mem.eql(u8, msg, "[ERROR] Kernel panic: integer overflow")) { + return true; + } + } + } + + /// + /// This tests the OS's scheduling by checking that we schedule a task that prints the success. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + /// Return: bool + /// Whether the test has passed or failed. + /// + fn test_scheduler(self: *RuntimeStep) bool { + var state: usize = 0; + while (true) { + const msg = self.get_msg() catch return false; + defer self.builder.allocator.free(msg); + + std.debug.warn("{}\n", .{msg}); + + // Make sure `[INFO] Switched` then `[INFO] SUCCESS: Scheduler variables preserved` are logged in this order + if (std.mem.eql(u8, msg, "[INFO] Switched") and state == 0) { + state = 1; + } else if (std.mem.eql(u8, msg, "[INFO] SUCCESS: Scheduler variables preserved") and state == 1) { + state = 2; + } + if (state == 2) { + return true; + } + } + } + + /// + /// The make function that is called by the builder. This will create the qemu process with the + /// stdout as a Pipe. Then create the read thread to read the logs from the qemu stdout. Then + /// will call the test function to test a specifics part of the OS defined by the test mode. + /// + /// Arguments: + /// IN/OUT step: *Step - The step of this step. + /// + /// Error: Thread.SpawnError || ChildProcess.SpawnError || Allocator.Error || Error + /// Thread.SpawnError - If there is an error spawning the real logs thread. + /// ChildProcess.SpawnError - If there is an error spawning the qemu process. + /// Allocator.Error.OutOfMemory - If there is no more memory to allocate. + /// Error.TestFailed - The error if the test failed. + /// + fn make(step: *Step) (Thread.SpawnError || ChildProcess.SpawnError || Allocator.Error || Error)!void { + const self = @fieldParentPtr(RuntimeStep, "step", step); + + // Create the qemu process + self.os_proc = try ChildProcess.init(self.argv, self.builder.allocator); + defer self.os_proc.deinit(); + + self.os_proc.stdout_behavior = .Pipe; + self.os_proc.stdin_behavior = .Inherit; + self.os_proc.stderr_behavior = .Inherit; + + try self.os_proc.spawn(); + + // Start up the read thread + var thread = try Thread.spawn(self, read_logs); + + // Call the testing function + const res = self.test_func(self); + + // Now kill our baby + _ = try self.os_proc.kill(); + + // Join the thread + thread.wait(); + + // Free the rest of the queue + while (self.msg_queue.get()) |node| { + self.builder.allocator.free(node.data); + self.builder.allocator.destroy(node); + } + + // If the test function returns false, then fail the build + if (!res) { + return Error.TestFailed; + } + } + + /// + /// This is to only be used in the read logs thread. This reads the stdout of the qemu process + /// and stores each line in the queue. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + fn read_logs(self: *RuntimeStep) void { + const stream = self.os_proc.stdout.?.reader(); + // Line shouldn't be longer than this + const max_line_length: usize = 1024; + while (true) { + const line = stream.readUntilDelimiterAlloc(self.builder.allocator, '\n', max_line_length) catch |e| switch (e) { + error.EndOfStream => { + // When the qemu process closes, this will return a EndOfStream, so can catch and return so then can + // join the thread to exit nicely :) + return; + }, + else => { + std.debug.warn("Unexpected error: {}\n", .{e}); + unreachable; + }, + }; + + // put line in the queue + var node = self.builder.allocator.create(Node) catch unreachable; + node.* = Node.init(line); + self.msg_queue.put(node); + } + } + + /// + /// This return a log message from the queue in the order it would appear in the qemu process. + /// The line will need to be free with allocator.free(line) then finished with the line. + /// + /// Arguments: + /// IN/OUT self: *RuntimeStep - Self. + /// + /// Return: []const u8 + /// A log line from the queue. + /// + /// Error: Error + /// error.QueueEmpty - If the queue is empty for more than the timeout, this will be thrown. + /// + fn get_msg(self: *RuntimeStep) Error![]const u8 { + var i: usize = 0; + while (i < queue_timeout) : (i += 1) { + if (self.msg_queue.get()) |node| { + defer self.builder.allocator.destroy(node); + return node.data; + } + std.time.sleep(std.time.ns_per_ms); + } + return Error.QueueEmpty; + } + + /// + /// Create a runtime step with a specific test mode. + /// + /// Argument: + /// IN builder: *Builder - The builder. This is used for the allocator. + /// IN test_mode: TestMode - The test mode. + /// IN qemu_args: [][]const u8 - The qemu arguments used to create the OS process. + /// + /// Return: *RuntimeStep + /// The Runtime step pointer to add to the build process. + /// + pub fn create(builder: *Builder, test_mode: TestMode, qemu_args: [][]const u8) *RuntimeStep { + const runtime_step = builder.allocator.create(RuntimeStep) catch unreachable; + runtime_step.* = RuntimeStep{ + .step = Step.init(.Custom, builder.fmt("Runtime {}", .{@tagName(test_mode)}), builder.allocator, make), + .builder = builder, + .msg_queue = Queue.init(), + .os_proc = undefined, + .argv = qemu_args, + .test_func = switch (test_mode) { + .None => print_logs, + .Initialisation => test_init, + .Panic => test_panic, + .Scheduler => test_scheduler, + }, + }; + return runtime_step; + } +};