ISRs PIC And Multitasking

From OSDev Wiki
Jump to: navigation, search

Contents

Introduction

This tutorial explains how to make interrupt service routines for the x86 platform using GCC and inline assembly. No external assembly is needed for this code to work. The code presented here allows easy handling of IRQs, software interrupts, exceptions without error code, and exceptions with error code, while being compatible with software multitasking. The IRQ handler in this article is only valid for handling IRQs received using the 8259 PIC, so it will need to be modified or extended when compatibility with the APIC is required.

Basic ISR function

void *irq0handler(void)
{
	volatile void *addr; // what we're going to return
	asm goto("jmp %l[endofISR]" ::: "memory" : endofISR); // skip ISR code when calling C function
	asm volatile(".align 16" ::: "memory"); // useful align
	startofISR:
	asm volatile("pushal\n\tpushl %%ebp\n\tmovl %%esp, %%ebp\n\tcld" ::: "memory"); // save registers, stack frame
	asm volatile(
		"pushl %%ds       \n\t" // save segment registers
		"pushl %%es       \n\t"
		"movw $16, %%cx   \n\t" // set segment registers for kernel
		"movw %%cx, %%ds  \n\t"
		"movw %%cx, %%es  \n\t"
		"pushl %%ebp      \n\t" // push previous stack pointer (points to the saved ESP), this is the context parameter
		"addl $4, (%%esp) \n\t" // add 4 to it (now it correctly points to the PUSHAD structure)
		"pushl %%ebx      \n\t" // this is the IRQ number parameter
		"call *%%eax      \n\t" // call the IRQ function
		"addl $8, %%esp       " // pop the 2 parameters
		:: "a"(irqfunc), "b"((uint32_t) 0) : "memory");
	asm volatile("popl %%es\n\tpopl %%ds\n\tleave\n\tpopal\n\tiret" ::: "memory"); // restore everything and iret
	endofISR:
	//return startofISR
	asm goto(
		".intel_syntax noprefix\n\t"
		"mov eax, offset %l[startofISR]\n\t"
		"mov [ebx], eax\n\t"
		".att_syntax"
		:: "b"(&addr) : "eax", "memory" : startofISR);
	return((void *) addr);
}

This code uses asm goto, which is the only way to write this kind of functions without requiring to link external assembly files. Also, as can be seen, the function clears the direction flag (CLD instruction). This is necessary because the ABI GCC uses requires it, so functions will assume the direction flag is clear.

The first goto

It is needed to be able to skip the ISR code without letting the optimizer remove it. Code which is known to be skipped may be removed in the optimization stage depending on the compiling options. Since the compiler doesn't know what the assembly statement does, it won't remove the ISR code.

The second goto

GCC doesn't allow to obtain the address of a C label. However, it is possible to obtain it by using "asm goto", an inline assembly statement that is able to accept one or more C labels. What to do with the address of those labels is up to the programmer. In our case, the "startofISR" label is being used to put that address in the "addr" variable so that it can be returned by the function. Note, that an "asm goto" statement is not allowed to produce outputs because it's considered a control transfer statement, which can't have any output due to the way GCC works internally. However, it is possible to produce an output via side effect. As can be seen in the code, the address of the "addr" variable is being given in the EBX register, then the address of the "startofISR" label is being put in [EBX], thus modifying the "addr" variable.

Macros

In order to easily manage the low-level ISRs which may be many depending on the implementation, C macros can be used to create them.

The following macro creates ISRs for handling IRQs:

