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.
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
is an exception can be used by user tasks for debuging.INT
3
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
Maskable interrupts, which are signaled via the INTR pin.
Non-maskable interrupts, which are signaled via the NMI (Non-Maskable Interrupt) pin.
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 on AT machine.
Exceptions
Processor detected. They are classified as faults, traps, and aborts, we are going to look into them in later tutorial.
Programmed. Instructions like INTO
,
, INT
3
,
and INT
nBOUND
trigger exceptions. These instructions are often called "software interrupts", but the processor handles them as 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 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__
("
\n\t")cli
#define sti() __asm__
("
\n\t")sti
#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
\n\t"::"dx
"(value), "al
"(port))dx
#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...
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,
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...
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
offset) {long
unsigned
long
idt_entry = 0x00008e0000000000ULL |long
((unsigned
long
)CODE_SEL<<16);long
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,
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
to prevent GCC from padding bits. Otherwise, GCC would make this structure 64-bit long for efficient accessing, like 2-byte __attribute__
((packed))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.
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
cs, int
unsigned
eflags,int
unsigned
int
old_esp, unsigned
old_ss) {int
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
.
With great pleasure, we can see a wonderful error message if we did it right:)
Feel free to use my code. Please contact me if you have any questions.