Our Goal

In this tutorial, how to handle interrupts and exceptions is going to be introduced. Adding exception handlers to Skelix gives us the oppotunity to execute specific code sequence when an exception happens.

Download source code


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 a cockroach in you food, a half of it, actually, and it is still moving, 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.

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

Based on Intel manuals, there are two sources for external interrupts and two sources for exceptions,

Interrupts

IRQ pinInterrupt vectorInterrupt
IRQ008system timer
IRQ109keyboard
IRQ20Abridged to PIC2
IRQ30BCOM2
IRQ40CCOM1
IRQ50DLPT2
IRQ60Efloppy disk drive
IRQ70FLPT1
IRQ870CMOS Real Time Clock
IRQ971
IRQ1072
IRQ1173
IRQ1274PS/2 Mouse
IRQ1375numeric coprocessor
IRQ1476hard disk drive IDE0
IRQ1577hard disk drive IDE1

IRQ is the physical pin connected from the PIC(explain it later) to hardware, there are 16 pins on AT machine.

Exceptions

Interrupt vectorException
00Divide error
01Debug exceptions
02Non-maskable interrupt (NMI)
03Breakpoint (INT 3 instruction)
04Overflow (INTO instruction)
05Bounds check (BOUND instruction)
06Invalid opcode
07Coprocessor not available
08Double fault
09Coprocessor segment overrun
0AInvalid TSS
0BSegment not present
0CStack exception
0DGeneral protection exception, the notorious blue screen under Windows 9x
0EPage fault
0FIntel reserved
10Coprecessor error
11-19Intel reserved
1A-FFNot used

Interestingly, we can find there are conflictions between interrupts and exceptions, IRQ0-IRQ7's interrupt vectors are overlapped by exception 08-10's vectors, 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 (several comments in pic_install has been in wrong position, till Shen Feng corrects it), that is really difficult, you'd better google it by yourself, but for the purpose of remapping, it uses the same routine basically,

04/init.c

static void 

pic_install(void) {

        outb(0x11, 0x20);

        outb(0x11, 0xa0);

        /*Remaps IRQ0-IRQ7 to 0x20-0x27 in interrupt vector table*/

        outb(0x20, 0x21); 

        /*Remaps IRQ8-IRQ15 to 0x28-0x2F in interrupt vector table*/

        outb(0x28, 0xa1);

        /*PIC2 is connected to PIC1 via IRQ2*/

        outb(0x04, 0x21);

        outb(0x02, 0xa1);

        /*Enables 8086/88 mode*/

        outb(0x01, 0x21);

        outb(0x01, 0xa1);

        /*Disables all interrupts from IRQ0-IRQ7*/

        outb(0xff, 0x21);

        /*Disables all interrupts from IRQ8-IRQ15*/

        outb(0xff, 0xa1);

}

By the way, there is a good news, from this tutorial we can 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, here is another problem, the interrupT 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 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 finishes, the control returns to the original task.

However, how can the processor know where to find those routines? The processor manages a 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 are 256 descriptors maximumly 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.

Let's take a look at the format of an IDT entry,

IDT entry

Most fields are familiar to us, I will explain them later in details. In facts, there are several kinds of descriptors, interrupt gates are used in Skelix.

Now Processor knows where to find the interrupt routine when an interrupt or exception occurs, then what an ISR should look like? Well, because the interrupt or exception stops the normal execution and after the ISR execution the processor has to go back to normal flow, that implies the ISR should reserve the environment of normal execution. 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 stack, 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 exceptions provide error codes which reports some information about the error, that error code must be pushed into the stack. After that, CS and EIP saved in the corresponding IDT descriptor will be loaded. Because we use interrupt gates, the IF flag in EFLAGS will be cleared. Returning from ISRs just reverses this order, roughly speaking...

ISR stack frame

Hello? Are you still with me? I guess you might be confused, so let the code talk. At the end of last tutorial, we make 03/load.s prints "Hello World!" on screen. At this tutorial, we substitute it with a call to C code.

        .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

The init function initializes hardwares and system tables etc, at this moment it looks like this,

04/init.c

unsigned long long *idt = ((unsigned long long *)IDT_ADDR);

unsigned long long *gdt = ((unsigned long long *)GDT_ADDR);

Just for convenience, we use one long long to represent one descriptor, for suppressing the GCC warning massage, we have to add a compiler option --Wno-long-long in Makefile.

static void 

isr_entry(int index, unsigned long long offset) {

        unsigned long long idt_entry = 0x00008e0000000000ULL |

                        ((unsigned long long)CODE_SEL<<16);

        idt_entry |= (offset<<32) & 0xffff000000000000ULL;

Function isr_entry fills the specific entry in IDT, the first arguments is the index in IDT, the second argument is the address of the ISR.

It makes a template entry at first, the IDT entry format is,

IDT format

Template IDT value is 0x00008e0000080000, it indicates it starts at address 0, using selector 0x8(the code selector for kernel), and it's presented and the DPL is 0.

        idt_entry |= (offset) & 0xffff;

        idt[index] = idt_entry;

Fills in with correct ISR address and puts it into IDT.

}

 

