HW5: Interrupts

This homework asks you to extend your "hello world" kernel with support for handling timer interupts. This assignments builds on top of your previous HW3 and HW4 submissions, i.e., you will extend the code of HW3 to implement this additional functionality for HW4. If you don't have a working HW4 submission talk to us.

Technically, you can do this assignment on any operating system that supports the Unix API and can run Qemu (CADE machines, your laptop that runs Linux or Linux VM, and even MacOS, etc.). You don't need to set up xv6 for this assignment, but if you're running on CADE you'll have to install QEMU, see QEMU setup instructions. Submit your programs and the shell through Gradescope (see instructions at the bottom of this page).

NOTE: YOU CANNOT PUBLICLY RELEASE SOLUTIONS TO THIS HOMEWORK. It's ok to show your work to your future employer as a private Git repo, however any public release is prohibited.

Overview

At a high level our goal is to learn how to construct an interrupt descriptor table and the low-level code for managing the interrupt entry and exit.

Configuring interrupt controllers

Download ioapic.c, lapic.c, and picirq.c files. These three files contain the code for initializing three interrupt controllers. The legacy programmable interrupt controller (PIC), the new local advanced programmable interrupt controller (LAPIC) and the Intel I/O Advanced Programmable Interrupt Controller (ioapic). We skip details of the initialization, and instead simply do the following two steps. First, you need to map two memory regions that allow communication with the LAPIC and IOAPIC controllers. Both controllers use memory-mapped I/O to communicate with hardware, i.e., a load or store in memory is translated into an I/O bus transaction that reaches the hardware. Specifically we need to map two virtual pages one to one to the same physical addresses:
DEFAULT_IOAPIC: 0xfec00000 -> 0xfec00000
DEFAULT_LAPIC: 0xfee00000 -> 0xfee00000
After mapping this memory region you can download the ./src/trap.c and ./src/traps.h files and invoke the initpics() function from your main. This will initialize all three controllers and enable delivery of the timer interrupt.

Configuring IDT

In this assignment your goal is to implement the tvinit() function in trap.c. Let us first understand IDT. IDT stands for Interrupt Description Table. The layout of the table is similar to GDT. However, the role of IDT is to configure all interrupts. Each entry in IDT describes a specific interrupt. The timer is delivered via interrupt vector 32.

