Skelix OS Tutorial
Prev Tutorial 04: Interrupts and Exceptions Part1
Next

Our Goal

In this tutorial, how to handle interrupts and exceptions is going to be introduced, and adding exception handlers in Skelix to enable it give debug information when an exception happens, it will give us great convenience in later tutorials.

Download source file

What is Interrupts and Exceptions anyway?

For example, when you are eating dinner at home, suddenly the phone rings, you have to pick up the phone, because it could be an emergency, that is an interrupt. For the exception, it is more like when you are eating, you suddenly find half of dead cockroach in you food, errr~~~~~. After answering the phone or vomiting, we have to go back to our normal life anyway. Hopefully you still have appetite after that "exception".

In a nut shell, interrupts and exceptions both stop the processor's processing flow and force it to do something else and after that it will go back to the normal flow. The difference between interrupts and exceptions is interrupts are triggered by external source, like hardwares or more specific, the keyboard input, system timer etc, but exceptions are caused by the execution of instruction under predefined condition, like divide by zero fault, and there are exceptions for exceptions :), like INT 3 is an exception can be used by user tasks for debuging intentionally.

The processor associates an unique number with each interrupt or exception, this number actually is the index in interrupt vector table, it starts from address 0 in real mode, it has been overwritten by our kernel already and actually we can not use those interrupt procedure in protected mode.

Bases on Intel manuals, there are two sources for external interrupts and two sources for exceptions:
Interrupts
IRQ pin Interrupt vector Interrupt
IRQ0 08 system timer
IRQ1 09 keyboard
IRQ2 0A bridged to PIC2
IRQ3 0B COM2
IRQ4 0C COM1
IRQ5 0D LPT2
IRQ6 0E floppy disk drive
IRQ7 0F LPT1
IRQ8 70 CMOS Real Time Clock
IRQ9 71
IRQ10 72
IRQ11 73
IRQ12 74 PS/2 Mouse
IRQ13 75 numeric coprocessor
IRQ14 76 hard disk drive IDE0
IRQ15 77 hard disk drive IDE1
IRQ is the physical pin connected from the PIC(explain it later) to hardware, there are 16 pins in AT machine.

Exceptions
Interrupt vector Exception
00 Divide error
01 Debug exceptions
02 Non-maskable interrupt (NMI)
03 Breakpoint (INT 3 instruction)
04 Overflow (INTO instruction)
05 Bounds check (BOUND instruction)
06 Invalid opcode
07 Coprocessor not available
08 Double fault
09 Coprocessor segment overrun
0A Invalid TSS
0B Segment not present
0C Stack exception
0D General protection exception, the notorious blue screen under Windows 9x
0E Page fault
0F Intel reserved
10 Coprecessor error
11-19 Intel reserved
1A-FF Not used

Interestingly, we can find there are conflicts between interrupts and exceptions, IRQ0-IRQ7's interrupt vectors are overlapped by exception 08-10's vectors, so we have to handle this problem.

The vector index of maskable interrupts are determined by two 8259A programmable interrupt controllers (PIC), they are cascaded together to handle hardware interrupts, PIC1 deals with IRQ0-IRQ7 and PIC2 deals with IRQ8-IRQ15, when an interrupt happens, they get the signal and inform the processor, then the processor stops normal execution and handle this interrupt by using the vector index to locate the ISR (interrupt service routine). The numbers assigned by 8259A can be remapped manually.

For remapping those IRQs, we have to program 8259A chips, that is really difficult, you'd better google it by yourself, but for the purpose of remapping, it almost use the same routine:
04/init.cstatic void
pic_install(void) {
    outb(0x11, 0x20);
    outb(0x11, 0xa0);
    outb(0x20, 0x21);
Remaps IRQ0-IRQ7 to 0x20-0x27 in interrupt vector table
    outb(0x28, 0xa1);
Remaps IRQ8-IRQ15 to 0x28-0x2F in interrupt vector table
    outb(0x04, 0x21);
    outb(0x02, 0xa1);
PIC2 is connected to PIC1 via IRQ2
    outb(0x01, 0x21);
    outb(0x01, 0xa1);
    outb(0xff, 0x21);
Disable all interrupts from IRQ0-IRQ7
    outb(0xff, 0xa1);
Disables all interrupts from IRQ8-IRQ15
}
By the way, there is a good news, from this tutorial we can start to use C, eventually. The outb in above code snippet is a micro we are going to use, it works likes outb(byte, port), there are some other macros we are going to use
04/include/asm.h#define cli() __asm__ ("cli\n\t")
#define sti() __asm__ ("sti\n\t")

