Skip to content
Lesson 12.7Loaders, Kernel Services, and Trust Boundaries

How Device Drivers Work

Drivers translate operating-system requests into hardware work through I/O packets, registers, interrupts, and DMA, including the path from key press to game input.

Lesson 12.7
Read or listenPrint chapter or book
About 18 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 12.7Key ideas, reference and questions

A one-page summary of “How Device Drivers Work” you can keep beside your notes. Plain Markdown.

Lesson 12.7 — How Device Drivers Work: cheatsheet

  • Remember: Permission to open a device and validation of one request’s contents are separate checks.
  • Also useful: A device register can act like a doorbell or status window: the same-looking memory access may command hardware rather than store an ordinary byte.

Key terms

  • dispatch routine: The driver's dispatch routine is the function the I/O manager calls with a new IRP.
  • polling: That is polling, and it wastes a processor core on a device that sits idle almost all the time.

Review

  • Q: A developer adds a debugging line to the toy keypad's driver that reads the DATA register every millisecond “just to display it.” What goes wrong? A: Each read removes the event it returns, so key presses shown in the log never reach the program waiting in ReadFile
  • Q: Why does Windows not let two programs drive the same disk controller directly? A: Devices hold shared state, so one program could overwrite another's command or aim a copy engine at it
Download cheatsheet (.md)

Lesson 12.4 showed why ordinary user programs go through operating-system interfaces to reach devices. Something has to turn a request into device work, though. When you press a key, bytes arrive from a keyboard; when a game loads a level, a disk controller delivers the file. The code that operates the device is its device driver: code that knows which registers to read or write and how to exchange commands with the device. The kernel-mode drivers studied here are .sys files in the same PE format as programs and DLLs (Chapter 5), loaded into privileged memory. Windows also supports some user-mode driver arrangements; this toy path follows the kernel-mode kind.

This lesson follows one request from a program down to a device and back, looks at how a driver talks to hardware through registers and interrupts, traces a key press from a USB keyboard to a game, and ends with why Windows is strict about which drivers it will load. Everything here stays conceptual. The next lesson decodes the same requests and registers bit by bit, and its lab runs a pretend device and driver as an ordinary user-mode program.

Suppose two programs could drive the same disk controller directly. One writes the number of the sector it wants; a microsecond later the other overwrites it with a different sector; the first program receives the wrong data. Worse, a program could point the controller’s copying engine at another process’s memory. Devices hold state that everyone shares, and many can copy data to or from physical RAM, so the operating system coordinates who may operate them and which buffers they may reach. The later DMA lesson examines those memory controls in detail.

Instead, Windows coordinates access through a driver stack. The I/O manager checks the caller’s handle and sends the request onward; the receiving driver must still validate the operation, lengths, and buffers it uses. This division matters: an access check on a handle does not prove that the request’s contents are safe for a device.

Windows represents each device as a device object, and several drivers can stack on one device:

  • a bus driver talks to the bus the device is plugged into, such as USB or PCI Express;
  • a function driver knows the device itself and does the real work;
  • optional filter drivers watch or adjust requests on their way past.

A request enters at the top of this driver stack, and each driver either handles it or passes it to the driver below.

A request’s path from a program to a device

Section titled “A request’s path from a program to a device”

A program cannot call a function inside a driver, because the driver lives in ring 0. Instead, the program opens the device much like a file, receives a handle, and makes requests through two general-purpose calls:

  • ReadFile and WriteFile move data from or to the device;
  • DeviceIoControl sends a numbered command, a control code, for anything that is not simply reading or writing, along with an input buffer and an output buffer.

Both travel the system-call path from Lesson 12.4, through kernelbase.dll, ntdll.dll, and syscall, and arrive at the kernel’s I/O manager, the part of the kernel that runs every device request. The I/O manager checks the handle and then describes the request in an I/O request packet, or IRP: a kernel data structure that serves as the request’s work order. It names the operation, carries the parameters and buffers, travels down the driver stack, and comes back up carrying the result.

Here is a read from this lesson’s toy keypad, a simple imaginary device that the lab in Lesson 12.8 simulates, made before any key has been pressed:

