Chapter 26 Exercises: Interrupts, Exceptions, and Kernel Mode

Section A: IDT Mechanics

1. IDT Entry Layout Given a handler at address 0xFFFFFFFF80101234, code segment selector 0x08, and type 0x8E (64-bit interrupt gate), manually construct the 16-byte IDT entry. Write the bytes in order (byte 0 through byte 15). Verify your answer by implementing the construction in NASM and examining the output with a hex dump.

Expected format:

Byte 0-1:  offset[15:0]  = ?
Byte 2-3:  selector      = 0x0008
Byte 4:    IST           = 0x00
Byte 5:    type/attr     = 0x8E
Byte 6-7:  offset[31:16] = ?
Byte 8-11: offset[63:32] = ?
Byte 12-15: reserved     = 0

2. Divide-by-Zero Handler in QEMU In the MinOS kernel (or a bare-metal test program in QEMU), implement a #DE divide-by-zero handler (vector 0) that: - Prints the message "EXCEPTION: Divide by Zero at RIP=0xXXXXXXXXXXXXXXXX" to VGA text mode - Halts the CPU with hlt (do not try to continue — a real kernel would kill the process)

Test it by executing a div instruction with a zero divisor. Verify in QEMU that your handler fires instead of a triple fault (which would silently reset the machine).

3. Uniform Exception Stubs Implement the uniform exception stub macro pattern from the chapter:

%macro EXCEPTION_NOERR 1
    ; push fake error code 0, push vector number, jump to common handler
%endmacro

%macro EXCEPTION_ERR 1
    ; push vector number (CPU already pushed error code), jump to common handler
%endmacro

Create stubs for vectors 0, 1, 2, 3, 6, 8, 13, and 14. Implement common_exception_handler that prints the vector number and halts.