// Macro to create hardware interrupt handling functions.
// It will call "irqfunc" with the register context and the IRQ number as parameters.
#define DEFIRQWRAPPER(irqnum)\
void *irq##irqnum##handler(void)\
{\
	volatile void *addr;\
	asm goto("jmp %l[endofISR]" ::: "memory" : endofISR);\
	asm volatile(".align 16" ::: "memory");\
	startofISR:\
	asm volatile("pushal\n\tpushl %%ebp\n\tmovl %%esp, %%ebp\n\tcld" ::: "memory");\
	asm volatile(\
		"pushl %%ds       \n\t"\
		"pushl %%es       \n\t"\
		"movw $16, %%cx   \n\t"\
		"movw %%cx, %%ds  \n\t"\
		"movw %%cx, %%es  \n\t"\
		"pushl %%ebp      \n\t"\
		"addl $4, (%%esp) \n\t"\
		"pushl %%ebx      \n\t"\
		"call *%%eax      \n\t"\
		"addl $8, %%esp       "\
		:: "a"(irqfunc), "b"((uint32_t) irqnum) : "memory");\
	asm volatile("popl %%es\n\tpopl %%ds\n\tleave\n\tpopal\n\tiret" ::: "memory");\
	endofISR:\
	asm goto(\
		".intel_syntax noprefix         \n\t"\
		"mov eax, offset %l[startofISR] \n\t"\
		"mov [ebx], eax                 \n\t"\
		".att_syntax                        "\
		:: "b"(&addr) : "eax", "memory" : startofISR);\
	return((void *) addr);\
}

The following macro creates ISRs for handling software interrupts and exceptions without error code:

// Macro to create sotfware interrupt handling functions.
// It will call "intfunc" with the register context and the interrupt number as parameters.
#define DEFINTWRAPPER(intnum)\
void *int##intnum##handler(void)\
{\
	volatile void *addr;\
	asm goto("jmp %l[endofISR]" ::: "memory" : endofISR);\
	asm volatile(".align 16" ::: "memory");\
	startofISR:\
	asm volatile("pushal\n\tpushl %%ebp\n\tmovl %%esp, %%ebp\n\tcld" ::: "memory");\
	asm volatile(\
		"pushl %%ds       \n\t"\
		"pushl %%es       \n\t"\
		"movw $16, %%cx   \n\t"\
		"movw %%cx, %%ds  \n\t"\
		"movw %%cx, %%es  \n\t"\
		"pushl %%ebp      \n\t"\
		"addl $4, (%%esp) \n\t"\
		"pushl %%ebx      \n\t"\
		"call *%%eax      \n\t"\
		"addl $8, %%esp       "\
		:: "a"(intfunc), "b"((uint32_t) intnum) : "memory");\
	asm volatile("popl %%es\n\tpopl %%ds\n\tleave\n\tpopal\n\tiret" ::: "memory");\
	endofISR:\
	asm goto(\
		".intel_syntax noprefix         \n\t"\
		"mov eax, offset %l[startofISR] \n\t"\
		"mov [ebx], eax                 \n\t"\
		".att_syntax                        "\
		:: "b"(&addr) : "eax", "memory" : startofISR);\
	return((void *) addr);\
}

The following macro creates ISRs for handling exceptions with error code:

// Macro to create exception handling functions, for exceptions with error code.
// It will call intfunc_err, with the error code, the register context, and the interrupt number as parameters.
#define DEFINTWRAPPER_ERR(intnum)\
void *int##intnum##handler(void)\
{\
	volatile void *addr;\
	asm goto("jmp %l[endofISR]" ::: "memory" : endofISR);\
	asm volatile(".align 16" ::: "memory");\
	startofISR:\
	asm volatile(\
		"pushal                \n\t"\
		"pushl %%ebp           \n\t"\
		"movl %%esp, %%ebp     \n\t"\
		"pushl %%ds            \n\t"\
		"pushl %%es            \n\t"\
		"movw $16, %%cx        \n\t"\
		"movw %%cx, %%ds       \n\t"\
		"movw %%cx, %%es       \n\t"\
		"movl 36(%%ebp), %%edx \n\t"\
		"movl %%ebp, %%esi     \n\t"\
		"addl $32, %%esi       \n\t"\
		"movl %%esi, %%edi     \n\t"\
		"addl $4, %%edi        \n\t"\
		"movl $11, %%ecx       \n\t"\
		"std                   \n\t"\
		"rep movsl             \n\t"\
		"add $4, %%esp         \n\t"\
		"cld                       "\
		::: "memory");\
	asm volatile(\
		"pushl %%edx       \n\t"\
		"pushl %%ebp       \n\t"\
		"addl $8, (%%esp)  \n\t"\
		"pushl %%ebx       \n\t"\
		"call *%%eax       \n\t"\
		"addl $12, %%esp       "\
		:: "a"(intfunc_err), "b"((uint32_t) intnum) : "memory");\
	asm volatile("popl %%es\n\tpopl %%ds\n\tleave\n\tpopal\n\tiret" ::: "memory");\
	endofISR:\
	asm goto(\
		".intel_syntax noprefix         \n\t"\
		"mov eax, offset %l[startofISR] \n\t"\
		"mov [ebx], eax                 \n\t"\
		".att_syntax                        "\
		:: "b"(&addr) : "eax", "memory" : startofISR);\
	return((void *) addr);\
}

