Windows IRQL explained
ProgrammingWritten by aXXo
Whether you're learning kernel programming or curious what happens when you move or click your mouse, interrupts are key to understand. In this article, we'll dive into what interrupts are and how the Windows IRQL system works.
What is an interrupt?
Interrupts are a way for the system to receive operation requests that require special attention. They can be emitted from both hardware and software. We call them interrupts because they interrupt the system.
More precisely, when one occurs, it hijacks execution in whatever processor core it targets and prevents other programs from executing until done.
When your operating system initializes, it sets up a table known as the interrupt descriptor table (IDT). Each IDT entry contains a pointer to an interrupt service routine (ISR), which is a function that handles the dispatching of the operation. The CPU is told where this table is so it knows where to direct interrupts.
When an interrupt is emitted, it's sent to an advanced programmable interrupt controller (APIC). Your CPU has I/O APICs that receive device interrupts, and APICs dedicated to each core where interrupts are directed after I/O APIC processing (software interrupts go directly to core-specific APICs).

We won't dive into APICs in this post, but understand their job is to process interrupts before the system does. We call them "programmable" because they can be configured by the operating system.

No worries if this is overwhelming, I just want to make sure you get the right idea. Don't worry, you don't need to perfectly understand all these concepts to learn about IRQLs, but it wouldn't hurt to take some notes and tackle these later 😉
So what is an IRQL??
The interrupt request level (IRQL) is a way for the operating system to specify what should and shouldn't execute at what point using a code system from 0 (lowest) to ~31 (this varies between systems and not all levels are actively used).
Level 0
PASSIVE_LEVEL
- The default IRQL at which everything executes normally, including user mode.Level 1
APC_LEVEL
- Used when executing asynchronous procedure calls (explained later on).Level 2
DISPATCH_LEVEL
- This is the level at which the scheduler runs. When the IRQL reaches this level and above, it can't do its job, meaning paged memory access is forbidden (page faults aren't handled so non-paged memory pools are mandatory) and context switching is gone.Level 3+
DIRQL
- Any IRQL above 2 is called a device IRQL and is used when handling hardware interrupts.
Higher levels mask lower levels. If the IRQL is set to 2, then anything that can only execute at level 0 and 1 will be interrupted until the IRQL goes back down to level 0 (for both to execute again).
How is IRQL changed?
Interrupts have IRQLs associated to them, so when x
interrupt occurs, y
IRQL will be set on the core that handles it, and once handled, the IRQL will be lowered back.
It can also be manually increased and lowered using KeRaiseIrql
and KeLowerIrql
, but this can only be done from the kernel. Note that to lower the IRQL with KeLowerIrql
, you must provide the old level, otherwise you'll get a nice blue flashbang in the middle of the night.
Deferred Procedure Calls (DPCs)
Deferred procedure calls are a system used by interrupt service routines when they need to execute something at IRQL 2.
They can allocate a DPC structure in the non-paged pool using KeInitializeDpc
:
DeferredRoutine
is a function with a specific template like this:
The DeferredContext
argument is a way to provide information to the DPC when it gets executed through an argument of the same name, as you can see in the example KdeferredRoutine
definition.
After the DPC is allocated, it can be added to the DPC queue using KeInsertQueueDpc
. An ISR would use a deferred procedure call if it needs to do anything that requires a lower IRQL, since DPCs execute at IRQL 2.
This could be completing an I/O request packet (IRP) using IoCompleteRequest
, since it cannot be invoked from device IRQLs for performance reasons. Interrupts restrict the system in many ways, so they have an inherent need to be completed ASAP. It's also useful for ISRs that need to do intensive operations that can be done later.
DPCs can be used from regular kernel code although I’m not sure why you would.
Asynchronous Procedure Calls (APCs)
Asynchronous procedure calls are very different from DPCs. First, they execute at IRQL 1 instead (thus why this level is called APC_LEVEL). Second, they can be used by both kernel and user mode.
Finally, their purpose is different. APCs run within the context of the specific thread they're assigned to. They're useful because they provide higher priority than normal code while still being possible to create at user mode or kernel mode, without preventing the scheduler from doing things it's prevented from when the IRQL reaches 2 and higher.
An example of why priority matters is ReadFileEx
. Take a look at its arguments:
lpCompletionRoutine
is a pointer to a callback function executed as an APC when the file read request completes and the thread becomes alertable. If you never signal the thread, nothing happens except your APCs for that thread will never execute.
Conclusion
Hopefully this blog post opened your eyes to an entire new world! Ok, I might be overdoing it, but I hope you enjoyed reading this and learned a thing or two.