#define halt() __asm__ ("cli;hlt\n\t");
#define idle() __asm__ ("jmp .\n\t");

#define inb(port) (__extension__({    \
unsigned char __res;    \
__asm__ ("inb    %%dx,    %%al\n\t"    \
                     :"=a"(__res)    \
                     :"dx"(port));    \
__res;    \
}))

#define outb(value, port) __asm__ (    \
"outb    %%al,    %%dx\n\t"::"al"(value), "dx"(port))

#define insl(port, buf, nr) \
__asm__ ("cld;rep;insl\n\t"    \
::"d"(port), "D"(buf), "c"(nr))

#define outsl(buf, nr, port) \
__asm__ ("cld;rep;outsl\n\t"    \
::"d"(port), "S" (buf), "c" (nr))

Now we know how to remap interrupts, and let processor know the correct index in interrupt vector table. However, there is another problem, the interrupt table in real mode has been overwritten by kernel, then what should we do? Bad news, we have to write it from scratch......

IDT and ISR

The processor handles interrupts and exceptions in almost the same way, when either of them happens, the processor stops the execution of current task and switch to a specific procedure, called interrupt service routine (ISR) to handle the interrupt or exception. Once the ISR finish handling the interrupt or exception, the control is returned to the original task.

However, how can the processor know where to find those routines? The processor manages a system table called interrupt descriptor table (IDT) which contains a collection of descriptors which describes how to access those ISRs. Just like GDT, the physical address of IDT is stored in IDTR.

Each interrupt and exception in the IDT has a unique number, called a vector which has been remapped by us. The IDT is an array of 64-bit long descriptors, there are256 descriptors at maximum in this table. LIDT instruction is used to tell processor where to locate the IDT by register IDTR, just like LGDT and GDT we used before.

Now let's take a look at the format of an IDT entry
IDT entry
Most fields are familiar to us, I will explain it by code later in details. In facts, there are several kinds of descriptors, here the interrupt gate is to be used.

Now we can let processor know how to find the interrupt routine when an interrupt or exception occurs, then what an ISR(Interrupt service routine) should look like? Well, because the interrupt or exception stops the normal execution and after the execution of ISR the processor has to go back to normal flow, so that implies the ISR should reserve the environment of normal execution, that is registers for processor. It has to save all registers it might affect and restore them before going back to normal execution.

If the ISR code segment has the same privilege level as the currently task, then the ISR uses the current task, or a stack switch occurs which stores the current SS, ESP, EFLAGS, CS and EIP, then loads the new CS and EIP and stack from TSS (we are going to talk about it in later tutorials) and pushs those saved registers on the new stack. After the stack switch ( if there is), the processor pushes EFLAGS, CS and EIP on the stack in that order, some types of exceptions provide error codes which reports some information about the error, the error code will be pushed on the stack. After that, CS and EIP saved in the corresponding IDT descriptor will be loaded. Because we use interrupt gates, so the IF flag in EFLAGS will be cleared. Returning from ISRs just reverses above order, roughly speaking.
ISR stack frame
I guess you might be confused, so let's take a look at our code to make it clear, at the end of load.s, we let it to display "Hello World!" on screen in last tutorial, at this tutorial we substitute them with a call to C code.
04/load.s        .text
        .globl    pm_mode
        .include "kernel.inc"
        .org 0
pm_mode:
        movl    $DATA_SEL,%eax
        movw    %ax,    %ds
        movw    %ax,    %es
        movw    %ax,    %fs
        movw    %ax,    %gs
        movw    %ax,    %ss
        movl    $STACK_BOT,%esp

        cld
        movl    $0x10200,%esi
        movl    $0x200, %edi
        movl    $KERNEL_SECT<<7,%ecx
        rep
        movsl

        call    init      # entry to C code

the init function will initialize hardwares and system tables etc, at this moment it looks like this:
04/init.cunsigned long long *idt = ((unsigned long long *)IDT_ADDR);
unsigned long long *gdt = ((unsigned long long *)GDT_ADDR);
Just for convenient, we use one long long to present one descriptor, for suppressing the warning massage given by GCC, we have to add a compiler option --Wno-long-long in Makefile.
static void
isr_entry(int index, unsigned long long offset) {
Function isr_entry is used to fill specific entry in IDT, the first argument is the index in IDT, the second one is the address of the ISR.
    unsigned long long idt_entry = 0x00008e0000000000ULL |
            ((unsigned long long)CODE_SEL<<16);
Let's make a template of an IDT entry at first, I paste the IDT entry format at here:
dit format
so idt_entry's value is 0x00008e0000080000, it indicates it starts at address 0, using selector 0x8 at here(the code selector for kernel), and it's presented and the DPL is 0.
    idt_entry |= (offset<<32) & 0xffff000000000000ULL;
    idt_entry |= (offset) & 0xffff;
Then fills in the correct ISR address
    idt[index] = idt_entry;
Puts this entry into IDT table.}

