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.

Figure 1: IDTR Register

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

0: kd> ~1
1: kd> r @idtr

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.

Figure 2: Interrupt Descriptor Table Entry

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
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() performs a bunch of housekeeping tasks as follows:

Interestingly, most parts of the function KiIsrLinkage() are created from macros, many of which are available in the WDK header file such as GENERATE_INTERRUPT_FRAME, ENTER_INTERRUPT, EXIT_INTERRUPT and RESTORE_TRAP_STATE.


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
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:

DispatchAddressPointer to the initial interrupt dispatch routine in the NTOSKRNL i.e. KiChainedDispatch() for shared interrupts and KiInterruptDispatch() for others. More on interrupt sharing later.
ServiceRoutinePointer to the interrupt service routine registered by drivers using the kernel APIs IoConnectInterrupt() or IoConnectInterruptEx().
MessageServiceRoutineUsed 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.
MessageIndexIndex 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 The actual switch to ISR stack is performed by the macro SWITCH_TO_ISR_STACK and is also available in

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:

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.

Figure 3: Interrupt Dispatching

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.


[1]Stealth hooking : Another way to subvert the Windows kernel
[2]Intel Software Developer’s Manuals Vol.3 : System Programming Guide
[3]Advanced Programmable Interrupt Controller
[4]Message Signaled Interrupts