Similar to xv6 we will use an array to represent IDT. You can read about IDT format here: OSDev: interrupt descriptor table.
struct gatedesc idt[256];
The table is an array that is pointed by the LDTR register. Each entry is 64bits, and describes a specific interrupt in following format.
struct gatedesc {
	uint off_15_0 : 16;   // low 16 bits of offset in segment
	uint cs : 16;         // code segment selector
	uint args : 5;        // # args, 0 for interrupt/trap gates
	uint rsv1 : 3;        // reserved(should be zero I guess)
	uint type : 4;        // type(STS_{IG32,TG32})
	uint s : 1;           // must be 0 (system)
	uint dpl : 2;         // descriptor(meaning new) privilege level
	uint p : 1;           // Present
	uint off_31_16 : 16;  // high bits of offset in segment
};
For example, in order to enable a keyboard interrupt (#33), we will do set the entry 33 of IDT. To simplify things, we provide a macro SETGATE in traps.h. You can use like so to point keyboard interrupt to jmp to ADDR.
SETGATE(idt[T_IRQ0 + IRQ_KBD], 0, CS, ADDR, DPL);

Your job is to initlize IDT with timer interrupt 32. We will implement vector32 in assembly similar to xv6 to make sure we save all the user state correctly.

Once IDT is properly initilized, you can go ahead and use a helper function lidt() to load the IDT in the hardware register IDTR.

To enable delivery of interrupts you can use the sti() helper which sets the interrupt flag (IF) in the EFLAGS register. You can enable the interrupts right at the end of the main() function.

Note, since interrupts will wake up the system from the halted state you need to make sure that you re-enter the halt again. Otherwise you will exit from main() and crash. Make sure that you change the end of the main() function to enter the halt in an infinite loop.

for(;;)
  halt()

Getting to Trap

Now lets take a look at how we can implement the low-level interrupt entry and exit code. Download ./src/vectors.asm. This file will provide a skeleton for alltrap, trapret, and vector32. We will implement them similar to xv6 but in Intel assembly.

The high-level goal is to save all low level state (registers) and then call the trap() function in trap.c.

In order to call trap(), we need to save user context onto trap frame, so we can properly return to user code. The trapframe is defined in traps.h -- we use a data structure that carefully defines all the fields in such a manner that they match the layout of the stack where we save all register state.

struct trapframe {
	// registers as pushed by pusha
	uint edi;
	uint esi;
	uint ebp;
	uint oesp;      
	uint ebx;
	uint edx;
	uint ecx;
	uint eax;

	// rest of trap frame
	ushort gs;
	ushort padding1;
	ushort fs;
	ushort padding2;
	ushort es;
	ushort padding3;
	ushort ds;
	ushort padding4;
	uint trapno;

	// below here defined by x86 hardware
	uint err;
	uint eip;
	ushort cs;
	ushort padding5;
	uint eflags;

	// below here only when crossing rings, such as from user to kernel
	uint esp;
	ushort ss;
	ushort padding6;
};

We borrow this data structure from xv6.

To save all register state, we need to implement an entry point for the interrupt vector 32, i.e., an assembly label vector32 in vectors.asm. This is the address we use when we configure the interrupt descriptor table.

In vector32, similar to xv6 we will save an error code and the trap number on the stack. This will later be used by trap() to tell which specific interrupt we are handling (Yes, giant switch statement :( !).

Similar to xv6 we will jump to the generic assembly function alltrap to save all other registers. The reason why we implement alltraps seperately because this is the common (reuseable code) that can be called by all the interrupts. In xv6, they use a perl script to generate all the vectors rather than write them out manually, but we will write one vector32 by hand.

global vector32
vector32:
	push 0
	push 32
	jmp alltraps

The alltrap label can look like this (don't forget to define SEG_KDATA to match the right entry in the GDT):

alltraps:
	; Build trap frame.
	push ds
	push es
	push fs
	push gs
	pusha

	; Set up data segments.
	mov ax, SEG_KDATA
	mov ds, ax
	mov es, ax

	; Call trap(tf), where tf=%esp
	push esp
	call trap
	add esp, 4

In order to return from the interrupt, we must implement the trapret function, which first deallocates space on the stack and then calls iret. Look at the trapframe data structure to understand how we restore the registers that we saved upon the interrupt entry on the stack.

; Return falls through to trapret...
trapret:
	popa
	pop gs
	pop fs
	pop es
	pop ds
	add esp, 8  ; trapno and errcode
	iret
Now everything is set up. Your job is to implement the functions above and the trap() function in trap.c in such a manner that whenever the timer interrupt is called, it will call printk(".") to print a dot on the serial line. Make sure that you call lapiceoi() to acknowledge the interrupt right after you print the dot on the serial line.

Back to main()

Be sure add a infinite loop around your halt so it will keep halting after retuning from interrupt. To keep you on track, your main should look like this (psuedo code below, copy at your own risk!)

int main(void)
{

	// Set up Page Table
	pagetable_init(); 

	// Initialize the console
	uartinit(); 

	// Initialize PICs
	initpics();

	// Initialize IDT, load IDT, enable interrupt
	tvinit();

	// Halt
	for(;;)
		halt(); 
}

Helping reading and additional resources

The following links provide a detailed description of the interrupt handling code in xv6 (remember we re-use bits of xv6 in this homework assignment, so pretty much everything can work in a manner similar to xv6).

Annotated code of xv6 trap() function

Submit your work

To make sure you didn't just printk(".") in main, you only need to submit partial files, and we will replace our implementation with those files. So make sure you follow the guide, and not add additional global variables or functions to those files as it might conflict with our solution.

Submit your solution through Gradescope CS5460/6460 Operating Systems. Please zip all of your files and submit them. The structure of the zip file should be the following:
/
- trap.c
- vectors.asm
Updated: April, 2024