{"article":{"slug":"building-a-64-bit-os-chapter-3-interrupt-descriptor-tables-the-magic-behind-a-working-keyboard","title":"Building a 64-Bit OS — Chapter 3: Interrupt Descriptor Tables (The magic behind a working keyboard)","subtitle":null,"summary":"Building a 64-Bit OS — Chapter 3: Interrupt Descriptor Tables (The magic behind a working keyboard) Interrupts Interrupts are easily one of my favorite topics.","content_type":"tutorial","language":"en","canonical_url":"https://medium.com/@devsharma.3624/building-a-64-bit-os-chapter-3-interrupt-descriptor-tables-the-magic-behind-a-working-keyboard-9a8fc2d0d1c8","author":{"name":"Dev Sharma","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Dev Sharma","url":null,"listing_slug":null,"listing":null},"topics":[{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"},{"name":"Hardware","slug":"hardware","url":"https://listedarticles.com/topics/hardware"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2612,"reading_minutes":11,"published_at":"2026-09-24T12:00:00.000Z","added_at":"2026-09-25T21:18:09.867Z","updated_at":"2026-09-25T21:18:09.867Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":false},"profile_url":"https://listedarticles.com/articles/building-a-64-bit-os-chapter-3-interrupt-descriptor-tables-the-magic-behind-a-working-keyboard","markdown_url":"https://listedarticles.com/articles/building-a-64-bit-os-chapter-3-interrupt-descriptor-tables-the-magic-behind-a-working-keyboard.md","example":false,"citation":"Dev Sharma, Dev Sharma. \"Building a 64-Bit OS — Chapter 3: Interrupt Descriptor Tables (The magic behind a working keyboard).\" 24 Sept 2026. https://medium.com/@devsharma.3624/building-a-64-bit-os-chapter-3-interrupt-descriptor-tables-the-magic-behind-a-working-keyboard-9a8fc2d0d1c8 (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://medium.com/@devsharma.3624/building-a-64-bit-os-chapter-3-interrupt-descriptor-tables-the-magic-behind-a-working-keyboard-9a8fc2d0d1c8"},"body_markdown":"# Building a 64-Bit OS — Chapter 3: Interrupt Descriptor Tables (The magic behind a working keyboard)\n\n## Interrupts\n\n**Interrupts** are easily one of my favorite topics. They came pretty early in my low-level development journey. The concept was so fascinating that it kept me driven to explore deeper systems programming concepts.\n\nSince I started my coding journey with **Android app development**, I was already familiar with event listeners like `onClick()`. However, I never stopped to think about how they worked under the hood. **Interrupts** took me down to the lower layers where that underlying mechanism becomes clear. Take one more step down, and you land directly at the hardware and electronics layer.\n\n## What Are Interrupts?\n\nThe word literally means “to stop something while it is happening.” The technical concept is identical. While the CPU is executing instructions, a specific condition occurs. The CPU immediately **stops its current work** to handle that situation.\n\nFor example, consider a single-core CPU executing one process at a time. Suppose the process currently running is doing background OS utility work. At that exact moment, you press a key on your keyboard. Do you want the OS to display that letter on your text editor immediately? Or do you want to wait for the background task to finish first? We all know the answer.\n\nThat is an **interrupt** in action:\n\n1. The background process runs.\n2. A keyboard press triggers an **interrupt** , pausing the background process.\n3. The CPU processes the keypress and displays the letter.\n4. The CPU **resumes the background process** at the exact point where it was stopped.\n\n## Types of Interrupts (Hardware vs. Software)\n\nInterrupts fall into two main categories: **Hardware Interrupts** and **Software Interrupts**.\n\n- **Hardware Interrupts:** Your machine contains various physical devices, including the keyboard, mouse, storage drive, and Network Interface Card (NIC). When one of these hardware components requests CPU attention, it raises a hardware interrupt.\n- **Software Interrupts:** When an interrupt is triggered by software (such as your OS or a user program), it is a software interrupt.\n\nSoftware interrupts further break down into two subcategories:\n\n- **Normal Software Interrupts:** Issued when a user program makes a**system call** to request OS services.\n- **Exceptions:** Triggered automatically by runtime errors or exceptional CPU conditions, such as a**divide-by-zero** error.\n\n## Interrupt Mechanism: Legacy Systems vs. Modern Systems\n\nModern systems rely on an **APIC (Advanced Programmable Interrupt Controller)** and **MSI (Message Signaled Interrupts)**.\n\nIn my own OS implementation, I built support for the older **PIC (Programmable Interrupt Controller)**. The PIC relies on physical wire connections linked to every hardware slot (such as PCI or PCIe slots) on the motherboard.\n\nThe structural difference between a PIC and an APIC is straightforward:\n\n- **APIC** is a modern, scalable version of the PIC. It handles significantly more interrupts and supports**multi-core processors** .\n\nHow does **MSI** differ from physical lines connected to PCI slots? In modern systems, if hardware needs CPU attention, how does it notify the CPU without a dedicated wire?\n\n- **Legacy Systems (Physical Lines):** Dedicated physical lines connected each hardware slot directly to the interrupt controller. Because of these dedicated traces, motherboard slots required additional pins beyond the standard system bus.\n- **Modern Systems (MSI):** There are no dedicated interrupt lines running from hardware slots to the PIC. Instead, devices use the standard system bus. When a hardware device needs to trigger an interrupt, it sends a data packet (a**message** ) to a specific memory address. The interrupt controller reads this message and triggers the interrupt for that device.\n\nThe fundamental architecture shifted:\n\n- In legacy systems, a hardware device **triggered** the interrupt over a dedicated physical wire.\n- In modern systems, a hardware device **notifies** the interrupt controller by writing a message to memory.\n\n## How to Enable Hardware Interrupts\n\nSince my project uses the **PIC (Programmable Interrupt Controller)**, I will focus on its operation here. The PIC acts as a gatekeeper for hardware interrupts reaching the CPU. Physical connections from hardware devices feed into the PIC, which then routes them to the processor.\n\nThe number of physical lines on a PIC dictates how many devices it can support. Early PICs had only 8 interrupt lines (**IRQ0 to IRQ7**). Engineers quickly ran out of lines, leading to a clever workaround: cascading two PIC chips in a **Master-Slave architecture**.\n\n- The **Master PIC** handles IRQ0 through IRQ7.\n- The **IRQ2 line** on the Master PIC connects directly to the**Slave PIC** .\n- The **Slave PIC** handles IRQ8 through IRQ15.\n\nConnecting IRQ2 to the Slave PIC yields **15 usable hardware interrupt lines** (16 total lines minus the 1 line used for cascading).\n\nWhen an interrupt occurs on lines IRQ8 through IRQ15:\n\n1. The **Slave PIC** catches the interrupt and cascades it to the**Master PIC** .\n2. The **Master PIC** pulls its**IRQ2 pin high** to inform the CPU.\n3. The CPU needs to identify the specific **interrupt vector** so it can execute the correct handler.\n4. The CPU transfers data bus control to the **Master PIC** .\n5. Control is handed to the **Slave PIC** , which writes the exact interrupt number onto the data bus for the CPU to read.\n\n## Interrupt Descriptor Tables (IDT)\n\nAn **Interrupt Descriptor Table (IDT)** is an array of descriptor structures defining interrupt gates. Each entry follows a rigid, hardware-defined format. Entries contain the target handler’s memory address, required privilege levels, and execution characteristics.\n\nIn the **x86 architecture**, the IDT contains up to **256 entries**. This allows you to configure up to 256 distinct interrupt handlers.\n\n## Structure of Each Table Entry\n\nEach entry in the table contains the following fields:\n\n```\ntypedef struct{\n    unsigned short int offset_0;\n    unsigned short int selector;\n    unsigned char ist;\n    unsigned char type_attribute;\n    unsigned short int offest_1;\n    unsigned int offset_2;\n    unsigned int ignore;\n} __attribute__ ((packed)) idt_desc_entry_t;\n```\n- **offsets :** These split fields combine to form the full memory address of the interrupt handler function.**offset_0** holds the lower 16 bits,**offset_1** holds bits 16–31, and**offset_2** holds the upper 32 bits.\n- **selector:** A segment selector referencing the**Global Descriptor Table (GDT)** . It identifies the memory segment containing the handler function. While modern 64-bit OS design rarely uses segmentation, x86 architecture still requires setting this field.\n- **ist (Interrupt Stack Table):** An offset into the Interrupt Stack Table. It provides a dedicated memory stack used during interrupt handling. When the field is set to 0, the CPU switches stack if it is coming from Ring 3 to Ring 0 and stays in the same stack if it already in Ring 0. If it is set to something else, it has to switch stack in any case. If you’re not familiar with Kernel-User isolation (Ring 0 — Ring 3). You can forget the last few sentences until that blog comes out. For now, just know that I’ve set it to 0.\n- **ignore:** 2 bytes of reserved padding at the end of each entry. This field is unused.\n- **type_attribute:** A bitmask containing flags that dictate the descriptor’s characteristics.\n\n```\n                                                                Bit position\n                                                                inside the\n  7     6       5       4       3       2       1       0   <-- Flags byte\n+---+-------+-------+-----------------------------------+\n| P |  DPL  |   0   | Gate Type                         |\n+---+-------+-------+-----------------------------------+\n| 1 | 2-bit | 1-bit |            4-bit                  |   <-- Size\n```\n- **Gate Type:** Defines the behavior upon triggering. For example, an**Interrupt Gate (****0b1110**\n**)** automatically disables subsequent interrupts until the current handler finishes. A**Trap Gate (****0b1111**\n**)** does not disable subsequent interrupts while dealing with the current one.\n- **DPL (Descriptor Privilege Level):** Specifies the CPU privilege level required to invoke the vector. Setting this to Ring 3 (`0b11` ) allows user-space programs to trigger the interrupt via the`INT` instruction (e.g., for system calls).\n- **P (Present Bit):** Indicates whether the descriptor is active. Must be set to`1` for the vector to be valid.\n\n## The LIDT Register\n\nThe IDT resides in main memory. The CPU tracks its location using a dedicated register called **IDTR (Interrupt Descriptor Table Register)**. x86 provides a special assembly instruction — `LIDT` (**Load Interrupt Descriptor Table**)—to load this table pointer into the register.\n\nHowever, the address loaded into the `IDTR` does not point directly to the table array. Instead, it holds a pointer to a **pseudo-descriptor structure** describing the table:\n\n```\ntypedef struct {\n    unsigned short int limit;\n    unsigned long long offset;\n} __attribute__ ((packed)) idtr_t;\n```\n- **offset:** The memory address where the IDT array starts. Since it is declared as an array, it is always contiguous in memory.\n- **limit:** The maximum byte length of the table minus 1 (`sizeof(IDT) - 1` ).\n\n## My Implementation\n\nWhen an interrupt fires, the CPU hardware automatically pushes state data onto the stack before beginning interrupt handling:\n\n- **Stack Segment (SS)**\n- **Stack Pointer (RSP)**\n- **RFLAGS** (represents the CPU state at any given point)\n- **Code Segment (CS)**\n- **Instruction Pointer (RIP)**\n\nAfter this hardware push, whatever happens next is handled entirely by your custom code.\n\nI divided my interrupt handling process into three distinct steps: **Context Saving**, **Interrupt Handling**, and **Context Restoring**. This architecture uses a mix of **Assembly** and **C** because C cannot perform low-level register manipulation directly.\n\n## Saving Context\n\nSuppose a process is executing on the CPU. While running, the process uses the memory stack and stores temporary data inside CPU general-purpose registers.\n\nNow an interrupt occurs. The CPU pauses current execution and jumps to handle the interrupt. The handler requires CPU registers and stack space to do its work. Once handling completes, the CPU registers and stack would no longer match the state they were in when the interrupt occurred.\n\nTo resolve this, we push all general-purpose CPU registers onto the stack. This process is **context saving**.\n\nAlong with the registers, my assembly code pushes an **error code** (for exception interrupts like Divide by Zero) and the **interrupt vector number** onto the stack. This allows the C handler to identify which interrupt is currently active.\n\n## The C Handlers\n\nOnce context is saved on the stack, the assembly wrapper calls a common **C interrupt handler**. This dispatcher evaluates the vector number and calls specific driver routines.\n\nWhen calling the common C interrupt handler, the assembly stub passes the current Stack Pointer value (`RSP`) as an argument. The C code receives this address as a pointer to a `trapframe` structure representing saved context.\n\nBecause the x86 stack grows downward (from higher memory to lower memory), the structure of the `trapframe` in C must be the **exact inverse** of the order in which elements were pushed onto the stack.\n\nHere’s the assembly code that does the context saving:\n\n```\n%macro SAVE_CONTEXT 0\n    push rax\n    push rbx\n    push rcx\n    push rdx\n    push rsi\n    push rdi\n    push rbp\n    push r8\n    push r9\n    push r10\n    push r11\n    push r12\n    push r13\n    push r14\n    push r15\n%endmacro\n```\nAnd here’s the structure of `trapframe`:\n\n```\ntypedef struct trap_frame_t {\n    // general purpose registers used by any running process\n    uint64_t r15;\n    uint64_t r14;\n    uint64_t r13;\n    uint64_t r12;\n    uint64_t r11;\n    uint64_t r10;\n    uint64_t r9;\n    uint64_t r8;\n    uint64_t rbp;\n    uint64_t rdi;\n    uint64_t rsi;\n    uint64_t rdx;\n    uint64_t rcx;\n    uint64_t rbx;\n    uint64_t rax;\n    uint64_t interrupt_no; // which interrupt triggerd switch\n    uint64_t error_code;\n    // these are pushed automatically by cpu\n    uint64_t rip; //instruction pointer\n    uint64_t cs; // code segment\n    uint64_t r_flags; // cpu flags\n    uint64_t rsp; // stack pointer\n    uint64_t ss; // stack segment\n} __attribute__((packed)) trap_frame_t;\n```\n## Restoring Context\n\n**Context restoring** pops the saved register values from the stack back into the CPU registers. When the interrupted process resumes, it finds all registers exactly as it left them.\n\n```\n%macro RESTORE_CONTEXT 0\n    pop r15\n    pop r14\n    pop r13\n    pop r12\n    pop r11\n    pop r10\n    pop r9\n    pop r8\n    pop rbp\n    pop rdi\n    pop rsi\n    pop rdx\n    pop rcx\n    pop rbx\n    pop rax\n%endmacro\n```\n## The Problem with My Initial Code\n\nIn my code, I have registered specific assembly functions as interrupt handlers. However, these were not final handlers; they are **placeholders**.\n\n```\n%macro ISR_NO_ERR 1\nglobal isr%1\nisr%1:\n    push 0                  ; Push dummy error code\n    push %1                 ; Push interrupt number\n    jmp isr_common_stub     ; Jump to the heavy lifting\n%endmacro\n%macro ISR_ERR 1\nglobal isr%1\nisr%1:\n    ; Error code is already on stack!\n    push %1                 ; Push interrupt number\n    jmp isr_common_stub\n%endmacro\nISR_NO_ERR 6   ; Invalid Opcode Exception\nISR_ERR    13   ; General protection fault\nISR_ERR    14   ; Page Fault\nISR_NO_ERR 32   ; IRQ0 (Timer) - This is the big one for scheduling!\nISR_NO_ERR 33   ; IRQ1 (Keyboard)\nISR_NO_ERR 128 ; System call\nISR_NO_ERR 43 ; E1000 network card\nisr_common_stub:\n    ; 1. Save all General Purpose Registers\n    SAVE_CONTEXT\n    ; 2. Prepare for C Function Call (System V AMD64 ABI)\n    ; The first argument (RDI) must contain the pointer to our struct.\n    ; RSP points to the top of the stack, which IS the start of our TrapFrame.\n    mov rdi, rsp\n    ; 3. Call the C Handler\n    ; void isr_handler(TrapFrame* frame);\n    call isr_handler\n    ; 4. Restore Context\n    ; If the scheduler switched tasks, RSP will be different now!\n    RESTORE_CONTEXT\n    ; 5. Cleanup Error Code and Int No\n    ; We pushed 2 uint64_t values (16 bytes) manually before SAVE_CONTEXT.\n    ; We need to remove them so RSP points to the return address (RIP).\n    add rsp, 16 \n    ; 6. Return from Interrupt\n    ; Pops RIP, CS, RFLAGS, RSP, SS automatically.\n    iretq\n```\nWhy were these placeholders necessary?\n\nWhen an interrupt fires, the CPU does not pass the interrupt vector number directly to your code. The placeholders existed solely to push a unique **interrupt number** onto the stack before jumping to the common C handler.\n\nEvery placeholder function performed identical tasks:\n\n1. Pushed its specific interrupt number and error code ( a dummy 0 in case it was not present).\n2. Jump to the isr_common_stub function.\n\nThe isr_common_stub function then:\n\n1. Performed context saving.\n2. Called the common C handler.\n3. Performed context restoration.\n4. Made the iretq call to signify end of interrupt handling and pop all the data that was pushed to the stack by the CPU automatically.\n\nSuppose an timer interrupt triggers, the function that will be called is isr32 which was set as the handler and generated by the assembly macro. This function then pushes the interrupt number to the stack, does context saving, then calls the C common interrupt handler and then restores context. isr6 does the same thing and so does isr14. Do you see the problem here, every function is doing the same thing except for what interrupt number they’re pushing on to the stack and yet you have a separate one for each and every one of it.\n\nNow, while it still serves the purpose and it works perfectly. I find it just too much unnecessary. What can I say? I never built an OS before this. I was supposed to do naive and somewhat stupid things.\n\n## The Solution\n\nWith the knowledge gained from this project, a cleaner architecture emerges.\n\nThe assembly placeholders served two functions: capturing the interrupt number and managing register context.\n\nHowever, the CPU already knows which handler entry was invoked. Instead of duplicating wrapper code across 256 separate assembly labels, context saving and restoring can be consolidated into central helper functions (`save_context` and `restore_context`).\n\nEvery interrupt handler that is declared and implemented just calls two functions — save_context() in the start of it and restore_context() at the end of it. With this we, completely eliminate the need of those placeholders and the need of any comman C handler. If it every happens that you need to find the interrupt number, you already have the IDT as an array in your code, just traverse it.\n\nThanks for reading till so far. I hope it was worth the time. If you would like to see my whole code here’s the **github link** to that. You can also follow me on **Linkedin** and **X** as well. And I know this one came after a very huge break. But now I hope to be regular with this series. So, Keep in touch!","body_html":"<h1 id=\"building-a-64-bit-os-chapter-3-interrupt-descriptor-tables-the-m\">Building a 64-Bit OS — Chapter 3: Interrupt Descriptor Tables (The magic behind a working keyboard)</h1>\n<h2 id=\"interrupts\">Interrupts</h2>\n<p><strong>Interrupts</strong> are easily one of my favorite topics. They came pretty early in my low-level development journey. The concept was so fascinating that it kept me driven to explore deeper systems programming concepts.</p>\n<p>Since I started my coding journey with <strong>Android app development</strong>, I was already familiar with event listeners like <code>onClick()</code>. However, I never stopped to think about how they worked under the hood. <strong>Interrupts</strong> took me down to the lower layers where that underlying mechanism becomes clear. Take one more step down, and you land directly at the hardware and electronics layer.</p>\n<h2 id=\"what-are-interrupts\">What Are Interrupts?</h2>\n<p>The word literally means “to stop something while it is happening.” The technical concept is identical. While the CPU is executing instructions, a specific condition occurs. The CPU immediately <strong>stops its current work</strong> to handle that situation.</p>\n<p>For example, consider a single-core CPU executing one process at a time. Suppose the process currently running is doing background OS utility work. At that exact moment, you press a key on your keyboard. Do you want the OS to display that letter on your text editor immediately? Or do you want to wait for the background task to finish first? We all know the answer.</p>\n<p>That is an <strong>interrupt</strong> in action:</p>\n<ol><li>The background process runs.</li><li>A keyboard press triggers an <strong>interrupt</strong> , pausing the background process.</li><li>The CPU processes the keypress and displays the letter.</li><li>The CPU <strong>resumes the background process</strong> at the exact point where it was stopped.</li></ol>\n<h2 id=\"types-of-interrupts-hardware-vs-software\">Types of Interrupts (Hardware vs. Software)</h2>\n<p>Interrupts fall into two main categories: <strong>Hardware Interrupts</strong> and <strong>Software Interrupts</strong>.</p>\n<ul><li><strong>Hardware Interrupts:</strong> Your machine contains various physical devices, including the keyboard, mouse, storage drive, and Network Interface Card (NIC). When one of these hardware components requests CPU attention, it raises a hardware interrupt.</li><li><strong>Software Interrupts:</strong> When an interrupt is triggered by software (such as your OS or a user program), it is a software interrupt.</li></ul>\n<p>Software interrupts further break down into two subcategories:</p>\n<ul><li><strong>Normal Software Interrupts:</strong> Issued when a user program makes a<strong>system call</strong> to request OS services.</li><li><strong>Exceptions:</strong> Triggered automatically by runtime errors or exceptional CPU conditions, such as a<strong>divide-by-zero</strong> error.</li></ul>\n<h2 id=\"interrupt-mechanism-legacy-systems-vs-modern-systems\">Interrupt Mechanism: Legacy Systems vs. Modern Systems</h2>\n<p>Modern systems rely on an <strong>APIC (Advanced Programmable Interrupt Controller)</strong> and <strong>MSI (Message Signaled Interrupts)</strong>.</p>\n<p>In my own OS implementation, I built support for the older <strong>PIC (Programmable Interrupt Controller)</strong>. The PIC relies on physical wire connections linked to every hardware slot (such as PCI or PCIe slots) on the motherboard.</p>\n<p>The structural difference between a PIC and an APIC is straightforward:</p>\n<ul><li><strong>APIC</strong> is a modern, scalable version of the PIC. It handles significantly more interrupts and supports<strong>multi-core processors</strong> .</li></ul>\n<p>How does <strong>MSI</strong> differ from physical lines connected to PCI slots? In modern systems, if hardware needs CPU attention, how does it notify the CPU without a dedicated wire?</p>\n<ul><li><strong>Legacy Systems (Physical Lines):</strong> Dedicated physical lines connected each hardware slot directly to the interrupt controller. Because of these dedicated traces, motherboard slots required additional pins beyond the standard system bus.</li><li><strong>Modern Systems (MSI):</strong> There are no dedicated interrupt lines running from hardware slots to the PIC. Instead, devices use the standard system bus. When a hardware device needs to trigger an interrupt, it sends a data packet (a<strong>message</strong> ) to a specific memory address. The interrupt controller reads this message and triggers the interrupt for that device.</li></ul>\n<p>The fundamental architecture shifted:</p>\n<ul><li>In legacy systems, a hardware device <strong>triggered</strong> the interrupt over a dedicated physical wire.</li><li>In modern systems, a hardware device <strong>notifies</strong> the interrupt controller by writing a message to memory.</li></ul>\n<h2 id=\"how-to-enable-hardware-interrupts\">How to Enable Hardware Interrupts</h2>\n<p>Since my project uses the <strong>PIC (Programmable Interrupt Controller)</strong>, I will focus on its operation here. The PIC acts as a gatekeeper for hardware interrupts reaching the CPU. Physical connections from hardware devices feed into the PIC, which then routes them to the processor.</p>\n<p>The number of physical lines on a PIC dictates how many devices it can support. Early PICs had only 8 interrupt lines (<strong>IRQ0 to IRQ7</strong>). Engineers quickly ran out of lines, leading to a clever workaround: cascading two PIC chips in a <strong>Master-Slave architecture</strong>.</p>\n<ul><li>The <strong>Master PIC</strong> handles IRQ0 through IRQ7.</li><li>The <strong>IRQ2 line</strong> on the Master PIC connects directly to the<strong>Slave PIC</strong> .</li><li>The <strong>Slave PIC</strong> handles IRQ8 through IRQ15.</li></ul>\n<p>Connecting IRQ2 to the Slave PIC yields <strong>15 usable hardware interrupt lines</strong> (16 total lines minus the 1 line used for cascading).</p>\n<p>When an interrupt occurs on lines IRQ8 through IRQ15:</p>\n<ol><li>The <strong>Slave PIC</strong> catches the interrupt and cascades it to the<strong>Master PIC</strong> .</li><li>The <strong>Master PIC</strong> pulls its<strong>IRQ2 pin high</strong> to inform the CPU.</li><li>The CPU needs to identify the specific <strong>interrupt vector</strong> so it can execute the correct handler.</li><li>The CPU transfers data bus control to the <strong>Master PIC</strong> .</li><li>Control is handed to the <strong>Slave PIC</strong> , which writes the exact interrupt number onto the data bus for the CPU to read.</li></ol>\n<h2 id=\"interrupt-descriptor-tables-idt\">Interrupt Descriptor Tables (IDT)</h2>\n<p>An <strong>Interrupt Descriptor Table (IDT)</strong> is an array of descriptor structures defining interrupt gates. Each entry follows a rigid, hardware-defined format. Entries contain the target handler’s memory address, required privilege levels, and execution characteristics.</p>\n<p>In the <strong>x86 architecture</strong>, the IDT contains up to <strong>256 entries</strong>. This allows you to configure up to 256 distinct interrupt handlers.</p>\n<h2 id=\"structure-of-each-table-entry\">Structure of Each Table Entry</h2>\n<p>Each entry in the table contains the following fields:</p>\n<pre><code>typedef struct{\n    unsigned short int offset_0;\n    unsigned short int selector;\n    unsigned char ist;\n    unsigned char type_attribute;\n    unsigned short int offest_1;\n    unsigned int offset_2;\n    unsigned int ignore;\n} __attribute__ ((packed)) idt_desc_entry_t;</code></pre>\n<ul><li><strong>offsets :</strong> These split fields combine to form the full memory address of the interrupt handler function.<strong>offset_0</strong> holds the lower 16 bits,<strong>offset_1</strong> holds bits 16–31, and<strong>offset_2</strong> holds the upper 32 bits.</li><li><strong>selector:</strong> A segment selector referencing the<strong>Global Descriptor Table (GDT)</strong> . It identifies the memory segment containing the handler function. While modern 64-bit OS design rarely uses segmentation, x86 architecture still requires setting this field.</li><li><strong>ist (Interrupt Stack Table):</strong> An offset into the Interrupt Stack Table. It provides a dedicated memory stack used during interrupt handling. When the field is set to 0, the CPU switches stack if it is coming from Ring 3 to Ring 0 and stays in the same stack if it already in Ring 0. If it is set to something else, it has to switch stack in any case. If you’re not familiar with Kernel-User isolation (Ring 0 — Ring 3). You can forget the last few sentences until that blog comes out. For now, just know that I’ve set it to 0.</li><li><strong>ignore:</strong> 2 bytes of reserved padding at the end of each entry. This field is unused.</li><li><strong>type_attribute:</strong> A bitmask containing flags that dictate the descriptor’s characteristics.</li></ul>\n<pre><code>                                                                Bit position\n                                                                inside the\n  7     6       5       4       3       2       1       0   &lt;-- Flags byte\n+---+-------+-------+-----------------------------------+\n| P |  DPL  |   0   | Gate Type                         |\n+---+-------+-------+-----------------------------------+\n| 1 | 2-bit | 1-bit |            4-bit                  |   &lt;-- Size</code></pre>\n<ul><li><p><strong>Gate Type:</strong> Defines the behavior upon triggering. For example, an<strong>Interrupt Gate (</strong><strong>0b1110</strong></p><p><strong>)** automatically disables subsequent interrupts until the current handler finishes. A</strong>Trap Gate (<strong>**0b1111</strong>\n*<em>)*</em> does not disable subsequent interrupts while dealing with the current one.</p></li><li><strong>DPL (Descriptor Privilege Level):</strong> Specifies the CPU privilege level required to invoke the vector. Setting this to Ring 3 (<code>0b11</code> ) allows user-space programs to trigger the interrupt via the<code>INT</code> instruction (e.g., for system calls).</li><li><strong>P (Present Bit):</strong> Indicates whether the descriptor is active. Must be set to<code>1</code> for the vector to be valid.</li></ul>\n<h2 id=\"the-lidt-register\">The LIDT Register</h2>\n<p>The IDT resides in main memory. The CPU tracks its location using a dedicated register called <strong>IDTR (Interrupt Descriptor Table Register)</strong>. x86 provides a special assembly instruction — <code>LIDT</code> (<strong>Load Interrupt Descriptor Table</strong>)—to load this table pointer into the register.</p>\n<p>However, the address loaded into the <code>IDTR</code> does not point directly to the table array. Instead, it holds a pointer to a <strong>pseudo-descriptor structure</strong> describing the table:</p>\n<pre><code>typedef struct {\n    unsigned short int limit;\n    unsigned long long offset;\n} __attribute__ ((packed)) idtr_t;</code></pre>\n<ul><li><strong>offset:</strong> The memory address where the IDT array starts. Since it is declared as an array, it is always contiguous in memory.</li><li><strong>limit:</strong> The maximum byte length of the table minus 1 (<code>sizeof(IDT) - 1</code> ).</li></ul>\n<h2 id=\"my-implementation\">My Implementation</h2>\n<p>When an interrupt fires, the CPU hardware automatically pushes state data onto the stack before beginning interrupt handling:</p>\n<ul><li><strong>Stack Segment (SS)</strong></li><li><strong>Stack Pointer (RSP)</strong></li><li><strong>RFLAGS</strong> (represents the CPU state at any given point)</li><li><strong>Code Segment (CS)</strong></li><li><strong>Instruction Pointer (RIP)</strong></li></ul>\n<p>After this hardware push, whatever happens next is handled entirely by your custom code.</p>\n<p>I divided my interrupt handling process into three distinct steps: <strong>Context Saving</strong>, <strong>Interrupt Handling</strong>, and <strong>Context Restoring</strong>. This architecture uses a mix of <strong>Assembly</strong> and *<em>C*</em> because C cannot perform low-level register manipulation directly.</p>\n<h2 id=\"saving-context\">Saving Context</h2>\n<p>Suppose a process is executing on the CPU. While running, the process uses the memory stack and stores temporary data inside CPU general-purpose registers.</p>\n<p>Now an interrupt occurs. The CPU pauses current execution and jumps to handle the interrupt. The handler requires CPU registers and stack space to do its work. Once handling completes, the CPU registers and stack would no longer match the state they were in when the interrupt occurred.</p>\n<p>To resolve this, we push all general-purpose CPU registers onto the stack. This process is <strong>context saving</strong>.</p>\n<p>Along with the registers, my assembly code pushes an <strong>error code</strong> (for exception interrupts like Divide by Zero) and the <strong>interrupt vector number</strong> onto the stack. This allows the C handler to identify which interrupt is currently active.</p>\n<h2 id=\"the-c-handlers\">The C Handlers</h2>\n<p>Once context is saved on the stack, the assembly wrapper calls a common <strong>C interrupt handler</strong>. This dispatcher evaluates the vector number and calls specific driver routines.</p>\n<p>When calling the common C interrupt handler, the assembly stub passes the current Stack Pointer value (<code>RSP</code>) as an argument. The C code receives this address as a pointer to a <code>trapframe</code> structure representing saved context.</p>\n<p>Because the x86 stack grows downward (from higher memory to lower memory), the structure of the <code>trapframe</code> in C must be the <strong>exact inverse</strong> of the order in which elements were pushed onto the stack.</p>\n<p>Here’s the assembly code that does the context saving:</p>\n<pre><code>%macro SAVE_CONTEXT 0\n    push rax\n    push rbx\n    push rcx\n    push rdx\n    push rsi\n    push rdi\n    push rbp\n    push r8\n    push r9\n    push r10\n    push r11\n    push r12\n    push r13\n    push r14\n    push r15\n%endmacro</code></pre>\n<p>And here’s the structure of <code>trapframe</code>:</p>\n<pre><code>typedef struct trap_frame_t {\n    // general purpose registers used by any running process\n    uint64_t r15;\n    uint64_t r14;\n    uint64_t r13;\n    uint64_t r12;\n    uint64_t r11;\n    uint64_t r10;\n    uint64_t r9;\n    uint64_t r8;\n    uint64_t rbp;\n    uint64_t rdi;\n    uint64_t rsi;\n    uint64_t rdx;\n    uint64_t rcx;\n    uint64_t rbx;\n    uint64_t rax;\n    uint64_t interrupt_no; // which interrupt triggerd switch\n    uint64_t error_code;\n    // these are pushed automatically by cpu\n    uint64_t rip; //instruction pointer\n    uint64_t cs; // code segment\n    uint64_t r_flags; // cpu flags\n    uint64_t rsp; // stack pointer\n    uint64_t ss; // stack segment\n} __attribute__((packed)) trap_frame_t;</code></pre>\n<h2 id=\"restoring-context\">Restoring Context</h2>\n<p><strong>Context restoring</strong> pops the saved register values from the stack back into the CPU registers. When the interrupted process resumes, it finds all registers exactly as it left them.</p>\n<pre><code>%macro RESTORE_CONTEXT 0\n    pop r15\n    pop r14\n    pop r13\n    pop r12\n    pop r11\n    pop r10\n    pop r9\n    pop r8\n    pop rbp\n    pop rdi\n    pop rsi\n    pop rdx\n    pop rcx\n    pop rbx\n    pop rax\n%endmacro</code></pre>\n<h2 id=\"the-problem-with-my-initial-code\">The Problem with My Initial Code</h2>\n<p>In my code, I have registered specific assembly functions as interrupt handlers. However, these were not final handlers; they are <strong>placeholders</strong>.</p>\n<pre><code>%macro ISR_NO_ERR 1\nglobal isr%1\nisr%1:\n    push 0                  ; Push dummy error code\n    push %1                 ; Push interrupt number\n    jmp isr_common_stub     ; Jump to the heavy lifting\n%endmacro\n%macro ISR_ERR 1\nglobal isr%1\nisr%1:\n    ; Error code is already on stack!\n    push %1                 ; Push interrupt number\n    jmp isr_common_stub\n%endmacro\nISR_NO_ERR 6   ; Invalid Opcode Exception\nISR_ERR    13   ; General protection fault\nISR_ERR    14   ; Page Fault\nISR_NO_ERR 32   ; IRQ0 (Timer) - This is the big one for scheduling!\nISR_NO_ERR 33   ; IRQ1 (Keyboard)\nISR_NO_ERR 128 ; System call\nISR_NO_ERR 43 ; E1000 network card\nisr_common_stub:\n    ; 1. Save all General Purpose Registers\n    SAVE_CONTEXT\n    ; 2. Prepare for C Function Call (System V AMD64 ABI)\n    ; The first argument (RDI) must contain the pointer to our struct.\n    ; RSP points to the top of the stack, which IS the start of our TrapFrame.\n    mov rdi, rsp\n    ; 3. Call the C Handler\n    ; void isr_handler(TrapFrame* frame);\n    call isr_handler\n    ; 4. Restore Context\n    ; If the scheduler switched tasks, RSP will be different now!\n    RESTORE_CONTEXT\n    ; 5. Cleanup Error Code and Int No\n    ; We pushed 2 uint64_t values (16 bytes) manually before SAVE_CONTEXT.\n    ; We need to remove them so RSP points to the return address (RIP).\n    add rsp, 16 \n    ; 6. Return from Interrupt\n    ; Pops RIP, CS, RFLAGS, RSP, SS automatically.\n    iretq</code></pre>\n<p>Why were these placeholders necessary?</p>\n<p>When an interrupt fires, the CPU does not pass the interrupt vector number directly to your code. The placeholders existed solely to push a unique <strong>interrupt number</strong> onto the stack before jumping to the common C handler.</p>\n<p>Every placeholder function performed identical tasks:</p>\n<ol><li>Pushed its specific interrupt number and error code ( a dummy 0 in case it was not present).</li><li>Jump to the isr_common_stub function.</li></ol>\n<p>The isr_common_stub function then:</p>\n<ol><li>Performed context saving.</li><li>Called the common C handler.</li><li>Performed context restoration.</li><li>Made the iretq call to signify end of interrupt handling and pop all the data that was pushed to the stack by the CPU automatically.</li></ol>\n<p>Suppose an timer interrupt triggers, the function that will be called is isr32 which was set as the handler and generated by the assembly macro. This function then pushes the interrupt number to the stack, does context saving, then calls the C common interrupt handler and then restores context. isr6 does the same thing and so does isr14. Do you see the problem here, every function is doing the same thing except for what interrupt number they’re pushing on to the stack and yet you have a separate one for each and every one of it.</p>\n<p>Now, while it still serves the purpose and it works perfectly. I find it just too much unnecessary. What can I say? I never built an OS before this. I was supposed to do naive and somewhat stupid things.</p>\n<h2 id=\"the-solution\">The Solution</h2>\n<p>With the knowledge gained from this project, a cleaner architecture emerges.</p>\n<p>The assembly placeholders served two functions: capturing the interrupt number and managing register context.</p>\n<p>However, the CPU already knows which handler entry was invoked. Instead of duplicating wrapper code across 256 separate assembly labels, context saving and restoring can be consolidated into central helper functions (<code>save_context</code> and <code>restore_context</code>).</p>\n<p>Every interrupt handler that is declared and implemented just calls two functions — save_context() in the start of it and restore_context() at the end of it. With this we, completely eliminate the need of those placeholders and the need of any comman C handler. If it every happens that you need to find the interrupt number, you already have the IDT as an array in your code, just traverse it.</p>\n<p>Thanks for reading till so far. I hope it was worth the time. If you would like to see my whole code here’s the <strong>github link</strong> to that. You can also follow me on <strong>Linkedin</strong> and *<em>X*</em> as well. And I know this one came after a very huge break. But now I hope to be regular with this series. So, Keep in touch!</p>","headings":[{"level":1,"text":"Building a 64-Bit OS — Chapter 3: Interrupt Descriptor Tables (The magic behind a working keyboard)","id":"building-a-64-bit-os-chapter-3-interrupt-descriptor-tables-the-m"},{"level":2,"text":"Interrupts","id":"interrupts"},{"level":2,"text":"What Are Interrupts?","id":"what-are-interrupts"},{"level":2,"text":"Types of Interrupts (Hardware vs. Software)","id":"types-of-interrupts-hardware-vs-software"},{"level":2,"text":"Interrupt Mechanism: Legacy Systems vs. Modern Systems","id":"interrupt-mechanism-legacy-systems-vs-modern-systems"},{"level":2,"text":"How to Enable Hardware Interrupts","id":"how-to-enable-hardware-interrupts"},{"level":2,"text":"Interrupt Descriptor Tables (IDT)","id":"interrupt-descriptor-tables-idt"},{"level":2,"text":"Structure of Each Table Entry","id":"structure-of-each-table-entry"},{"level":2,"text":"The LIDT Register","id":"the-lidt-register"},{"level":2,"text":"My Implementation","id":"my-implementation"},{"level":2,"text":"Saving Context","id":"saving-context"},{"level":2,"text":"The C Handlers","id":"the-c-handlers"},{"level":2,"text":"Restoring Context","id":"restoring-context"},{"level":2,"text":"The Problem with My Initial Code","id":"the-problem-with-my-initial-code"},{"level":2,"text":"The Solution","id":"the-solution"}]}}