Differences

Here are the differences between these three macros:

  • DEFINTWRAPPER is different from DEFIRQWRAPPER in that it calls "intfunc" instead of "irqfunc", so no IRQ will be acknowledged, since it is for software interripts, not for hardware ones.
  • DEFINTWRAPPER_ERR is different from DEFINTWRAPPER in that it calls "intfunc_err" instead of "intfunc" and also pushes the exception error code as an additional parameter. It is also different in that it needs to reorder the stack in order to have the correct layout. This is because an error code was pushed by the processor, and it is between the PUSHAD structure (general purpose registers) and the IRET structure (what will be popped from stack when returning from the interrupt). The ISR has to obtain the error code, and then move the PUSHAD structure next to the IRET structure. The complete structure (PUSHAD+IRET), will not only be used for returning to the interrupted code, but also for saving the interrupted task context for doing software multitasking.

Declaring the functions

Here is the suggested way to use the above macros:

//IRQs (the 16 IRQs the PIC has)
DEFIRQWRAPPER(0)
DEFIRQWRAPPER(1)
DEFIRQWRAPPER(2)
DEFIRQWRAPPER(3)
DEFIRQWRAPPER(4)
DEFIRQWRAPPER(5)
DEFIRQWRAPPER(6)
DEFIRQWRAPPER(7)
DEFIRQWRAPPER(8)
DEFIRQWRAPPER(9)
DEFIRQWRAPPER(10)
DEFIRQWRAPPER(11)
DEFIRQWRAPPER(12)
DEFIRQWRAPPER(13)
DEFIRQWRAPPER(14)
DEFIRQWRAPPER(15)
 
//exceptions without error code
DEFINTWRAPPER(0)//division by 0
DEFINTWRAPPER(1)//debug
DEFINTWRAPPER(2)//NMI
DEFINTWRAPPER(3)//breakpoint
DEFINTWRAPPER(4)//INTO
DEFINTWRAPPER(5)//BOUND
DEFINTWRAPPER(6)//invalid opcode
DEFINTWRAPPER(7)//coprocessor not available
DEFINTWRAPPER(9)//coprocessor segment overrun
DEFINTWRAPPER(16)//coprocessor error
 
//exceptions with error code
DEFINTWRAPPER_ERR(8)//double fault
DEFINTWRAPPER_ERR(10)//TSS error
DEFINTWRAPPER_ERR(11)//segment not present
DEFINTWRAPPER_ERR(12)//stack fault
DEFINTWRAPPER_ERR(13)//general protection fault
DEFINTWRAPPER_ERR(14)//page fault
 
//software interrupts
DEFINTWRAPPER(0x80)//system call

Bear in mind that calling the macro "DEFINTWRAPPER(0)" will create a function called "int0handler", and calling "DEFINTWRAPPER(0x80)" will create "int0x80handler" and NOT "int128handler".

Required functions, variables, and types

The code presented here calls some functions and needs some types. The called functions also depend on other functions and variables. Here, the dependencies will be shown.