4. Trap Gate vs. Interrupt Gate Modify your IDT to install vector 3 (#BP breakpoint) as a trap gate (type 0x8F) instead of an interrupt gate. Then install vector 1 (#DB debug) as an interrupt gate (type 0x8E). Write a test that executes INT3 in a loop with a counter variable. Observe: - With trap gate (0x8F): does the INT3 loop work? Can another interrupt fire during the handler? - With interrupt gate (0x8E): what changes about interrupt behavior during the handler?


Section B: Hardware Interrupts

5. PIC Remapping Implement the complete PIC remapping routine from the chapter. After remapping: - IRQ0 should map to vector 32 - IRQ8 should map to vector 40 Verify by installing a dummy handler at vector 32 that increments a counter and sends EOI. Enable the timer IRQ (unmask bit 0 in PIC1 mask), then enable interrupts with STI. Confirm the counter increments.

6. Keyboard Interrupt Handler Implement a complete IRQ1 keyboard handler that: - Reads the scancode from port 0x60 - Distinguishes make codes (key press, bit 7 = 0) from break codes (key release, bit 7 = 1) - Translates make codes to ASCII using the scancode table - Stores printable ASCII characters in a ring buffer (circular buffer, 256 bytes) - Tracks the Shift key state (scancode 0x2A = left Shift, 0x36 = right Shift) - Sends EOI to PIC

Add a main loop in the kernel that reads from the ring buffer and prints characters to VGA, creating a functional keyboard echo console.

7. Timer Handler (IRQ0) Implement a timer interrupt handler for IRQ0 that: - Increments a global 64-bit tick counter - Sends EOI to PIC1 - Every 100 ticks, updates a "seconds elapsed" counter visible in VGA

Use the PIT to generate interrupts at 100Hz (see Chapter 29 for full PIT setup; for this exercise, use the default 18.2Hz tick if you haven't set up PIT yet).

8. IRQ Masking Write two functions:

; irq_enable: enable (unmask) a specific IRQ
; RDI = IRQ number (0-15)
irq_enable:

; irq_disable: disable (mask) a specific IRQ
; RDI = IRQ number (0-15)
irq_disable:

Handle the cascade correctly: to mask/unmask IRQ0-7, write to PIC1 mask register (0x21); for IRQ8-15, write to PIC2 mask register (0xA1). Remember: masking IRQ2 (the cascade) would disable all of IRQ8-15.


Section C: Exception Analysis

9. Decoding the #GP Error Code The General Protection Fault (#GP) error code encodes what caused the fault:

Bit 0 (EXT): 1 = fault was external to processor
Bit 1 (IDT): 1 = table selector refers to IDT (not GDT/LDT)
Bits 15:3 = segment selector index (if table fault)

Write a gp_fault_handler that: - Decodes the error code - Prints a formatted message: "GP Fault at RIP=0x... error_code=0x... (selector=N)" - Halts

Test by executing a far jump to an invalid segment selector (tricky but educational in QEMU).

10. Page Fault Decoder Write a page fault handler that, when vector 14 fires: 1. Reads CR2 for the faulting address 2. Pops the error code from the stack 3. Decodes the error code bits: P, W, U, R, I 4. Prints: "Page Fault: addr=0x... [not-present|protection] [read|write] [kernel|user]" 5. Halts

Test by: (a) accessing an unmapped virtual address, (b) writing to a read-only page (set a page to read-only in your page tables first).

11. Double Fault Handler Implement a double fault handler (#DF, vector 8) using an Interrupt Stack Table (IST) entry. A double fault fires when an exception occurs while handling another exception — for example, a page fault during the stack-switch if RSP0 in the TSS is invalid.

Why IST? If the kernel stack itself is corrupt, you cannot handle the fault on the same stack. The IST mechanism lets you specify an alternate, known-good stack for specific vectors.

Configure IST1 in the TSS to point to a separate 4KB emergency stack. Set the IST field of the double fault IDT entry to 1. Implement a double fault handler that prints "DOUBLE FAULT" and halts.


Section D: Debugger Implementation

12. Software Breakpoints Using INT3 Implement a simple breakpoint mechanism: 1. Install a #BP handler (vector 3) 2. The handler should: - Print the instruction address (RIP, which points to the byte AFTER the INT3) - Print the contents of all registers (RAX through R15) - Resume execution (single instruction handler)

Write a test program that inserts INT3 at several points and verify that each breakpoint is hit and the register state is printed.

13. Hardware Single-Step The x86-64 #DB exception (vector 1) fires after every instruction when the TF (Trap Flag, bit 8) in RFLAGS is set. Implement single-step mode: 1. Install a #DB handler 2. The handler prints the current RIP (the address of the NEXT instruction to execute) 3. The handler leaves TF set so single-stepping continues

Write a function enable_single_step() that sets the TF flag. Test by single-stepping through a short loop and confirming every instruction address is printed.

14. Counting Exception Frequency Add counters for each exception vector in your IDT setup. Every time an exception handler fires, increment the counter for that vector. Implement a kernel function dump_exception_stats() that prints, for each vector that has been triggered at least once:

Vector  0 (#DE): 3 times
Vector 14 (#PF): 127 times

This is a simplified version of the exception counting done in production OS kernels for debugging.


Section E: MinOS Integration

15. Complete MinOS Interrupt Infrastructure Combine the work from this chapter into a complete MinOS interrupt subsystem: 1. idt_init() — set up all 256 IDT entries (exceptions, hardware IRQs, and a placeholder for syscall via vector 0x80) 2. pic_remap() — remap PIC to vectors 32-47 3. irq_enable(1) — enable keyboard 4. irq_enable(0) — enable timer 5. sti — enable interrupts

The resulting MinOS should: - Boot (Chapter 28 bootloader) - Set up GDT and IDT - Handle keyboard input (echo to VGA) - Maintain a timer tick counter - Handle and report exceptions without triple-faulting

This is the functional core of an OS kernel. Everything else is built on top of this interrupt infrastructure.