Keypad hardwareKeypad driverI/O managerYour programKeypad hardwareKeypad driverI/O managerYour programthe handle may read, sobuild an IRP for a readno key events yet, so parkthe IRP and return STATUS_PENDINGthe calling thread waitsISR acknowledges, DPC readsDATA and fills the IRP's bufferReadFile on the keypad handle, 8-byte bufferdispatch routine receives the IRPSTATUS_PENDINGinterrupt, a key changed statecomplete the IRP with 2 bytesReadFile returns, 2 bytes read

The driver’s dispatch routine is the function the I/O manager calls with a new IRP. If the dispatch routine can finish at once, it completes the IRP, handing it back with a status and a byte count. If it has to wait for the hardware, it keeps the IRP, returns STATUS_PENDING, and completes the IRP later, from the code that runs when the device interrupts. Meanwhile the program’s thread waits inside ReadFile.

A read with no data yet completes after a key interruptThe toy read owns eight bytes of capacity. Only two event bytes are returned after the interrupt.
  1. Request Read with an 8-byte buffer

    Step 1: Caller to Driver

    The request carries room for eight bytes.

  2. Alone No key event yet: park the IRP

    Step 2: Driver (alone)

    There is nothing to return, so the driver keeps the request instead of blocking.

  3. Control STATUS_PENDING

    Step 3: Driver to Caller

    The caller is told the request is not finished.

  4. Data Key interrupt: 2 event bytes

    Step 4: Keypad to Driver

    Later, a key press arrives as an interrupt carrying two bytes.

  5. Reply Copy 2 bytes, complete: count 2

    Step 5: Driver to Caller

    The driver fills the first two of the eight bytes and completes the IRP. The count says how many bytes are meaningful, not how large the buffer was.

The toy read owns eight bytes of capacity. Only two event bytes are returned after the interrupt.

A DeviceIoControl request is checked twice. Each control code declares the rights that the caller’s handle must hold, and the I/O manager refuses a request from a handle without them before the driver ever runs. The IRP also records that the request came from user mode, so the driver must treat every parameter as untrusted: it compares the whole control code with the ones it defines, and it never writes more bytes than the request says the caller’s buffer can hold. Lesson 12.8 unpacks a control code into its four fields.

A device exposes a small set of registers: storage locations inside the device that software reads to learn the device’s state and writes to give it commands. On a modern PC most devices expose their registers through memory-mapped I/O: the registers answer to a range of physical addresses, as if they were memory. When the processor reads or writes one of those addresses, the access goes to the device instead of to RAM. The driver asks the kernel to map that physical range into kernel virtual addresses, and then reads and writes it with ordinary load and store instructions. x86 also has an older mechanism, port I/O, with its own in and out instructions; the classic PS/2 keyboard controller answers at ports 0x60 and 0x64.

The toy keypad, modeled loosely on that PS/2 controller, has four registers, each 4 bytes wide, one after another:

+0x00CONTROL
+0x04STATUS
+0x08DATA
+0x0CACK
driver writes
driver reads
driver writes
The toy keypad's register block. Each register is 4 bytes, so each one starts 4 bytes after the last (0x0C is 12) and the block fills 4 × 4 = 16 bytes.
  • CONTROL is where the driver switches the device on and allows it to interrupt.
  • STATUS tells the driver whether key events are waiting, and how many.
  • DATA holds the event at the front of the device’s queue: a scan code, a number that names a physical key position, plus whether that key was pressed or released. The A key’s scan code is 0x1E.
  • ACK is where the driver tells the device that its interrupt has been handled.

Unlike ordinary RAM, these registers can change because of device activity, and a read can consume data. Reading DATA removes the event it returns, so the next read returns the next event, and a tool that read DATA “just to look” would silently swallow a key press. STATUS can change between two reads with no instruction in between, because the device changes it. So register accesses are never cached, merged, or skipped: the kernel maps device memory as uncacheable, and drivers use dedicated read-register and write-register routines that the compiler must carry out exactly as written. Lesson 12.8 places the four registers at a physical address and decodes every bit in them.

Interrupts: how a device gets the driver’s attention

Section titled “Interrupts: how a device gets the driver’s attention”