Variables

The arrays of pointers to functions need to be allocated. These are just ponters to pointers to functions, the actual arrays are in the memory the programmer allocates for them.

  • "irqfuncs" requires 16*sizeof(void *) bytes, for the 16 pointers.
  • "intfuncs" requires 256*sizeof(void *) bytes, for the 256 pointers.
  • "intfuncs_err" requires 32*sizeof(void *) bytes, for the 32 pointers.
void *(**irqfuncs)(void *ctx);//array of pointers to functions for the IRQs
void *(**intfuncs)(void *ctx);//array of pointers to functions for the interrupts
void *(**intfuncs_err)(void *ctx, uint32_t errcode);//array of pointers to functions for the exceptions with error code

Required functions

This code has (of course, because it is unavoidably in x86), one low-level ISR per interrupt. Those are the functions declared in the macros. They call centralized functions which will dispatch to the appropriate high-level handler. The high-level handlers are pointed to by the corresponding array item. There are three mid-level centralized functions. The low-level ISRs always call the mid-level functions. The mid-level functions call a high-level hancdlers only when the pointer to it is not NULL. Here, a NULL pointer means there's no high-level handler for a specific interrupt, and the code will return without any problem. In case of the IRQ mid-level function, the corresponding hardware interrupt will be acknowledged. Of course, if it was a spourious interrupt, the high-level handler is NOT called, and the interrupt is acknowledged in a different way. Additionally, if the pointer to the register context returned by a high-level handler is not NULL, the mid-level functions will call the "taskswitch" function, which will pop the registers from this context (it is used a stack) and then do an IRET. If the pointer to the register context returned by a high-level handler is NULL (or if the handler was not called because it doesn't exist), the "taskswitch" function will not be called, and the code will just go on until the POPAD+IRET instructions, which will return to the interrupted task.

//this function is called in all the IRQs
//it will call the corresponding function in the irqfuncs array, as long as it's not NULL (and the interrupt is not spourious)
//if the called function returns a non-NULL pointer, that pointer will be used as a stack to switch the task
//this function correctly acknowledges normal and spourious hardware interrupts
void irqfunc(uint32_t irqnum, void *ctx)
{
	void *stack = NULL;
	if(PIC_isnormalIRQ(irqnum))
	{
		if(irqfuncs[irqnum] != NULL)
			stack = irqfuncs[irqnum](ctx);
		PIC_EOI(irqnum);
		if(stack)
			taskswitch(stack);
	}
	else
	{
		PIC_EOI_spurious(irqnum);
	}
}
 
//this function is called in all the software interrupts, and in exceptions without error code
//it will call the corresponding function in the intfuncs array, as long as it's not NULL
//if the called function returns a non-NULL pointer, that pointer will be used as a stack to switch the task
void intfunc(uint32_t intnum, void *ctx)
{
	void *stack = NULL;
	if(intfuncs[intnum] != NULL)
		stack = intfuncs[intnum](ctx);
	if(stack)
		taskswitch(stack);
}
 
//this function is called in exceptions with error code
//it will call the corresponding function in the intfuncs_err array, as long as it's not NULL
//if the called function returns a non-NULL pointer, that pointer will be used as a stack to switch the task
void intfunc_err(uint32_t intnum, void *ctx, uint32_t errcode)
{
	void *stack = NULL;
	if(intfuncs_err[intnum] != NULL)
		stack = intfuncs_err[intnum](ctx, errcode);
	if(stack)
		taskswitch(stack);
}

Additional required functions

There are still some functions to be implemented. These are:

  • void taskswitch(void *ctx): Gets a register context and uses it to switch to another task. The format of the context is the same as what the processor expects to correctly go to another task by executing a POPAD instruction and then an IRET. This function should also set DS and ES to the user-mode data segment selector, and FS and GS to 0.
  • int PIC_isnormalIRQ(uint8_t irqnum): Returns whether an IRQ was normal (returns 0) or spourious (returns 1).
  • void PIC_EOI(uint8_t irqnum): Acknowledges a normal interrupt.
  • void PIC_EOI_spourious(uint8_t irqnum): Acknowledges a spourious interrupt.

