![]() |
|||
HW5: InterruptsThis 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. OverviewAt 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 controllersDownload 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 -> 0xfee00000After 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 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 To enable delivery of interrupts you can use the
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 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
The high-level goal is to save all low level state (registers) and then call the
In order to call 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
In vector32, similar to xv6 we will save an error code and the trap number on the stack. This will later be used
by
Similar to xv6 we will jump to the generic assembly function 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 iretNow 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
|
|||
![]() |
|||
Updated: April, 2024
|