The driver could read STATUS over and over until an event appears. That is polling, and it wastes a processor core on a device that sits idle almost all the time. Instead, the keypad raises an interrupt, a hardware signal that makes the processor stop between two instructions and run the handler the kernel registered for that device’s vector (Lesson 12.4).

A driver’s interrupt handling comes in two halves:

  • The interrupt service routine, or ISR, runs immediately, while other work on that core waits, so it does the minimum. It reads STATUS to confirm the interrupt really came from its device, because several devices can share one interrupt line; writes ACK so the device lowers the line; and queues the second half.
  • The deferred procedure call, or DPC, runs moments later at a lower priority, with interrupts enabled again. It does the real work: it drains the events, fills the waiting IRP’s buffer, and completes the IRP.

For a press and release of A, the DPC collects two events, A pressed and A released, and completes the parked read with those 2 bytes. The I/O manager copies them into the program’s buffer and wakes its thread, and ReadFile returns. Lesson 12.8 follows both halves register by register.

DMA: letting the device copy the data itself

Section titled “DMA: letting the device copy the data itself”

Registers suit a keypad, which delivers a byte now and then. A disk or a network card moves megabytes, and fetching them one register read at a time would keep a core busy doing nothing but copying. Such devices use direct memory access, or DMA: the driver tells the device where a buffer is and how big it is, the device copies the data straight into or out of RAM, and it interrupts once when the whole transfer is done.

writes the buffer's address
and length into registers
copies data into RAM,
checked by the IOMMU
one interrupt when
the transfer is done
driver
device
buffer in RAM

The address the driver gives the device is not a virtual address, because a device has no access to any process’s page tables. It is a physical address or, on a machine with an IOMMU, a device address that the IOMMU translates and limits to the buffers the driver approved. Lesson 13.1 explains that translation and why Windows uses it to stop a malicious device from reading all of memory.

A key press, from a USB keyboard to a game

Section titled “A key press, from a USB keyboard to a game”

A real keyboard is more complicated than the toy keypad, but every layer it passes through is a device, a driver, or a queue from this lesson. Lesson 1.2 introduced the keyboard as an input device the game uses through Windows. Here are the steps inside that path, for a USB keyboard on a modern PC:

An input event still has a recipient. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

USB report contains A usage 04
HID path produces the key event
US layout supplies virtual key 41
post WM_KEYDOWN to the foreground queue
the foreground program consumes the event
report whether this game received it

Result: this game observes A down from the delivered message.

Final values: foreground = yes; usb_usage = 04; scan_code = 1E; virtual_key = 41; recipient = game; game_received = true.

Read every step of the lesson’s example
  1. Step 1, line 1. Start with the selected inputs and follow how the next operation changes the state.

    foreground = yes.

  2. Step 2, line 2. The device report enters the operating-system input path.

    foreground = yes; usb_usage = 04.

  3. Step 3, line 3. The worked US-layout path identifies the A key.

    foreground = yes; usb_usage = 04; scan_code = 1E; virtual_key = 41.

  4. Step 4, line 4. The foreground queue belongs to the game.

    foreground = yes; usb_usage = 04; scan_code = 1E; virtual_key = 41; recipient = game.

  5. Step 5, line 5. Only the recipient consumes this window message.

    foreground = yes; usb_usage = 04; scan_code = 1E; virtual_key = 41; recipient = game; game_received = true.

  6. Step 6, line 6. Result: this game observes A down from the delivered message.

    foreground = yes; usb_usage = 04; scan_code = 1E; virtual_key = 41; recipient = game; game_received = true.

Watch it happenA report becomes a queued key press in the game

An eight-byte USB report carrying A usage 04 moves into RAM. A moving event passes through USB and HID drivers, KBDHID scan code 1E, KBDCLASS’s queue, and raw input’s US-layout virtual key 41. WM_KEYDOWN is posted to the foreground queue and consumed by the game, which sees A down.

