Interrupt Service Routines

From OSDev.wiki
Revision as of 14:14, 26 July 2009 by osdev>Steve23 (→‎External Links: Add link to James Molloy's tutorial)
Jump to navigation Jump to search

The x86 architecture is an interrupt driven system. External events trigger an interrupt - the normal control flow is interrupted and a Interrupt Service Routine (ISR) is called.

Such events can be triggered by hardware or software. An example of a hardware interrupt is the keyboard: Every time you press a key, the keyboard triggers IRQ1 (Interrupt Request 1), and the corresponding interrupt handler is called. Timers, and disk request completion are other possible sources of hardware interrupts.

Software driven interrupts are triggered by the int opcode; e.g. the services provided by MS-DOS are called by the software triggering INT 21h and passing the applicable parameters in CPU registers.

For the system to know which interrupt service routine to call when a certain interrupt occurs, offsets to the ISR's are stored in the Interrupt Descriptor Table when you're in Protected mode, or in the Interrupt Vector Table when you're in Real Mode.

An ISR is called directly by the CPU, and the protocol for calling an ISR differs from calling e.g. a C function. Most importantly, an ISR has to end with the iret opcode, whereas usual C functions end with ret or retf. The obvious but nevertheless wrong solution leads to one of the most "popular" tripple-fault errors among OS programmers.

The Problem

Many people shun away from Assembler, and want to do as much as possible in their favorite high-level language. GCC (as well as other compilers) allow you to add inline Assembler, so many programmers are tempted to write an ISR like this:

/* How NOT to write an interrupt handler           */
void interrupt_handler(void)
{
    __asm__("pushad"); /* Save registers.          */
    /* do something */
    __asm__("popad");  /* Restore registers.       */
    __asm__("iret");   /* This will triple-fault! */
}

This cannot work. The compiler adds stack handling code before and after your function, which together with the iret results in Assembler code resembling this:

push   %ebp
mov    %esp,%ebp
sub    $<size of local variables>,%esp
pushad
# C code comes here
popad
iret
# 'leave' if you use local variables, 'pop %ebp' otherwise.
leave
ret

It should be obvious how this messes up the stack (ebp gets push'ed but never pop'ed). Don't do this. Instead, these are your options. Solutions

Plain Assembler

Learn enough about Assembler to write your interrupt handlers in it. ;-)

Two-Stage Assembler Wrapping

Write an Assembler wrapper calling the C function to do the real work, and then doing the iret.

/* filename : isr_wrapper.asm */
.globl   _isr_wrapper
.align   4

_isr_wrapper:
    pushad
    call    _interrupt_handler
    popad
    iret
/* filename : interrupt_handler.c */
void interrupt_handler(void)
{
    /* do something */
}

Compiler Specific Directives

Some compilers for some processors have directives allowing you to declare a routine interrupt, offer a #pragma interrupt, or a dedicated macro. Borland C, Watcom C/C++, Microsoft C 6.0 and Free Pascal Compiler 1.9.* and up offer this, while VisualC++ and GCC don't:

/* Borland C */
void interrupt interrupt_handler(void)
{
    /* do something */
}
/* Watcom C/C++ */
void _interrupt interrupt_handler(void)
{
    /* do something */
}

Actually, VisualC++ can be used to make interrupts, by making them naked. adding _declspec(naked) to your function will cause the compiler to leave out the stack handling code. This means you are free to set up and release stack space however you want. Just be careful that you put in a return statement, because the compiler won't, it will just allow execution to continue past the end of your code on into garbage. Also, if you plan to use local variables or function arguments in the C code, you need to set up the stack frame the way the compiler expects it. This is not such a problem in interrupts though, because since they are non-reentrant, you can simply use static variables.

/* Microsoft Visual C++ */
void _declspec(naked) interrupt_handler()
{
    _asm pushad;

    /* do something */

    _asm{
        popad
        iretd
    }
}

Black Magic

Look at the faulty code above, where the proper C function exit code was skipped, screwing up the stack. Now, consider this code snippet, where the exit code is added manually:

/* BLACK MAGIC - Strongly Discouraged! */
void interrupt_handler() {
    __asm__("pushad");
    /* do something */
    __asm__("popad; leave; iret"); /* BLACK MAGIC! */
}

The corresponding output would look somewhat like this:

push   %ebp
mov    %esp,%ebp
sub    $<size of local variables>,%esp
pushad
# C code comes here
popad
leave
iret
leave # dead code
ret   # dead code

This assumes that leave is the correct end-of-function handling - you are doing the function return code "by hand", and leave the compiler-generated handling as "dead code". Needless to say, such assumptions on compiler internals are dangerous. This code can break on a different compiler, or even a different version of the same compiler. It is therefore strongly discouraged, and listed only for completeness.

See Also

External Links