static void
idt_install(void) {
Function idt_install installs all 256 ISRs one by one
    unsigned int i = 0;
    struct DESCR {
        unsigned short length;
        unsigned long address;
    } __attribute__((packed)) idt_descr = {256*8-1, IDT_ADDR};
create the structure for IDTR, it has the same format as GDTR, but here we need a GCC extension "__attribute__((packed))" to prevent GCC padding bits in this structure to make it 64-bit long for efficient accessing, like 2-byte length, 2-byte blank, 4-byte address, that is not what we want.
    for (i=0; i<VALID_ISR; ++i)
        isr_entry(i, (unsigned int)(isr[(i<<1)+1]));
The array isr is newly introduced, it stores all valid ISR, it will be explained in a short while. Making yourself solid, there will be a long table of stack frame....... VALID_ISR is defined in include/isr.h, it indicates how many IDT entries are actually used, here it is defined as 32, 32 is the amount of all those exceptions we discussed above.
    for (++i; i<256; ++i)
        isr_entry(i, (unsigned int)default_isr);
Adds default ISR to those interrupts we are not going to use, it just changes a letter on screen to tell use there is an unhandled interrupt occurs.
    __asm__ __volatile__ ("lidt    %0\n\t"::"m"(idt_descr));
Okay, now everything is done, we load the IDT to IDTR by instruction LIDT
}

static void
pic_install(void) {
This function has been explained earlier    outb(0x11, 0x20);
    outb(0x11, 0xa0);
    outb(0x20, 0x21);
    outb(0x28, 0xa1);
    outb(0x04, 0x21);
    outb(0x02, 0xa1);
    outb(0x01, 0x21);
    outb(0x01, 0xa1);
    outb(0xff, 0x21);
    outb(0xff, 0xa1);
}

void
init(void) {
    int a = 3, b = 0;

    idt_install();
    pic_install();
    sti();
    a /= b;
We are going to do something naughty, we let a divided by zero, so an exception should happen
}

Okay, now is a boring part, we are going to look at some assembly. We know once the hardware send a single to PIC, then PIC informs the processor to stop current execution, then processor find the ISR to do something we want, then ISR gives up the control and go back to normal flow. So actually the ISR is what we are concern about.
04/isr.s        .text
        .include "kernel.inc"
        .globl   default_isr, isr
       
        .macro   isrnoerror        nr
        isr\nr:
        pushl    $0
        pushl    $\nr
        jmp       isr_comm
        .endm
Here is a macro for exceptions without error code, it is a wrapper actually, it pushs an extra 0 as error code and the ISR index onto the stack
        .macro    isrerror        nr
        isr\nr:
        pushl    $\nr
        jmp        isr_comm
        .endm
With these two macros, we can make all exceptions and interrupts have identical stack frame, which will be given in a short whileisr:    .long    divide_error, isr0x00, debug_exception, isr0x01
        .long    breakpoint, isr0x02, nmi, isr0x03
        .long    overflow, isr0x04, bounds_check, isr0x05
        .long    invalid_opcode, isr0x06, cop_not_avalid, isr0x07
        .long    double_fault, isr0x08, overrun, isr0x09
        .long    invalid_tss, isr0x0a, seg_not_present, isr0x0b
        .long    stack_exception, isr0x0c, general_protection, isr0x0d
        .long    page_fault, isr0x0e, reversed, isr0x0f
        .long    coprocessor_error, isr0x10, reversed, isr0x11
        .long    reversed, isr0x12, reversed, isr0x13
        .long    reversed, isr0x14, reversed, isr0x15
        .long    reversed, isr0x16, reversed, isr0x17
        .long    reversed, isr0x18, reversed, isr0x19
        .long    reversed, isr0x1a, reversed, isr0x1b
        .long    reversed, isr0x1c, reversed, isr0x1d
        .long    reversed, isr0x1e, reversed, isr0x1f