static void 

idt_install(void) {

Function idt_install installs all 256 ISRs.

        unsigned int i = 0;

        struct DESCR {

                unsigned short length;

                unsigned long address;

        } __attribute__((packed)) idt_descr = {256*8-1, IDT_ADDR};

Creates the structure for IDTR, it has the same format as GDTR. We need a GCC extension __attribute__((packed)) to prevent GCC from padding bits. Otherwise, GCC would make this structure 64-bit long for efficient accessing, like 2-byte length, 2-byte padding, 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]));

        for (++i; i<256; ++i)

                isr_entry(i, (unsigned int)default_isr);

Array isr stores all valid ISRs, it will be explained in a short while. Please prepare yourself, there will be a long table of stack frame.... VALID_ISR is defined in 04/include/isr.h, it indicates how many IDT entries are actually used, it is defines as 32 in this tutorial, it is the number of all exceptions we have discussed.

The rest of IDT entries are filled with a default ISR, which changes a letter on screen lets us know there is an unhandled interrupt.

        __asm__ __volatile__("lidt  %0\n\t"::"m"(idt_descr));

Loads IDT to IDTR by instruction LIDT.

}

 

static void 

pic_install(void) {

        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();

        a /= b;

}

At the end of init, we did something naughty, 3/0 should trigger a Divided by Zero exception.

We know once the hardware sends a signal to PIC, then PIC informs the processor to stop current execution, then processor finds the ISR to do something we want. After execution of ISR, it gives up control and goes 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

isrnoerror 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 into stack.

                .macro  isrerror                nr

                isr\nr:

                pushl   $\nr

                jmp             isr_comm

                .endm

With these two macros, all exceptions with or without error code can be treated identically.

isr:            .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 04/init.c, its elements defined as pairs, for example, divide_error and isr0x00, divide_error is the entry of the function which does the real work. isr0x20 is a code snippet which is generated by one of those 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

                +-----------+

*/

What an awesome stack frame:). It shows our identical stack frame for all exceptions and interrupts.

isr_comm:

After pushing extra informations into stack, two macros isrnoerror and isrerror jump to here to do some 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

All data segments load privilege 0 selector.

                movl    52(%esp),%ecx

                call    *isr(, %ecx, 8)

Invokes the ISR to do the real work, 52 is the ISR index in that long stack frame.

                addl    $4,             %esp    # for %ss

After ISR call, we have to rewind the stack. Last statement skips the SS in stack, because we cannot just POP it as others.

                popl    %gs

                popl    %fs

                popl    %es

                popl    %ds

                popal

                addl    $8,             %esp    # for isr_nr and err_code

Skips error code and ISR index in stack, recovers the stack to its original states before this ISR call.

                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 exceptions.

default_isr:

                incb    0xb8000

                movb    $2,             0xb8001

                movb    $0x20,  %al

                outb    %al,    $0x20

                outb    %al,    $0xa0

Tells PIC1 and PIC2 the ISR has finished, they can accept new interrupts.

                iret

This is the default ISR for unused IDT entries, we used it in 04/init.c, it just prints a character with noticeable color on the screen to tell us it just happened and tell PICs the ISR has finished. Those two outb are important, otherwise no new interrupts can come in.


Dvided By Zero, Yeah!

At this stage, all exception handlers does nothing more than printing register information, I will demostrate how it works.

04/exceptions.c

#include <kprintf.h>

#include <asm.h>

#include <scr.h>

 

void

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 call info to print some information on screen.

void

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 to get code compiled.

AS=as -Iinclude

LD=ld

CC=gcc

CPP=gcc -E -nostdinc -Iinclude

GCC is used to generate dependency automatically.

CFLAGS=-Wall -pedantic -W -nostdlib -nostdinc -Wno-long-long -I include -fomit-frame-pointer

-Wall -pedantic -W are warning options, -nostdlib tells GCC do not use standard library, -nostdinc -Iinclude tells GCC to find header files under directory include instead of the standard path of standard header files, -Wno-long-long suppresses 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

 

#.c.s:

#    ${CC} ${CFLAGS} -S -o $*.s $<

 

.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

 

### Dependencies:

Generates dependencies automatically, ### Dependencies: is important, do not delete it.

Execute make clean && make dep && make, make dep must be called before make.

making process of tutorial04

With great pleasure, we can see a wonderful error message if we did it right:)

divide by zero exception


Feel free to use my code. Please contact me if you have any questions.