Installing the handlers

In order to get the handlers called, they have to be installed. It's assumed that the basic system tables (mainly the IDT) are already initialized.

Low level

The following code uses a function called "fillidte", which fills an entry in the IDT. This function is not shown here, but what it does is self-explaining.

Its parameters are the following:

  • Pointer to the IDT entry to modify.
  • ISR code selector. Should be at privilege level 0.
  • ISR code offset. As can be seen, it is the return value of one of the functions which were created using the macros.
  • Gate type. It is higly recommended to use only 0xe, which is for an interrupt gate.
  • Privilege level. This sets which privilege levels are allowed to call this interrupt gate. They are all 0, except for the system calls, where it's 3 so that user-mode programs can use the interrupt without causing an exception.

This code assumes that the PIC is mapped so that IRQs 0-15 will cause interrupts 32-47.

fillidte(idt+32, 8, irq0handler(), 0xe, 0);//IRQ handlers
fillidte(idt+33, 8, irq1handler(), 0xe, 0);
fillidte(idt+34, 8, irq2handler(), 0xe, 0);
fillidte(idt+35, 8, irq3handler(), 0xe, 0);
fillidte(idt+36, 8, irq4handler(), 0xe, 0);
fillidte(idt+37, 8, irq5handler(), 0xe, 0);
fillidte(idt+38, 8, irq6handler(), 0xe, 0);
fillidte(idt+39, 8, irq7handler(), 0xe, 0);
fillidte(idt+40, 8, irq8handler(), 0xe, 0);
fillidte(idt+41, 8, irq9handler(), 0xe, 0);
fillidte(idt+42, 8, irq10handler(), 0xe, 0);
fillidte(idt+43, 8, irq11handler(), 0xe, 0);
fillidte(idt+44, 8, irq12handler(), 0xe, 0);
fillidte(idt+45, 8, irq13handler(), 0xe, 0);
fillidte(idt+46, 8, irq14handler(), 0xe, 0);
fillidte(idt+47, 8, irq15handler(), 0xe, 0);
 
fillidte(idt+0, 8, int0handler(), 0xe, 0);//exception handlers
fillidte(idt+1, 8, int1handler(), 0xe, 0);
fillidte(idt+2, 8, int2handler(), 0xe, 0);
fillidte(idt+3, 8, int3handler(), 0xe, 0);
fillidte(idt+4, 8, int4handler(), 0xe, 0);
fillidte(idt+5, 8, int5handler(), 0xe, 0);
fillidte(idt+6, 8, int6handler(), 0xe, 0);
fillidte(idt+7, 8, int7handler(), 0xe, 0);
fillidte(idt+8, 8, int8handler(), 0xe, 0);
fillidte(idt+9, 8, int9handler(), 0xe, 0);
fillidte(idt+10, 8, int10handler(), 0xe, 0);
fillidte(idt+11, 8, int11handler(), 0xe, 0);
fillidte(idt+12, 8, int12handler(), 0xe, 0);
fillidte(idt+13, 8, int13handler(), 0xe, 0);
fillidte(idt+14, 8, int14handler(), 0xe, 0);
fillidte(idt+16, 8, int16handler(), 0xe, 0);
 
fillidte(idt+0x80, 8, int0x80handler(), 0xe, 3);//system call handler

High level

The high-level handlers are referenced in the previously mentioned arrays. These arrays sould be zeroed after allocating space for them, so that they are initialized with NULL pointers. The following code shows an example of how to install the handlers so they will be called when an interrupt occurs:

irqfuncs[0] = timer0handler;//IRQ 0
irqfuncs[1] = kbhandler;//IRQ 1
 
