1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
const std = @import("std");
const limine = @import("limine.zig");
const serial = @import("serial.zig");
const framebuffer = @import("framebuffer.zig");
const gdt = @import("gdt.zig");
const idt = @import("idt.zig");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
const vmm = @import("vmm.zig");
const user = @import("user.zig");
const keyboard = @import("keyboard.zig");
const bridge = @import("bridge");
// We need a panic handler otherwise zig won't be happy,
// std usually formats a message through std.Io.Writer which
// emits SSE instructions which I haven't enabled. Ours
// dumps the message to serial, then halts.
pub const panic = std.debug.FullPanic(struct {
fn halt(msg: []const u8, ret_addr: ?usize) noreturn {
serial.init();
serial.write("\r\n!!! uhhhh engine kaput : ");
serial.write(msg);
if (ret_addr) |ra| {
serial.write("\r\n at ");
serial.writeHex(ra);
}
serial.write("\r\n");
while (true) asm volatile ("hlt");
}
}.halt);
// std.debug.print (revo hits it on a few error paths) defaults to a threaded
// stderr IO that doesn't exist on freestanding and won't even compile. This
// just points it to the custom one we wrote in bridge tthat uses the serial
// console and shit
pub const std_options_debug_io: std.Io = bridge.debug_io;
// Check `linker.ld`
export var base_revision: limine.BaseRevision linksection(".limine_requests") = limine.BaseRevision.init(3);
export var requests_start: limine.RequestsStartMarker linksection(".limine_requests_start") = .{};
export var requests_end: limine.RequestsEndMarker linksection(".limine_requests_end") = .{};
// the loader fills these before we run so we read them through volatile pointers or
// the optimizer assumes they're still the null we initialized them to
export var hhdm_request: limine.HhdmRequest linksection(".limine_requests") = .{};
export var memmap_request: limine.MemoryMapRequest linksection(".limine_requests") = .{};
// 4 MiB stack
export var stack_size_request: limine.StackSizeRequest linksection(".limine_requests") = .{ .stack_size = 4 * 1024 * 1024 };
// ENTRY(_start)
export fn _start() callconv(.c) noreturn {
serial.init();
serial.write("if you see this, it means revel didn't shit the bed\r\n");
gdt.load();
idt.init();
user.installSyscall(); // int 0x80 gate at DPL 3
// NOTE: CPU interrupts stay off until the event loop's sti
// which is after the VM has run that\
serial.write("gdt + idt loaded\r\n");
// HHDM + memory map -> physical frame allocator -> kernel heap
const hhdm_req: *volatile limine.HhdmRequest = &hhdm_request;
const memmap_req: *volatile limine.MemoryMapRequest = &memmap_request;
const hhdm = hhdm_req.response orelse @panic("limine gave us no HHDM");
const memmap = memmap_req.response orelse @panic("limine gave us no memory map");
pmm.init(hhdm, memmap);
pmm.dump(memmap);
heap.init();
// NOTE: this is a sanity check
// two allocations should be distinct and non-overlapping,
// and a freed frame should come straight back on the next alloc.
const a = pmm.alloc().?;
const b = pmm.alloc().?;
serial.write("pmm test: a=");
serial.writeHex(a);
serial.write(" b=");
serial.writeHex(b);
serial.write("\r\n");
pmm.free(a);
const c = pmm.alloc().?;
serial.write("pmm test: freed a, realloc=");
serial.writeHex(c);
serial.write(if (c == a) " (reused, good)\r\n" else " (mismatch!)\r\n");
pmm.free(b);
pmm.free(c);
// this is just a VMM smoke test
// we map a fresh frame at an unused higher half address in the
// active address space, write a pattern, read it back through the mapping
{
const pml4 = vmm.activePml4();
const frame = pmm.alloc().?;
const test_virt: u64 = 0xffff_c000_0000_0000;
_ = vmm.map(pml4, test_virt, frame, vmm.WRITE);
const cell: *volatile u64 = @ptrFromInt(test_virt);
cell.* = 0xDEADBEEFCAFEBABE;
serial.write("vmm test: map+rw ");
serial.write(if (cell.* == 0xDEADBEEFCAFEBABE) "OK" else "FAIL");
serial.write(", translate ");
const back = vmm.translate(pml4, test_virt) orelse 0;
serial.write(if (back == frame) "OK\r\n" else "FAIL\r\n");
// create a separate address space, switch into it (the scary part -- a
// bad CR3 triple-faults), map a lower-half user page, read/write it,
// then switch back. if the kernel keeps running, address spaces work.
const as = vmm.createAddressSpace().?;
const user_frame = pmm.alloc().?;
const user_virt: u64 = 0x0000_0000_4000_0000; // 1 GiB, lower half (user)
_ = vmm.map(as, user_virt, user_frame, vmm.WRITE | vmm.USER);
vmm.loadPml4(as);
serial.write("vmm test: switched CR3 OK\r\n"); // reached => kernel still mapped
const ucell: *volatile u64 = @ptrFromInt(user_virt);
ucell.* = 0x1234_5678_9ABC_DEF0;
serial.write("vmm test: user page rw ");
serial.write(if (ucell.* == 0x123456789ABCDEF0) "OK\r\n" else "FAIL\r\n");
vmm.loadPml4(pml4); // back to the original address space
serial.write("vmm test: switched back OK\r\n");
}
user.runTestProgram();
// hand the kernel heap + raw framebuffer to revo and bring up the VM
const fb: ?bridge.Fb = if (framebuffer.get()) |s| .{
.ptr = s.address,
.width = s.width,
.height = s.height,
.pitch = s.pitch,
} else null;
bridge.boot(heap.allocator(), serial.write, fb);
// - sleep until an interrupt
// - drain the keyboard
// - repeat.
//
// the cli/pop/sti-hlt bullshit closes the lost-wakeup race (a key
// arriving between "queue empty" and hlt would otherwise sit until
// the next keypress)
while (true) {
asm volatile ("cli");
if (keyboard.pop()) |sc| {
asm volatile ("sti");
bridge.onKey(sc);
} else {
asm volatile ("sti; hlt");
}
}
}
|