Interrupt Dispatching Internals
Microsoft has changed the way interrupts are dispatched in recent versions of Windows. While there has been some published research [1] on interrupt dispatching on older versions of Windows and on 32-bit systems, not much information is available on how this works on modern Windows systems. This article attempts to provide a description of interrupt dispatching on 64-bit Windows 10 systems, specifically Windows 10 RS1 Anniversary Update Build 10.0.10586.
Interrupts are used by operating systems get notified about events from hardware devices. Interrupt dispatching is a mechanism wherein the CPU passes execution control to software to handle the hardware event. Interrupts are dispatched by the Windows kernel which performs some housekeeping before transferring execution control to hardware drivers that have registered interrupt service routines (ISRs). The Interrupt Descriptor Table (IDT) is the key data structure involved in dispatching interrupts and its' format is specified by the CPU vendor [2]. The IDT must be populated by the operating system during boot-up and is subsequently used by the CPU to dispatch interrupts arriving from hardware devices.
IDTR Register
CPUs have a built-in register called the IDTR register which Windows populates with the kernel virtual address of the IDT, which it sets up for each CPU during system startup. The format of the IDTR is specified by the CPU vendor [2] and is included below for reference.
The following kernel debugger output shows the value of the IDTR register for a particular CPU. It is noteworthy that each CPU on a multi-processor system has its own IDTR register which points to a private copy of the IDT, as shown below for a system with 2 CPUs.
0: kd> r @idtr idtr=fffff80197c7e070 0: kd> ~1 1: kd> r @idtr idtr=ffffdf010b313bf0
Interrupt Descriptor Table
The IDT contains a total of 256 entries of which some entries are used for exceptions, some for software interrupts and the rest of hardware interrupts. The index into the IDT which selects a particular entry is called an interrupt vector. The format of each entry in the IDT is described by the CPU vendor [2] and is included below for reference.
The Windows kernel defines a structure called KIDTENTRY64 which represents an individual entry in the IDT on a 64-bit CPU. Using the output from the "r @idtr" command from the previous section, we can display the first entry of the IDT as pointed to by the IDTR on CPU #0.
0: kd> dt nt!_KIDTENTRY64 fffff80197c7e070 +0x000 OffsetLow : 0xe800 +0x002 Selector : 0x10 +0x004 IstIndex : 0y000 +0x004 Reserved0 : 0y00000 (0) +0x004 Type : 0y01110 (0xe) +0x004 Dpl : 0y00 +0x004 Present : 0y1 +0x006 OffsetMiddle : 0x95f5 +0x008 OffsetHigh : 0xfffff801 +0x00c Reserved1 : 0 +0x000 Alignment : 0x95f58e00`0010e800
The combination of OffsetHigh, OffsetMiddle and OffsetLow fields provide the virtual address that the CPU will transfer control to when an interrupt occurs. In the above output this virtual address is 0xfffff80195f5e800. This matches the output of the "!idt 0" command below, and points to the function KiDivideErrorFault(). The value of the Type field in the above output (i.e. 0xe) shows that the IDT entry represents an Interrupt Gate. More details on IDT entries can be found in [2].
0: kd> !idt 0 Dumping IDT: fffff80197c7e070 00: fffff80195f5e800 nt!KiDivideErrorFault
The first 19 (0x13) entries in the IDT are for dispatching exceptions and are defined by the CPU vendor in [2]. The rest of the entries in the IDT are either used for software interrupts or hardware interrupt or are left unused. In the output of the "!idt" command, hardware interrupts can be easily identified by the presence of the pointer to the KINTERRUPT structure. The "!idt -a" command displays the contents of the entire IDT as shown in the following output which has been edited to add comments and remove unused entries.
0: kd> !idt -a Dumping IDT: fffff801fe04b070 * Exceptions 00: fffff801fc1dc100 nt!KiDivideErrorFault 01: fffff801fc1dc200 nt!KiDebugTrapOrFault 02: fffff801fc1dc3c0 nt!KiNmiInterrupt Stack = 0xFFFFF801FE066000 03: fffff801fc1dc780 nt!KiBreakpointTrap 04: fffff801fc1dc880 nt!KiOverflowTrap 05: fffff801fc1dc980 nt!KiBoundFault 06: fffff801fc1dcc00 nt!KiInvalidOpcodeFault 07: fffff801fc1dce40 nt!KiNpxNotAvailableFault 08: fffff801fc1dcf00 nt!KiDoubleFaultAbort Stack = 0xFFFFF801FE064000 09: fffff801fc1dcfc0 nt!KiNpxSegmentOverrunAbort 0a: fffff801fc1dd080 nt!KiInvalidTssFault 0b: fffff801fc1dd140 nt!KiSegmentNotPresentFault 0c: fffff801fc1dd280 nt!KiStackFault 0d: fffff801fc1dd3c0 nt!KiGeneralProtectionFault 0e: fffff801fc1dd4c0 nt!KiPageFault 0f: fffff801fc1d60c8 nt!KiIsrThunk+0x78 10: fffff801fc1dd880 nt!KiFloatingErrorFault 11: fffff801fc1dda00 nt!KiAlignmentFault 12: fffff801fc1ddb00 nt!KiMcheckAbort Stack = 0xFFFFF801FE068000 13: fffff801fc1de1c0 nt!KiXmmException * Software Interrupts 1f: fffff80195f59af0 nt!KiApcInterrupt 20: fffff80195f5def0 nt!KiSwInterrupt 29: fffff80195f60a80 nt!KiRaiseSecurityCheckFailure 2c: fffff80195f60b80 nt!KiRaiseAssertion 2d: fffff80195f60c80 nt!KiDebugServiceTrap 2f: fffff80195f59de0 nt!KiDpcInterrupt 30: fffff80195f5a020 nt!KiHvInterrupt * Hardware Interrupts 35: fffff801fc1d61f8 hal!HalpInterruptCmciService (KINTERRUPT fffff801fc062be0) 50: fffff801fc1d62d0 storport!RaidpAdapterMSIInterruptRoutine (KINTERRUPT ffffc280e2d33c80) 51: fffff801fc1d62d8 HDAudBus!HdaController::Isr (KINTERRUPT ffffc280e2d333c0) 60: fffff801fc1d6350 i8042prt!I8042MouseInterruptService (KINTERRUPT ffffc280e2d33140) 61: fffff801fc1d6358 TeeDriverW8x64+0xff44 (KMDF) (KINTERRUPT ffffc280e2d33b40) 70: fffff801fc1d63d0 i8042prt!I8042KeyboardInterruptService (KINTERRUPT ffffc280e2d33280) 71: fffff801fc1d63d8 USBXHCI!Interrupter_WdfEvtInterruptIsr (KMDF) (KINTERRUPT ffffc280e2d338c0) 81: fffff801fc1d6458 dptf_cpu+0x3250 (KMDF) (KINTERRUPT ffffc280e2d33a00) USBPORT!USBPORT_InterruptService (KINTERRUPT ffffc280e2d33780) 91: fffff801fc1d64d8 dxgkrnl!DpiFdoMessageInterruptRoutine (KINTERRUPT ffffc280e2d33000) a1: fffff801fc1d6558 USBPORT!USBPORT_InterruptService (KINTERRUPT ffffc280e2d33640) b0: fffff801fc1d65d0 ACPI!ACPIInterruptServiceRoutine (KINTERRUPT ffffc280e2d33dc0) b1: fffff801fc1d65d8 Smb_driver_Intel+0x3378 (KMDF) (KINTERRUPT ffffc280e2d33500) d1: fffff801fc1d66d8 hal!HalpTimerClockInterrupt (KINTERRUPT fffff801fc0633e0) d2: fffff801fc1d66e0 hal!HalpTimerClockIpiRoutine (KINTERRUPT fffff801fc0632e0) d7: fffff801fc1d6708 hal!HalpInterruptRebootService (KINTERRUPT fffff801fc0630e0) d8: fffff801fc1d6710 hal!HalpInterruptStubService (KINTERRUPT fffff801fc062ee0) df: fffff801fc1d6748 hal!HalpInterruptSpuriousService (KINTERRUPT fffff801fc062de0) e1: fffff801fc1d89c0 nt!KiIpiInterrupt e2: fffff801fc1d6760 hal!HalpInterruptLocalErrorService (KINTERRUPT fffff801fc062fe0) e3: fffff801fc1d6768 hal!HalpInterruptDeferredRecoveryService (KINTERRUPT fffff801fc062ce0) fd: fffff801fc1d6838 hal!HalpTimerProfileInterrupt (KINTERRUPT fffff801fc0634e0) fe: fffff801fc1d6840 hal!HalpPerfInterrupt (KINTERRUPT fffff801fc0631e0)
In this article, we will focus on the hardware part of the IDT i.e. the last set of entries in the above output. The hexadecimal number in the first column is the interrupt vector or the index at which that particular entry resides in the IDT. As noted before, each entry in the IDT points to the set of instructions which execute as the first of many steps in the interrupt dispatching process.
Let's examine the second entry in the hardware part of the IDT i.e. entry at vector 0x50.
0: kd> !idt 50 Dumping IDT: fffff801fe04b070 50: fffff801fc1d62d0 storport!RaidpAdapterMSIInterruptRoutine (KINTERRUPT ffffc280e2d33c80)
And display the raw IDT entry at vector 0x50 using the IDTR.
0: kd> dt @idtr + @@c++(0x50 * sizeof(nt!_KIDTENTRY64)) nt!_KIDTENTRY64 +0x000 OffsetLow : 0x62d0 +0x002 Selector : 0x10 +0x004 IstIndex : 0y000 +0x004 Reserved0 : 0y00000 (0) +0x004 Type : 0y01110 (0xe) +0x004 Dpl : 0y00 +0x004 Present : 0y1 +0x006 OffsetMiddle : 0xfc1d +0x008 OffsetHigh : 0xfffff801 +0x00c Reserved1 : 0 +0x000 Alignment : 0xfc1d8e00`001062d0
We observe that when the interrupt occurs execution control will be transferred to the address 0xfffff801fc1d62d0. This address points to an executable code page in NTOSKRNL and contains the following instructions:
0: kd> u 0xfffff801fc1d62d0 nt!KiIsrThunk+0x280: fffff801`fc1d62d0 6a50 push 50h fffff801`fc1d62d2 55 push rbp fffff801`fc1d62d3 e988050000 jmp nt!KiIsrLinkage (fffff801`fc1d6860)
The NTOSKRNL variable KiIsrThunk points to a kernel code page that contains a series of 256 templates that are similar to the above instructions. After pushing the interrupt vector number, i.e. 0x50 in this case and the contents of the RBP register on to the stack, the KiIsrThunk stub transfers control to KiIsrLinkage(). These 2 elements on the stack are accessed by the function KiIsrLinkage() through the KTRAP_FRAME structure.
KiIsrLinkage()
KiIsrLinkage() performs a bunch of housekeeping tasks as follows:
- Saves the volatile register context in a partial KTRAP_FRAME created on the stack.
- Checks if at the time of the interrupt, the CPU was executing instructions within a certain region of the function ExpInterlockedPopEntrySList() and if so it resets the RIP to a valid loop resume instruction in the function.
- Checks if the interrupts are disabled and if so bugchecks the system with the stop code TRAP_CAUSE_UNKNOWN.
- Retrieves a pointer to the interrupt structure associated with the interrupt and dispatches the interrupt (more on this later).
- Restores the volatile register context from the KTRAP_FRAME.
- Returns from the interrupt.
Interestingly, most parts of the function KiIsrLinkage() are created from macros, many of which are available in the WDK header file kxamd64.inc such as GENERATE_INTERRUPT_FRAME, ENTER_INTERRUPT, EXIT_INTERRUPT and RESTORE_TRAP_STATE.
KINTERRUPT
The KINTERRUPT structure is key to dispatching interrupts and contains all the information required to invoke the interrupt service routine (ISR) registered by a driver. KiIsrLinkage() locates the KINTERRUPT structure corresponding to the interrupt vector by using it as an index into the array of KINTERRUPT structure pointers stored at KPCR.CurrentPrcb.InterruptObject[]. The NTOSKRNL function KiGetInterruptObjectAddress() retrieves the pointer to the KINTERRUPT object as shown below:
kd> uf nt!KiGetInterruptObjectAddress nt!KiGetInterruptObjectAddress: fffff803`22f81588 65488b142520000000 mov rdx,qword ptr gs:[20h] ; KPCR->CurrentPrcb fffff803`22f81591 4881c2002e0000 add rdx,2E00h ; InterruptObject[] fffff803`22f81598 8bc1 mov eax,ecx fffff803`22f8159a 488d04c2 lea rax,[rdx+rax*8] ; InterruptObject[rax] fffff803`22f8159e c3 ret
We repeat the "!idt" command again, this time focusing on the KINTERRUPT structure.
0: kd> !idt 50
Dumping IDT: fffff801fe04b070
50: fffff801fc1d62d0 storport!RaidpAdapterMSIInterruptRoutine (KINTERRUPT ffffc280e2d33c80)
0: kd> dt @$pcr nt!_KPCR -a Prcb.InterruptObject[50]
+0x180 Prcb :
+0x2e00 InterruptObject : [80] 0xffffc280`e2d33c80 Void
0: kd> dt nt!_KINTERRUPT ffffc280e2d33c80
+0x000 Type : 0n22
+0x002 Size : 0n256
+0x008 InterruptListEntry : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
+0x018 ServiceRoutine : 0xfffff801`fc1590d8 unsigned char nt!KiInterruptMessageDispatch+0
+0x020 MessageServiceRoutine : 0xfffff804`89c276d0 unsigned char storport!RaidpAdapterMSIInterruptRoutine+0
+0x028 MessageIndex : 0
+0x030 ServiceContext : 0xffffa201`bbff51a0 Void
+0x038 SpinLock : 0
+0x040 TickCount : 0
+0x048 ActualLock : 0xffffa201`bbff44c0 -> 0
+0x050 DispatchAddress : 0xfffff801`fc1d5620 void nt!KiInterruptDispatch+0
+0x058 Vector : 0x50
+0x05c Irql : 0x5 ''
+0x05d SynchronizeIrql : 0x5 ''
+0x05e FloatingSave : 0 ''
+0x05f Connected : 0x1 ''
+0x060 Number : 0
+0x064 ShareVector : 0x1 ''
+0x065 EmulateActiveBoth : 0 ''
+0x066 ActiveCount : 0
+0x068 InternalState : 0n0
+0x06c Mode : 1 ( Latched )
+0x070 Polarity : 0 ( InterruptPolarityUnknown )
+0x074 ServiceCount : 0
+0x078 DispatchCount : 0
+0x080 PassiveEvent : (null)
+0x088 TrapFrame : 0xffffc280`e485c960 _KTRAP_FRAME
+0x090 DisconnectData : (null)
+0x098 ServiceThread : (null)
+0x0a0 ConnectionData : 0xffffa201`bbff44d0 _INTERRUPT_CONNECTION_DATA
+0x0a8 IntTrackEntry : 0xffffa201`bc7b4f60 Void
+0x0b0 IsrDpcStats : _ISRDPCSTATS
+0x0f0 RedirectObject : (null)
+0x0f8 Padding : [8] ""
The fields in the KINTERRUPT structure that are relevant to dispatching interrupts are listed in the following table:
| DispatchAddress | Pointer to the initial interrupt dispatch routine in the NTOSKRNL i.e. KiChainedDispatch() for shared interrupts and KiInterruptDispatch() for others. More on interrupt sharing later. |
| ServiceRoutine | Pointer to the interrupt service routine registered by drivers using the kernel APIs IoConnectInterrupt() or IoConnectInterruptEx(). |
| MessageServiceRoutine | Used only for message signaled interrupts (MSI) [4] i.e. interrupts that are delivered by writing to reserved memory locations instead of toggling hardware lines. These interrupts show up as negative (-) numbers in device manager. For such interrupts, the ServiceRoutine points to the kernel function KiInterruptMessageDispatch() which calls the driver supplied ISR in MessageServiceRoutine. |
| MessageIndex | Index of the MSI, passed as a parameter to the ISR at MessageServiceRoutine. |
In older versions of Windows the KINTERRUPT was allocated from executable non-paged pool since it contained the initial dispatch code that was registered directly in the IDT. By transitioning to the KiIsrThunk() and KiIsrLinkage() mechanism described above, the initial interrupt stub is now in executable memory in NTOSKRNL and consequently the KINTERRUPT structure no longer needs to be allocated from executable memory. KINTERRUPT structures are now pre-allocated and stored in a list at KPCR.Prcb.InterruptObjectPool. The function KeAllocateInterrupt() pulls out pre-allocated KINTERRUPT structures from this list when called upon to allocate a new KINTERRUPT structure. When this list gets depleted, it allocates another page worth of structures using MmAllocateIndependentPages() and adds them individually to the list.
Interrupt Dispatching
One of the critical steps taken by KiIsrLinkage is to invoke the function in the KINTERRUPT.DispatchAddress which results in either KiInterruptDispatch() or KiChainedDispatch() getting called. Both these functions are called with the pointer to the KINTERRUPT structure such that they have access to all the information pertaining to the interrupt being serviced.
Modern systems use the Advanced Programmable Interrupt Controller (APIC) [3] hardware to route interrupts from hardware devices to the CPU. Hardware devices deliver their interrupt signals to the APIC via interrupt request (IRQ) lines. There are however, more devices that need to deliver interrupts to the CPU, than there are IRQ lines. Interrupt sharing mitigates the problem by allowing multiple devices to multiplex their interrupts through the same IRQ line. When an IRQ is shared, multiple drivers register their ISRs for the same IRQ and interrupt vector. This results in multiple KINTERRUPT structures, corresponding to the devices that are sharing the interrupt, being linked together through their KINTERRUPT.InterruptListEntry fields. Interrupt sharing can be observed in the output of the "!idt -a" command when a single interrupt vector has multiple KINTERRUPT structures associated with it. KiChainedDispatch() handles interrupts that are shared among multiple hardware devices and KiInterruptDispatch() handles the rest of the interrupts.
The routines KiInterruptDispatch() and KiChainedDispatch() switch to the per CPU interrupt stack, a pointer to which is stored in KPCR.Prb.IsrStack. This stack is allocated by the function MmAllocateIsrStack(). The ISR stack size is 0x7000 bytes as defined by the variables ISR_STACK_SIZE and PAGE_SIZE in the WDK header ksamd64.inc. The actual switch to ISR stack is performed by the macro SWITCH_TO_ISR_STACK and is also available in ksamd64.inc.
Once executing on the ISR stack, the functions KiInterruptDispatch() and KiChainedDispatch() propagate execution to the next stage by calling KiInterruptSubDispatch() or KiScanInterruptObjectList() respectively. KiInterruptSubDispatch() calls KiCallInterruptServiceRoutine() for a single KINTERRUPT structure. KiScanInterruptObjectList() iterates through all the KINTERRUPT objects registered for a single vector using the list at KINTERRUPT.InterruptListEntry and calls KiCallInterruptServiceRoutine() for each KINTERRUPT structure in the chain.
KiCallInterruptServiceRoutine() performs the following tasks:
- Marks the interrupt as active in KINTERRUPT.IsrDpcStats.IsrActive.
- Records the ISR start time in KINTERRUPT.IsrDpcStats.IsrTimeStart.
- Acquires the interrupt spinlock in KINTERRUPT.ActualLock.
- Calls the driver registered ISR in KINTERRUPT.ServiceRoutine.
- Records the ISR duration in KINTERRUPT.IsrDpcStats.IsrTime.
- If the ISR was interrupted by another one at a higher IRQL, it adjusts the IsrTime for accurate time accounting.
- Marks the interrupt as inactive in KINTERRUPT.IsrDpcStats.IsrActive.
- Increments the interrupt instance counter in IsrCount.
The driver registered ISR can tell the caller KiCallInterruptServiceRoutine() if it claimed the interrupt by returning TRUE. This becomes important in the case of shared interrupts where the decision to call the ISR in the next KINTERRUPT in the chain depends on whether the current ISR has claimed the interrupt or not.
The following diagram shows all the data structures described thus far and the relationship between them.
As in prior versions of 64-bit Windows, both the IDTR and the contents of the IDT are protected by kernel patch protection (PatchGuard). By making the KINTERRUPT structure non-executable and removing the dispatch code from within the structure, another vector for code subversion has been closed. However, even with these new interrupt dispatching changes, it is still possible for a kernel mode driver to hook ISRs in the system to implement functionality such as keystroke logging. The driver supplied ISR in the KINTERRUPT.ServiceRoutine field can be replaced with pointer to a hook function and PatchGuard does not seem to notice. Neither does it notice when that the pointer to the KINTERRUPT stored in the KPCR.Prcb.InterruptObject[] is replaced with a cloned KINTERRUPT structure which can direct execution to arbitrary code.
We have looked at how interrupt handling in the Windows kernel has changed from using an executable stub in every KINTERRUPT routine to having a pre-populated array of stubs i.e. KiIsrThunk() and KiIsrLinkage(). We have seen how execution propagates from initial stub pointed to by the IDT to driver supplied ISRs, while looking at all the important kernel data structures that are involved in this execution chain.
Special thanks to Takashi Toyota (@t_toyota) for his review and valuable feedback on this article.