The i386 Interrupt Descriptor Table


Interrupt Descriptor Table

The Interrupt Descriptor Table (IDT) is an array of 8 byte interrupt descriptors in memory devoted to specifying (at most) 256 interrupt service routines. The first 32 entries are reserved for processor exceptions, and any 16 of the remaining entries can be used for hardware interrupts. The rest are available for software interrupts.

The address of the IDT is stored in a processor register called the IDTR. You can access this register with the following C functions:

void lidt(void *base, unsigned int limit) {
   unsigned int i[2];

   i[0] = limit << 16;
   i[1] = (unsigned int) base;
   asm ("lidt (%0)": :"p" (((char *) i)+2));
}

void *sidt(void) {
    unsigned int ptr[2];
    asm ("sidt (%0)": :"p" (((char *) ptr)+2));
    return (void *) ptr[1];
}

Note that you can only get the base, and not the size of the current IDT, with the sidt() function. I don't think the limit is very important; I keep it set to its maximum value (0x7ff, or 256*8) to avoid problems.

The IDTR is not initialized by our boot loader. You must find space in memory for it and load it yourself.

Interrupt Descriptor Table Entry

An entry in the IDT takes up two words (64-bits), and is laid out like this:


IDT Entry bit layout

The Target Segment Selector needs to be a code segment, probably 0x08 (see the predefined selectors in the boot loader).

Exception handlers should either be defined as an Interrupt Gate (type = 14) or a Trap Gate (15). The difference is simple: interrupts are automatically disabled by the processor when an interrupt gate is called, and they are left alone (whether enabled or disabled) for a trap gate. An entry in the IDT can also be a Task Gate (5), which will cause a context switch when encountered; however, exceptions are usually not handled in this manner.

C Exception Handlers

Exception and interrupt handlers can be straight C functions, as follows:

void GeneralProtectionFault(void);
void setExceptionHandler(int excnum, void (*handler)(void));

void GeneralProtectionFault(void)
{
   /* handle fault, print information, etc. */

   ...
   /* DOES NOT RETURN */
}

setExceptionHandler(13, &GeneralProtectionFault);
assuming that setExceptionHandler() installs a valid entry in the IDT.

This works fine for handlers that do not need to return. However, handlers that must return (page faults, hardware interrupts, and more) need to use an assembly wrapper around their C code, for two reasons:

These two conditions could conceivably be satisfied with some well- placed inline assembly; however, this requires knowledge of how the compiler generates code, which may not be constant.

We can use either global inline assembly (inline assembly instructions in a .c file, but outside the scope of any C function), or make a seperate .S (assembler) file entirely. Each exception handler needs its own assembly wrapper, since the only way to determine which exception occurred is by which function has been called.


.globl asmExc13      ; make this symbol globally accessible

asmExc13:
    pusha            ; push all registers 
    
    call GeneralProtectionFault   ; may need to tweak this symbol, 
                                  ; depending on how the compiler mangles it 
    
    popa             ; restore all registers 
    add $4, %esp     ; remove error code, only necessary for #8, 10-14 
    iret             ; and return from exception 

You may need to do some tweaking with predeclaring symbols or adding leading underscores, depending on how the compiler mangles your function names.

Stack Layout

The stack layout on entry to an C exception handler is different from a normal C function call, as follows:

LocationValue on ExceptionValue on Call
%esp+20eFlags4th parameter
%esp+16CS3rd parameter
%esp+12EIP2nd parameter
%esp+8error code1st parameter
%esp+4return valuereturn value
%espold frame pointerold frame pointer

Note that only exceptions 8 and 10-14 have an error code. For all the rest, EIP is on the top of stack before the assembly wrapper, and everything else is shifted down accordingly.

You can get these values on the stack from a C function by using the following function definition:

struct regs {
   unsigned int edi, esi, ebp, esp, ebx, edx, ecx, eax;
};

/* assembly wrapper with pusha */
void SavedRegsExceptionHandler(struct regs r, 
         unsigned int errcode, unsigned int eip, 
         unsigned int cs, unsigned int eflags);  

/* assembly wrapper without pusha, or direct call to C function */
void DirectExceptionHandler(unsigned int errcode, unsigned int eip, 
                      unsigned int cs, unsigned int eflags);  

These definitions work by knowing the C stack calling convention and then tricking the compiler into generating code that believes it is being passed parameters. In a way, it is; the processor (and assembly wrapper, if there is one) are passing them.

Exceptions that don't push an error code on the stack need to have the "unsigned int errcode" parameter removed from their definitions.

Processor Exceptions

Exception Types:

NumberDescriptionType
0Divide-by-zerofault
1Debug exceptiontrap or fault
2Non-Maskable Interrupt (NMI)trap
3Breakpoint (INT 3)trap
4Overflow (INTO with EFlags[OF] set)trap
5Bound exception (BOUND on out-of-bounds access)trap
6Invalid Opcodetrap
7FPU not availabletrap
8*Double Faultabort
9Coprocessor Segment Overrunabort
10*Invalid TSSfault
11*Segment not presentfault
12*Stack exceptionfault
13*General Protectionfault or trap
14*Page faultfault
15Reserved
16Floating-point errorfault
17Alignment Checkfault
18Machine Checkabort
19-31Reserved By Intel
32-255Available for Software and Hardware Interrupts
*These exceptions have an associated error code.