intfuncs[0] = div0handler;//interrupt 0
intfuncs[1] = debughandler;//interrupt 1
intfuncs[2] = NMIhandler;//interrupt 2
intfuncs[3] = INT3handler;//interrupt 3
intfuncs[4] = INTOhandler;//interrupt 4
intfuncs[5] = BOUNDhandler;//interrupt 5
intfuncs[6] = invopcodehandler;//interrupt 6
intfuncs[7] = noFPUhandler;//interrupt 7
intfuncs[9] = FPUseghandler;//interrupt 9
intfuncs[16] = FPUerrhandler;//interrupt 16
 
intfuncs_err[8] = dblflthandler;//interrupt 8
intfuncs_err[10] = invTSShandler;//interrupt 10
intfuncs_err[11] = segnphandler;//interrupt 11
intfuncs_err[12] = stackflthandler;//interrupt 12
intfuncs_err[13] = GPFhandler;//interrupt 13
intfuncs_err[14] = pgflthandler;//interrupt 14
 
intfuncs[0x80] = sysinthandler;//system call (0x80)

This is how the high-level handler functions have to be declared so that they can be correctly called:

//IRQ handler
void *timer0handler(void *ctx)
{
	//code goes here
	return(NULL);
}
 
//exception with error code handler
void *GPFhandler(void *ctx, uint32_t errcode)
{
	//code goes here
	return(NULL);
}

For software interrupts and exceptions without error code, the high-level handlers are declared the same way as IRQ handlers.

When the interrupted task is to be resumed, the handlers would return NULL. When the system has to jump to another task, the handlers would save the current context (ctx parameter) in the task control block for the current task, and then return a pointer to the context of the next task. This way, software multitasking can be eslily implemented.

Stack layout

Here the stack layout is presented so that it's easier to understand what needs to be in a task switch stack in order to correctly perform a task switch.

This is just an example of a task control block structure. It starts with the same information which was pushed when an interrupt occured (or has to be popped when switching the task in the "taskswitch" function).

These are the CPU registers in the stack, from lower to upper address:

  • PUSHAD structure (EDI, ESI, EBP, unused ESP, EBX, EDX, ECX, EAX). To be popped by POPAD.
  • Registers always pushed in an interrupt (EIP, CS, EFLAGS). IRET always pops them.
  • Registers pushed only when interrupting from less-privileged to more-privileged level (ESP, SS). IRET only pops them when going from a more-privileged to a less-privileged level.
  • Registers pushed only when interrupting from v86 mode to kernel mode (ES, DS, FS, GS). IRET only pops them when going from kernel mode to v86 mode, thats to say, the EFLAGS field in the stack contains the VM flag set to 1.

Note: v86 tasks are considered to be running always at privilege level 3.

The other fields in this structure are just a suggestion and can be modified to meet the needs of the OS multitasking engine.

//task control block (structure to be used in a 2-way linked list)
typedef struct __attribute__((packed)) task_t
{
	uint32_t edi;//CPU registers (same format as in the interrupted task stack)
	uint32_t esi;
	uint32_t ebp;
	uint32_t esp_discarded;
	uint32_t ebx;
	uint32_t edx;
	uint32_t ecx;
	uint32_t eax;
	uint32_t eip;
	uint32_t cs;
	uint32_t eflags;
	uint32_t esp;
	uint32_t ss;
	uint32_t es;
	uint32_t ds;
	uint32_t fs;
	uint32_t gs;
	uint32_t taskflags;//b0=paused, b1=exception waiting to be handled by the monitor, b2=error, b3=successfully finished
	uint32_t ticks;//number of ticks the task has been executing since it started its turn
	uint32_t maxticks;//maximum number of ticks allowed per turn, when ticks==maxticks, set ticks to 0 and switch to next task
	uint32_t excep_num;//(for exceptions) exception number which interrupted the task
	uint32_t excep_code;//(for exceptions) exception error code
	struct task_t *prev;//pointer to previous task control block
	struct task_t *next;//pointer to next task control block
} task_t;
Personal tools
Namespaces
Variants
Actions
Navigation
About
Toolbox