Read the steps
  1. On the host’s poll, the USB keyboard replies with its eight-byte report. A is HID usage 0x04. The host controller copies the report into RAM by DMA and raises an interrupt.
  2. The USB and HID driver stack interprets the report. The pictured 04 field identifies A; the other report fields are kept as placeholders because this tour follows only that key.
  3. KBDHID maps A’s HID usage 0x04 to Windows scan code 0x1E. The representation changes; it is still the same physical key press.
  4. KBDCLASS queues the key-down event. A queue holds the event until its next consumer can take it; posting an event does not directly execute the game’s input code.
  5. The raw input thread consumes the event and maps scan code 0x1E using the active layout. On the US layout this is VK_A, 0x41, or 4 times 16 plus 1 equals 65.
  6. Windows posts the key event to the foreground thread’s message queue. This route uses WM_KEYDOWN; a game registered for Raw Input instead consumes WM_INPUT.
  7. The game’s GetMessage or PeekMessage loop removes the queued event. Only now does the game observe A down. The report in RAM and the game’s key state remain different representations.

Only A’s known usage is shown as a byte value. “mod” names the modifier field; dashes leave unrelated fields unspecified. Real keyboard layouts can map the same scan code differently.

A USB device never speaks first. The host controller asks each device for news on a fixed schedule, and a keyboard asked 125 times a second is asked every 1,000 ÷ 125 = 8 milliseconds; many gaming keyboards are asked every millisecond. The keyboard answers with a HID report (HID, human interface device, is the USB standard for keyboards, mice, and controllers), which lists the keys held down at that moment by their HID usage numbers. A is usage 0x04.

The host controller writes the report into RAM by DMA and interrupts, and the report climbs the driver stack. The keyboard mapper driver, KBDHID.SYS, converts usage 0x04 into scan code 0x1E, the numbering that Windows keyboard drivers have used since PS/2 keyboards, and the keyboard class driver, KBDCLASS.SYS, queues a key-down event.

The raw input thread, inside win32k, the kernel part of the Windows window manager, reads those events. It translates the scan code into a virtual-key code using the active keyboard layout, updates the key-state table that GetAsyncKeyState reads (Lesson 6.9), and posts the event to the message queue of the thread that owns the foreground window. On a US layout, scan code 0x1E becomes VK_A, 0x41, which is also the ASCII code for the letter A: 4 × 16 + 1 = 65. For security, Windows opens keyboards exclusively for this thread, so an ordinary program cannot read keystrokes straight from the keyboard’s driver.

The game’s message loop then takes the event out with GetMessage or PeekMessage, as a WM_KEYDOWN message, or as WM_INPUT if the game registered for Raw Input. Lesson 12.8 reads the report and the message bit by bit. Synthetic input from SendInput joins this path at the raw input thread, above every driver in the diagram, which is why programs watching input at that level can tell it apart from key presses that came through a keyboard.

Loading a driver puts new code into ring 0, where, as Lesson 12.4 showed, nothing sits beneath it to catch its mistakes and nothing can overrule it. So 64-bit Windows refuses kernel-mode code without a valid digital signature, the kind of signature Lesson 10.8 checked on game files.

A signature answers two questions: who published this driver, and has it changed since? It does not answer the question that matters most: is the code safe? A correctly signed driver can still offer a control code that reads or writes any memory for any caller. Attackers look for exactly those drivers, old and legitimately signed, and load one to borrow its bug. So Windows also keeps a vulnerable driver blocklist: a list of signed drivers known to be exploitable, which it refuses to load.

The same reasoning shows why a cheat that ships as a driver endangers its own user. It runs with the kernel’s full authority and comes from an anonymous author. It often comes with instructions to switch off Secure Boot, the firmware check that lets only signed boot components start, or memory integrity, the Windows feature that refuses to run kernel code it has not checked, or the blocklist. Each of those protects the machine against all kernel malware, not only against cheats. Lesson 12.9 gives the signing rules in full and lists a cheat driver’s risks one by one, then builds a read-only inventory of the drivers your own machine has loaded.

References: example I/O request, security issues for I/O control codes, keyboard and mouse HID client drivers, driver signing policy, and Microsoft recommended driver block rules.

Knowledge check

Registers are not memory

A developer adds a debugging line to the toy keypad's driver that reads the DATA register every millisecond “just to display it.” What goes wrong?

Finished Lesson 12.7?

Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.

Chapter 12: 10 lessons

This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.