Adding all the ISRs
Before we make the other ISRs, we need to develop our current solution a little further. We can create a common stub that saves the processor state and passes it to our C interrupt handler. Since every ISR needs to perform this same setup, using a common stub prevents us from having to duplicate the same code for every interrupt. This stub pushes all our needed registers onto the stack to be used as the parameter
[BITS 32]
extern isr_handler
global idt_load
global isr0
idt_load:
mov eax, [esp + 4]
lidt [eax]
ret
isr_common_stub:
pusha
mov ax, ds
push eax
push esp
call isr_handler
add esp, 4
pop eax
popa
add esp, 8
iret
isr0:
cli
; error code
push dword 0
; interrupt number
push dword 0
jmp isr_common_stub
If you're wondering why we don't push registers such as EIP, CS or
EFLAGS, this is because the CPU automatically pushes them onto the stack
when the interrupt occurs. Our stub only needs to save the general-purpose registers
and the data-segment register that we want to make available to the C handler.
Also, another thing to consider is that passing memory from assembly
to C can be confusing, this is why I covered the stack prior. We may
find some bugs relating to that in future. It may be good to print your
values received in C and ensure that they are correct.
We are creating ISR1-ISR31. This is tedious, so we can use assembly macros to generate most of the repetitive code. There's one important distinction: some exceptions automatically push an error code onto the stack while others do not. Our two macros account for this difference.
Add this to the header file:
extern void isr0(void);
extern void isr1(void);
extern void isr2(void);
extern void isr3(void);
extern void isr4(void);
extern void isr5(void);
extern void isr6(void);
extern void isr7(void);
extern void isr8(void);
extern void isr9(void);
extern void isr10(void);
extern void isr11(void);
extern void isr12(void);
extern void isr13(void);
extern void isr14(void);
extern void isr15(void);
extern void isr16(void);
extern void isr17(void);
extern void isr18(void);
extern void isr19(void);
extern void isr20(void);
extern void isr21(void);
extern void isr22(void);
extern void isr23(void);
extern void isr24(void);
extern void isr25(void);
extern void isr26(void);
extern void isr27(void);
extern void isr28(void);
extern void isr29(void);
extern void isr30(void);
extern void isr31(void);
And this to the implementation file:
idt_set_gate(0, (uint32_t)isr0, 0x08, 0x8E);
idt_set_gate(1, (uint32_t)isr1, 0x08, 0x8E);
idt_set_gate(2, (uint32_t)isr2, 0x08, 0x8E);
idt_set_gate(3, (uint32_t)isr3, 0x08, 0x8E);
idt_set_gate(4, (uint32_t)isr4, 0x08, 0x8E);
idt_set_gate(5, (uint32_t)isr5, 0x08, 0x8E);
idt_set_gate(6, (uint32_t)isr6, 0x08, 0x8E);
idt_set_gate(7, (uint32_t)isr7, 0x08, 0x8E);
idt_set_gate(8, (uint32_t)isr8, 0x08, 0x8E);
idt_set_gate(9, (uint32_t)isr9, 0x08, 0x8E);
idt_set_gate(10, (uint32_t)isr10, 0x08, 0x8E);
idt_set_gate(11, (uint32_t)isr11, 0x08, 0x8E);
idt_set_gate(12, (uint32_t)isr12, 0x08, 0x8E);
idt_set_gate(13, (uint32_t)isr13, 0x08, 0x8E);
idt_set_gate(14, (uint32_t)isr14, 0x08, 0x8E);
idt_set_gate(15, (uint32_t)isr15, 0x08, 0x8E);
idt_set_gate(16, (uint32_t)isr16, 0x08, 0x8E);
idt_set_gate(17, (uint32_t)isr17, 0x08, 0x8E);
idt_set_gate(18, (uint32_t)isr18, 0x08, 0x8E);
idt_set_gate(19, (uint32_t)isr19, 0x08, 0x8E);
idt_set_gate(20, (uint32_t)isr20, 0x08, 0x8E);
idt_set_gate(21, (uint32_t)isr21, 0x08, 0x8E);
idt_set_gate(22, (uint32_t)isr22, 0x08, 0x8E);
idt_set_gate(23, (uint32_t)isr23, 0x08, 0x8E);
idt_set_gate(24, (uint32_t)isr24, 0x08, 0x8E);
idt_set_gate(25, (uint32_t)isr25, 0x08, 0x8E);
idt_set_gate(26, (uint32_t)isr26, 0x08, 0x8E);
idt_set_gate(27, (uint32_t)isr27, 0x08, 0x8E);
idt_set_gate(28, (uint32_t)isr28, 0x08, 0x8E);
idt_set_gate(29, (uint32_t)isr29, 0x08, 0x8E);
idt_set_gate(30, (uint32_t)isr30, 0x08, 0x8E);
idt_set_gate(31, (uint32_t)isr31, 0x08, 0x8E);
And then add this to assembly:
global isr0
global isr1
global isr2
global isr3
global isr4
global isr5
global isr6
global isr7
global isr8
global isr9
global isr10
global isr11
global isr12
global isr13
global isr14
global isr15
global isr16
global isr17
global isr18
global isr19
global isr20
global isr21
global isr22
global isr23
global isr24
global isr25
global isr26
global isr27
global isr28
global isr29
global isr30
global isr31
%macro ISR_NOERRCODE 1
isr%1:
push dword 0
push dword %1
jmp isr_common_stub
%endmacro
%macro ISR_ERRCODE 1
isr%1:
push dword %1
jmp isr_common_stub
%endmacro
ISR_NOERRCODE 0
ISR_NOERRCODE 1
ISR_NOERRCODE 2
ISR_NOERRCODE 3
ISR_NOERRCODE 4
ISR_NOERRCODE 5
ISR_NOERRCODE 6
ISR_NOERRCODE 7
ISR_ERRCODE 8
ISR_NOERRCODE 9
ISR_ERRCODE 10
ISR_ERRCODE 11
ISR_ERRCODE 12
ISR_ERRCODE 13
ISR_ERRCODE 14
ISR_NOERRCODE 15
ISR_NOERRCODE 16
ISR_ERRCODE 17
ISR_NOERRCODE 18
ISR_NOERRCODE 19
ISR_NOERRCODE 20
ISR_NOERRCODE 21
ISR_NOERRCODE 22
ISR_NOERRCODE 23
ISR_NOERRCODE 24
ISR_NOERRCODE 25
ISR_NOERRCODE 26
ISR_NOERRCODE 27
ISR_NOERRCODE 28
ISR_NOERRCODE 29
ISR_ERRCODE 30
ISR_NOERRCODE 31
This code block uses NASM macros, which act as a simple code generator
during assembly. Each time we invoke one of these macros, NASM expands it into
the instructions defined inside the macro. We have two macros because
the CPU handles error codes differently for different exceptions. ISR_NOERRCODE pushes
a dummy error code so that the stack has the same layout as an exception that provides one,
while ISR_ERRCODE relies on the CPU's existing error code and
only pushes the interrupt number.
We should be able to run our code again and isr0 should still work, another thing we need to do is add a printing for each exception, here is a nice array you can use:
const char* exception_messages[32] =
{
"Divide By Zero",
"Debug",
"Non Maskable Interrupt",
"Breakpoint",
"Overflow",
"Bound Range Exceeded",
"Invalid Opcode",
"Device Not Available",
"Double Fault",
"Coprocessor Segment Overrun",
"Invalid TSS",
"Segment Not Present",
"Stack Segment Fault",
"General Protection Fault",
"Page Fault",
"Reserved",
"x87 Floating Point Exception",
"Alignment Check",
"Machine Check",
"SIMD Floating Point Exception",
"Virtualization Exception",
"Control Protection Exception",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Hypervisor Injection Exception",
"VMM Communication Exception",
"Security Exception",
"Reserved"
};
The interrupt number stored in regs->interrupt_number tells us which
exception occurred. Since our array is indexed from zero, we can use that
number directly to select the appropriate message. This gives us a nice and
simple way to turn an exception number into something readable on the screen.
And then in the ISR handler we can just have
vga_text_writeline(&terminal, exception_messages[regs->interrupt_number]);
And now we can run some tests:
We should then test our exceptions to make sure the IDT
and ISR stubs are working correctly. You do not need to trigger all 32,
but testing a reasonable selection is useful. You can also look up the exceptions and
deliberately trigger some of them with assembly instructions. This is a good
way to become familiar with how the CPU transfers control to the handlers.
Exception 3, the breakpoint exception, is particularly useful because it can also
be triggered with the int 3 instruction and can be used while debugging.