im making an os kernel in zig (grub to boot) and im trying to get interrupts to work specifically hardware interrupts and as soon as i enable them (i have a pic, gdt and idt setup) it gives me a double fault
but the crazy thing is when i mask the first hardware interrupt at 32 IRQ0 it gives me a stack segment fault
you can take a look at the code but i feel like i'm making no progress and yes im a beginner this is my second time trying but this time im using a language im more use to ZIG!
https://github.com/levi73159/LazyOS/tree/main
Unhandled exception 8 double_fault
eax=8 ebx=10000 ecx=0 edx=f000ff53 esi=0 edi=0
ebp=1ca008 esp=0 eip=8 eflags=1331ff
cs=206 ds=10
error=13340a interrupt=8
Unhandled exception 12 stack_segment_fault
eax=c ebx=10000 ecx=0 edx=f000ff53 esi=0 edi=0
ebp=1ca008 esp=0 eip=8 eflags=1331ff
cs=10206 ds=10
error=13340a interrupt=c
and i do not know why esp is zero since i am setting it up correctly here
const STACK_SIZE = 16 * 1024;
var stack_bytes: [STACK_SIZE]u8 align(16) linksection(".bss") = undefined;
export fn __kernel_start() callconv(.naked) noreturn {
asm volatile (
// make sure interrupts are disabled
// set up the stack
\\ cli
\\ movl %[stack_top], %%esp
\\ movl %%esp, %%ebp
\\ call %[_start:P]
:
: [stack_top] "r" (@as([*]align(16) u8, @ptrCast(&stack_bytes)) + @sizeOf(@TypeOf(stack_bytes))),
// We let the compiler handle the reference to kmain by passing it as an input operand as well.
[_start] "X" (&_start),
);
while (true) {}
}
idk what it is but whatever it is, it not wanting to go away. I don't think it a real double fault tho because a real one should have an error code of zero but this one don't
and the qemu log shows me that it a hardware interrupt that calling it for some reason:
SMM: after RSM
EAX=00000000 EBX=00000000 ECX=02000000 EDX=02000628
ESI=0000000b EDI=02000000 EBP=1efed210 ESP=00006d54
EIP=000f05ae EFL=00000046 [---Z-P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0010 00000000 ffffffff 00c09300 DPL=0 DS [-WA]
CS =0008 00000000 ffffffff 00c09b00 DPL=0 CS32 [-RA]
SS =0010 00000000 ffffffff 00c09300 DPL=0 DS [-WA]
DS =0010 00000000 ffffffff 00c09300 DPL=0 DS [-WA]
FS =0010 00000000 ffffffff 00c09300 DPL=0 DS [-WA]
GS =0010 00000000 ffffffff 00c09300 DPL=0 DS [-WA]
LDT=0000 00000000 0000ffff 00008200 DPL=0 LDT
TR =0000 00000000 0000ffff 00008b00 DPL=0 TSS32-busy
GDT= 000f7440 00000037
IDT= 000f747e 00000000
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000
DR6=ffff0ff0 DR7=00000400
CCS=00000044 CCD=00000000 CCO=EFLAGS
EFER=0000000000000000
Servicing hardware INT=0x08
SMM: enter
EAX=000000b5 EBX=000f86be ECX=00001234 EDX=00006cff
ESI=00006cc8 EDI=1efff755 EBP=00006c88 ESP=00006c88
EIP=000086bb EFL=00000006 [-----P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =de80 000de800 ffffffff 008f9300
CS =f000 000f0000 ffffffff 008f9b00
SS =0000 00000000 ffffffff 008f9300
DS =0000 00000000 ffffffff 008f9300
FS =0000 00000000 ffffffff 008f9300
GS =0000 00000000 ffffffff 008f9300
LDT=0000 00000000 0000ffff 00008200
TR =0000 00000000 0000ffff 00008b00
GDT= 00000000 00000000
IDT= 00000000 000003ff
CR0=00000010 CR2=00000000 CR3=00000000 CR4=00000000
DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000
DR6=ffff0ff0 DR7=00000400
CCS=00000000 CCD=00006c88 CCO=ADDL
EFER=0000000000000000
First a nitpick: esp
in your InterruptFrame
would only be filled on inter-privilege interrupt/exception; and on such an exception the stack would be switched onto more privileged stack as defined in the task register. You don't ever set your task register so more-privileged-stack is undefined. I think you didn't yet intented to ever drop onto CPL > 0, right? Everything is in ring 0 so far? There is no stack switch then, and esp
would never be filled there and you'd just print whatever nonsense was on stack so not sure why you would pay attention to that (actual esp
is in useless
field).
Now to the issue! Is this actually a double-fault/stack error? Running qemu
with -d int
shows things like:
Servicing hardware INT=0x08
0: v=08 e=0000 i=0 cpl=0 IP=0008:0013340a pc=0013340a SP=0010:001c9ff8 env->regs[R_EAX]=00000000
EAX=00000000 EBX=00010000 ECX=00000000 EDX=f000ff53
ESI=00000000 EDI=00000000 EBP=001ca008 ESP=001c9ff8
EIP=0013340a EFL=00000206 [-----P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0010 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
CS =0008 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =0010 00000000 ffffffff 00cf9300 DPL=0 DS [-WA]
...
[isr] debug: Interrupt 8
!!! UNHANDLED EXCEPTION !!!
Unhandled exception 8 double_fault
eax=8 ebx=10000 ecx=0 edx=f000ff53 esi=0 edi=0
ebp=1ca008 esp=0 eip=8 eflags=1331ff
cs=206 ds=10
error=13340a interrupt=8
!!! KERNEL PANIC !!!
Or
Servicing hardware INT=0x0c
...
[isr] debug: Interrupt 12
!!! UNHANDLED EXCEPTION !!!
Unhandled exception 12 stack_segment_fault
This can't be a coincidence! It's not that hardware IRQ causes an exception; this looks very much like a normal hardware interrupt and CPU enter its handling with sane register values, but then it is treated as an exception with the same number. Moreover double fault (as well as segment stack fault) is an exception with an error code but in interrupt case it wouldn't be there of course; therefore interrupt frame would be skewed, and everything past "pusha" portion would be shifted off by 4 (so eip is shown as error, cs shown as eip, eflags shown as cs, and the esp/eflags is just bogus since it was never filled).
So the question is - why is hardware IRQ 8/12 gets raised? Well, in default BIOS settings IRQ 8 is a master PIC IRQ 0 which is timer IRQ and IRQ 12 is a serial port IRQ I think? Not sure why serial would trigger but timer IRQ sounds about right. So let's see PIC status to double-check:
(qemu) info pic
...
pic1: irr=00 imr=00 isr=00 hprio=0 irq_base=70 rr_sel=0 elcr=0c fnm=0
pic0: irr=03 imr=00 isr=10 hprio=0 irq_base=08 rr_sel=0 elcr=00 fnm=0
Yep, PICs are in their default post-BIOS state.
Let's see at its init code:
// Init control word 1
const icw1_expect_icw4 = 1 << 0;
const icw1_single_mode = 1 << 1;
const icw1_interval4 = 1 << 2;
const icw1_edge_trigger = 1 << 3;
const icw1_level_trigger = 1 << 4;
const icw1_initialize = 1 << 5;
...
// Init control word 1
io.outb(pic1_command_port, icw1_expect_icw4 | icw1_initialize);
io.wait();
io.outb(pic2_command_port, icw1_expect_icw4 | icw1_initialize);
io.wait();
icw1_initialize should be 0x10, not 0x20, and icw1_level_trigger should be 0x08, not 0x10. There is no separate "edge" bit! So that is the problem - with init bit not set all the rest is useless, PIC stays mapped onto 0x08 base and everything that follows stems from here.