Microkernel thing OS experiment (Zig ⚡)
1const arch = @import("../root.zig");
2const std = @import("std");
3const interrupts = arch.interrupts;
4const StandardGdt = arch.structures.gdt.StandardGdt;
5
6// The actual IDT memory
7const entry_count = 256;
8export var interrupt_descriptor_table: [entry_count]Entry = undefined;
9
10// Pointers to the actual ISRs which the global interrupt handler call
11// Each IDT entry pushes the interrupt number then calls the global
12// handler, which pushes more information then calls the user
13// defined handler. Use common sense and don't return from an exception
14// which shouldn't be returned from.
15pub const CallConv: std.builtin.CallingConvention = .{ .x86_64_sysv = .{} };
16pub fn InterruptHandler(comptime E: type) type {
17 return *const fn (*InterruptFrame(E)) callconv(CallConv) void;
18}
19pub export var defined_handlers: [entry_count]InterruptHandler(u64) = undefined;
20
21// The actual handlers with addresses in the IDT.
22const ActualHandler = *const fn () callconv(.naked) void;
23const actual_handlers: [entry_count]ActualHandler = blk: {
24 @setEvalBranchQuota(100000000);
25 var ret: [entry_count]ActualHandler = undefined;
26 for (0..256) |i| {
27 ret[i] = make_actual_handler(.{ .interrupt = i });
28 }
29 break :blk ret;
30};
31
32fn make_actual_handler(comptime interrupt: Interrupt) ActualHandler {
33 var asm_code: []const u8 = "";
34 // Make the stack consistent
35 if (!interrupt.has_error_code()) {
36 asm_code = asm_code ++ "pushq $0\n";
37 }
38 // Push the interrupt number
39 asm_code = asm_code ++ std.fmt.comptimePrint("pushq ${}\n", .{interrupt.interrupt});
40 // Jump to the common interrupt handler code
41 asm_code = asm_code ++ "jmp _int_handler_common";
42 const code = asm_code;
43
44 const tmp_struct = struct {
45 fn actual_handler() callconv(.naked) void {
46 asm volatile (code);
47 }
48 };
49 return tmp_struct.actual_handler;
50}
51
52// The global assembly for the common interrupt handler
53comptime {
54 // Construct the push and pop instructions from SavedRegisters
55 var push_instrs: []const u8 = "\n";
56 var pop_instrs: []const u8 = "\n";
57
58 const saved_regs = @typeInfo(SavedRegisters).@"struct".fields;
59 for (saved_regs) |saved_reg| {
60 // We must prepend to push because pushes are basically
61 // building the struct in reverse order, so reverse the effects
62 push_instrs = "\n pushq %" ++ saved_reg.name ++ push_instrs;
63 // Of course, pop in the opposite order as push
64 pop_instrs = pop_instrs ++ "popq %" ++ saved_reg.name ++ "\n";
65 }
66
67 asm (
68 \\ .global _int_handler_common
69 \\ .type _int_handler_common, @function
70 \\ _int_handler_common:
71
72 // Push the general purpose registers and then CR3
73 ++ push_instrs ++
74 \\ mov %cr3, %rax
75 \\ pushq %rax
76
77 // Now, rsp points to the start of InterruptFrame. Read int_num and call into an offset of
78 // the defined handlers list after setting the first arg to the created structure.
79 ++ std.fmt.comptimePrint("\nmov {}(%rsp), %rcx\n", .{@offsetOf(InterruptFrame(u64), "int_num")}) ++
80 \\ mov %rsp, %rdi
81 \\ callq *defined_handlers(, %rcx, 8)
82
83 // Skip the stack all the way down to the general purpose
84 // registers, then pop all of them
85 // TODO: restore CR3??
86 ++ std.fmt.comptimePrint("\nadd ${}, %rsp\n", .{@offsetOf(InterruptFrame(u64), "regs")}) ++ pop_instrs ++
87 // Skip the interrupt number and error code and iretq
88 \\ add $16, %rsp
89 \\ iretq
90 );
91}
92
93/// IDT Register
94const Idtr = packed struct(u80) {
95 limit: u16,
96 addr: u64,
97
98 /// Load the IDT Register
99 pub fn load(self: *const Idtr) void {
100 asm volatile ("lidt (%[idtr_addr])"
101 :
102 : [idtr_addr] "r" (self),
103 );
104 }
105};
106
107// A raw IDT entry
108const Entry = extern struct {
109 func_low: u16,
110 gdt_selector: u16,
111 options: Options,
112 func_mid: u16,
113 func_high: u32,
114 _reserved0: u32 = 0,
115
116 pub const Options = packed struct(u16) {
117 ist_index: u3,
118 _reserved0: u5 = 0,
119 disable_interrupts: bool = true,
120 // type: Type,
121 must_be_one: u3 = 0b111,
122 must_be_zero: u1 = 0,
123 dpl: u2,
124 present: bool,
125 };
126
127 pub const Owner = enum {
128 kernel,
129 user,
130 };
131
132 const Self = @This();
133 pub fn init(func_ptr: usize, dpl: u2, ist: u3) Entry {
134 // _ = typ;
135 return .{
136 .func_low = @truncate(func_ptr),
137 .gdt_selector = StandardGdt.selectors.kernel_code,
138 .options = .{
139 .ist_index = ist,
140 .dpl = dpl,
141 .present = true,
142 },
143 .func_mid = @truncate(func_ptr >> 16),
144 .func_high = @truncate(func_ptr >> 32),
145 };
146 }
147
148 // Changes the address without changing anything else
149 pub fn set_func(self: *Self, ptr: usize) void {
150 self.func_low = @truncate(ptr);
151 self.func_mid = @truncate(ptr >> 16);
152 self.func_high = @truncate(ptr >> 32);
153 }
154};
155
156/// A selector error code indexing into the GDT, IDT, or LDT.
157/// Used in a general protection fault for example.
158pub const SelectorErrorCode = packed struct(u64) {
159 external: bool,
160 interrupt: bool,
161 // Only valid if not interrupt
162 type: enum(u1) {
163 gdt = 0,
164 ldt = 1,
165 },
166 idx: u13,
167 _reserved0: u48 = 0,
168
169 pub const Target = union(enum) {
170 interrupt: Interrupt,
171 gdt_sel: u16,
172 ldt_sel: u13,
173 };
174
175 const Self = @This();
176 pub fn parse(self: Self) Target {
177 return switch (self.interrupt) {
178 true => .{ .interrupt = .{ .interrupt = @truncate(self.idx) } },
179 false => switch (self.type) {
180 .gdt => .{ .gdt_sel = self.idx },
181 .ldt => .{ .ldt_sel = self.idx },
182 },
183 };
184 }
185};
186
187/// List of the general built in exceptions
188pub const Exception = enum(u8) {
189 divide_error = 0x00,
190 debug_exeption = 0x01,
191 non_maskable_interrupt = 0x02,
192 breakpoint = 0x03,
193 overflow = 0x04,
194 bound_range_exceeded = 0x05,
195 invalid_opcode = 0x06,
196 device_not_available = 0x07,
197 double_fault = 0x08,
198 // _coprocessor_segment_overrun = 0x09,
199 invalid_tss = 0x0a,
200 segment_not_present = 0x0b,
201 stack_segment_fault = 0x0c,
202 general_protection_fault = 0x0d,
203 page_fault = 0x0e,
204 // _reserved0 = 0x0f,
205 x87_floating_point = 0x10,
206 alignment_check = 0x11,
207 machine_check = 0x12,
208 simd_floating_point = 0x13,
209 virtualization = 0x14,
210 control_protection = 0x15,
211 hypervisor = 0x1c,
212 vmm = 0x1d,
213 security_fault = 0x1e,
214 _,
215
216 fn has_error_code(self: Exception) bool {
217 return switch (self) {
218 .double_fault, .invalid_tss, .segment_not_present, .general_protection_fault, .page_fault, .security_fault => true,
219 else => false,
220 };
221 }
222};
223
224pub const Interrupt = packed union {
225 exception: Exception,
226 interrupt: u8,
227
228 pub fn has_error_code(self: Interrupt) bool {
229 return self.exception.has_error_code();
230 }
231};
232
233/// Basically all the general purpose registers except rsp,
234/// because RSP is already in the InterruptFrame. Ordered
235/// in the order of the X.Reg from the instruction encoding lol
236pub const SavedRegisters = extern struct {
237 rax: u64,
238 rcx: u64,
239 rdx: u64,
240 rbx: u64,
241 // rsp: u64,
242 rbp: u64,
243 rsi: u64,
244 rdi: u64,
245 r8: u64,
246 r9: u64,
247 r10: u64,
248 r11: u64,
249 r12: u64,
250 r13: u64,
251 r14: u64,
252 r15: u64,
253
254 pub const default = std.mem.zeroes(SavedRegisters);
255};
256
257/// The Interrupt frame which we help generate
258pub fn InterruptFrame(comptime ErrorCode: type) type {
259 if (@bitSizeOf(ErrorCode) != 64) {
260 @compileError("ErrorCode for InterruptFrame must be exactly 64 bits!");
261 }
262 return extern struct {
263 // CR3
264 cr3: u64,
265 // All the general purpose registers
266 regs: SavedRegisters align(8),
267 // The interrupt number
268 int_num: Interrupt align(8),
269 // Pushed by the CPU (error_code may be pushed by us)
270 error_code: ErrorCode,
271 rip: u64,
272 cs: u16 align(8),
273 eflags: u64,
274 rsp: u64,
275 ss: u16 align(8),
276
277 pub fn normalize(self: *InterruptFrame(ErrorCode)) *InterruptFrame(u64) {
278 return @ptrCast(self);
279 }
280 };
281}
282
283// Initialize the IDT with the default unhandled exception
284pub fn init() void {
285 // Set every IDT entry to the corresponding ActualHandler
286 for (0..entry_count) |i| {
287 const actual_handler = @intFromPtr(actual_handlers[i]);
288 interrupt_descriptor_table[i] = Entry.init(actual_handler, 0, 0);
289 }
290 // Now, set every defined handler to the default one
291 @memset(&defined_handlers, arch.interrupts.unhandled_interrupt);
292
293 // Finally, load the idt
294 load();
295}
296
297pub fn load() void {
298 const idtr: Idtr = .{
299 .addr = @intFromPtr(&interrupt_descriptor_table),
300 .limit = 0xFFF,
301 };
302 idtr.load();
303}
304
305pub fn add_handler(interrupt: Interrupt, comptime E: type, handler: InterruptHandler(E), dpl: u2, ist: u3) void {
306 // Modify the type, dpl, and ist in place
307 var tmp = interrupt_descriptor_table[interrupt.interrupt];
308 tmp.options.dpl = dpl;
309 tmp.options.ist_index = ist;
310 interrupt_descriptor_table[interrupt.interrupt] = tmp;
311
312 // Add the InterruptHandler
313 defined_handlers[interrupt.interrupt] = @ptrCast(handler);
314}