This is that isr array we used in init.c, its elements defined as pairs actually, for example, divide_error and isr0x00, do_timer actually is the entry of the function which actually do the real work. isr0x20 is a code snippet which is generated by one of two macros mentioned before to make a identical stack frame.
/*
        +-----------+
        |  old  ss  |    76
        +-----------+
        |  old esp  |    72
        +-----------+
        |  eflags   |    68
        +-----------+
        |    cs     |    64
        +-----------+
        |   eip     |    60
        +-----------+
        |  0/err    |    56
        +-----------+
        |  isr_nr   | tmp = esp
        +-----------+
        |   eax     |    48
        +-----------+
        |   ecx     |    44
        +-----------+
        |   edx     |    40
        +-----------+
        |   ebx     |    36
        +-----------+
        |   tmp     |    32
        +-----------+
        |   ebp     |    28
        +-----------+
        |   esi     |    24
        +-----------+
        |   edi     |    20
        +-----------+
        |    ds     |    16
        +-----------+
        |    es     |    12
        +-----------+
        |    fs     |    8
        +-----------+
        |    gs     |    4
        +-----------+
        |    ss     |    0
        +-----------+
*/
That is an awesome stack frame :), it took me lots of time to finish it, it shows our identical stack frame for all exceptions and interrupt
isr_comm:
After pushing extra informations into stack, two macros isrnoerror and isrerror then jump to here to do common processing
        pushal
        pushl    %ds
        pushl    %es
        pushl    %fs
        pushl    %gs
        pushl    %ss
Pushing, pushing, pushing......
        movw     $DATA_SEL,%ax
        movw     %ax,    %ds
        movw     %ax,    %es
        movw     %ax,    %fs
        movw     %ax,    %gs
Make all date segments load privilege 0 selector
        movl     52(%esp),%ecx
Please check out the stack frame, 52 is the ISR index
        call     *isr(, %ecx, 8)
Invokes the function that really do the work, that is the first element in specific pair
        addl     $4,      %esp    # for %ss
jump the SS on stack, we can't just POP it.
        popl     %gs
        popl     %fs
        popl     %es
        popl     %ds
        popal
        addl     $8,      %esp    # for isr_nr and err_code
Jump error code and ISR index on stack, recover the stack to the original situation when it's been called
        iret

        isrNoError        0x00
        isrNoError        0x01
        isrNoError        0x02
        isrNoError        0x03
        isrNoError        0x04
        isrNoError        0x05
        isrNoError        0x06
        isrNoError        0x07
        isrError          0x08
        isrNoError        0x09
        isrError          0x0a
        isrError          0x0b
        isrError          0x0c
        isrError          0x0d
        isrError          0x0e
        isrNoError        0x0f
        isrError          0x10
        isrNoError        0x11
        isrNoError        0x12
        isrNoError        0x13
        isrNoError        0x14
        isrNoError        0x15
        isrNoError        0x16
        isrNoError        0x17
        isrNoError        0x18
        isrNoError        0x19
        isrNoError        0x1a
        isrNoError        0x1b
        isrNoError        0x1c
        isrNoError        0x1d
        isrNoError        0x1e
        isrNoError        0x1f
Generates all isr0x?? entries for exceptionsdefault_isr:
        incb    0xb8000
        movb    $2,     0xb8001
        movb    $0x20,  %al
        outb    %al,    $0x20
Tells PIC1 the ISR has finished, it can accept new incoming interrupts        outb    %al,    $0xa0
Tells PIC2 the ISR has finished, it can accept new incoming interrupts
        iret
This is the default ISR for unused IDT entry, we used it in init.c, it just print a character and noticeable color on the screen to tell us it just happened and tell PICs the ISR has finished. Those two outb are important, or there will be no more interrupts happen.

Divided By Zero, Yeah!

At this stage, all exception handlers are nothing more than printing register information, I just demonstrate how it works.
04/exceptions.cvoid
divide_error(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
debug_exception(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
breakpoint(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
nmi(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
overflow(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
bounds_check(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
invalid_opcode(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
cop_not_avalid(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
double_fault(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
overrun(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
invalid_tss(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
seg_not_present(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
stack_exception(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
general_protection(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
page_fault(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
reversed(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}

void
coprocessor_error(void) {
    __asm__ ("pushl    %%eax;call    info"::"a"(KPL_PANIC));
    halt();
}
All ISRs just use function info to print some information on screenvoid
info(enum KP_LEVEL kl,
     unsigned int ret_ip, unsigned int ss, unsigned int gs, unsigned int fs,
     unsigned int es, unsigned int ds, unsigned int edi, unsigned int esi,
     unsigned int ebp, unsigned int esp, unsigned int ebx, unsigned int edx,
     unsigned int ecx, unsigned int eax, unsigned int isr_nr, unsigned int err,
     unsigned int eip, unsigned int cs, unsigned int eflags,
     unsigned int old_esp, unsigned int old_ss) {
    static const char *exception_msg[] = {
        "DIVIDE ERROR",
        "DEBUG EXCEPTION",
        "BREAK POINT",
        "NMI",
        "OVERFLOW",
        "BOUNDS CHECK",
        "INVALID OPCODE",
        "COPROCESSOR NOT VALID",
        "DOUBLE FAULT",
        "OVERRUN",
        "INVALID TSS",
        "SEGMENTATION NOT PRESENT",
        "STACK EXCEPTION",
        "GENERAL PROTECTION",
        "PAGE FAULT",
        "REVERSED",
        "COPROCESSOR_ERROR",
    };
    unsigned int cr2, cr3;
    (void)ret_ip;
    __asm__ ("movl    %%cr2,    %%eax":"=a"(cr2));
    __asm__ ("movl    %%cr3,    %%eax":"=a"(cr3));
    if (isr_nr < sizeof exception_msg)
        kprintf(kl, "EXCEPTION %d: %s\n=======================\n",
                isr_nr, exception_msg[isr_nr]);
    else
        kprintf(kl, "INTERRUPT %d\n=======================\n", isr_nr);
    kprintf(kl, "cs:\t%x\teip:\t%x\teflags:\t%x\n", cs, eip, eflags);
    kprintf(kl, "ss:\t%x\tesp:\t%x\n", ss, esp);
    kprintf(kl, "old ss:\t%x\told esp:%x\n", old_ss, old_esp);
    kprintf(kl, "errcode:%x\tcr2:\t%x\tcr3:\t%x\n", err, cr2, cr3);
    kprintf(kl, "General Registers:\n=======================\n");
    kprintf(kl, "eax:\t%x\tebx:\t%x\n", eax, ebx);
    kprintf(kl, "ecx:\t%x\tedx:\t%x\n", ecx, edx);
    kprintf(kl, "esi:\t%x\tedi:\t%x\tebp:\t%x\n", esi, edi, ebp);
    kprintf(kl, "Segment Registers:\n=======================\n");
    kprintf(kl, "ds:\t%x\tes:\t%x\n", ds, es);
    kprintf(kl, "fs:\t%x\tgs:\t%x\n", fs, gs);
}

There are lots of changes in Makefile, before we continue compiling the source code, we have to check out the Makefile or you can not get correct image file
04/MakefileAS=as -Iinclude
LD=ld
CC=gcc
No need to say, we use GCC
CPP=gcc -E -nostdinc -Iinclude
We use GCC to generate dependency automatically.CFLAGS=-Wall -pedantic -W -nostdlib -nostdinc -Wno-long-long -I include -fomit-frame-pointer
-Wall -pedantic -W opens all warning options, -nostdlib tells GCC do not use standard library, -nostdinc -I include tells GCC go to find header files under directory include instead of the standard path of standard header files, -Wno-long-long suppress the warning message about using long long type because it is not a part of C89, -fomit-frame-pointer is important for function info to keep a correct stack or it might not work properly.KERNEL_OBJS= load.o init.o isr.o libcc.o scr.o kprintf.o exceptions.o
Adds new modules into kernel.s.o:
    ${AS} -a $< -o $*.o >$*.map

all: final.img

final.img: bootsect kernel
    cat bootsect kernel > final.img
    @wc -c final.img

bootsect: bootsect.o
    ${LD} --oformat binary -N -e start -Ttext 0x7c00 -o bootsect $<

kernel: ${KERNEL_OBJS}
    ${LD} --oformat binary -N -e pm_mode -Ttext 0x0000 -o $@ ${KERNEL_OBJS}
    @wc -c kernel

clean:
    rm -f *.img kernel bootsect *.o

dep:
    sed '/\#\#\# Dependencies/q' < Makefile > tmp_make
    (for i in *.c;do ${CPP} -M $$i;done) >> tmp_make
    mv tmp_make Makefile
Generates the dependency automatically, next line ### Dependencies is important, do not delete it### Dependencies:

Then execute make clean && make dep && make, make dep is very important before make.
result of tutorial04

Then with great pleasure, we can see this wonderful error message from exception handler as we wanted
divide by zero exception

Subject:

Your Name:

Your Email Address:

Comments:


Prev Home Next
Up