0% found this document useful (0 votes)
216 views100 pages

Programming The Microsoft Windows Driver Model 2nd Edition (001-100)

Uploaded by

gaugen
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
0% found this document useful (0 votes)
216 views100 pages

Programming The Microsoft Windows Driver Model 2nd Edition (001-100)

Uploaded by

gaugen
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
Download as pdf or txt
You are on page 1/ 100

PUBLISHED BY

Microsoft Press
A Division of Microsoft Corporation
One Microsoft Way
Redmond, Washington 98052-6399
Copyright © 2003 by Walter Oney
All rights reserved. No part of the contents of this book may be reproduced or transmitted in any form or by any means without
the written permission of the publisher.
Library of Congress Cataloging-in-Publication Data
Oney, Walter.
Programming the Microsoft Windows Driver Model / Walter Oney -- 2nd ed.
p. cm.
Includes index.
ISBN 0-7356-1803-8
1. Microsoft Windows NT device drivers (Computer programs) 2. Computer
programming. I. Title.
QA76.76.D49 O54 2002
005.7'126--dc21 2002038650
Printed and bound in the United States of America.
1 2 3 4 5 6 7 8 9 QWT 8 7 6 5 4 3
Distributed in Canada by H.B. Fenn and Company Ltd.
A CIP catalogue record for this book is available from the British Library.
Microsoft Press books are available through booksellers and distributors worldwide. For further information about
international editions, contact your local Microsoft Corporation office or contact Microsoft Press International directly at fax
(425) 936-7329. Visit our Web site at www.microsoft.com/mspress. Send comments to mspinput@microsoft.com.
Klingon font Copyright 2002, Klingon Language Institute. Active Directory, DirectX, Microsoft, MSDN, MS-DOS, Visual
C++, Visual Studio, Win32, Windows, and Windows NT are either registered trademarks or trademarks of Microsoft
Corporation in the United States and/or other countries. Other product and company names mentioned herein may be the
trademarks of their respective owners.
The example companies, organizations, products, domain names, e-mail addresses, logos, people, places, and events depicted
herein are fictitious. No association with any real company, organization, product, domain name, e-mail address, logo, person,
place, or event is intended or should be inferred.
Acquisitions Editor: Juliana Aldous
Project Editor: Dick Brown
Technical Editor: Jim Fuchs
I

Table of Contents

Acknowledgments IX
Introduction X
1 Beginning a Driver Project -1-
1.1 A Brief History of Device Drivers -1-
1.2 An Overview of the Operating Systems -3-
1.2.1 Windows XP Overview -3-
1.2.2 Windows 98/Windows Me Overview -4-
1.3 What Kind of Driver Do I Need? -6-
1.3.1 WDM Drivers -7-
1.3.2 Other Types of Drivers -8-
1.3.3 Management Overview and Checklist -9-
2 Basic Structure of a WDM Driver - 11 -
2.1 How Drivers Work - 11 -
2.1.1 How Applications Work - 11 -
2.1.2 Device Drivers - 12 -
2.2 How the System Finds and Loads Drivers - 14 -
2.2.1 Device and Driver Layering - 14 -
2.2.2 Plug and Play Devices - 15 -
2.2.3 Legacy Devices - 17 -
2.2.4 Recursive Enumeration - 18 -
2.2.5 Order of Driver Loading - 19 -
2.2.6 IRP Routing - 20 -
2.3 The Two Basic Data Structures - 23 -
2.3.1 Driver Objects - 23 -
2.3.2 Device Objects - 25 -
2.4 The DriverEntry Routine - 27 -
2.4.1 Overview of DriverEntry - 28 -
2.4.2 DriverUnload - 29 -
2.5 The AddDevice Routine - 29 -
2.5.1 Creating a Device Object - 30 -
2.5.2 Naming Devices - 31 -
2.5.3 Other Global Device Initialization - 39 -
2.5.4 Putting the Pieces Together - 42 -
2.6 Windows 98/Me Compatibility Notes - 43 -
2.6.1 Differences in DriverEntry Call - 43 -
2.6.2 DriverUnload - 43 -
2.6.3 The \GLOBAL?? Directory - 43 -
2.6.4 Unimplemented Device Types - 43 -
3 Basic Programming Techniques - 45 -
3.1 The Kernel-Mode Programming Environment - 45 -
3.1.1 Using Standard Run-Time Library Functions - 46 -
3.1.2 A Caution About Side Effects - 46 -
II

3.2 Error Handling - 46 -


3.2.1 Status Codes - 47 -
3.2.2 Structured Exception Handling - 48 -
3.2.3 Bug Checks - 54 -
3.3 Memory Management - 55 -
3.3.1 User-Mode and Kernel-Mode Address Spaces - 55 -
3.3.2 Heap Allocator - 60 -
3.3.3 Linked Lists - 64 -
3.3.4 Lookaside Lists - 67 -
3.4 String Handling - 69 -
3.5 Miscellaneous Programming Techniques - 71 -
3.5.1 Accessing the Registry - 71 -
3.5.2 Accessing Files - 76 -
3.5.3 Floating-Point Calculations - 78 -
3.5.4 Making Debugging Easier - 79 -
3.6 Windows 98/Me Compatibility Notes - 82 -
3.6.1 File I/O - 82 -
3.6.2 Floating Point - 82 -
4 Synchronization - 83 -
4.1 An Archetypal Synchronization Problem - 83 -
4.2 Interrupt Request Level - 85 -
4.2.1 IRQL in Operation - 86 -
4.2.2 IRQL Compared with Thread Priorities - 86 -
4.2.3 IRQL and Paging - 87 -
4.2.4 Implicitly Controlling IRQL - 87 -
4.2.5 Explicitly Controlling IRQL - 88 -
4.3 Spin Locks - 88 -
4.3.1 Some Facts About Spin Locks - 89 -
4.3.2 Working with Spin Locks - 89 -
4.3.3 Queued Spin Locks - 90 -
4.4 Kernel Dispatcher Objects - 91 -
4.4.1 How and When You Can Block - 91 -
4.4.2 Waiting on a Single Dispatcher Object - 92 -
4.4.3 Waiting on Multiple Dispatcher Objects - 93 -
4.4.4 Kernel Events - 94 -
4.4.5 Kernel Semaphores - 96 -
4.4.6 Kernel Mutexes - 97 -
4.4.7 Kernel Timers - 98 -
4.4.8 Using Threads for Synchronization - 101 -
4.4.9 Thread Alerts and APCs - 102 -
4.5 Other Kernel-Mode Synchronization Primitives - 104 -
4.5.1 Fast Mutex Objects - 104 -
4.5.2 Interlocked Arithmetic - 106 -
4.5.3 Interlocked List Access - 108 -
4.5.4 Windows 98/Me Compatibility Notes - 110 -
5 The I/O Request Packet - 111 -
5.1 Data Structures - 111 -
5.1.1 Structure of an IRP - 111 -
III

5.1.2 The I/O Stack - 113 -


5.2 The “Standard Model” for IRP Processing - 114 -
5.2.1 Creating an IRP - 114 -
5.2.2 Forwarding to a Dispatch Routine - 116 -
5.2.3 Duties of a Dispatch Routine - 119 -
5.2.4 The StartIo Routine - 123 -
5.2.5 The Interrupt Service Routine - 124 -
5.2.6 Deferred Procedure Call Routine - 124 -
5.3 Completion Routines - 125 -
5.4 Queuing I/O Requests - 132 -
5.4.1 Using the DEVQUEUE Object - 134 -
5.4.2 Using Cancel-Safe Queues - 136 -
5.5 Cancelling I/O Requests - 140 -
5.5.1 If It Weren’t for Multitasking… - 140 -
5.5.2 Synchronizing Cancellation - 140 -
5.5.3 Some Details of IRP Cancellation - 141 -
5.5.4 How the DEVQUEUE Handles Cancellation - 142 -
5.5.5 Cancelling IRPs You Create or Handle - 146 -
5.5.6 Handling IRP_MJ_CLEANUP - 151 -
5.5.7 Cleanup with a DEVQUEUE - 152 -
5.5.8 Cleanup with a Cancel-Safe Queue - 153 -
5.6 Summary—Eight IRP-Handling Scenarios - 153 -
5.6.1 Scenario 1—Pass Down with Completion Routine - 154 -
5.6.2 Scenario 2—Pass Down Without Completion Routine - 154 -
5.6.3 Scenario 3—Complete in the Dispatch Routine - 155 -
5.6.4 Scenario 4—Queue for Later Processing - 156 -
5.6.5 Scenario 5—Your Own Asynchronous IRP - 157 -
5.6.6 Scenario 6—Your Own Synchronous IRP - 158 -
5.6.7 Scenario 7—Synchronous Pass Down - 159 -
5.6.8 Scenario 8—Asynchronous IRP Handled Synchronously - 160 -
6 Plug and Play for Function Drivers - 163 -
6.1 IRP_MJ_PNP Dispatch Function - 164 -
6.2 Starting and Stopping Your Device - 165 -
6.2.1 IRP_MN_START_DEVICE - 166 -
6.2.2 IRP_MN_STOP_DEVICE - 167 -
6.2.3 IRP_MN_REMOVE_DEVICE - 168 -
6.2.4 IRP_MN_SURPRISE_REMOVAL - 169 -
6.3 Managing PnP State Transitions - 169 -
6.3.1 Starting the Device - 171 -
6.3.2 Is It OK to Stop the Device? - 171 -
6.3.3 While the Device Is Stopped - 172 -
6.3.4 Is It OK to Remove the Device? - 173 -
6.3.5 Synchronizing Removal - 174 -
6.3.6 Why Do I Need This @#$! Remove Lock, Anyway? - 176 -
6.3.7 How the DEVQUEUE Works with PnP - 178 -
6.4 Other Configuration Functionality - 181 -
6.4.1 Filtering Resource Requirements - 181 -
6.4.2 Device Usage Notifications - 182 -
IV

6.4.3 PnP Notifications - 184 -


6.5 Windows 98/Me Compatibility Notes - 191 -
6.5.1 Surprise Removal - 191 -
6.5.2 PnP Notifications - 191 -
6.5.3 The Remove Lock - 191 -
7 Reading and Writing Data - 193 -
7.1 Configuring Your Device - 193 -
7.2 Addressing a Data Buffer - 195 -
7.2.1 Specifying a Buffering Method - 195 -
7.3 Ports and Registers - 198 -
7.3.1 Port Resources - 200 -
7.3.2 Memory Resources - 201 -
7.4 Servicing an Interrupt - 201 -
7.4.1 Configuring an Interrupt - 202 -
7.4.2 Handling Interrupts - 203 -
7.4.3 Deferred Procedure Calls - 204 -
7.4.4 A Simple Interrupt-Driven Device - 207 -
7.5 Direct Memory Access - 211 -
7.5.1 Transfer Strategies - 212 -
7.5.2 Performing DMA Transfers - 213 -
7.5.3 Using a Common Buffer - 222 -
7.5.4 A Simple Bus-Master Device - 224 -
7.6 Windows 98/Me Compatibility Notes - 225 -
8 Power Management - 227 -
8.1 The WDM Power Model - 227 -
8.1.1 The Roles of WDM Drivers - 227 -
8.1.2 Device Power and System Power States - 227 -
8.1.3 Power State Transitions - 228 -
8.1.4 Handling IRP_MJ_POWER Requests - 229 -
8.2 Managing Power Transitions - 231 -
8.2.1 Required Infrastructure - 232 -
8.2.2 Initial Triage - 233 -
8.2.3 System Power IRPs That Increase Power - 233 -
8.2.4 System Power IRPs That Decrease Power - 238 -
8.2.5 Device Power IRPs - 238 -
8.2.6 Flags to Set in AddDevice - 244 -
8.3 Additional Power-Management Details - 245 -
8.3.1 Device Wake-Up Features - 245 -
8.3.2 Powering Off When Idle - 250 -
8.3.3 Using Sequence Numbers to Optimize State Changes - 252 -
8.4 Windows 98/Me Compatibility Notes - 253 -
8.4.1 The Importance of DO_POWER_PAGABLE - 253 -
8.4.2 Completing Power IRPs - 253 -
8.4.3 Requesting Device Power IRPs - 253 -
8.4.4 PoCallDriver - 253 -
8.4.5 Other Differences - 253 -
9 I/O Control Operations - 255 -
V

9.1 The DeviceIoControl API - 255 -


9.1.1 Synchronous and Asynchronous Calls to DeviceIoControl - 256 -
9.1.2 Defining I/O Control Codes - 257 -
9.2 Handling IRP_MJ_DEVICE_CONTROL - 258 -
9.3 METHOD_BUFFERED - 259 -
9.3.1 The DIRECT Buffering Methods - 260 -
9.3.2 METHOD_NEITHER - 261 -
9.3.3 Designing a Safe and Secure IOCTL Interface - 262 -
9.4 Internal I/O Control Operations - 263 -
9.5 Notifying Applications of Interesting Events - 264 -
9.5.1 Using a Shared Event for Notification - 265 -
9.5.2 Using a Pending IOCTL for Notification - 266 -
9.6 Windows 98/Me Compatibility Notes - 269 -
10 Windows Management Instrumentation - 271 -
10.1 WMI Concepts - 271 -
10.1.1 A Sample Schema - 272 -
10.1.2 Mapping WMI Classes to C Structures - 273 -
10.2 WDM Drivers and WMI - 274 -
10.2.1 Delegating IRPs to WMILIB - 275 -
10.2.2 Advanced Features - 280 -
10.3 Windows 98/Me Compatibility Notes - 285 -
11 Controller and Multifunction Devices - 287 -
11.1 Overall Architecture - 287 -
11.1.1 Child Device Objects - 287 -
11.2 Handling PnP Requests - 289 -
11.2.1 Telling the PnP Manager About Our Children - 290 -
11.2.2 PDO Handling of PnP Requests - 290 -
11.2.3 Handling Device Removal - 293 -
11.2.4 Handling IRP_MN_QUERY_ID - 293 -
11.2.5 Handling IRP_MN_QUERY_DEVICE_RELATIONS - 294 -
11.2.6 Handling IRP_MN_QUERY_INTERFACE - 294 -
11.2.7 Handling IRP_MN_QUERY_PNP_DEVICE_STATE - 297 -
11.3 Handling Power Requests - 297 -
11.4 Handling Child Device Resources - 299 -
12 The Universal Serial Bus - 301 -
12.1 Programming Architecture - 301 -
12.1.1 Device Hierarchy - 302 -
12.1.2 What’s in a Device? - 303 -
12.1.3 Information Flow - 304 -
12.1.4 Descriptors - 310 -
12.2 Working with the Bus Driver - 315 -
12.2.1 Initiating Requests - 315 -
12.2.2 Configuration - 317 -
12.2.3 Managing Bulk Transfer Pipes - 323 -
12.2.4 Managing Interrupt Pipes - 329 -
12.2.5 Control Requests - 329 -
12.2.6 Managing Isochronous Pipes - 331 -
VI

12.2.7 Idle Power Management for USB Devices - 340 -


13 Human Interface Devices - 343 -
13.1 Drivers for HID Devices - 343 -
13.2 Reports and Report Descriptors - 343 -
13.2.1 Sample Keyboard Descriptor - 343 -
13.2.2 HIDFAKE Descriptor - 345 -
13.3 HIDCLASS Minidrivers - 346 -
13.3.1 DriverEntry - 346 -
13.3.2 Driver Callback Routines - 347 -
13.3.3 Internal IOCTL Interface - 351 -
13.3.4 IOCTL_HID_GET_DEVICE_DESCRIPTOR - 353 -
13.4 Windows 98/Me Compatibility Notes - 360 -
13.4.1 Handling IRP_MN_QUERY_ID - 360 -
13.4.2 Joysticks - 361 -
14 Specialized Topics - 363 -
14.1 Logging Errors - 363 -
14.1.1 Creating an Error Log Packet - 364 -
14.1.2 Creating a Message File - 365 -
14.2 System Threads - 367 -
14.2.1 Creating and Terminating System Threads - 368 -
14.2.2 Using a System Thread for Device Polling - 369 -
14.3 Work Items - 371 -
14.3.1 Watchdog Timers - 372 -
14.4 Windows 98/Me Compatibility Notes - 375 -
14.4.1 Error Logging - 375 -
14.4.2 Waiting for System Threads to Finish - 375 -
14.4.3 Work Items - 375 -
15 Distributing Device Drivers - 377 -
15.1 The Role of the Registry - 377 -
15.1.1 The Hardware (Instance) Key - 378 -
15.1.2 The Class Key - 379 -
15.1.3 The Driver Key - 380 -
15.1.4 The Service (Software) Key - 380 -
15.1.5 Accessing the Registry from a Program - 381 -
15.1.6 Device Object Properties - 382 -
15.2 The INF File - 383 -
15.2.1 Install Sections - 386 -
15.2.2 Populating the Registry - 388 -
15.2.3 Security Settings - 391 -
15.2.4 Strings and Localization - 392 -
15.2.5 Device Identifiers - 392 -
15.2.6 Driver Ranking - 396 -
15.2.7 Tools for INF Files - 397 -
15.3 Defining a Device Class - 399 -
15.3.1 A Property Page Provider - 400 -
15.4 Customizing Setup - 402 -
15.4.1 Installers and Co-installers - 403 -
VII

15.4.2 Preinstalling Driver Files - 407 -


15.4.3 Value-Added Software - 407 -
15.4.4 Installing a Driver Programmatically - 408 -
15.4.5 The RunOnce Key - 408 -
15.4.6 Launching an Application - 409 -
15.5 The Windows Hardware Quality Lab - 409 -
15.5.1 Running the Hardware Compatibility Tests - 409 -
15.5.2 Submitting a Driver Package - 413 -
15.6 Windows 98/Me Compatibility Notes - 417 -
15.6.1 Property Page Providers - 417 -
15.6.2 Installers and Co-installers - 417 -
15.6.3 Preinstalled Driver Packages - 417 -
15.6.4 Digital Signatures - 418 -
15.6.5 Installing Drivers Programmatically - 418 -
15.6.6 CONFIGMG API - 418 -
15.6.7 INF Incompatibilities - 418 -
15.6.8 Registry Usage - 418 -
15.7 Getting Device Properties - 419 -
16 Filter Drivers - 421 -
16.1 The Role of a Filter Driver - 421 -
16.1.1 Upper Filter Drivers - 421 -
16.1.2 Lower Filter Drivers - 423 -
16.2 Mechanics of a Filter Driver - 424 -
16.2.1 The DriverEntry Routine - 424 -
16.2.2 The AddDevice Routine - 424 -
16.2.3 The DispatchAny Function - 426 -
16.2.4 The DispatchPower Routine - 426 -
16.2.5 The DispatchPnp Routine - 427 -
16.3 Installing a Filter Driver - 428 -
16.3.1 Installing a Class Filter - 428 -
16.3.2 Installing a Device Filter with the Function Driver - 430 -
16.3.3 Installing a Device Filter for an Existing Device - 430 -
16.4 Case Studies - 430 -
16.4.1 Lower Filter for Traffic Monitoring - 430 -
16.4.2 Named Filters - 431 -
16.4.3 Bus Filters - 433 -
16.4.4 Keyboard and Mouse Filters - 433 -
16.4.5 Filtering Other HID Devices - 435 -
16.5 Windows 98/Me Compatibility Notes - 435 -
16.5.1 WDM Filters for VxD Drivers - 435 -
16.5.2 INF Shortcut - 436 -
16.5.3 Class Filter Drivers - 436 -
A Coping with Cross-Platform Incompatibilities - 437 -
Determining the Operating System Version - 437 -
Run-Time Dynamic Linking - 437 -
Checking Platform Compatibility - 438 -
Defining Win98/Me Stubs for Kernel-Mode Routines - 439 -
Version Compatibility - 440 -
VIII

Stub Functions - 440 -


Using WDMSTUB - 442 -
Interaction Between WDMSTUB and WDMCHECK - 442 -
Special Licensing Note - 442 -
B Using WDMWIZ.AWX - 443 -
Basic Driver Information - 443 -
DeviceIoControl Codes - 444 -
I/O Resources - 445 -
USB Endpoints - 445 -
WMI Support - 446 -
Parameters for the INF File - 447 -
Now What? - 448 -
IX

Acknowledgments

Many people helped me write this book. At the beginning of the project, Anne Hamilton, Senior Acquisitions Editor at
Microsoft Press, had the vision to realize that a revision of this book was needed. Juliana Aldous, the Acquisitions Editor,
shepherded the project through to the complete product you're holding in your hands. Her team included Dick Brown, Jim
Fuchs, Shawn Peck, Rob Nance, Sally Stickney, Paula Gorelick, Elizabeth Hansford, and Julie Kawabata. That the grammar
and diction in the book are correct, that the figures are correctly referenced and intelligible, and that the index accurately
correlates with the text are due to all of them.
Marc Reinig and Dr. Lawrence M. Schoen provided valuable assistance with a linguistic and typographical issue.
Mike Tricker of Microsoft deserves special thanks for championing my request for a source code license, as does Brad
Carpenter for his overall support of the revision project.
Eliyas Yakub acted as the point man to obtain technical reviews of the content of the book and to facilitate access to all sorts of
resources within Microsoft. Among the developers and managers who took time from busy schedules to make sure that this
book would be as accurate as possible are—in no particular order—Adrian Oney (no relation, but I'm fond of pointing out his
vested interest in a book that has his name on the spine), Allen Marshall, Scott Johnson, Martin Borve, Jean Valentine, Doron
Holan, Randy Aull, Jake Oshins, Neill Clift, Narayanan Ganapathy, Fred Bhesania, Gordan Lacey, Alan Warwick, Bob Fruth,
and Scott Herrboldt.
Lastly, my wife, Marty, provided encouragement and support throughout the project.
X

Introduction

This book explains how to write device drivers for the newest members of the Microsoft Windows family of operating systems
using the Windows Driver Model (WDM). In this Introduction, I'll explain who should be reading this book, the organization
of the book, and how to use the book most effectively. You'll also find a note on errors and a section on other resources you can
use to learn about driver programming. Looking ahead, Chapter 1 explains how the two main branches of the Windows family
operate internally, what a WDM device driver is, and how it relates to the rest of Windows.

Who Should Read This Book


I've aimed this book at experienced programmers who don't necessarily know anything about writing device drivers for
Windows operating systems. This book is for you if you want to learn how to do that. To succeed at driver writing, you will
need to understand the C programming language very well because WDM drivers are written in C. You'll also need to be
exceptionally able to tolerate ambiguity and to reverse-engineer portions of the operating system because a good deal of trial
and error in the face of incomplete or inaccurate information is required.
Writing a WDM driver is much like writing a kernel-mode driver for Windows NT4.0. It's a bit easier because you don't have
to detect and configure your own hardware. Ironically, it's simultaneously harder because correctly handling Plug and Play and
power management is fiendishly difficult. If you've written kernel-mode drivers for Windows NT, you'll have no trouble at all
reading this book. You'll also be glad to have some code samples that you can cut and paste to deal with the aforementioned
fiendishly difficult areas.
Writing a WDM driver is completely unlike writing a virtual device driver (VxD) for Windows 3.0 and its successors, a UNIX
driver, or a real-mode driver for MS-DOS. If your experience lies in those areas, expect to work hard learning this new
technology. Nonetheless, I think programming WDM drivers is easier than programming those other drivers because you have
more rules to follow, leading to fewer choices between confusing alternatives. Of course, you have to learn the rules before you
can benefit from that fact.
If you already own a copy of the first edition of this book and are wondering whether you should buy this revised edition,
here's a bit of information to help you decide. Windows XP and Windows Me made few changes in the way you develop
drivers for Windows 2000 and Windows 98, respectively. The main reason we decided to revise this book is that so many
changes had accumulated on my update/errata Web page. This edition does, of course, explain some of the new bells and
whistles that Windows XP brings with it. It contains more explicit advice about writing robust, secure drivers. It also, frankly,
explains some things much better than the first edition does.
Chapter 1 has some information that will be useful to development managers and others who need to plan hardware projects.
It's very embarrassing to be brought up short near the end of a hardware development project by the realization that you need a
driver. Sometimes you'll be able to find a generic driver that will handle your hardware. Often, however, such a driver won't
exist and you'll need to write one yourself. I hope to convince you managers in the first chapter that writing drivers is pretty
hard and deserves your attention earlier rather than later. When you're done reading that chapter, by the way, give the book to
the person who's going to carry the oar. And buy lots more copies. (As I told one of my college friends, you can always use the
extra copies as dining room chair extenders for a young family.)

Organization of This Book


After teaching driver programming seminars for many years, I've come to understand that people learn things in fundamentally
different ways. Some people like to learn a great deal of theory about something and then learn how to apply that theory to
practical problems. Other people like to learn practical things first and then learn the general theory. I call the former approach
deductive and the latter approach inductive. I personally prefer an inductive approach, and I've organized this book to suit that
style of learning.
My aim is to explain how to write device drivers. Broadly speaking, I want to provide the minimum background you'll need to
write an actual driver and then move on to more specialized topics. That "minimum background" is pretty extensive, however;
it consumes seven chapters. Once past Chapter 7, you'll be reading about topics that are important but not necessarily on the
fall line that leads straight downhill to a working driver.
Chapter 1, "Beginning a Driver Project," as I've mentioned, describes WDM device drivers and how they relate to Windows
itself. Along the way, I'll relate the story of how we got to where we are today in operating system and driver technology. The
chapter also explains how to choose the kind of driver you need, provides an overview and checklist specifically for
development managers, and addresses the issue of binary compatibility.
Chapter 2, "Basic Structure of a WDM Driver," explains the basic data structures that Windows 2000 uses to manage I/O
devices and the basic way your driver relates to those data structures. I'll discuss the driver object and the device object. I'll
also discuss how you write two of the subroutines—the DriverEntry and AddDevice routines—that every WDM driver
package contains.
Driver Security and Reliability>>>> XI

Chapter 3, "Basic Programming Techniques," describes the most important service functions you can call on to perform
mundane programming tasks. In that chapter, I'll discuss error handling, memory management, and a few other miscellaneous
tasks.
Chapter 4, "Synchronization," discusses how your driver can synchronize access to shared data in the multitasking,
multiprocessor world of Windows XP. You'll learn the details about interrupt request level (IRQL) and about various
synchronization primitives that the operating system offers for your use.
Chapter 5, "The I/O Request Packet," introduces the subject of input/output programming, which of course is the real reason
for this book. I'll explain where I/O request packets come from, and I'll give an overview of what drivers do with them when
they follow what I call the "standard model" for IRP processing. I'll also discuss the knotty subject of IRP queuing and
cancellation, wherein accurate reasoning about synchronization problems becomes crucial.
Chapter 6, "Plug and Play for Function Drivers," concerns just one type of I/O request packet, namely IRP_MJ_PNP. The Plug
and Play Manager component of the operating system sends you this IRP to give you details about your device's configuration
and to notify you of important events in the life of your device.
Chapter 7, "Reading and Writing Data," is where we finally get to write driver code that performs I/O operations. I'll discuss
how you obtain configuration information from the PnP Manager and how you use that information to prepare your driver for
"substantive" IRPs that read and write data. I'll present two simple driver sample programs as well: one for dealing with a PIO
device and one for dealing with a bus-mastering DMA device.
Chapter 8, "Power Management," describes how your driver participates in power management. I think you'll find, as I did,
that power management is pretty complicated. Unfortunately, you have to participate in the system's power management
protocols, or else the system as a whole won't work right. Luckily, the community of driver writers already has a grand
tradition of cutting and pasting, and that will save you.
Chapter 9, "I/O Control Operations," contains a discussion of this important way for applications and other drivers to
communicate "out of band" with your driver.
Chapter 10, "Windows Management Instrumentation," concerns a scheme for enterprisewide computer management in which
your driver can and should participate. I'll explain how you can provide statistical and performance data for use by monitoring
applications, how you can respond to standard WMI controls, and how you can alert controlling applications of important
events when they occur.
Chapter 11, "Controller and Multifunction Devices," discusses how to write a driver for a device that embodies multiple
functions, or multiple instances of the same function, in one physical device.
Chapter 12, "The Universal Serial Bus," describes how to write drivers for USB devices.
Chapter 13, "Human Interface Devices," explains how to write a driver for this important class of devices.
Chapter 14, "Specialized Topics," describes system threads, work items, error logging, and other special programming topics.
Chapter 15, "Distributing Device Drivers," tells you how to arrange for your driver to get installed on end user systems. You'll
learn the basics of writing an INF file to control installation, and you'll also learn some interesting and useful things to do with
the system registry. This is where to look for information about WHQL submissions too.
Chapter 16, "Filter Drivers," discusses when you can use filter drivers to your advantage and how to build and install them.
Appendix A, "Coping with Cross-Platform Incompatibilities," explains how to determine which version of the operating
system is in control and how to craft a binary-compatible driver.
Appendix B, "Using WDMWIZ.AWX," describes how to use my Visual C++ application wizard to build a driver.
WDMWIZ.AWX is not intended to take the place of a commercial toolkit. Among other things, that means that it's not easy
enough to use that you can dispense with documentation.

Driver Security and Reliability


Software security and reliability is everybody's job. Those of us who write drivers have a special responsibility because our
code runs in the trusted kernel. When our code crashes, it usually takes the whole system with it. When our code has a trap
door, a hacker can squeeze through to take over the whole system and, perhaps, the enterprise it serves. It behooves all of us to
take these issues seriously. If we don't, real people can suffer economic and physical injury.
Because of the seriousness of security issues in driver programming, this edition uses a special icon to highlight areas that
are especially important to driver reliability and security.
The Driver Verifier component of the operating system performs a variety of checks on a driver—if we ask it to. The
Windows Hardware Quality Laboratory (WHQL) will run your driver with all sorts of Driver Verifier tests enabled, so
you might as well beat them to it by enabling Driver Verifier as soon as your driver is minimally functional. We'll use this icon
to mark discussions of how the Driver Verifier can help you debug your driver.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
XII <<<<Introduction

Sample Files
You can find sample files for this book at the Microsoft Press Web site at http://www.microsoft.com/mspress/books/6262.asp.
Clicking the Companion Content link takes you to a page from which you can download the sa mples. You can also find the
files on the book's companion CD.
This book’s companion content contains a great many sample drivers and test programs. I crafted each sample with a view
toward illustrating a particular issue or technique that the text discusses. Each of the samples is, therefore, a “toy” that you
can’t just ship after changing a few lines of code. I wrote the samples this way on purpose. Over the years, I've observed that
programmer-authors tend to build samples that illustrate their prowess at overcoming complexity rather than samples that teach
beginners how to solve basic problems, so I won’t do that to you. Chapter 7 and Chapter 12 have some drivers that work with
“real” hardware, namely development boards from the makers of a PCI chip set and a USB chip set. Apart from that, however,
all the drivers are for nonexistent hardware.
In nearly every case, I built a simple user-mode test program that you can use to explore the operation of the sample driver.
These test programs are truly tiny: they contain just a few lines of code and are concerned with only whatever point the driver
sample attempts to illustrate. Once again, I think it’s better to give you a simple way to exercise the driver code that I assume
you’re really interested in instead of trying to show off every MFC programming trick I’ve ever learned.
You’re free to use all the sample code in this book in your own projects without paying me or anyone else a royalty. (Of course,
you must consult the detailed license agreement at the end of this book—this paraphrase is not intended to override that
agreement in any way.) Please don’t ship GENERIC.SYS to your customers, and please don't ship a driver that calls functions
from GENERIC.SYS. The GENERIC.CHM help file in the companion content contains instructions on how to rename
GENERIC to something less, well, generic. I intend readers to ship WDMSTUB.SYS and the AutoLaunch.exe modules, but
I’ll ask you to execute a royalty-free license agreement before doing so. Simply e-mail me at waltoney@oneysoft.com, and I’ll
tell you what to do. The license agreement basically obligates you to ship only the latest version of these components with an
installation program that will prevent end users from ending up with stale copies.
About the Companion CD
The CD that comes with this book contains the complete source code and an executable copy of each sample. To access those
files, insert the companion CD in your computer’s CD-ROM drive, and make a selection from the menu that appears. If the
AutoRun feature isn’t enabled on your system (if a menu doesn’t appear when you insert the disc in your computer’s CD-ROM
drive), run StartCD.exe in the root folder of the companion CD. Installing the sample files on your hard disk requires
approximately 50 MB of disk space.
The companion CD also contains a few utility programs that you might find useful in your own work. Open the file
WDMBOOK.HTM in your Web browser for an index to the samples and an explanation of how to use these tools.
The setup program on the CD gives you the option to install all the samples on your own disk or to leave them on the CD.
However, setup will not actually install any kernel-mode components on your system. Setup will ask your permission to add
some environment variables to your system. The build procedure for the samples relies on these environment variables. They
will be correctly set immediately on Windows XP and the next time you reboot Windows 98/Windows Me.
If your computer runs both Windows XP and Windows 98/Windows Me, I recommend performing a full install under both
operating systems so that the registry and the environment are correctly set up in both places. Run the setup program from the
installed sample directory the second time too, to avoid useless file copying. It isn’t necessary or desirable to specify different
target directories for the two installations.
Each sample includes an HTML file that explains (very briefly)
what the sample does, how to build it, and how to test it. I
recommend that you read the file before trying to install the
sample because some of the samples have unusual installation
requirements. Once you’ve installed a sample driver, you’ll find
that the Device Manager has an extra property page from which
you can view the same HTML file, as shown here:
How the Samples Were Created
There’s a good reason why my sample drivers look as though
they all came out of a cookie cutter: they did. Faced with so
many samples to write, I decided to write a custom application
wizard. The wizard functionality in Microsoft Visual C++
version 6.0 is almost up to snuff for building a WDM driver
project, so I elected to depend on it. The wizard is named
WDMWIZ.AWX, and you’ll find it in the companion content.
I’ve documented how to use it in Appendix B. Use it, if you
want, to construct the skeletons for your own drivers. But be
aware that this wizard is not of product grade—it’s intended to
help you learn about writing drivers rather than to replace or compete with a commercial toolkit. Be aware too that you need to
change a few project settings by hand because the wizard support is only almost what’s needed. Refer to the
WDMBOOK.HTM in the root directory of the companion CD for more information.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
System Requirements>>>>> XIII

Building the Samples


I vastly prefer using the Microsoft Visual Studio 6.0 integrated development environment for driver projects. If you share this
preference, you can follow suit when you work with my samples. The WDMBOOK.HTM file in the companion content
contains detailed instructions about how to set up the development environment. I’m deliberately not repeating those
instructions here because they may change in the future. Each sample also includes a standard SOURCES file for use with the
Driver Development Kit (DDK) build environments, in case your preference lies in that direction.
Updates to the Samples
At my Web site,http://www.oneysoft.com, you’ll find a page concerning service packs for the sample drivers. In the three years
since the first edition was printed, I issued about a dozen service packs. Service packs fix bugs and offer new samples. If you
install my sample drivers, I recommend that you also install each new service pack as it comes out.
If you want to find out when a new service pack is available, you can fill out a simple online form to be added to my mailing
list. First edition subscribers needn’t reregister, by the way: you’re all grandfathered in.
GENERIC.SYS
A WDM driver contains a great deal of code that you could call boilerplate for handling Plug and Play and power management.
This code is long. It’s boring. It’s easy to get wrong. My samples all rely on what amounts to a kernel-mode DLL named
GENERIC.SYS. WDMWIZ.AWX will build a project that uses GENERIC.SYS or that doesn’t, as you specify.
GENERIC.CHM in the companion content details the support functions that GENERIC.SYS exports, in case you want to use
them yourself.
The downside to my using GENERIC all over the place is that I managed to obscure how some crucial things occur in the
driver. The drivers that use GENERIC delegate all of the IRP_MJ_PNP (see Chapter 6) and IRP_MJ_POWER (see Chapter 8)
handling to GENERIC, which then calls back to driver-specific routines to handle details. The following table describes the
important callback functions.

IRP Type Callback Function Purpose


Start the device (map memory registers, connect interrupt, and so
IRP_MJ_PNP StartDevice
on).
Halt device and release I/O resources (unmap memory registers,
StopDevice
disconnect interrupt, and so on).
Undo steps performed in AddDevice (disconnect from lower device
RemoveDevice
object, delete device object, and so on).
(Optional) Is it OK to stop this device now (used while processing
OkayToStop
IRP_MN_QUERY_STOP_DEVICE)?
(Optional) Is it OK to remove this device now (used while processing
OkayToRemove
IRP_MN_QUERY_REMOVE_DEVICE)?
(Optional) Take any required action to force pending operations to
FlushPendingIo
finish in the near future.
(Optional) Is a proposed change in device power OK (used while
IRP_MJ_POWER QueryPower
processing IRP_MN_QUERY_POWER)?
(Optional) Save any device context that will be lost during a period of
SaveDeviceContext
low power.
RestoreDeviceContext (Optional) Restore device context after a period of low power.
(Optional) Get device power state corresponding to a given system
GetDevicePowerState
power state.

System Requirements
To run the sample programs in the companion content, you’ll need a computer running Windows 98 Second Edition, Windows
Me, Windows 2000, Windows XP, or any later version of Windows. Some of the samples require a USB port and an EZ-USB
development kit from Cypress Semiconductors. Two of the samples require an ISA expansion slot and an S5933-DK
development board (or equivalent) from Applied Micro Circuits Corporation.
To build the sample programs, you’ll need a set of software tools that will change over time whenever I issue service packs.
The file WDMBOOK.HTM describes the requirements and will be updated when requirements change. At the time this book
is published, you’ll need the following:
„ The Microsoft Windows .NET DDK.
„ Microsoft Visual Studio 6.0. Any edition will do, and it doesn’t matter whether you’ve installed any of the service packs.
When you’re building the driver samples, you’ll be using just the integrated development environment provided by
Visual Studio. The compiler and other build tools will be coming from the DDK.
„ For one of the samples only (PNPMON), the Windows 98 DDK.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
XIV <<<<Introduction

If you have to use Windows 98 or Windows Me as your only build and test environment, you’ll also need to obtain a copy of
the Windows DDK for a pre-.NET platform. Microsoft denied me permission to distribute a version of the resource compiler
that would work on Windows 98/Windows Me or a cross-platform-compatible version of USBD.LIB. Grab these from
wherever you can find them before Microsoft stops supporting earlier versions of the DDK. Bear in mind that drivers built on
Windows 98/Windows Me might not run on Windows 2000 and later platforms due to an error in checksum computation in the
image helper DLL.

Support
Every effort has been made to ensure the accuracy of this book and the contents of the companion content. Microsoft Press
provides corrections for books through the World Wide Web at the following address:
http://www.microsoft.com/mspress/support
To connect directly to the Microsoft Press Knowledge Base and enter a query regarding a question or an issue that you might
have, go to:
http://www.microsoft.com/mspress/support/search.asp
If you have comments, questions, or ideas regarding this book or the companion content, or questions that aren’t answered by
querying the Knowledge Base, please send them to Microsoft Press by e-mail to:
mspinput@microsoft.com
Or by postal mail to:
Microsoft Press
Attn:
Programming Microsoft SQL Server 2000 with Microsoft Visual Basic .NET
Editor
One Microsoft Way
Redmond, WA 98052-6399

Please note that product support is not offered through the preceding mail address. For product support information, please
visit the Microsoft Support Web site at:
http://support.microsoft.com
Note on Errors
Despite heroic attention to detail, I and the editors at Microsoft Press let a few errors slip by from my original manuscript to
the finished first edition of this book. I overlooked a few technical things, slipped up on some others, and learned about still
others after the book was in print. My personal favorite was the “Special Sauce” layer in Figure 3-1, which was a typically
lame attempt to introduce humor into the editorial process that went awry when the original draft of the figure made it into the
finished book. At any rate, my errata/update Web page has grown to about 30 printed pages, and my desire to start over at zero
was one of the main reasons for this edition.
But, sigh, there will still be corrections and updates to be made to this edition too. I’ll continue to publish updates and errata at
http://www.oneysoft.com for at least the next couple of years. I recommend you go there first and often to stay up-to-date. And
please send me your comments and questions so that I can correct as many errors as possible.

Other Resources
This book shouldn’t be the only source of information you use to learn about driver programming. It emphasizes the features
that I think are important, but you might need information I don’t provide, or you might have a different way of learning than I
do. I don’t explain how the operating system works except insofar as it bears on what I think you need to know to effectively
write drivers. If you’re a deductive learner, or if you simply want more theoretical background, you might want to consult one
of the additional resources listed next. If you’re standing in a bookstore right now trying to decide which book to buy, my
advice is to buy all of them: a wise craftsperson never skimps on his or her tools. Besides, you can never tell when a young
dinner guest may need help reaching the table.
Books Specifically About Driver Development
Art Baker and Jerry Lozano, The Windows 2000 Device Driver Book: A Guide for Programmers, 2nd edition (Prentice Hall,
2001). Quite readable. Some errors survive from the first edition.
Edward N. Dekker and Joseph M. Newcomer, Developing Windows NT Device Drivers: A Programmer’s Handbook
(Addison-Wesley, 1999). A fine book with a fine sense of humor. Written just before WDM came out, so not much coverage of
that.
Rajeev Nagar, Windows NT File System Internals: A Developer’s Guide (O’Reilly & Associates, 1997). Nothing at all to do

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
About the Author XV

with WDM, but the only book that attempts to explain the internals of the Windows NT file system.
Peter G. Viscarola and W. Anthony Mason, Windows NT Device Driver Development (Macmillan, 1998). Technical and
authoritative. A WDM edition is supposedly coming someday.
Other Useful Books
Michael Howard and David LeBlanc, Writing Secure Code (Microsoft Press, 2001). Exceptionally detailed and readable
discussion of security issues in applications. I’ll be reiterating many of Writing Secure Code’s lessons throughout this book.
Gary Nebbett, Windows NT/2000 Native API Reference (MacMillan, 2000). Detailed exposition of the underdocumented native
API.
David A. Solomon and Mark E. Russinovich, Inside Windows 2000, Third Edition (Microsoft Press, 2000). All about the
operating system. How come they got their pictures on the cover, inquiring minds would like to know?
Magazines
Old editions of Microsoft Systems Journal and Windows Developer Journal contain many articles about driver programming.
Both of the magazines have gone to that Great Publishers Clearinghouse in the sky, however, and I can’t speak for how well or
often their successors cover driver issues.
Online Resources
The comp.os.ms-windows.programmer.nt.kernel-mode newsgroup provides a forum for technical discussion on kernel-mode
programming issues. On the msnews.microsoft.com server, you can subscribe to microsoft.public.development.device.drivers.
You can find mailing list servers for file system and driver programming issues by going to http://www.osr.com.
Roedy Green, “How to Write Unmaintainable Code” (2002), which I found at http://www.mindprod.com/unmain.html.
Seminars and Development Services
I conduct public and on-site seminars on WDM programming. Visit my Web site at http://www.oneysoft.com for more
information and schedules. I also develop custom drivers for hardware manufacturers all over the world. I promise this is the
only commercial in the book. (Not counting the back cover of the book, that is, which is full of statements aimed at getting you
to buy the book and whose correspondence, if any, to reality will become susceptible to evaluation only if you succumb and
actually read the book.)

About the Author


Walter Oney has 35 years of experience in systems-level programming and has been teaching Windows device driver classes
for 10 years. He was a contributing editor to Microsoft Systems Journal during its heyday and is a Microsoft MVP. He has
written several books, including Systems Programming for Windows 95 and the first edition of Programming the Microsoft
Windows Driver Model. In his free time, he’s a committed jogger, a fan of classical dance, and an amateur oboist. He and his
wife, Marty, live in Boston, Massachusetts.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
-1-

Chapter 1
1 Beginning a Driver Project

In this chapter, I’ll present an overview of the driver writing process. My own personal involvement with personal computing
dates from the mid-1980s, when IBM introduced its personal computer (PC) with MS-DOS as the operating system. Decisions
made by IBM and Microsoft that long ago are still being felt today. Consequently, a bit of historical perspective will help you
understand how to program device drivers.
Windows Driver Model (WDM) drivers run in two radically different operating system environments, and I’ll provide an
overview of the architecture of these environments in this chapter. Windows XP, like Windows 2000 and earlier versions of
Windows NT, provides a formal framework in which drivers play well-defined roles in carrying out I/O operations on behalf of
applications and other drivers. Windows Me, like Windows 9x and Windows 3.x before it, is a more freewheeling sort of
system in which drivers play many roles.
The first step in any driver project is to decide what kind of driver you need to write—if indeed you need to write one at all.
I’ll describe many different classes of device in this chapter with a view toward helping you make this decision.
Finally I’ll round out the chapter with a management checklist to help you understand the scope of the project.

1.1 A Brief History of Device Drivers


The earliest PCs ran on an Intel processor chip that provided addressability for 640 KB of “real” memory—so called because
the memory was really there in the form of memory chips that the processor addressed directly by means of a 20-bit physical
address. The processor itself offered just one mode of operation, the so-called real mode, wherein the processor combined
information from two 16-bit registers to form a 20-bit memory address for every instruction that referenced memory. The
computer architecture included the concept of expansion slots that brave users could populate with cards purchased separately
from the computer itself. The cards themselves usually came with instructions about how to set DIP switches (later, jumpers
between pins) in order to make slight changes in I/O configuration. You had to keep a map of all the I/O and interrupt
assignments for your PC in order to do this correctly. MS-DOS incorporated a scheme based on the CONFIG.SYS file whereby
the operating system could load real-mode device drivers for original equipment and for add-on cards. Inevitably, these drivers
were programmed in assembly language and relied to a greater or lesser extent on the INT instruction to talk to the BIOS and
to system services within MS-DOS itself. End users perforce learned how to invoke applications via commands. Application
programmers perforce learned how to program the video display, keyboard, and mouse directly because neither MS-DOS nor
the system BIOS did so adequately.
Later on, IBM introduced the AT class of personal computers based on the Intel 80286 processor. The 286 processor added a
protected mode of operation wherein programs could address up to 16 MB of main and extended memory using a 24-bit
segment address (specified indirectly via a segment selector in a 16-bit segment register) and a 16-bit offset. MS-DOS itself
remained a real-mode operating system, so several software vendors built DOS extender products to allow programmers to
migrate their real-mode applications to protected mode and gain access to all the memory that was becoming available on the
market. Since MS-DOS was still in charge of the computer, driver technology didn’t advance at this point.
The watershed change in PC technology occurred—in my view, anyway—when Intel released the 80386 processor chip. The
386 allowed programs to access up to 4 GB of virtual memory addressed indirectly via page tables, and it allowed programs to
easily use 32-bit quantities for arithmetic and addressing. There was a flurry of activity in the software tools market as
compiler vendors and DOS extender companies raced to capture the ever-growing volume of large applications hungry for
memory and processor speed. Device drivers were still 16-bit real-mode programs written in assembly language and installed
via CONFIG.SYS, and end users still needed to manually configure cards.
Subsequent advances in processor chips have been mainly in the area of performance and capacity. As I write this chapter,
computers operating faster than 1 GHz with 50-GB hard drives and 512 MB (or more) of memory are commonplace and easily
affordable by large segments of the population.
In parallel with the evolution of the platform, another evolution was occurring with operating system technology. Most people,
even including programmers of system software, prefer graphics-based ways of interacting with computers to character-based
ways. Microsoft was late to the graphical operating system party—Apple beat them with the first Macintosh—but has come to
dominate it with the Windows family of operating systems. In the beginning, Windows was just a graphical shell for real-mode
MS-DOS. Over time, a collection of Windows drivers for common hardware, including the display, keyboard, and mouse,
came into existence. These drivers were executable files with a .DRV extension, and they were written primarily in assembly
language.
With the advent of the AT class of computer, Microsoft added a protected-mode version of Windows. Microsoft ported the
real-mode .DRV drivers to protected mode as well. Hardware other than the standard Windows devices (the display, keyboard,
and mouse) continued to be handled by real-mode MS-DOS drivers.
-2- Beginning a Driver Project | Chapter 1

Finally, some time after PCs with 386 processors became widely available, Microsoft released Windows 3.0, whose
“enhanced” mode of operation took full advantage of the virtual memory capabilities. Even so, it was still true that every new
piece of hardware needed a real-mode driver. But now there was a big problem. To support multitasking of MS-DOS
applications (a requirement for end user acceptance of Windows), Microsoft had built a virtual-machine operating system.
Each MS-DOS application ran in its own virtual machine, as did the Windows graphical environment. But all those MS-DOS
applications were trying to talk directly to hardware by issuing IN and OUT instructions, reading and writing device memory,
and handling interrupts from the hardware. Furthermore, two or more applications sharing processor time could be issuing
conflicting instructions to the hardware. They would certainly conflict over use of the display, keyboard, and mouse, of course.
To allow multiple applications to share physical hardware, Microsoft introduced the concept of a virtual device driver, whose
broad purpose is to “virtualize” a hardware device. Such drivers were generically called VxDs because most of them had
filenames fitting the pattern VxD.386, where x indicated the type of device they managed. Using this concept, Windows 3.0
created the appearance of virtual machines outfitted with separate instances of many hardware devices. But the devices
themselves continued, in most cases, to be driven by real-mode MS-DOS drivers. A VxD’s role was to mediate application
access to hardware by first intercepting the application’s attempts to touch the hardware and briefly switching the processor to
a sort of real mode called virtual 8086 mode to run the MS-DOS driver.
Not to put too fine a face on it, mode switching to run real-mode drivers was a hack whose only virtue was that it allowed for a
reasonably smooth growth in the hardware platform and operating system. Windows 3.0 had many bugs whose root cause was
that very feature of the architecture. Microsoft’s answer was to be OS/2, which it was developing in harmony (using a
twentieth-century definition of harmony, that is) with IBM.
Microsoft’s version of OS/2 became Windows NT, whose first public release was in the early 1990s, shortly after Windows 3.1.
Microsoft built Windows NT from the ground up with the intention of making it a durable and secure platform on which to run
Windows. Drivers for Windows NT used a brand-new kernel-mode technology that shared practically nothing with the other
two driver technologies then in vogue. Windows NT drivers used the C programming language almost exclusively so that they
could be recompiled for new CPU architectures without requiring any source changes.
Another thing happened along about the Windows 3.0 time frame that has an important ramification for us today. Windows 3.0
formally divided the software world into user-mode and kernel-mode programs. User-mode programs include all the
applications and games that people buy computers to run, but they are not to be trusted to deal robustly (or even honestly) with
hardware or with other programs. Kernel-mode programs include the operating system itself and all the device drivers that
people like you and me write. Kernel-mode programs are fully trusted and can touch any system resource they please.
Although Windows 3.0 segregated programs by their mode of operation, no version of Windows (not even Windows Me) has
actually put memory protection in place to yield a secure system. Security is the province of Windows NT and its successors,
which do forbid user-mode programs from seeing or changing the resources managed by the kernel.
Computing power didn’t really advance to the point where an average PC could run Windows NT well until quite recently.
Microsoft therefore had to keep the Windows product line alive. Windows 3.0 grew into 3.1, 3.11, and 95. Starting with
Windows 95, if you wanted to write a device driver, you would write something called a VxD that was really just a 32-bit
protected-mode driver. Also starting with Windows 95, end users could throw away their I/O maps because the new Plug and
Play feature of the operating system identified and configured hardware somewhat automatically. As a hardware maker, though,
you might have had to write a real-mode driver to keep happy those of your customers who weren’t upgrading from Windows
3.1. Meanwhile, Windows NT grew into 3.5, 4.0. You would have needed a third driver to support these systems, and not much
of your programming knowledge would have been portable between projects.
Enough was enough. Microsoft designed a new technology for device drivers, the Windows Driver Model (WDM), and put it
into Windows 98 and Windows Me, the successors to Windows 95. They also put this technology into Windows 2000 and
Windows XP, the successors to Windows NT 4.0. By the time of Windows Me, MS-DOS was present only by courtesy and
there was finally no need for a hardware maker to worry about real-mode device drivers. Because WDM was, at least by
original intention, practically the same on all platforms, it became possible to write just one driver.
To summarize, we stand today in the shadow of the original PC architecture and of the first versions of MS-DOS. End users
still occasionally have to open the skin of their PCs to install expansion cards, but we use a different and more powerful bus
nowadays than we did originally. Plug and Play and the Peripheral Component Interconnect (PCI) bus have largely removed
the need for end users to keep track of I/O, memory, and interrupt request usage. There is still a BIOS in place, but its job
nowadays is mostly to boot the system and to inform the real operating system (Windows XP or Windows Me) about
configuration details discovered along the way. And WDM drivers still have the file extension .SYS, just as the first real-mode
drivers did.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
1.2 An Overview of the Operating Systems -3-

1.2 An Overview of the Operating Systems


The Windows Driver Model provides a framework for device drivers that operate in two operating systems—Windows
98/Windows Me and Windows 2000/Windows XP. As discussed in the preceding historical summary, these two pairs of
operating systems are the products of two lines of parallel evolution. In fact, I’ll refer to the former pair of systems with the
abbreviation “98/Me” to emphasize their common heritage and to the latter pair simply as XP. Although to the end user these
two pairs of systems are similar, they work quite differently on the inside. In this section, I’ll present a brief overview of the
two systems.

1.2.1 Windows XP Overview

Figure 1-1. Windows XP architecture.


Figure 1-1 is a highly abbreviated functional diagram of the Windows XP operating system, wherein I emphasize the features
that are important to people who write device drivers. Every platform where Windows XP runs supports two modes of
execution. Software executes either in user mode or in kernel mode. A user-mode program that wants to, say, read some data
from a device would call an application programming interface (API) such as ReadFile. A subsystem module such as
KERNEL32.DLL implements this API by invoking a native API function such as NtReadFile. Refer to the sidebar for more
information about the native API.
We often say that NtReadFile is part of a system component called the I/O Manager. The term I/O Manager is perhaps a little
misleading because there isn’t any single executable module with that name. We need a name to use when discussing the
“cloud” of operating system services that surrounds our own driver, though, and this name is the one we usually pick.
Many routines serve a purpose similar to NtReadFile. They operate in kernel mode in order to service an application’s request
to interact with a device in some way. They all validate their parameters, thereby ensuring that they don’t inadvertently allow a
security breach by performing an operation, or accessing some data, that the user-mode program wouldn’t have been able to
perform or access by itself. They then create a data structure called an I/O request packet (IRP) that they pass to an entry point
in some device driver. In the case of an original ReadFile call, NtReadFile would create an IRP with the major function code
IRP_MJ_READ (a constant in a DDK [Driver Development Kit] header file). Processing details at this point can differ, but a
likely scenario is for a routine such as NtReadFile to return to the user-mode caller with an indication that the operation
described by the IRP hasn’t finished yet. The user-mode program might continue about its business and then wait for the
operation to finish, or it might wait immediately. Either way, the device driver proceeds independently of the application to
service the request.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
-4- Beginning a Driver Project | Chapter 1

The Native API


NtReadFile is part of the so-called native API of Windows XP. The reason there is a native API is historical. The
original Windows NT operating system contained a number of subsystems to implement the semantics of
several new and existing operating systems. There was an OS/2 subsystem, a POSIX subsystem, and a Win32
subsystem. The subsystems were implemented by making user-mode calls to the native API, which was itself
implemented in kernel mode.

A user-mode DLL named (rather redundantly, I’ve always thought) NTDLL.DLL implements the native API for
Win32 callers. Each entry in this DLL is a thin wrapper around a call to a kernel-mode function that actually
carries out the function. The call uses a platform-dependent system service interface to transfer control across
the user-mode/kernel-mode boundary. On newer Intel processors, this system service interface uses the
SYSENTER instruction. On older Intel processors, the interface uses the INT instruction with the function code
0x2E. On other processors, still other mechanisms are employed. You and I don’t need to understand the details
of the mechanism to write drivers, though. All we need to understand is that the mechanism allows a program
running in user mode to call a subroutine that executes in kernel mode and that will eventually return to its
user-mode caller. No thread context switching occurs during the process: all that changes is the privilege level
of the executing code (along with a few other details that only assembly language programmers would ever
notice or care about).

The Win32 subsystem is the one most application programmers are familiar with because it implements the
functions one commonly associates with the Windows graphical user interface. The other subsystems have
fallen by the wayside over time. The native API remains, however, and the Win32 subsystem still relies on it in
the way I’m describing by example in the text.

A device driver may eventually need to actually access its hardware to perform an IRP. In the case of an IRP_MJ_READ to a
programmed I/O (PIO) sort of device, the access might take the form of a read operation directed to an I/O port or a memory
register implemented by the device. Drivers, even though they execute in kernel mode and can therefore talk directly to their
hardware, use facilities provided by the hardware abstraction layer (HAL) to access hardware. A read operation might involve
calling READ_PORT_UCHAR to read a single data byte from an I/O port. The HAL routine uses a platform-dependent
method to actually perform the operation. On an x86 computer, the HAL would use the IN instruction; on some other future
Windows XP platform, it might perform a memory fetch.
After a driver has finished with an I/O operation, it completes the IRP by calling a particular kernel-mode service routine.
Completion is the last act in processing an IRP, and it allows the waiting application to resume execution.

1.2.2 Windows 98/Windows Me Overview


Figure 1-2 shows one way of thinking about Windows 98/Windows Me. The operating system kernel is called the Virtual
Machine Manager (VMM) because its main job is to create one or more virtual machines that share the hardware of a single
physical machine. The original purpose of a virtual device driver in Windows 3.0 was to virtualize a specific device in order to
help the VMM create the fiction that each virtual machine has a full complement of hardware. The same VMM architecture
introduced with Windows 3.0 is in place today in Windows 98/Me, but with a bunch of accretions to handle new hardware and
32-bit applications.

Figure 1-2. Windows 98/Me architecture.


Windows 98/Me doesn’t handle I/O operations in quite as orderly a way as Windows XP. There are major differences in the

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
1.2 What Kind of Driver Do I Need? -5-

way Windows 98/Me handles operations directed to disks, to communication ports, to keyboards, and so on. There are also
fundamental differences between how Windows services 32-bit and 16-bit applications. See Figure 1-3.

Figure 1-3. I/O requests in Windows 98/Me


The left column of Figure 1-3 shows how 32-bit applications get I/O done for them. An application calls a Win32 API such as
ReadFile, which a system DLL such as KERNEL32.DLL services. But applications can use ReadFile only for reading disk
files, communication ports, and devices that have WDM drivers. For any other kind of device, an application must use some ad
hoc mechanism based on DeviceIoControl. The system DLL contains different code than its Windows XP counterpart too. The
user-mode implementation of ReadFile, for example, validates parameters—a step done in kernel mode on Windows XP—and
uses one or another special mechanism to reach a kernel-mode driver. There’s one special mechanism for disk files, another for
serial ports, another for WDM devices, and so on. The mechanisms all use software interrupt 30h to transition from user mode
to kernel mode, but they’re otherwise completely different.
The middle column of Figure 1-3 shows how 16-bit Windows-based applications (Win16 applications) perform I/O. The right
column illustrates the control flow for MS-DOS-based applications. In both cases, the user-mode program calls directly or
indirectly on the services of a user-mode driver that, in principle, could stand alone by itself on a bare machine. Win16
programs perform serial port I/O by indirectly calling a 16-bit DLL named COMM.DRV, for example. (Up until Windows 95,
COMM.DRV was a stand-alone driver that hooked IRQ 3 and 4 and issued IN and OUT instructions to talk directly to the
serial chip.) A virtual communications device driver (VCD) intercepts the port I/O operations in order to guard against having
two different virtual machines access the same port simultaneously. In a weird way of thinking about the process, these
user-mode drivers use an “API” interface based on interception of I/O operations. “Virtualizing” drivers like VCD service
these pseudo-API calls by simulating the operation of hardware.
Whereas all kernel-mode I/O operations in Windows XP use a common data structure (the IRP), no such uniformity exists in
Windows 98/Me, even after an application's request reaches kernel mode. Drivers of serial ports conform to a port driver
function-calling paradigm orchestrated by VCOMM.VXD. Disk drivers, on the other hand, participate in a packet-driven
layered architecture implemented by IOS.VXD. Other device classes use still other means.
When it comes to WDM drivers, though, the interior architecture of Windows 98/Me is necessarily very similar to that of
Windows XP. A system module (NTKERN.VXD) contains Windows-specific implementations of a great many Windows NT
kernel support functions. NTKERN creates IRPs and sends them to WDM drivers in just about the same way as Windows XP.
WDM drivers can almost not tell the difference between the two environments, in fact.
Despite the similarities in the WDM environments of Windows XP and Windows 98/Me, however, there are some significant
differences. You'll find compatibility notes throughout this book that highlight the differences that matter to most driver
programmers. Appendix A outlines a scheme whereby you can use the same binary driver on Windows 2000/XP and Windows
98/Me despite these differences.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
-6- Beginning a Driver Project | Chapter 1

1.3 What Kind of Driver Do I Need?


Many kinds of drivers form a complete Windows XP system. Figure 1-4 diagrams several of them.

Figure 1-4. Types of device drivers in Windows XP.


„ A virtual device driver (VDD) is a user-mode component that allows MS-DOS-based applications to access hardware on
Intel x86 platforms. A VDD relies on the I/O permission mask to trap port access, and it essentially simulates the
operation of hardware for the benefit of applications that were originally programmed to talk directly to hardware on a
bare machine. Don’t confuse a Windows XP VDD with a Windows 98/Me VxD. Both are called virtual device drivers,
and they serve the same basic purpose of virtualizing hardware, but they employ completely different software
technology.
„ The category kernel-mode drivers includes many subcategories. A PnP driver is a kernel-mode driver that understands
the Plug and Play protocols of Windows XP. To be perfectly accurate, this book concerns PnP drivers and nothing else.
„ A WDM driver is a PnP driver that additionally understands power-management protocols and is source compatible with
both Windows 98/Me and Windows 2000/XP. Within the category of WDM driver, you can also distinguish between
class drivers (which manage a device belonging to some well-defined class of device) and minidrivers (which supply
vendor-specific help to a class driver), and between monolithic function drivers (which embody all the functionality
needed to support a hardware device) and filter drivers (which “filter” the I/O operations for a particular device in order
to add or modify behavior).
„ File system drivers implement the standard PC file system model (which includes the concepts of a hierarchical directory
structure containing named files) on local hard disks or over network connections. These, too, are kernel-mode drivers.
„ Legacy device drivers are kernel-mode drivers that directly control a hardware device without help from other drivers.
This category essentially includes drivers for earlier versions of Windows NT that are running without change in
Windows XP.
Not all the distinctions implied by this classification scheme are important all of the time. As I remarked in my previous book
Systems Programming for Windows 95 (Microsoft Press, 1996), you haven’t stumbled into a nest of pedants by buying my
book. In particular, I’m not always going to carefully distinguish between WDM and PnP drivers in the rigorous way implied
by the preceding taxonomy. The distinction is a phenomenological one based on whether a given driver runs both in Windows
2000/XP and Windows 98/Me. Without necessarily using the technically exact term, I’ll be very careful to discuss system
dependencies when they come up hereafter.
Faced with all these categories of driver, a new driver writer or manager would understandably be confused about what sort of
driver he or she needs for a given piece of hardware. For some devices, you don’t need to write any driver at all because
Microsoft already ships a generic driver that will work with your device. Here are some examples:
„ SCSI-compatible or ATAPI-compatible mass storage device
„ Any device connected to a USB port that is fully compatible with an approved specification, provided you’re happy with
any limitations in the standard Microsoft driver
„ Standard serial or PS/2 mouse
„ Standard keyboard

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
1.3 What Kind of Driver Do I Need? -7-

„ Video adapter without acceleration or other special features


„ Standard parallel or serial port
„ Standard floppy disk drive

1.3.1 WDM Drivers


For most devices that Microsoft doesn’t directly support, you need to write a WDM driver. You will decide first whether to
write a monolithic function driver, a filter driver, or just a minidriver. You’ll probably never need to write a class driver
because Microsoft would like to reserve that specialty to itself in order to serve the broadest range of hardware makers.

WDM Minidrivers
The basic rule of thumb is that if Microsoft has written a class driver for the type of device you’re trying to support, you should
write a minidriver to work with that class driver. Your minidriver is nominally in charge of the device, but you’ll call
subroutines in the class driver that basically take over the management of the hardware and call back to you to do various
device-dependent things. The amount of work you need to do in a minidriver varies tremendously from one class of device to
another.
Here are some examples of device classes for which you should plan to write a minidriver:
„ Non-USB human input devices (HID), including mice, keyboards, joysticks, steering wheels, and so on. If you have a
USB device for which the generic behavior of HIDUSB.SYS (the Microsoft driver for USB HID devices) is insufficient,
you would write a HIDCLASS minidriver too. The main characteristic of these devices is that they report user input by
means of reports that can be described by a descriptor data structure. For such devices, HIDCLASS.SYS serves as the
class driver and performs many functions that Direct-Input and other higher layers of software depend on, so you’re
pretty much stuck with using HIDCLASS.SYS. This is hard enough that I’ve devoted considerable space to it later in this
book. As an aside, HIDUSB.SYS is itself a HIDCLASS minidriver.
„ Windows Image Acquisition (WIA) devices, including scanners and cameras. You will write a WIA minidriver that
essentially implements some COM-style interfaces to support vendor-specific aspects of your hardware.
„ Streaming devices, such as audio, DVD, and video devices, and software-only filters for multimedia data streams. You
will write a stream minidriver.
„ Network interface devices on nontraditional buses, such as USB or 1394. For such a device, you will write a Network
Driver Interface Specification (NDIS) miniport driver “with a WDM lower edge,” to use the same phrase as the DDK
documentation on this subject. Such a driver is unlikely to be portable between operating systems, so you should plan on
writing several of them with minor differences to cope with platform dependencies.
„ Video cards. These devices require a video minidriver that works with the video port class driver.
„ Printers, which require user-mode DLLs instead of kernel-mode drivers.
„ Batteries, for which Microsoft supplies a generic class driver. You would write a minidriver (which the DDK calls a
miniclass driver, but it’s the same thing) to work with BATTC.SYS.

WDM Filter Drivers


You may have a device that operates so closely to a recognized standard that a generic Microsoft driver is almost adequate. In
some situations, you may be able to write a filter driver that modifies the behavior of the generic driver just enough to make
your hardware work. This doesn’t happen very frequently, by the way, because it’s often not easy to change the way a generic
driver accesses the hardware. I’ll discuss filter drivers in great detail in Chapter 16.

Monolithic WDM Function Drivers


With some exceptions to be noted in the next section, most other types of device require what I’ve called here a monolithic
WDM function driver. Such a driver essentially stands alone and handles all the details of controlling your hardware.
When this style of driver is appropriate, I recommend the following approach so that you can end up with a single binary that
will work on Intel x86 platforms in all operating systems. First, build with the most recent DDK—I used a beta version of
the .NET DDK for the samples in the companion content. You can use IoIsWdmVersionAvailable to decide which operating
system you happen to be using. If you happen to be running in Windows 2000 or Windows XP, you can call
MmGetSystemRoutineAddress to get a pointer to a Windows XP-only function. I also suggest shipping WDMSTUB.SYS,
which is discussed in Appendix A, to define MmGetSystemRoutineAddress and other critical kernel functions in Windows
98/Me; otherwise, your driver simply won’t load in Windows 98/Me because of undefined imports.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
-8- Beginning a Driver Project | Chapter 1

Here are some examples of devices for which you might write a monolithic WDM function driver:
„ Any kind of SmartCard reader except one attached to a serial port
„ Digital-to-analog converter
„ ISA card supporting proprietary identification tag read/write transducer

More About Binary Compatibility


Originally, WDM was to have been binary portable across all versions of Windows. Because of release schedules
and second (and higher-order) thoughts, every release since Windows 98 has included support for more and
more kernel functions that are useful, and sometimes even essential, for robust and convenient programming.
An example is the IoXxxWorkItem family of functions, discussed in Chapter 14, which was added to Windows
2000 and which must be used instead of the similar but less robust ExXxxWorkItem family. Unless you do
something extra, a driver that calls IoXxxWorkItem functions simply won’t load in Windows 98/Me because the
operating system doesn’t export the functions. MmGetSystemRoutineAddress is another function that didn’t
make it into Windows 98/Me, unfortunately, so you can’t even make a run-time decision regarding which work
item functions to call. As if this weren’t enough, the WHQL tests for all drivers flag calls to the ExXxxWorkItem
functions.

In Windows 98/Me, a VxD named NTKERN implements the WDM subset of kernel support functions. As
discussed in more detail in Appendix A, NTKERN relies on defining new export symbols for use by the run-time
loader. You can also define your own export symbols, which is how WDMSTUB manages to define missing
symbols for use by the kind of binary-portable driver I’m advocating you build.

The companion content for this book includes the WDMCHECK utility, which you can run on a Windows 98/Me
system to check a driver for missing imports. If you’ve developed a driver that works perfectly in Windows XP,
I suggest copying the driver to a Windows 98/Me system and running WDMCHECK first thing. If WDMCHECK
shows that your driver calls some unsupported functions, the next thing to check is whether WDMSTUB
supports those functions. If so, just add WDMSTUB to your driver package as shown in Appendix A. If not, either
modify your driver or send me an e-mail asking me to modify WDMSTUB. Either way, you’ll eventually end up
with a binary-compatible driver.

1.3.2 Other Types of Drivers


A few situations exist in which a monolithic WDM function driver won’t suffice because of architectural differences between
Windows 98/Me and Windows 2000/XP. In the following cases, you would need to write two drivers: a WDM driver for
Windows 2000/XP and a VxD driver for Windows 98/Me:
„ A driver for a serial port. The Windows 98/Me driver is a VxD that offers the VCOMM port driver interface at its upper
edge, whereas the Windows 2000/XP driver is a WDM driver that offers a rich and rigidly specified IOCTL interface at
its upper edge. The two upper-edge specifications have nothing in common.
„ A driver for a device connected to a serial port. The Windows 98/Me driver is a VxD that calls VCOMM in order to talk
to the port. The Windows 2000/XP driver is a WDM driver that talks to SERIAL.SYS or some other serial port driver that
implements the same IOCTL interface.
„ A driver for a nonstandard USB mass storage device. For Windows 98/Me, you’ll write a VxD that fits into the I/O
Supervisor hierarchy of layered drivers. For Windows 2000/XP, you’ll write a monolithic WDM function driver that
accepts SCSI Request Blocks at its upper edge and communicates with the USB device at its lower edge.
„ For two classes of device, Microsoft defined a portable driver architecture long before WDM:
„ Small Computer System Interface (SCSI) adapters use a “SCSI miniport” driver, which doesn’t use any of the standard
kernel support functions and relies instead on a special API exported by SCSIPORT.SYS or SCSIPORT.VXD, as the case
may be. The miniport is portable between systems.
„ Network interface cards use an “NDIS miniport driver,” which relies exclusively on a special API exported by NDIS.SYS
or NDIS.VXD, as the case may be. At one time, NDIS miniport drivers were portable between systems, but portability
has pretty much been lost by now. Network protocol drivers and so-called “intermediate” drivers that provide filtering
functionality also orbit around NDIS.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
1.3 What Kind of Driver Do I Need? -9-

1.3.3 Management Overview and Checklist


If you’re a development manager, or if you’re otherwise responsible for delivering a hardware device to market, there are a few
things you need to know about device drivers. You need to decide, first of all, whether you need a customized driver and, if so,
what kind. The preceding section should help you with that decision, but you might want to hire an expert consultant for the
limited purpose of advising you on that score.
If your evaluation leads you to believe you need a custom driver, you then need to locate an appropriate programmer. The sad
truth is that WDM driver programming is pretty hard, and only experienced (and expensive!) programmers are capable of
doing it well. Some companies have cadres of driver programmers, but most can’t afford to. If you’re in the latter situation,
your basic choice is between training someone who’s already on your staff, hiring a programmer who already has the necessary
skills, engaging a consultant or contract programmer, or outsourcing the development to a company that specializes in driver
programming. All of these alternatives have pluses and minuses, and you will have to weigh them based on your own unique
needs.
Driver programming should start as soon as there is a reasonably firm specification for how the hardware will work. You
should expect to modify the specification in light of unpleasant discoveries during driver development, and you should also
expect to iterate your hardware/firmware and driver design several times. Flexibility and a willingness to start over will really
help you.
You should also expect driver programming to last longer and cost more than you initially imagine. All software is subject to
time and cost overruns. Additional overruns in this kind of programming stem from communication difficulties between the
hardware and software people, from ambiguity in specifications and in the DDK documentation, from bugs in all the
components, and from delays in engineering and production.
In most cases, you’ll want to submit your hardware and software to the Windows Hardware Quality Lab (WHQL) in order to
obtain a digital certificate that will streamline installation and provide an entrée to one of Microsoft’s logo programs. You’ll do
most of the testing yourself, and you’ll need specific computer setups to do it, so find out early what the testing requirements
are for your class of device to avoid being caught short at the end of your project. (Just as an example, testing a USB device
requires you to have a variety of audio hardware in a specific topology, even if your device has nothing to do with audio or any
other kind of streaming media.)
Also prepare your business infrastructure for working with WHQL. At a minimum, this will require obtaining a Data Universal
Numbering System (DUNS) number from Dun and Bradstreet (or providing equivalent proof of business organization) and a
digital signature certificate from Verisign. As of this writing, the DUNS number was free, but the Verisign certificate was not.
And working through all the processes of multiple companies will take time.
Pay attention early on to how end users will install the driver software. Most vendors of add-on hardware prefer to ship a
custom installation program on a CD-ROM, and writing the installer is a lengthy process that can consume an experienced
programmer for several weeks. Web-based driver repositories are quite common and require special attention to installation
issues.
Drivers can provide statistical and other management information in two ways. The Windows Management Instrumentation
(WMI) subsystem provides a language- and transport-independent pathway for various sorts of binary data. Microsoft has
established standard WMI classes for certain types of device, and your own industry subgroup may have established other
standards to which your driver should conform. Chapter 10 contains information on how to conform to the Microsoft standards,
but finding out how to fit in with the rest of the industry may be a job for your company’s trade group representatives.
The second way of providing management information is by means of the system event log, which has been part of Windows
NT since the beginning and which gives administrators a quick way of learning about exceptional conditions that have arisen
in the recent past. Your driver should report events that an administrator would be interested in and can do something about.
Whoever programs your driver should consult with an experienced system administrator to decide which events to log, so as to
avoid cluttering the log with routine and unexceptional information. Your driver executable file will also probably include the
text of messages in a special multilingual message resource, and it would be a good idea to have a trained writer compose that
text. (I’m not saying your driver programmer can’t do this, but he or she may not be the best choice.)
In addition to a driver, you may need control panel or other configuration software. The driver programmer and a specialist in
user interaction should work together to build these components. Since they’ll be installed along with the driver, they’ll be part
of the package that WHQL digitally signs, so they need to be finished at the same time as the driver.
Finally, don’t treat your drivers as unimportant details. Having a good driver with a smooth installation is at least as important
as the exterior appearance of the product. To put it simply, if your driver crashes the operating system, reviewers will alert the
public, and anyone who doesn’t read the reviews will be irately returning your product to the stores. You won’t have any repeat
business from people whose systems have crashed, even once, because of your driver. So a myopic decision to short-fund
driver development could easily have a dramatic, negative effect on your bottom line for years to come. This advice is
especially important for hardware manufacturers in developing countries, where managers have a tendency to look for every
possible way to cut costs. I suggest that driver development is one place where cost-based decision making is inappropriate.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 10 - Beginning a Driver Project | Chapter 1

To summarize, plan your project with the following milestones in mind:


„ Evaluation of required driver and selection of programming talent
„ Programming specification for hardware complete enough for driver work to begin
„ Prototype hardware available for driver testing
„ Driver and hardware/firmware working together as originally intended
„ Installation (INF) file tested on all operating systems
„ Control panels and other ancillary software done
„ WMI and event log functionality done and tested
„ WHQL self-tests passed and submission made
„ Custom installation program done (not part of WHQL submission)
„ Ready to burn discs and ship product!

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 11 -

Chapter 2
2 Basic Structure of a WDM Driver

In the first chapter, I described the basic architecture of the Microsoft Windows XP and Microsoft Windows 98/Me operating
systems. I explained that the purpose of a device driver is to manage a piece of hardware on behalf of the system, and I
discussed how to decide what kind of driver your hardware will need. In this chapter, I’ll describe more specifically what
program code goes into a WDM driver and how different kinds of drivers work together to manage hardware. I’ll also present
an overview of how the system finds and loads drivers.

2.1 How Drivers Work


A useful way to think of a complete driver is as a container for a collection of subroutines that the operating system calls to
perform various operations that relate to your hardware. Figure 2-1 illustrates this concept. Some routines, such as the
DriverEntry and AddDevice routines, as well as dispatch functions for a few types of I/O Request Packet (IRP), will be present
in every such container. Drivers that need to queue requests might have a StartIo routine. Drivers that perform direct memory
access (DMA) transfers will have an AdapterControl routine. Drivers for devices that generate hardware interrupts will have an
interrupt service routine (ISR) and a deferred procedure call (DPC) routine. Most drivers will have dispatch functions for
several types of IRP besides the three that are shown in Figure 2-1. One of your jobs as the author of a WDM driver, therefore,
is to select the functions that need to be included in your particular container.

Figure 2-1. A driver considered as a package of subroutines.


I’ll show you in this chapter how to write the DriverEntry and AddDevice routines for a monolithic function driver, one of the
types of WDM driver this book discusses. As you’ll learn in later chapters, filter drivers also have DriverEntry and AddDevice
routines that are similar to what you’ll see here. As you’ll also learn, minidrivers have very different DriverEntry routines and
may or may not have AddDevice routines, all depending on how the author of the associated class driver designed the class
driver interface.

2.1.1 How Applications Work


It’s worth a moment to reflect on the implications of the “package of subroutines” model for a driver by contrasting it with the
“main program and helpers” model that applies to an application. Consider this program, which is among the first that many of
us learn to write:
- 12 - Basic Structure of a WDM Driver | Chapter 2

int main(int argc, char* argv[])


{
printf("Hello, world!\n");
return 0;
}

This program consists of a main program named main and a library of helper routines, most of which we don’t explicitly call.
One of the helper routines, printf, prints a message to the standard output file. After compiling the source module containing
the main program and linking it with a runtime library containing printf and the other helper routines needed by the main
program, you would end up with an executable module that you might name HELLO.EXE. I’ll go so far as to call this module
by the grandiose name application because it’s identical in principle to every other application now in existence or hereafter
written. You could invoke this application from a command prompt this way:

C:\>hello
Hello, world!

C:\>

Here are some other common facts about applications:


„ Some of the helper routines an application uses come from a static library, from which the linkage editor extracts them as
part of the build process. printf is one of these functions.

Other helper routines are dynamically linked from system dynamic-link libraries (DLLs). For these routines, the linkage
editor places special import references in the executable file, and the runtime loader fixes up these references to point to
the actual system code. As a matter of fact, the entire Win32 API used by application programs is dynamically linked, so
you can see that dynamic linking is a very important concept in Windows programming.
„ Executable files can contain symbolic information that allows debuggers to associate runtime addresses with the original
source code.
„ Executable files can also contain resource data, such as dialog box templates, text strings, and version identification.
Placing this sort of data within the file is better than using separate auxiliary files because it avoids the problem of having
mismatched files.
The interesting thing about HELLO.EXE is that once the operating system gives it control, it doesn’t return until it’s
completely done with the task it performs. That’s a characteristic of every application you’ll ever use in Windows, actually. In
a console-mode application such as HELLO, the operating system initially transfers control to an initialization function that’s
part of the compiler’s runtime library. The initialization function eventually calls main to do the application’s work.
Graphical applications in Windows work in much the same way except that the main program is named WinMain instead of
main. WinMain operates a message pump to receive and dispatch messages to window procedures. It returns to the operating
system when the user closes the main window. If the only Windows applications you ever build use Microsoft Foundation
Classes (MFC), the WinMain procedure is buried in the library where you might never spot it, but rest assured it’s there.
More than one application can appear to be running simultaneously on a computer, even a computer that has just one central
processing unit. The operating system kernel contains a scheduler that gives short blocks of time, called time slices, to all the
threads that are currently eligible to run. An application begins life with a single thread and can create more if it wants. Each
thread has a priority, given to it by the system and subject to adjustment up and down for various reasons. At each decision
point, the scheduler picks the highest-priority eligible thread and gives it control by loading a set of saved register images,
including an instruction pointer, into the processor registers. A processor interrupt accompanies expiration of the thread’s time
slice. As part of handling the interrupt, the system saves the current register images, which can be restored the next time the
system decides to redispatch the same thread.
Instead of just waiting for its time slice to expire, a thread can block each time it initiates a time-consuming activity in another
thread until the activity finishes. This is better than spinning in a polling loop waiting for completion because it allows other
threads to run sooner than they would if the system had to rely solely on expiration of a time slice to turn its attention to some
other thread.
Now, I know you already knew what I just said. I just wanted to focus attention on the fact that an application is, at bottom, a
selfish thread that grabs the CPU and tries to hold on until it exits and that the operating system scheduler acts like a
playground monitor to make a bunch of selfish threads play well together.

2.1.2 Device Drivers


Like HELLO.EXE, a driver is also an executable file. It has the file extension .SYS, but structurally the disk file looks exactly
like any 32-bit Windows or console-mode application. Also like HELLO.EXE, a driver uses a number of helper routines, many
of which are dynamically linked from the operating system kernel or from a class driver or other supporting library. A driver
file can have symbolic debugging information and resource data too.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.1 How Drivers Work - 13 -

The System Is in Charge


Unlike HELLO.EXE, however, a driver doesn’t contain a main program. Instead, it contains a collection of subroutines that the
system can call when the system thinks it’s time to. To be sure, these subroutines can use helper subroutines in the driver, in
static libraries, and in the operating system, but the driver isn’t in charge of anything except its own hardware: the system is in
charge of everything else, including the decisions about when to run your driver code.
Here’s a brief snapshot of how the operating system might call subroutines in your driver:
1. The user plugs in your device, so the system loads your driver executable into virtual memory and calls your DriverEntry
routine. DriverEntry does a few things and returns.
2. The Plug and Play Manager (PnP Manager) calls your AddDevice routine, which does a few things and returns.
3. The PnP Manager sends you a few IRPs. Your dispatch function processes each IRP in turn and returns.
4. An application opens a handle to your device, whereupon the system sends you another IRP. Your dispatch routine does a
little work and returns.
5. The application tries to read some data, whereupon the system sends you an IRP. Your dispatch routine puts the IRP in a
queue and returns.
6. A previous I/O operation finishes by signaling a hardware interrupt to which your driver is connected. Your interrupt
routine does a little bit of work, schedules a DPC, and returns.
7. Your DPC routine runs. Among other things, it removes the IRP you queued at step 5 and programs your hardware to
read the data. Then the DPC routine returns to the system.
8. Time passes, during which the system makes many other brief calls into your subroutines.
9. Eventually, the end user unplugs your device. The PnP Manager sends you some IRPs, which you process and return. The
operating system calls your DriverUnload routine, which usually just does a tiny amount of work and returns. Then the
system removes your driver code from virtual memory.
At each step of this process, the system decided that your driver needed to do something, be it initializing, processing an IRP,
handling an interrupt, or whatever. So the system selected the appropriate subroutine within your driver. Your routine did what
it was supposed to do and returned to the system.

Threads and Driver Code


Another way in which drivers are dissimilar to applications is that the system doesn’t create a special thread in which to run the
driver code. Instead, a driver subroutine executes in the context of whatever thread happens to be currently active at the time
the system decides to call that subroutine.
It’s not possible to predict which thread will be current at the time a hardware interrupt occurs. As an analogy, imagine that
you’re watching a carousel at an amusement park. The horses on the carousel are like threads in a running system. Call the
horse that’s nearest to you the “current” horse. Now suppose you decide to take a picture with your camera the next time you
overhear someone say the phrase, “That’s awesome, dude.” (In my experience at amusement parks, this does not entail a long
wait.) You wouldn’t expect to be able to predict which horse would be “current” in your snapshot. Which of all the eligible
threads that happens to be executing at the time of hardware interrupt is likewise not predictable. We call this an arbitrary
thread, and we speak of running in an arbitrary thread context.
The system is often running in an arbitrary thread context when it decides to call a subroutine in your driver. The thread
context would be arbitrary—for example, when your interrupt service routine gets control. If you schedule a DPC, the thread in
which your DPC routine runs will be arbitrary. If you queue IRPs, your StartIo routine will be called in an arbitrary thread. In
fact, if some driver outside your own stack sends you an IRP, you have to assume that the thread context is arbitrary. Such
would normally be the case for a storage driver since a file system driver will be the agent ultimately responsible for doing
reads and writes.
The system doesn’t always execute driver code in an arbitrary thread context. A driver can create its own system threads by
calling PsCreateSystemThread. A driver can also ask the system to call it back in the context of a system thread by scheduling
a work item. In these situations, we consider the thread context to be nonarbitrary. I’ll discuss the mechanics of system threads
and work items in Chapter 14.
Another situation in which the thread context is not arbitrary occurs when an application issues an API call that causes the I/O
Manager to send an IRP directly to a driver. You can know when you write a driver whether or not this will be the case with
respect to each type of IRP you handle.
You care whether the thread context is arbitrary for two reasons. First, a driver shouldn’t block an arbitrary thread: it would be
unfair to halt one thread while you carry out activities that benefit some other thread.
The second reason applies when a driver creates an IRP to send to some other driver. As I’ll discuss more fully in Chapter 5,
you need to create one kind of IRP (an asynchronous IRP) in an arbitrary thread, but you might create a different kind of IRP (a

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 14 - Basic Structure of a WDM Driver | Chapter 2

synchronous IRP) in a nonarbitrary thread. The I/O Manager ties the synchronous kind of IRP to the thread within which you
create the IRP. It will cancel the IRP automatically if that thread terminates. The I/O Manager doesn’t tie an asynchronous IRP
to any particular thread, though. The thread in which you create an asynchronous IRP may have nothing to do with the I/O
operation you’re trying to perform, and it would be incorrect for the system to cancel the IRP just because that thread happens
to terminate. So it doesn’t.

Symmetric Multiprocessing
Windows XP uses a so-called symmetric model for managing computers with multiple central processors. In this model, each
CPU is treated exactly like every other CPU with respect to thread scheduling. Each CPU has its own current thread. It’s
perfectly possible for the I/O Manager, executing in the context of the threads running on two or more CPUs, to call
subroutines in your driver simultaneously. I’m not talking about the sort of fake simultaneity with which threads execute on a
single CPU—on the time scale of the computer, the threads are really taking turns. Rather, on a multiprocessor machine,
different threads really do execute at the same time. As you can imagine, simultaneous execution leads to exacting
requirements for drivers to synchronize access to shared data. In Chapter 4, I’ll discuss the various synchronization methods
that you can use for this purpose.

2.2 How the System Finds and Loads Drivers


I emphasized in the preceding section how the operating system is in overall charge of the computer and calls on device drivers
to do small amounts of work with respect to hardware. Drivers have a similarly passive role in the process that causes them to
be loaded in the first place. It will help you understand the rest of the book if you understand right away how the system
detects hardware, determines which driver to load, and then configures the driver to manage the hardware. The system uses
two slightly different methods, depending on whether the hardware is Plug and Play compatible:
„ A Plug and Play device has an electronic signature that the system can detect. For Plug and Play devices, a system bus
driver detects the existence of the hardware and reads the signature to determine what kind of hardware it is. Thereafter,
an automatic process based on the registry and INF files allows the system to load the right driver.
„ A legacy device does not have any electronic signature, so the system can’t detect it automatically. The end user must
therefore initiate the “detection” process by invoking the Add New Hardware Wizard, which ends with the system
knowing that a certain new piece of hardware exists. Thereafter, the system uses the same automatic registry-and-INF-file
process that’s used for Plug and Play devices to load the right driver.
Whichever method the system uses to detect hardware and load a driver, the driver itself will be a WDM driver that reacts
passively to calls from the operating system. On this point, WDM drivers contrast sharply with kernel-mode drivers for earlier
versions of Windows NT and with VxD drivers prior to Windows 95. In those environments, you had to somehow arrange for
the system to load your driver. Your driver would then scan hardware buses looking for its own hardware and decide whether
to stay resident or not. In addition, your driver had to determine which I/O resources to use and take steps to prevent other
drivers from taking the same resources.

2.2.1 Device and Driver Layering


Before I can make sense of the hardware detection and driver loading processes, I need to explain the concept of driver
layering illustrated in Figure 2-2. In the figure, the left column represents an upwardly linked stack of kernel
DEVICE_OBJECT structures, all of which relate to how the system manages a single piece of hardware. The middle column
represents the collection of device drivers that have roles to play in the management. The right column illustrates the flow of
an IRP through the drivers.
In the Windows Driver Model, each hardware device has at least two device drivers. One of these drivers, which we call the
function driver, is what you’ve always thought of as being the device driver. It understands all the details about how to make
the hardware work. It’s responsible for initiating I/O operations, for handling the interrupts that occur when those operations
finish, and for providing a way for the end user to exercise any control over the device that might be appropriate.

NOTE
A monolithic WDM function driver is a single executable file with dynamic links to NTOSKRNL.EXE, which
contains the kernel of the operating system, and HAL.DLL, which contains the hardware abstraction layer
(HAL). A function driver can dynamically link to other kernel-mode DLLs too. In situations in which Microsoft has
provided a class driver for your type of hardware, your minidriver will dynamically link to the class driver. The
combination of minidriver and class driver adds up to a single function driver. You may see pictures in which
things called class drivers appear to be above or below a minidriver. I prefer to think of those so-called “class”
drivers as free-standing filter drivers and to use the term class driver solely for drivers that are next to
minidrivers, reached by means of explicit imports, and acting as partners that the minidriver willingly brings
into play.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.2 How the System Finds and Loads Drivers - 15 -

Figure 2-2. Layering of device objects and drivers in the Windows Driver Model.
We call the other of the two drivers that every device has the bus driver. It’s responsible for managing the connection between
the hardware and the computer. For example, the bus driver for the Peripheral Component Interconnect (PCI) bus is the
software component that actually detects that your card is plugged into a PCI slot and determines the requirements your card
has for I/O-mapped or memory-mapped connections with the host. It’s also the software that turns on or off the flow of
electrical current to your card’s slot.
Some devices have more than two drivers. We use the generic term filter driver to describe these other drivers. Some filter
drivers simply watch as the function driver performs I/O. More often, a software or hardware vendor supplies a filter driver to
modify the behavior of an existing function driver in some way. Upper filter drivers see IRPs before the function driver, and
they have the chance to support additional features that the function driver doesn’t know about. Sometimes an upper filter can
perform a workaround for a bug or other deficiency in the function driver or the hardware. Lower filter drivers see IRPs that
the function driver is trying to send to the bus driver. (A lower filter is below the function driver in the stack but still above the
bus driver.) In some cases, such as when the device is attached to a universal serial bus (USB), a lower filter can modify the
stream of bus operations that the function driver is trying to perform.
Referring once again to Figure 2-2, notice that each of the four drivers shown for a hypothetical device has a connection to one
of the DEVICE_OBJECT structures in the left column. The acronyms used in the structures are these:
„ PDO stands for physical device object. The bus driver uses this object to represent the connection between the device and
the bus.
„ FDO stands for function device object. The function driver uses this object to manage the functionality of the device.
„ FiDO stands for filter device object. A filter driver uses this object as a place to store the information it needs to keep
about the hardware and its filtering activities. (The early beta releases of the Windows 2000 DDK used the term FiDO,
and I adopted it then. The DDK no longer uses this term because, I guess, it was considered too frivolous.)

What Is a Bus?
I’ve already used the terms bus and bus driver pretty freely without explaining what they mean. For purposes
of the WDM, a bus is anything that you can plug a device into, either physically or metaphorically.

This is a pretty broad definition. Not only does it include items such as the PCI bus, but it also includes a Small
Computer System Interface (SCSI) adapter, a parallel port, a serial port, a USB hub, and so on—anything, in
fact, that can have another device plugged into it.

The definition also includes a notional root bus that exists only as a figment of our imagination. Think of the root
bus as being the bus into which all legacy devices plug. Thus, the root bus is the parent of a non-PnP Industry
Standard Architecture (ISA) card or of a SmartCard reader that connects to the serial port but doesn’t answer
with a standard identification string to the serial port enumeration signals. We also consider the root bus to be
the parent of the PCI bus—this is because the PCI bus does not itself announce its presence electronically, and
the operating system therefore has to treat it like a legacy device.

2.2.2 Plug and Play Devices


To repeat what I said earlier, a Plug and Play device is one that has an electronic signature that a bus driver can interrogate to
learn the identity of a device. Here are some examples of these signatures:
„ A PCI card has a configuration space that the PCI bus driver can read via dedicated memory or I/O port addresses. The
configuration space contains vendor and product identification information.
„ A USB device returns a device descriptor in response to a standardized control-pipe transaction. The device descriptor
contains vendor and product identification information.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 16 - Basic Structure of a WDM Driver | Chapter 2

„ A Personal Computer Memory Card International Association (PCMCIA) device has attribute memory that the PCMCIA
bus driver can read in order to determine the identity of the card.
A bus driver for a Plug and Play bus has the ability to enumerate its bus by scanning all possible slots at start-up time. Drivers
for buses that support hot plugging of devices during a session (as do USB and PCMCIA) also monitor some sort of hardware
signal that indicates arrival of a new device, whereupon the driver reenumerates its bus. The end result of the enumeration or
reenumeration process is a collection of PDOs. See point 1 in Figure 2-3.

Figure 2-3. Installing a Plug and Play device.


When a bus driver detects the insertion or removal of hardware, it calls IoInvalidateDeviceRelations to notify the PnP Manager
that the bus’s population of child devices has changed. To obtain an updated list of the PDOs for the child devices, the PnP
Manager sends an IRP to the bus driver. The major function code for this IRP is IRP_MJ_PNP, and the minor function code is
IRP_MN_QUERY_DEVICE_RELATIONS, with a code indicating that the PnP Manager is looking for the so-called “bus”
relations. This is point 2 in Figure 2-3.

NOTE
Each IRP has a major and a minor function code. The major function code indicates what sort of request the IRP
contains. IRP_MJ_PNP is the major function code for requests that the PnP Manager makes. With some of the
major function codes, including IRP_MJ_PNP, the minor function code is required to further specify the
operation.In response to the bus relations query, the bus driver returns its list of PDOs. The PnP Manager can
easily determine which of the PDOs represent devices that it hasn’t yet initialized. Let’s focus on the PDO for
your hardware for the time being and see what happens next.

The PnP Manager will send another IRP to the bus driver, this time with the minor function code IRP_MN_QUERY_ID. This is
point 3 in Figure 2-3. In fact, the PnP Manager sends several such IRPs, each with an operand that instructs the bus driver to
return a particular type of identifier. One of the identifiers, the device identifier, uniquely specifies the type of device. A device
identifier is just a string, and it might look like one of these examples:

PCI\VEN_102C&DEV_00E0&SUBSYS_00000000
USB\VID_0547&PID_2125&REV_0002
PCMCIA\MEGAHERTZ-CC10BT/2-BF05

NOTE
Each bus driver has its own scheme for formatting the electronic signature information it gathers into an
identifier string. I’ll discuss the identifier strings used by common bus drivers in Chapter 15. That chapter is also
the place to look for information about INF files and about where the various registry keys described in the text
are in the registry hierarchy and what sorts of information are kept in the keys.The PnP Manager uses the device
identifier to locate a hardware key in the system registry. For the moment, let’s assume that this is the first time
your particular device has been plugged into the computer. In that case, there won’t yet be a hardware key for
your type of device. This is where the setup subsystem steps in to figure out what software is needed to support
your device. (See point 4 in Figure 2-3.)

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.2 How the System Finds and Loads Drivers - 17 -

The PnP Manager uses the device identifier to locate a hardware key in the system registry. For the moment, let’s assume that
this is the first time your particular device has been plugged into the computer. In that case, there won’t yet be a hardware key
for your type of device. This is where the setup subsystem steps in to figure out what software is needed to support your device.
(See point 4 in Figure 2-3.)
Installation instructions for all types of hardware exist in files with the extension .INF. Each INF contains one or more model
statements that relate particular device identifier strings to install sections within that INF file. Confronted with brand-new
hardware, then, the setup subsystem tries to find an INF file containing a model statement that matches the device identifier. It
will be your responsibility to provide this file, which is why I labeled the box You that corresponds to this step. I’m being
deliberately vague at this point about how the system searches for INF files and ranks the several model statements it’s likely
to find. I’ll burden you with these details in Chapter 15, but it would be a bit much to do that just yet.
When the setup subsystem finds the right model statement, it carries out the instructions you provide in an install section.
These instructions probably include copying some files onto the end user’s hard drive, defining a new driver service in the
registry, and so on. By the end of the process, the setup program will have created the hardware key in the registry and
installed all of the software you provided.
Now step back a few paragraphs and suppose that this was not the first time this particular computer had seen an instance of
your hardware. For example, maybe we’re talking about a USB device that the user introduced to the system long ago and that
the user is now reattaching to the system. In that case, the PnP Manager would have found the hardware key and would not
have needed to invoke the setup program. So the PnP Manager would skip around all the setup processing to point 5 in Figure
2-3.
At this point, the PnP Manager knows there is a device and that your driver is responsible for it. If your driver isn’t already
loaded in virtual memory, the PnP Manager calls the Memory Manager to map it in. The system doesn’t read the disk file
containing your driver directly into memory. Instead, it creates a file mapping that causes the driver code and data to be fetched
by paging I/O. The fact that the system uses a file mapping really doesn’t affect you much except that it has the side effect of
making you be careful later on about when you allow your driver to be unmapped. The Memory Manager then calls your
DriverEntry routine.
Next the PnP Manager calls your AddDevice routine to inform your driver that a new instance of your device has been
discovered. (See point 5 in Figure 2-3.) Then the PnP Manager sends an IRP to the bus driver with the minor function code
IRP_MN_QUERY_RESOURCE_REQUIREMENTS. This IRP is basically asking the bus driver to describe the requirements
your device has for an interrupt request line, for I/O port addresses, for I/O memory addresses, and for system DMA channels.
The bus driver constructs a list of these resource requirements and reports them back. (See point 6 in Figure 2-3.)
Finally the PnP Manager is ready to configure the hardware. It works with a set of resource arbitrators to assign resources to
your device. If that can be done—and it usually can be—the PnP Manager sends an IRP_MJ_PNP to your driver with the
minor function code IRP_MN_START_DEVICE. Your driver handles this IRP by configuring and connecting various kernel
resources, following which your hardware is ready to use.

Windows NT Drivers Contrasted


The process described in the text for how Windows XP (and, indeed, Windows 2000, Windows 95, and all
successors of Windows 95) finds and loads drivers requires the driver to be relatively passive. Windows NT 4.0
and before worked quite differently. In those systems, you would have provided some sort of setup program to
install your driver. Your setup program would have modified the registry to cause your driver to be loaded during
the next system restart. At that time, the system would load your driver and call your DriverEntry routine.

Your DriverEntry would have somehow determined which instances of your hardware were actually present. You
might have scanned all possible slots of a PCI bus, for example, or assumed that each instance of your device
corresponded to a subkey in the registry.

After detecting your own hardware, your DriverEntry routine would go on to assign and reserve I/O resources
and then to do the configuration and connection steps that a present-day WDM driver does. As you can see,
then, WDM drivers have much less work to do to get started than did drivers for earlier versions of Windows NT.

2.2.3 Legacy Devices


I use the term legacy device to describe any device that isn’t Plug and Play, meaning that the operating system can’t detect its
existence automatically. Let’s suppose your device fits this category. After purchasing your device, the end user will first
invoke the Add New Hardware Wizard and will make a series of dialog selections to lead the setup program to an install
section in an INF file. (See Figure 2-4, point 1.)
The setup program follows the instructions in the install section by creating registry entries for use by the root enumerator
(point 2 in Figure 2-4). The registry entries might include a logical configuration that lists the I/O resource requirements for the
device (point 3).
Finally the setup program instructs the end user to restart the system (point 4). The designers of the setup system expected that
the end user would now need to follow the manufacturer’s directions to configure the card by setting jumpers or switches and
would then need to insert the card into an expansion slot of a powered-down computer.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 18 - Basic Structure of a WDM Driver | Chapter 2

Figure 2-4. The detection process for a legacy device.


Following the restart (or following the end user’s decision to bypass the restart), the root enumerator will scan the registry and
find the newly added device. Thereafter, the process of loading your driver is nearly identical to that for a Plug and Play device.
See Figure 2-5.

NOTE
Most of the sample drivers in the companion content are for fake hardware, and you install them as if the
(nonexistent) hardware were a legacy device. One or two of the samples work with I/O ports and interrupts.
The respective INF files contain LogConfig sections to cause the PnP Manager to assign these resources. If you
install one of these drivers via the Add New Hardware Wizard, the system will think that a power-off restart is
needed, but you don’t really need to restart.

Figure 2-5. Loading a legacy driver.

2.2.4 Recursive Enumeration


In the preceding sections, I described how the system loads the correct driver for a single device. That description begs the
question of how the system manages to load drivers for all the hardware in the computer. The answer is that it uses a recursive
process.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.2 How the System Finds and Loads Drivers - 19 -

In the first instance, the PnP Manager invokes the root enumerator to find all hardware that can’t electronically announce its
presence—including the primary hardware bus (such as PCI). The root bus driver gets information about the computer from
the registry, which was initialized by the Windows XP Setup program. Setup got the information by running an elaborate
hardware detection program and by asking the end user suitable questions. Consequently, the root bus driver knows enough to
create a PDO for the primary bus.
The function driver for the primary bus can then enumerate its own hardware electronically. When a bus driver enumerates
hardware, it acts in the guise of an ordinary function driver. Having detected a piece of hardware, however, the driver switches
roles: it becomes a bus driver and creates a new PDO for the detected hardware. The PnP Manager then loads drivers for this
device PDO, as previously discussed. It might happen that the function driver for the device enumerates still more hardware, in
which case the whole process repeats recursively. The end result will be a tree like that shown in Figure 2-6, wherein a
bus-device stack branches into other device stacks for the hardware attached to that bus. The dark-shaded boxes in the figure
illustrate how one driver can wear an “FDO hat” to act as the function driver for its hardware and a “PDO hat” to act as the bus
driver for the attached devices.

2.2.5 Order of Driver Loading


I said earlier that devices can have upper and lower filter drivers as well as a function driver. Two registry keys associated with
the device contain information about filter drivers. The hardware key, which contains information about an instance of your
hardware, can have UpperFilters and LowerFilters values that specify filter drivers for that instance. There is another registry
key for the class to which the device belongs. For example, a mouse belongs to the Mouse class, which you could probably
have figured out without me telling you. The class key can also contain UpperFilters and LowerFilters values. They specify
filter drivers that the system will load for every device belonging to the class.

Figure 2-6. Layering of recursively enumerated devices.


No matter where it appears, an UpperFilters or LowerFilters value is of type REG_MULTI_SZ and can therefore contain one
or more null-terminated Unicode string values.

NOTE
Windows 98/Me doesn’t support the REG_MULTI_SZ registry type and doesn’t fully support Unicode. In
Windows 98/Me, the UpperFilters and LowerFilters values are REG_BINARY values that contain multiple
null-terminated ANSI strings followed by an extra null terminator.

It may be important in some situations to understand in what order the system calls drivers. The actual process of “loading” a
driver entails mapping its code image into virtual memory, and the order in which that’s done is actually not very interesting.
You might be interested, however, in knowing the order of calls to the AddDevice functions in the various drivers. (Refer to
Figure 2-7.)
1. The system first calls the AddDevice functions in any lower filter drivers specified in the device key for the device, in the
order in which they appear in the LowerFilters value.
2. Then the system calls AddDevice in any lower filter drivers specified in the class key. Again, the calls occur in the order

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 20 - Basic Structure of a WDM Driver | Chapter 2

in which the drivers appear in the LowerFilters string.


3. The system calls AddDevice in the driver specified by the Service value in the device key. This is the function driver.
4. The system calls AddDevice for any upper filter drivers specified in the device key, in the order in which they appear in
the UpperFilters data string.
5. Finally the system calls AddDevice for any upper filter drivers specified in the class key, in the order in which they
appear in the UpperFilters data string.
As I explain later in this chapter, each AddDevice function creates a kernel DEVICE_OBJECT and links it into the stack rooted
in the PDO. Therefore, the order of calls to AddDevice governs the order of device objects in the stack and, ultimately, the
order in which drivers see IRPs.

NOTE
You might have noticed that the loading of upper and lower filters belonging to the class and to the device
instance isn’t neatly nested as you might have expected. Before I knew the facts, I guessed that device-level
filters would be closer to the function driver than class-level filters.

Figure 2-7. Order of AddDevice calls

2.2.6 IRP Routing


The formal layering of drivers in the WDM facilitates routing IRPs from one driver to another in a predictable way. Figure 2-2
illustrates the general idea: whenever the system wants to carry out an operation on a device, it sends an IRP to the topmost
filter driver in the stack. That driver can decide to process the IRP, to pass the IRP down to the next level, or to do both. Each
driver that sees the IRP makes the same decision. Eventually, the IRP might reach the bus driver in its PDO role. The bus
driver does not usually pass the IRP any further, despite what Figure 2-6 might seem to imply. Rather, the bus driver usually
completes the IRP. In some situations, the bus driver will pass the same IRP to the stack (the parent driver stack) in which it
plays the FDO role. In other situations, the bus driver will create a secondary IRP and pass it to the parent driver stack.

How the Device Stack Is Implemented


I’ll show you the DEVICE_OBJECT data structure a bit later in this chapter. The opaque field AttachedDevice
links device objects into a vertical stack. Starting with the PDO, each device object points to the object
immediately above it. There is no documented downward pointer—drivers must keep track on their own of
what’s underneath them. (In fact, IoAttachDeviceToDeviceStack does set up a downward pointer in a structure
for which the DDK doesn’t have a complete declaration. It would be unwise to try to reverse-engineer that
structure because it’s subject to change at any time.)

The AttachedDevice field is purposely not documented because its proper use requires synchronization with
code that might be deleting device objects from memory. You and I are allowed to call
IoGetAttachedDeviceReference to find the topmost device object in a given stack. That function also increments
a reference count that will prevent that object from being prematurely removed from memory. If you wanted to
work your way down to the PDO, you could send your own device an IRP_MJ_PNP request with the minor
function code IRP_MN_QUERY_DEVICE_RELATIONS and the Type parameter TargetDeviceRelation. The PDO’s
driver will answer by returning the address of the PDO. It would be a great deal easier just to remember the PDO
address when you first create the device object.

Similarly, to know which device object is immediately underneath you, you need to save a pointer when you first
add your object to the stack. Since each of the drivers in a stack will have its own unknowable way of
implementing the downward pointers used for IRP dispatching, it’s not practical to alter the device stack once
the stack has been created.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.2 How the System Finds and Loads Drivers - 21 -

A few examples should clarify the relationship between FiDOs, FDOs, and PDOs. The first example concerns a read operation
directed to a device that happens to be on a secondary PCI bus that itself attaches to the main bus through a PCI-to-PCI bridge
chip. To keep things simple, let’s suppose there’s one FiDO for this device, as illustrated in Figure 2-8. You’ll learn in later
chapters that a read request turns into an IRP with the major function code IRP_MJ_READ. Such a request would flow first to
the upper FiDO and then to the function driver for the device. (That driver is the one for the device object marked FDOdev in
the figure.) The function driver calls the HAL directly to perform its work, so none of the other drivers in the figure will see the
IRP.

Figure 2-8. The flow of a read request for a device on a secondary bus.
A variation on the first example is shown in Figure 2-9. Here we have a read request for a device plugged into a USB hub that
itself is plugged into the host controller. The complete device tree therefore contains stacks for the device, for the hub, and for
the host controller. The IRP_MJ_READ flows through the FiDO to the function driver, which then sends one or more IRPs of a
different kind downward to its own PDO. The PDO driver for a USB device is USBHUB.SYS, and it forwards the IRPs to the
topmost driver in the host controller device stack, skipping the two-driver stack for the USB hub in the middle of the figure.

Figure 2-9. The flow of a read request for a USB device.


The third example is similar to the first except that the IRP in question is a notification concerning whether a disk drive on a
PCI bus will be used as the repository for a system paging file. You’ll learn in Chapter 6 that this notification takes the form of
an IRP_MJ_PNP request with the minor function code IRP_MN_DEVICE_USAGE_NOTIFICATION. In this case, the FiDO
driver passes the request to the FDOdev driver, which takes note of it and passes it further down the stack to the PDOdev driver.
This particular notification has implications about how other I/O requests that concern the PnP system or power management
will be handled, so the PDOdev driver sends an identical notification to the stack within which is the FDObus, as illustrated in
Figure 2-10. (Not all bus drivers work this way, but the PCI bus does.)

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 22 - Basic Structure of a WDM Driver | Chapter 2

Figure 2-10. The flow of a device usage notification

Visualizing the Device Tree


To better visualize the way device objects and drivers are layered, it helps to have a tool. I wrote the DEVVIEW
utility, which you’ll find in the companion content, for this purpose. With the USB42 sample for Chapter 12
plugged into a secondary USB hub, I ran DEVVIEW and generated the two screen shots shown in Figure 2-11
and Figure 2-12.

This particular device uses only two device objects. The PDO is managed by USBHUB.SYS, whereas the FDO is
managed by USB42. In the first of these screen shots, you can see other information about the PDO.

It’s worth experimenting with DEVVIEW on your own system to see how various drivers are layered for the
hardware you own.

Figure 2-11. DEVVIEW information about USB42’s PDO.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.3 The Two Basic Data Structures - 23 -

Figure 2-12. DEVVIEW information about USB42’s FDO.

2.3 The Two Basic Data Structures


This section describes the two most basic data structures that concern a WDM driver: the driver object and the device object.
The driver object represents the driver itself and contains pointers to all the driver subroutines that the system will ever call on
its own motion. (For the sake of completeness, you should know that you often provide pointers to other routines within your
driver as arguments in various kernel-mode service calls.) The device object represents an instance of hardware and contains
data to help you manage that instance.

2.3.1 Driver Objects


The I/O Manager uses a driver object data structure to represent each device driver. (See Figure 2-13.) Like many of the data
structures we’ll be discussing, the driver object is partially opaque. This means that you and I are supposed to directly access or
change only certain fields in the structure, even though the DDK headers declare the entire structure. I’ve shown the opaque
fields of the driver object in the figure with a gray background. These opaque fields are analogous to the private and protected
members of a C++ class, and the accessible fields are analogous to public members.
The DDK headers declare the driver object, and all other kernel-mode data structures for that matter, in a stylized way, as this
excerpt from WDM.H illustrates:

typedef struct _DRIVER_OBJECT {


CSHORT Type;
CSHORT Size;

} DRIVER_OBJECT, *PDRIVER_OBJECT;

That is, the header declares a structure with the type name DRIVER_OBJECT. It also declares a pointer type
(PDRIVER_OBJECT) and assigns a structure tag (_DRIVER_OBJECT). This declaration pattern appears many places in the
DDK, and I won’t mention it again. The headers also declare a small set of type names (such as CSHORT) to describe the
atomic data types used in kernel mode. CSHORT, for example, means “signed short integer used as a cardinal number.” Table
2-1 lists some of these names.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 24 - Basic Structure of a WDM Driver | Chapter 2

Type Name Description


PVOID, PVOID64 Generic pointers (default precision and 64-bit precision)
Used with service function declarations to force use of __stdcall calling convention on i86
NTAPI
architectures
VOID Equivalent to “void”
CHAR, PCHAR 8-bit character, pointer to same (signed or not according to compiler default)
UCHAR, PUCHAR Unsigned 8-bit character, pointer to same
SCHAR, PSCHAR Signed 8-bit character, pointer to same
SHORT, PSHORT Signed 16-bit integer, pointer to same
CSHORT Signed short integer, used as a cardinal number
USHORT, PUSHORT Unsigned 16-bit integer, pointer to same
LONG, PLONG Signed 32-bit integer, pointer to same
ULONG, PULONG Unsigned 32-bit integer, pointer to same
WCHAR, PWSTR,
Wide (Unicode) character or string
PWCHAR
PCWSTR Pointer to constant Unicode string
NTSTATUS Status code (typed as signed long integer)
LARGE_INTEGER Signed 64-bit integer
ULARGE_INTEGER Unsigned 64-bit integer
PSZ, PCSZ Pointer to ASCIIZ (single-byte) string or constant string
BOOLEAN, PBOOLEAN TRUE or FALSE (equivalent to UCHAR)
Table 2-1. Common Type Names for Kernel-Mode Drivers

Figure 2-13. The DRIVER_OBJECT data structure.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.3 The Two Basic Data Structures - 25 -

NOTE
Note on 64-bit Types: The DDK headers contain type names that will make it relatively painless for driver
authors to compile the same source code for either 32-bit or 64-bit Intel platforms. For example, instead of
blithely assuming that a long integer and a pointer are the same size, you should declare variables that might
be either a LONG_PTR or a ULONG_PTR. Such a variable can hold either a long (or unsigned long) or a pointer
to something. Also, for example, use the type SIZE_T to declare an integer that can count as high as a pointer
might span—you’ll get a 64-bit integer on a 64-bit platform. These and other 32/64 typedefs are in the DDK
header file named BASETSD.H.

I’ll briefly discuss the accessible fields of the driver object structure now.
DeviceObject (PDEVICE_OBJECT) anchors a list of device object data structures, one for each of the devices managed by the
driver. The I/O Manager links the device objects together and maintains this field. The DriverUnload function of a non-WDM
driver would use this field to traverse the list of device objects in order to delete them. A WDM driver probably doesn’t have
any particular need to use this field.
DriverExtension (PDRIVER_EXTENSION) points to a small substructure within which only the AddDevice
(PDRIVER_ADD_DEVICE) member is accessible to the likes of us. (See Figure 2-14.) AddDevice is a pointer to a function
within the driver that creates device objects; this function is rather a big deal, and I’ll discuss it at length in the section “The
AddDevice Routine” later in this chapter.

Figure 2-14. The DRIVER_EXTENSION data structure.


HardwareDatabase (PUNICODE_STRING) describes a string that names a hardware database registry key for the device. This
is a name like \Registry\Machine\Hardware\Description\System and names the registry key within which resource allocation
information resides. WDM drivers have no need to access the information below this key because the PnP Manager performs
resource allocation automatically. The name is stored in Unicode. (In fact, all kernel-mode string data uses Unicode.) I’ll
discuss the format and the use of the UNICODE_STRING data structure in the next chapter.
FastIoDispatch (PFAST_IO_DISPATCH) points to a table of function pointers that file system and network drivers export.
How these functions are used is beyond the scope of this book. If you’re interested in learning more about file system drivers,
consult Rajeev Nagar’s Windows NT File System Internals: A Developer’s Guide (O’Reilly & Associates, 1997).
DriverStartIo (PDRIVER_STARTIO) points to a function in your driver that processes I/O requests that the I/O Manager has
serialized for you. I’ll discuss request queuing in general and the use of this routine in particular in Chapter 5.
DriverUnload (PDRIVER_UNLOAD) points to a cleanup function in your driver. I’ll discuss this function a bit further on in
connection with DriverEntry, but you might as well know now that a WDM driver probably doesn’t have any significant
cleanup to do anyway.
MajorFunction (array of PDRIVER_DISPATCH) is a table of pointers to functions in your driver that handle each of the
roughly two dozen types of I/O requests. This table is also something of a big deal, as you might guess, because it defines how
I/O requests make it into your code.

2.3.2 Device Objects


Figure 2-15 illustrates the format of a device object and uses the same shading convention for opaque fields that I used in the
preceding discussion of driver objects. As the author of a WDM driver, you’ll create some of these objects by calling
IoCreateDevice.
DriverObject (PDRIVER_OBJECT) points to the object describing the driver associated with this device object, usually the
one that called IoCreateDevice to create it.
NextDevice (PDEVICE_OBJECT) points to the next device object that belongs to the same driver as this one. This field is the
one that links device objects together starting from the driver object’s DeviceObject member. There’s probably no reason for a
WDM driver to use this field. That’s just as well because proper use of this pointer requires synchronization using an internal
system lock that’s not exposed for access by device drivers.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 26 - Basic Structure of a WDM Driver | Chapter 2

Figure 2-15. The DEVICE_OBJECT data structure.


CurrentIrp (PIRP) is used by the Microsoft IRP queuing routines StartPacket and StartNextPacket to record the IRP most
recently sent to your StartIo routine. WDM drivers should implement their own IRP queues (see Chapter 5) and may have no
use for this field.
Flags (ULONG) contains a collection of flag bits. Table 2-2 lists the bits that are accessible to driver writers.

Flag Description
Reads and writes use the buffered method (system copy buffer) for accessing
DO_BUFFERED_IO
user-mode data.
DO_EXCLUSIVE Only one thread at a time is allowed to open a handle.
Reads and writes use the direct method (memory descriptor list) for accessing
DO_DIRECT_IO
user-mode data.
DO_DEVICE_INITIALIZING Device object isn’t initialized yet.
DO_POWER_PAGABLE IRP_MJ_PNP must be handled at PASSIVE_LEVEL.
DO_POWER_INRUSH Device requires large inrush of current during power-on.

Table 2-2. Flags in a DEVICE_OBJECT Data Structure

Flag Description
FILE_REMOVABLE_MEDIA Media can be removed from device.
FILE_READ_ONLY_DEVICE Media can only be read, not written.
FILE_FLOPPY_DISKETTE Device is a floppy disk drive.
FILE_WRITE_ONCE_MEDIA Media can be written once.
FILE_REMOTE_DEVICE Device accessible through network connection.
FILE_DEVICE_IS_MOUNTED Physical media is present in device.
FILE_VIRTUAL_VOLUME This is a virtual volume.
FILE_AUTOGENERATED_DEVICE_NAME I/O Manager should automatically generate a name for this device.
FILE_DEVICE_SECURE_OPEN Force security check during open.
Table 2-3. Characteristics Flags in a DEVICE_OBJECT Data Structure
Characteristics (ULONG) is another collection of flag bits describing various optional characteristics of the device. (See Table
2-3.) The I/O Manager initializes these flags based on an argument to IoCreateDevice. Filter drivers propagate some of them

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.4 The DriverEntry Routine - 27 -

upward in the device stack. (See the detailed discussion of filter drivers in Chapter 16 for more information about flag
propagation.)
DeviceExtension (PVOID) points to a data structure you define that will hold per-instance information about the device. The
I/O Manager allocates space for the structure, but its name and contents are entirely up to you. A common convention is to
declare a structure with the type name DEVICE_EXTENSION. To access it given a pointer (for example, fdo) to the device
object, use a statement like this one:

PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;

It happens to be true (now, anyway) that the device extension immediately follows the device object in memory. It would be a
bad idea to rely on this always being true, though, especially when the documented method of following the DeviceExtension
pointer will always work.
DeviceType (DEVICE_TYPE) is an enumeration constant describing what type of device this is. The I/O Manager initializes
this member based on an argument to IoCreateDevice. Filter drivers might conceivably need to inspect it. At the date of this
writing, there are over 50 possible values for this member. Consult the DDK documentation entry “Specifying Device Types”
in the MSDN Library for a list.
StackSize (CCHAR) counts the number of device objects starting from this one and descending all the way to the PDO. The
purpose of this field is to inform interested parties regarding how many stack locations should be created for an IRP that will
be sent first to this device’s driver. WDM drivers don’t normally need to modify this value, however, because the support
routine they use for building the device stack (IoAttachDeviceToDeviceStack) does so automatically.
AlignmentRequirement (ULONG) specifies the required alignment for data buffers used in read or write requests to this device.
WDM.H contains a set of manifest constants ranging from FILE_BYTE_ALIGNMENT and FILE_WORD_ALIGNMENT up to
FILE_512_BYTE_ALIGNMENT for these values. The values are just powers of 2 minus 1. For example, the value 0x3F is
FILE_64_BYTE_ALIGNMENT.

2.4 The DriverEntry Routine


In preceding sections, I said that the PnP Manager loads the drivers needed for hardware and calls their AddDevice functions. A
given driver might be used for more than one piece of similar hardware, and there’s some global initialization that the driver
needs to perform only once when it’s loaded for the first time. That global initialization is the responsibility of the DriverEntry
routine:

extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,


IN PUNICODE_STRING RegistryPath)
{

NOTE
You call the main entry point to a kernel-mode driver “DriverEntry” because the build script—if you use standard
procedures—will instruct the linker that DriverEntry is the entry point, and it’s best to make your code match
this assumption (or else change the build script, but why bother?).

Sample Code
You can experiment with the ideas discussed in this chapter using the STUPID sample driver. STUPID
implements DriverEntry and AddDevice but nothing else. It’s similar to the very first driver I attempted to write
when I was learning.

Before I describe the code you’d write inside DriverEntry, I want to mention a few things about the function prototype itself.
Unbeknownst to you and me (unless we look carefully at the compiler options used in the build script), kernel-mode functions
and the functions in your driver use the __stdcall calling convention when compiled for an x86 computer. This shouldn’t affect
any of your programming, but it’s something to bear in mind when you’re debugging. I used the extern “C” directive because,
as a rule, I package my code in a C++ compilation unit—mostly to gain the freedom to declare variables wherever I please
instead of only immediately after left braces. This directive suppresses the normal C++ decoration of the external name so that
the linker can find this function. Thus, an x86 compile produces a function whose external name is _DriverEntry@8.
Another point about the prototype of DriverEntry is those “IN” keywords. IN and OUT are both noise words that the DDK
defines as empty strings. By original intention, they perform a documentation function. That is, when you see an IN parameter,
you’re supposed to infer that it’s purely input to your function. An OUT parameter is output by your function, while an IN
OUT parameter is used for both input and output. As it happens, the DDK headers don’t always use these keywords intuitively,
and there’s not a great deal of point to them. To give you just one example out of many: DriverEntry claims that the
DriverObject pointer is IN; indeed, you don’t change the pointer, but you will assuredly change the object to which it points.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 28 - Basic Structure of a WDM Driver | Chapter 2

The last general thing I want you to notice about the prototype is that it declares this function as returning an NTSTATUS value.
NTSTATUS is actually just a long integer, but you want to use the typedef name NTSTATUS instead of LONG so that people
understand your code better. A great many kernel-mode support routines return NTSTATUS status codes, and you’ll find a list
of them in the DDK header NTSTATUS.H. I’ll have a bit more to say about status codes in the next chapter; for now, just be
aware that your DriverEntry function will be returning a status code when it finishes.

2.4.1 Overview of DriverEntry


The first argument to DriverEntry is a pointer to a barely initialized driver object that represents your driver. A WDM driver’s
DriverEntry function will finish initializing this object and return. Non-WDM drivers have a great deal of extra work to
do—they must also detect the hardware for which they’re responsible, create device objects to represent the hardware, and do
all the configuration and initialization required to make the hardware fully functional. The relatively arduous detection and
configuration steps are handled automatically for WDM drivers by the PnP Manager, as I’ll discuss in Chapter 6. If you want
to know how a non-WDM driver initializes itself, consult Art Baker and Jerry Lozano’s The Windows 2000 Device Driver
Book (Prentice Hall, 2d ed., 2001) and Viscarola and Mason’s Windows NT Device Driver Development (Macmillan, 1998).
The second argument to DriverEntry is the name of the service key in the registry. This string is not persistent—you must copy
it if you plan to use it later. In a WDM driver, the only use I’ve ever made of this string is as part of WMI registration. (See
Chapter 10.)
A WDM driver’s main job in DriverEntry is to fill in the various function pointers in the driver object. These pointers indicate
to the operating system where to find the subroutines you’ve decided to place in your driver container. They include these
pointer members of the driver object:
„ DriverUnload
Set this to point to whatever cleanup routine you create. The I/O Manager will call this routine just prior to unloading the
driver. If there’s nothing to clean up, you need to have a DriverUnload function for the system to be able to unload your
driver dynamically.
„ DriverExtension->AddDevice
Set this to point to your AddDevice function. The PnP Manager will call AddDevice once for each hardware instance
you’re responsible for. Since AddDevice is so important to the way WDM drivers work, I’ve devoted the next main
section of this chapter (“The AddDevice Routine”) to explaining what it does.
„ DriverStartIo
If your driver uses the standard method of queuing I/O requests, you’ll set this member of the driver object to point to
your StartIo routine. Don’t worry (yet, that is) if you don’t understand what I mean by the “standard” queuing method; all
will become clear in Chapter 5, where you’ll discover that WDM drivers shouldn’t use it.
„ MajorFunction
The I/O Manager initializes this vector of function pointers to point to a dummy dispatch function that fails every request.
You’re presumably going to be handling certain types of IRPs—otherwise, your driver is basically going to be deaf and
inert—so you’ll set at least some of these pointers to your own dispatch functions. Chapter 5 discusses IRPs and dispatch
functions in detail. For now, all you need to know is that you must handle two kinds of IRPs and that you’ll probably be
handling several other kinds as well.
A nearly complete DriverEntry routine will, then, look like this:

extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,


IN PUNICODE_STRING RegistryPath)
{

DriverObject->DriverUnload = DriverUnload;
DriverObject->DriverExtension->AddDevice = AddDevice;

DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;
DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower;
DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = DispatchWmi;

servkey.Buffer = (PWSTR) ExAllocatePool(PagedPool,


RegistryPath->Length + sizeof(WCHAR));
if (!servkey.Buffer)
return STATUS_INSUFFICIENT_RESOURCES;
servkey.MaximumLength = RegistryPath->Length + sizeof(WCHAR);
RtlCopyUnicodeString(&servkey, RegistryPath);
servkey.Buffer[RegistryPath->Length/sizeof(WCHAR)] = 0;

return STATUS_SUCCESS;

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.5 The AddDevice Routine - 29 -

1. These two statements set the function pointers for entry points elsewhere in the driver. I elected to give them simple
names indicative of their function: DriverUnload and AddDevice.
2. Every WDM driver must handle PNP, POWER, and SYSTEM_CONTROL I/O requests, and it should handle
SYSTEM_CONTROL I/O requests. This is where you specify your dispatch functions for these requests. What’s now
IRP_MJ_SYSTEM_CONTROL was called IRP_MJ_WMI in some early beta releases of the Windows XP DDK, which is
why I called my dispatch function DispatchWmi.
3. In place of this ellipsis, you’ll have code to set several additional MajorFunction pointers.
4. If you ever need to access the service registry key elsewhere in your driver, it’s a good idea to make a copy of the
RegistryPath string here. I’ve assumed that you declared a global variable named servkey as a UNICODE_STRING
elsewhere. I’ll explain the mechanics of working with Unicode strings in the next chapter.
5. Returning STATUS_SUCCESS is how you indicate success. If you were to discover something wrong, you’d return an
error code chosen from the standard set in NTSTATUS.H or from a set of error codes that you define yourself.
STATUS_SUCCESS happens to be numerically 0.

Subroutine Naming
Many driver writers give the subroutines in their drivers names that include the name of the driver. For example,
instead of defining AddDevice and DriverUnload functions, many programmers would define Stupid_AddDevice
and Stupid_DriverUnload. I’m told that earlier versions of Microsoft’s WinDbg debugger forced a convention like
this onto (possibly unwilling) programmers because it had just one global namespace. Later versions of this
debugger don’t have that limitation, but you’ll observe that the DDK sample drivers still follow the convention.

Now, I’m a great fan of code reuse and an indifferent typist. For me, it has seemed much simpler to have short
subroutine names that are exactly the same from one project to the next. That way, I can just lift a body of code
from one driver and paste it into another without needing to make a bunch of name changes. I can also compare
one driver with another without having extraneous name differences clutter up the comparison results.

2.4.2 DriverUnload
The purpose of a WDM driver’s DriverUnload function is to clean up after any global initialization that DriverEntry might
have done. There’s almost nothing to do. If you made a copy of the RegistryPath string in DriverEntry, though, DriverUnload
would be the place to release the memory used for the copy:

VOID DriverUnload(PDRIVER_OBJECT DriverObject)


{
RtlFreeUnicodeString(&servkey);
}

If your DriverEntry routine returns a failure status, the system doesn’t call your DriverUnload routine. Therefore, if
DriverEntry generates any side effects that need cleaning up prior to returning an error status, DriverEntry has to perform the
cleanup.

2.5 The AddDevice Routine


In the preceding main section, I showed how you initialize a WDM driver when it’s first loaded. In general, though, a driver
might be called upon to manage more than one actual device. In the WDM architecture, a driver has a special AddDevice
function that the PnP Manager can call for each such device. The function has the following skeleton:

NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)


{

return STATUS_SOMETHING; // e.g., STATUS_SUCCESS


}

The DriverObject argument points to the same driver object that you initialized in your DriverEntry routine. The pdo argument
is the address of the physical device object at the bottom of the device stack, even if there are already filter drivers below.
The basic responsibility of AddDevice in a function driver is to create a device object and link it into the stack rooted in this
PDO. The steps involved are as follows:
1. Call IoCreateDevice to create a device object and an instance of your own device extension object.
2. Register one or more device interfaces so that applications know about the existence of your device. Alternatively, give

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 30 - Basic Structure of a WDM Driver | Chapter 2

the device object a name and then create a symbolic link.


3. Next initialize your device extension and the Flags member of the device object.
4. Call IoAttachDeviceToDeviceStack to put your new device object into the stack.
Now I’ll explain these steps in more detail. I’ll show a complete example of AddDevice at the very end of this discussion.

NOTE
In the code snippets that follow, I’ve deliberately left out all the error handling that should be there. That’s so
I could concentrate on the normal control flow through AddDevice. You mustn’t imitate this programming style
in a production driver—but of course you already knew that. I’ll discuss how to handle errors in the next chapter.
Every code sample in the companion content has full error checking in place too.

2.5.1 Creating a Device Object


You create a device object by calling IoCreateDevice. For example:

PDEVICE_OBJECT fdo;
NTSTATUS status = IoCreateDevice(DriverObject,
sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN, FALSE, &fdo);

The first argument (DriverObject) is the same value supplied to AddDevice as the first parameter. This argument establishes
the connection between your driver and the new device object, thereby allowing the I/O Manager to send you IRPs intended
for the device. The second argument is the size of your device extension structure. As I discussed earlier in this chapter, the I/O
Manager allocates this much additional memory and sets the DeviceExtension pointer in the device object to point to it.
The third argument, which is NULL in this example, can be the address of a UNICODE_STRING providing a name for the
device object. Deciding whether to name your device object and which name to give it requires some thought, and I’ll describe
these surprisingly complex considerations a bit further on in the section “Should I Name My Device Object?”
The fourth argument (FILE_DEVICE_UNKNOWN) is one of the device types defined in WDM.H. Whatever value you specify
here can be overridden by an entry in the device’s hardware key or class key. If both keys have an override, the device key has
precedence. For devices that fit into one of the established categories, specify the right value in one of these places because
some details about the interaction between your driver and the surrounding system depend on it. In fact, the device type is
crucial for the correct functioning of a file system driver or a disk or tape driver. Additionally, the default security settings for
your device object depend on this device type.
The fifth argument (FILE_DEVICE_SECURE_OPEN) provides the Characteristics flag for the device object. (See Table 2-3.)
Most of these flags are relevant for mass storage devices. The flag bit FILE_AUTOGENERATED_DEVICE_NAME is for use
by bus and multifunction drivers when creating PDOs. I’ll discuss the importance of FILE_DEVICE_SECURE_OPEN later in
this chapter in the section “Should I Name My Device Object?” Whatever value you specify here can be overridden by an
entry in the device’s hardware key or class key. If both keys have an override, the hardware key has precedence.
The sixth argument to IoCreateDevice (FALSE in my example) indicates whether the device is exclusive. The I/O Manager
allows only one handle to be opened by normal means to an exclusive device. Whatever value you specify here can be
overridden by an entry in the device’s hardware key or class key. If both keys have an override, the hardware key has
precedence.

NOTE
The exclusivity attribute matters only for whatever named device object is the target of an open request. If you
follow Microsoft’s recommended guidelines for WDM drivers, you won’t give your device object a name. Open
requests will then target the PDO, but the PDO will not usually be marked exclusive because the bus driver
generally has no way of knowing whether you need your device to be exclusive. The only time the PDO will be
marked exclusive is when there’s an Exclusive override in the device’s hardware key or the class key’s
Properties subkey. You’re best advised, therefore, to avoid relying on the exclusive attribute altogether.
Instead, make your IRP_MJ_CREATE handler reject open requests that would violate whatever restriction you
require.

The last argument (&fdo) points to a location where IoCreateDevice will store the address of the device object it creates.
If IoCreateDevice fails for some reason, it returns a status code and doesn’t alter the PDEVICE_OBJECT described by the last
argument. If it succeeds, it returns a successful status code and sets the PDEVICE_OBJECT pointer. You can then proceed to
initialize your device extension and do the other work associated with creating a new device object. Should you discover an
error after this point, you should release the device object and return a status code. The code to accomplish these tasks would
be something like this:

NTSTATUS status = IoCreateDevice(...);

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.5 The AddDevice Routine - 31 -

if (!NT_SUCCESS(status))
return status;

if (<some other error discovered>)


{
IoDeleteDevice(fdo);
return status;
}

I’ll explain the NTSTATUS status codes and the NT_SUCCESS macro in the next chapter.

2.5.2 Naming Devices


Windows XP uses a centralized Object Manager to manage many of its internal data structures, including the driver and device
objects I’ve been talking about. David Solomon and Mark Russinovich present a fairly complete explanation of the Object
Manager and namespace in Chapter 3, “System Mechanisms,” of Inside Windows 2000, Third Edition (Microsoft Press, 2000).
Objects have names, which the Object Manager maintains in a hierarchical namespace. Figure 2-16 is a screen shot of my
DEVVIEW application showing the top level of the name hierarchy. The objects displayed as folders in this screen shot are
directory objects, which can contain subdirectories and “regular” objects. The objects displayed with other icons are examples
of these regular objects. (In this respect, DEVVIEW is similar to the WINOBJ utility that you’ll find in the BIN\WINNT
directory of the Platform SDK. WINOBJ can’t give you information about device objects and drivers, though, which is why I
wrote DEVVIEW in the first place.)

Figure 2-16. Using DEVVIEW to view the namespace.


Device objects can have names that conventionally live in the \Device directory. Names for devices serve two purposes in
Windows XP. Giving your device object a name allows other kernel-mode components to find it by calling service functions
such as IoGetDeviceObjectPointer. Having found your device object, they can send you IRPs.
The other purpose of naming a device object is to allow applications to open handles to the device so they can send you IRPs.
An application uses the standard CreateFile API to open a handle, whereupon it can use ReadFile, WriteFile, and
DeviceIoControl to talk to you. The pathname an application uses to open a device handle begins with the prefix \\.\ rather than
with a standard Universal Naming Convention (UNC) name such as C:\MYFILE.CPP or \\FRED\C-Drive\HISFILE.CPP.
Internally, the I/O Manager converts this prefix to \??\ before commencing a name search. To provide a mechanism for
connecting names in the \?? directory to objects whose names are elsewhere (such as in the \Device directory), the Object
Manager implements an object called a symbolic link.
The name \?? has a special meaning in Windows XP. Confronted with this name, the Object Manager first searches a portion of
the kernel namespace that is local to the current user session. To see how this works, establish two or more sessions and start
DEVVIEW in one of them. Expand the \Sessions folder, and you will eventually see folders for each user. Figure 2-18, which
appears later in this chapter, provides an example. If the local search isn’t successful, the Object Manager then searches the
\GLOBAL?? folder.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 32 - Basic Structure of a WDM Driver | Chapter 2

Symbolic Links
A symbolic link is a little bit like a desktop shortcut in that it points to some other entity that’s the real object of attention. One
use of symbolic links in Windows XP is to connect the leading portion of MS-DOS-style names to devices. Figure 2-17 shows
a portion of the \GLOBAL?? directory, which includes a number of symbolic links. Notice, for example, that C and other drive
letters in the MS-DOS file-naming scheme are actually links to objects whose names are in the \Device directory. These links
allow the Object Manager to jump somewhere else in the namespace as it parses through a name. So if I call CreateFile with
the name C:\MYFILE.CPP, the Object Manager will take this path to open the file:

Figure 2-17. The \GLOBAL?? directory with several symbolic links.


1. Kernel-mode code initially sees the name \??\C:\MYFILE.CPP. The Object Manager special-cases the “??” name to mean
the DosDevices directory for the current session. (In Figure 2-18, this directory is one of the subdirectories of
\Sessions\0\DosDevices.)
2. The Object Manager doesn’t find “C:” in the session DosDevices directory, so it follows a symbolic link named “Global”
to the “GLOBAL??” directory.
3. The Object Manager now looks up “C:” in the \GLOBAL?? directory. It finds a symbolic link by that name, so it forms
the new kernel-mode pathname \Device\HarddiskVolume1\MYFILE.CPP and parses that.
4. Working with the new pathname, the Object Manager looks up “Device” in the root directory and finds a directory object.
5. The Object Manager looks up “HarddiskVolume1” in the \Device directory. It finds a device object by that name.

Opening a Disk File


The overall process that occurs when an application opens a disk file is complicated almost beyond belief. To
continue the example in the text, the driver for HarddiskVolume1 would be a file system driver such as
NTFS.SYS, FASTFAT.SYS, or CDFS.SYS. How a file system realizes that a particular disk volume belongs to it and
initializes to handle the volume is itself a saga of Norse proportions. That would have already happened before
an application could get far enough to call CreateFile with a particular volume letter in the pathname, though,
so we can ignore that process.

The file system driver will locate the topmost device object in the storage stack that includes the physical disk
drive on which the C volume happens to be mounted. The I/O Manager and file system driver share
management of a Volume Parameters Block (VPB) that ties the storage stack and the file system’s volume stack
together. In principle, the file system driver sends IRPs to the storage driver in order to read directory entries
that it can search while parsing the pathname specified by the original CreateFile call. In practice, the file
system calls the kernel cache manager, which fulfills requests from an in-memory cache if possible and makes
recursive calls to the file system driver to fill cache buffers. Deadlock prevention and surprise dismount handling
during this process require heroic efforts, including a mechanism whereby a file system driver can stash a
pointer to an automatic variable (that is, one allocated on the call/return stack) to be used in deeper layers of
recursion within the same thread.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.5 The AddDevice Routine - 33 -

Luckily, you needn’t worry about VPBs and other complications arising from the way file system drivers work in
any driver besides one for a storage device. I won’t say any more about this in the book.

At this point in the process, the Object Manager will create an IRP that it will send to the driver or drivers for
HarddiskVolume1. The IRP will eventually cause some file system driver or another to locate and open a disk file. Describing
how a file system driver works is beyond the scope of this book, but the sidebar “Opening a Disk File” will give you a bit of
the flavor.
If we were dealing with a device name such as COM1, the driver that ended up receiving the IRP would be the driver for
\Device\Serial0. How a device driver handles an open request is definitely within the scope of this book, and I’ll be discussing
it in this chapter (in the section “Should I Name My Device Object?”) and in Chapter 5, when I’ll talk about IRP processing in
general.
A user-mode program can create a symbolic link in the local (session) namespace by calling DefineDosDevice, as in this
example (see Figure 2-18):

BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH,


"barf", "\\Device\\Beep");

Figure 2-18. Symbolic link created by DefineDosDevice


You can create a symbolic link in a WDM driver by calling IoCreateSymbolicLink,

IoCreateSymbolicLink(linkname, targname);

where linkname is the name of the symbolic link you want to create and targname is the name to which you’re linking.
Incidentally, the Object Manager doesn’t care whether targname is the name of any existing object: someone who tries to
access an object by using a link that points to an undefined name simply receives an error. If you want to allow user-mode
programs to override your link and point it somewhere else, you should call IoCreateUnprotectedSymbolicLink instead.
The kernel-mode equivalent of the immediately preceding DefineDosDevice call is this:

UNICODE_STRING linkname;
UNICODE_STRING targname;
RtlInitUnicodeString(&linkname, L"\\DosDevices\\barf");
RtlInitUnicodeString(&targname, L"\\Device\\Beep");
IoCreateSymbolicLink(&linkname, &targname);

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 34 - Basic Structure of a WDM Driver | Chapter 2

Should I Name My Device Object?


Deciding whether to give your device object a name requires, as I said earlier, a little thought. If you give your object a
name, it will be possible for any kernel-mode program to try to open a handle to your device. Furthermore, it will be
possible for any kernel-mode or user-mode program to create a symbolic link to your device object and to use the
symbolic link to try to open a handle. You might or might not want to allow these actions.
The primary consideration in deciding whether to name your device object is security. When someone opens a handle to a
named object, the Object Manager verifies that they have permission to do so. When IoCreateDevice creates a device object
for you, it assigns a default security descriptor based on the device type you specify as the fourth argument. The I/O Manager
uses three basic categories to select a security descriptor:
„ Most file system device objects (that is, disk, CD-ROM, file, and tape) receive the “public default unrestricted” access
control list (ACL). This list gives just SYNCHRONIZE, READ_CONTROL, FILE_READ_ATTRIBUTES, and
FILE_TRAVERSE access to everyone except the System account and all administrators. File system device objects, by
the way, exist only so that there can be a target for a CreateFile call that will open a handle to a file managed by the file
system.
„ Disk devices and network file system objects receive the same ACL as the file system objects, with some modifications.
For example, everyone gets full access to a named floppy disk device object, and administrators get sufficient rights to
run ScanDisk. (User-mode network provider DLLs need greater access to the device object for their corresponding file
system driver, which is why network file systems are treated differently from other file systems.)
„ All other device objects receive the public open unrestricted ACL, which allows anyone with a handle to the device to do
pretty much anything.
You can see that anyone will be able to access a nondisk device for both reading and writing if the driver gives the device
object a name at the time it calls IoCreateDevice. This is because the default security allows nearly full access and because no
security check at all is associated with creating a symbolic link—the security checks happen at open time, based on the named
object’s security descriptor. This is true even if other device objects in the same stack have more restrictive security.

NOTE
IoCreateDeviceSecure, a function in the .NET DDK, allows you to specify a nondefault security descriptor in
situations in which no override is in the registry. This function is too new for us to describe it more fully here.

DEVVIEW will show you the security attributes of the device objects it displays. You can see the operation of the default rules
I just described by examining a file system, a disk device, and any other random device.
The PDO also receives a default security descriptor, but it’s possible to override it with a security descriptor stored in the
hardware key or in the Properties subkey of the class key. (The hardware key has precedence if both keys specify a descriptor.)
Even lacking a specific security override, if either the hardware key or the class key’s Properties subkey overrides the hardware
type or characteristics specification, the I/O Manager constructs a new default security descriptor based on the new type. The
I/O Manager does not, however, override the security setting for any of the other device objects above the PDO. Consequently,
for the overrides (and the administrative actions that set them up) to have any effect, you shouldn’t name your device object.
Don’t despair though—applications can still access your device by means of a registered interface, which I’ll discuss soon.
You need to know about one last security concern. As the Object Manager parses its way through an object name, it
needs only FILE_TRAVERSE access to the intermediate components of the name. It performs a full security check only
on the object named by the final component. So suppose you have a device object reachable under the name
\Device\Beep or by the symbolic link \??\Barf. A user-mode application that tries to open \\.\Barf for writing will be blocked if
the object security has been set up to deny write access. But if the application tries to open a name like \\.\Barf\ExtraStuff that
has additional name qualifications, the open request will make it all the way to the device driver (in the form of an
IRP_MJ_CREATE I/O request) if the user merely has FILE_TRAVERSE permission, which is routinely granted. (In fact, most
systems even run with the option to check for traverse permission turned off.) The I/O Manager expects the device driver to
deal with the additional name components and to perform any required security checks with regard to them.
To avoid the security concern I just described, you can supply the flag FILE_DEVICE_SECURE_OPEN in the device
characteristics argument to IoCreateDevice. This flag causes Windows XP to verify that someone has the right to open a handle
to a device even if additional name components are present.

The Device Name


If you decide to name the device object, you’ll normally put the name in the \Device branch of the namespace. To give it a
name, you have to create a UNICODE_STRING structure to hold the name, and you have to specify that string as an argument
to IoCreateDevice:

UNICODE_STRING devname;
RtlInitUnicodeString(&devname, L"\\Device\\Simple0");
IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname,
...);

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.5 The AddDevice Routine - 35 -

I’ll discuss the use of RtlInitUnicodeString in the next chapter.

NOTE
Starting in Windows XP, device object names are case insensitive. In Windows 98/Me and in Windows 2000,
they are case sensitive. Be sure to spell \Device exactly as shown if you want your driver to be portable across
all the systems. Note also the spelling of \DosDevices, particularly if your mother tongue doesn’t inflect the
plural form of nouns!

Conventionally, drivers assign their device objects a name by concatenating a string naming their device type (“Simple” in this
code fragment) with a 0-based integer denoting an instance of that type. In general, you don’t want to hard-code a name as I
just did—you want to compose it dynamically using string-manipulation functions like the following:

UNICODE_STRING devname;
static LONG lastindex = -1;
LONG devindex = InterlockedIncrement(&lastindex);
WCHAR name[32];
_snwprintf(name, arraysize(name), L"\\Device\\SIMPLE%2.2d",devindex);
RtlInitUnicodeString(&devname, name);
IoCreateDevice(...);

I’ll explain the various service functions used in this code fragment in the next couple of chapters. The instance number you
derive for private device types might as well be a static variable, as shown in the code fragment.

Notes on Device Naming


The \GLOBAL?? directory used to be named \DosDevices. The change was made to move the often-searched
directory of user-mode names to the front of the alphabetical list of directories. Windows 98/Me doesn’t
recognize the name \?? or \GLOBAL??.

Windows 2000 defines a symbolic link named \DosDevices that points to the \?? directory. Windows XP treats
\DosDevices differently depending on the process context at the time you create an object. If you create an
object, such as a symbolic link, in a system thread, \DosDevices refers to \GLOBAL??, and you end up with a
global name. If you create an object in a user thread, \DosDevices refers to \??, and you end up with a
session-specific name. In most situations, a device driver creates symbolic links in its AddDevice function,
which runs in a system thread, and so ends up with globally named objects in all WDM environments simply by
putting the symbolic link in \DosDevices. If you create a symbolic link at another time, you should use
\GLOBAL?? in Windows XP and \DosDevices in earlier systems. See Appendix A for a discussion of how to
distinguish between WDM platforms.

A quick-and-dirty shortcut for testing is to name your device object in the \DosDevices directory, as many of the
sample drivers in the companion content do. A production driver should name the device object in \Device,
however, to avoid the possibility of creating an object that ought to be global in a session-private namespace.

In previous versions of Windows NT, drivers for certain classes of devices (notably disks, tapes, serial ports, and
parallel ports) called IoGetConfigurationInformation to obtain a pointer to a global table containing counts of
devices in each of these special classes. A driver would use the current value of the counter to compose a name
like Harddisk0, Tape1, and so on and would also increment the counter. WDM drivers don’t need to use this
service function or the table it returns, however. Constructing names for the devices in these classes is now the
responsibility of a Microsoft type-specific class driver (such as DISK.SYS).

Device Interfaces
The older method of naming I just discussed—naming your device object and creating a symbolic link name that applications
can use—has two major problems. We’ve already discussed the security implications of giving your device object a name. In
addition, the author of an application that wants to access your device has to know the scheme you adopted to name your
devices. If you’re the only one writing the applications that will be accessing your hardware, that’s not much of a problem. But
if many different companies will be writing applications for your hardware, and especially if many hardware companies are
making similar devices, devising a suitable naming scheme is difficult.
To solve these problems, WDM introduces a new naming scheme for devices that is language-neutral, easily extensible, usable
in an environment with many hardware and software vendors, and easily documented. The scheme relies on the concept of a
device interface, which is basically a specification for how software can access hardware. A device interface is uniquely
identified by a 128-bit GUID. You can generate GUIDs by running the Platform SDK utilities UUIDGEN or GUIDGEN—both
utilities generate the same kind of number, but they output the result in different formats. The idea is that some industry group
gets together to define a standard way of accessing a certain kind of hardware. As part of the standard-making process,
someone runs GUIDGEN and publishes the resulting GUID as the identifier that will be forever after associated with that
interface standard.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 36 - Basic Structure of a WDM Driver | Chapter 2

More About GUIDs


The GUIDs used to identify software interfaces are the same kind of unique identifier that’s used in the
Component Object Model (COM) to identify COM interfaces and in the Open Software Foundation (OSF)
Distributed Computing Environment (DCE) to identify the target of a remote procedure call (RPC). For an
explanation of how GUIDs are generated so as to be statistically unique, see page 66 of Kraig Brockschmidt’s
Inside OLE, Second Edition (Microsoft Press, 1995), which contains a further reference to the original algorithm
specification by the OSF. I found the relevant portion of the OSF specification on line at
http://www.opengroup.org/onlinepubs/9629399/apdxa.htm.

The mechanics of creating a GUID for use in a device driver involve running either UUIDGEN or GUIDGEN and
then capturing the resulting identifier in a header file. GUIDGEN is easier to use because it allows you to choose
to format the GUID for use with the DEFINE_GUID macro and to copy the resulting string onto the Clipboard.
Figure 2-19 shows the GUIDGEN window. You can paste its output into a header file to end up with this:

// {CAF53C68-A94C-11d2-BB4A-00C04FA330A6}
DEFINE_GUID(<<name>>,
0xcaf53c68, 0xa94c, 0x11d2, 0xbb, 0x4a, 0x0, 0xc0, 0x4f,
0xa3, 0x30, 0xa6);

You then replace <<name>> with something more mnemonic like GUID_DEVINTERFACE_SIMPLE and include
the definition in your driver and applications.

Figure 2-19. Using GUIDGEN to generate a GUID


I think of an interface as being analogous to the protein markers that populate the surface of living cells. An application
desiring to access a particular kind of device has its own protein markers that fit like a key into the markers exhibited by
conforming device drivers. See Figure 2-20.

Figure 2-20. Using device interfaces to match applications and devices

Registering a Device Interface


A function driver’s AddDevice function should register one or more device interfaces by calling IoRegisterDeviceInterface, as
shown here:

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.5 The AddDevice Routine - 37 -

#include <initguid.h>

#include "guids.h"

NTSTATUS AddDevice(...)
{

IoRegisterDeviceInterface(pdo, &GUID_DEVINTERFACE_SIMPLE,NULL, &pdx->ifname);

1. We’re about to include a header (GUIDS.H) that contains one or more DEFINE_GUID macros. DEFINE_GUID
normally declares an external variable. Somewhere in the driver, though, we have to actually reserve initialized storage
for every GUID we’re going to reference. The system header file INITGUID.H works some preprocessor magic to make
DEFINE_GUID reserve the storage even if the definition of the DEFINE_GUID macro happens to be in one of the
precompiled header files.
2. I’m assuming here that I put the GUID definitions I want to reference into a separate header file. This would be a good
idea, inasmuch as user-mode code will also need to include these definitions and won’t want to include a bunch of
extraneous kernel-mode declarations relevant only to our driver.
3. The first argument to IoRegisterDeviceInterface must be the address of the PDO for your device. The second argument
identifies the GUID associated with your interface, and the third argument specifies additional qualified names that
further subdivide your interface. Only Microsoft code uses this name subdivision scheme. The last argument is the
address of a UNICODE_STRING structure that will receive the name of a symbolic link that resolves to this device
object.
The return value from IoRegisterDeviceInterface is a Unicode string that applications will be able to determine without
knowing anything special about how you coded your driver and will then be able to use in opening a handle to the device. The
name is pretty ugly, by the way; here’s an example that I generated for one of my sample devices:
\\?\ROOT#UNKNOWN#0000#{b544b9a2-6995-11d3-81b5-00c04fa330a6}.
All that registration actually does is create the symbolic link name and save it in the registry. Later on, in response to the
IRP_MN_START_DEVICE Plug and Play request we’ll discuss in Chapter 7, you’ll make the following call to
IoSetDeviceInterfaceState to enable the interface:

IoSetDeviceInterfaceState(&pdx->ifname, TRUE);

In response to this call, the I/O Manager creates an actual symbolic link object pointing to the PDO for your device. You’ll
make a matching call to disable the interface at a still later time (just call IoSetDeviceInterfaceState with a FALSE argument),
whereupon the I/O Manager will delete the symbolic link object while preserving the registry entry that contains the name. In
other words, the name persists and will always be associated with this particular instance of your device; the symbolic link
object comes and goes with the hardware.
Since the interface name ends up pointing to the PDO, the PDO’s security descriptor ends up controlling whether people can
access your device. That’s good because it’s the PDO’s security that you control in the INF used to install the driver.

Enumerating Device Interfaces


Both kernel-mode and user-mode code can locate all the devices that happen to support an interface in which they’re interested.
I’m going to explain how to enumerate all the devices for a particular interface in user mode. The enumeration code is so
tedious to write that I eventually wrote a C++ class to make my own life simpler. You’ll find this code in the
DEVICELIST.CPP and DEVICELIST.H files that are part of the HIDFAKE and DEVPROP samples in Chapter 8. These files
declare and implement a CDeviceList class, which contains an array of CDeviceListEntry objects. These two classes have the
following declaration:

class CDeviceListEntry
{
public:
CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname);
CDeviceListEntry(){}
CString m_linkname;
CString m_friendlyname;
};

class CDeviceList
{

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 38 - Basic Structure of a WDM Driver | Chapter 2

public:
CDeviceList(const GUID& guid);
~CDeviceList();
GUID m_guid;
CArray<CDeviceListEntry, CDeviceListEntry&> m_list;
int Initialize();
};

The classes rely on the CString class and CArray template class that are part of the Microsoft Foundation Classes (MFC)
framework. The constructors for these two classes simply copy their arguments into the obvious data members:

CDeviceList::CDeviceList(const GUID& guid)


{
m_guid = guid;
}
CDeviceListEntry::CDeviceListEntry(LPCTSTR linkname,LPCTSTR friendlyname)
{
m_linkname = linkname;
m_friendlyname = friendlyname;
}

All the interesting work occurs in the CDeviceList::Initialize function. The executive overview of what it does is this: it will
enumerate all of the devices that expose the interface whose GUID was supplied to the constructor. For each such device, it
will determine a friendly name that we’re willing to show to an unsuspecting end user. Finally it will return the number of
devices it found. Here’s the code for this function:

int CDeviceList::Initialize()
{

HDEVINFO info = SetupDiGetClassDevs(&m_guid, NULL, NULL,


DIGCF_PRESENT │ DIGCF_INTERFACEDEVICE);
if (info == INVALID_HANDLE_VALUE)
return 0;
SP_INTERFACE_DEVICE_DATA ifdata;
ifdata.cbSize = sizeof(ifdata);
DWORD devindex;

for (devindex = 0;
SetupDiEnumDeviceInterfaces(info,NULL, &m_guid, devindex, &ifdata);
++devindex)
{
DWORD needed;

SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL);

PSP_INTERFACE_DEVICE_DETAIL_DATA detail =
(PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc(needed);
detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)};
SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail, needed, NULL, &did));

TCHAR fname[256];
if (!SetupDiGetDeviceRegistryProperty(info, &did,
SPDRP_FRIENDLYNAME, NULL, (PBYTE) fname,
sizeof(fname), NULL)
&& !SetupDiGetDeviceRegistryProperty(info, &did,
SPDRP_DEVICEDESC,
NULL, (PBYTE) fname, sizeof(fname), NULL))
_tcsncpy(fname, detail->DevicePath, 256);
fname[255] = 0;

CDeviceListEntry e(detail->DevicePath, fname);


free((PVOID) detail);

m_list.Add(e);
}

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.5 The AddDevice Routine - 39 -

SetupDiDestroyDeviceInfoList(info);
return m_list.GetSize();
}

1. This statement opens an enumeration handle that we can use to find all devices that have registered an interface that uses
the same GUID.
2. Here we call SetupDiEnumDeviceInterfaces in a loop to find each device.
3. The only two items of information we need are the detail information about the interface and information about the
device instance. The detail is just the symbolic name for the device. Since it’s variable in length, we make two calls to
SetupDiGetDeviceInterfaceDetail. The first call determines the length. The second call retrieves the name.
4. We obtain a friendly name for the device from the registry by asking for either the FriendlyName or the DeviceDesc.
5. We create a temporary instance named e of the CDeviceListEntry class, using the device’s symbolic name as both the link
name and the friendly name.

NOTE
You might be wondering how the registry comes to have a FriendlyName for a device. The INF file you use to
install your device driver—see Chapter 15—can have an HW section that specifies registry parameters for the
device. You can provide a FriendlyName as one of these parameters, but bear in mind that every instance of
your hardware will have the same name if you do. The MAKENAMES sample describes a DLL-based way of
defining a unique friendly name for each instance. You can also write a CoInstaller DLL that will define unique
friendly names.

If you don’t define a FriendlyName, by the way, most system components will use the DeviceDesc string in the
registry. This string originates in the INF file and will usually describe your device by manufacturer and model.

Sample Code
The DEVINTERFACE sample is a user-mode program that enumerates all instances of all known device interface
GUIDs on your system. One way to use this sample is as a way to determine which GUID you need to enumerate
to find a particular device.

2.5.3 Other Global Device Initialization


You need to take some other steps during AddDevice to initialize your device object. I’m going to describe these steps in the
order you should do them, which isn’t exactly the same order as their respective logical importance. I want to emphasize that
the code snippets in this section are even more fragmented than usual—I’m going to show only enough of the entire
AddDevice routine to establish the surrounding context for the small pieces I’m trying to illustrate.

Initializing the Device Extension


The content and management of the device extension are entirely up to you. The data members you place in this structure will
obviously depend on the details of your hardware and on how you go about programming the device. Most drivers would need
a few items placed there, however, as illustrated in the following fragment of a declaration:

typedef struct _DEVICE_EXTENSION {

PDEVICE_OBJECT DeviceObject;

PDEVICE_OBJECT LowerDeviceObject;

PDEVICE_OBJECT Pdo;

UNICODE_STRING ifname;

IO_REMOVE_LOCK RemoveLock;

DEVSTATE devstate;
DEVSTATE prevstate;
DEVICE_POWER_STATE devpower;
SYSTEM_POWER_STATE syspower;

DEVICE_CAPABILITIES devcaps;

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 40 - Basic Structure of a WDM Driver | Chapter 2

} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

1. I find it easiest to mimic the pattern of structure declaration used in the official DDK, so I declared this device extension
as a structure with a tag as well as a type and pointer-to-type name.
2. You already know that you locate your device extension by following the DeviceExtension pointer from the device object.
It’s also useful in several situations to be able to go the other way—to find the device object given a pointer to the
extension. The reason is that the logical argument to certain functions is the device extension itself (since that’s where all
of the per-instance information about your device resides). Hence, I find it useful to have this DeviceObject pointer.
3. I’ll mention in a few paragraphs that you need to record the address of the device object immediately below yours when
you call IoAttachDeviceToDeviceStack, and LowerDeviceObject is the place to do that.
4. A few service routines require the address of the PDO instead of some higher device object in the same stack. It’s very
difficult to locate the PDO, so the easiest way to satisfy the requirement of those functions is to record the PDO address
in a member of the device extension that you initialize during AddDevice.
5. Whichever method (symbolic link or device interface) you use to name your device, you’ll want an easy way to
remember the name you assign. In this code fragment, I’ve declared a Unicode string member named ifname to record a
device interface name. If you were going to use a symbolic link name instead of a device interface, it would make sense
to give this member a more mnemonic name, such as linkname.
6. I’ll discuss in Chapter 6 a synchronization problem affecting how you decide when it’s safe to remove this device object
by calling IoDeleteDevice. The solution to that problem involves using an IO_REMOVE_LOCK object that needs to be
allocated in your device extension as shown here. AddDevice needs to initialize that object.
7. You’ll probably need a device extension variable to keep track of the current Plug and Play state and current power states
of your device. DEVSTATE is an enumeration that I’m assuming you’ve declared elsewhere in your own header file. I’ll
discuss the use of all these state variables in later chapters.
8. Another part of power management involves remembering some capability settings that the system initializes by means
of an IRP. The devcaps structure in the device extension is where I save those settings in my sample drivers.
The initialization statements in AddDevice (with emphasis on the parts involving the device extension) would be as follows:

NTSTATUS AddDevice(...)
{
PDEVICE_OBJECT fdo;
IoCreateDevice(..., sizeof(DEVICE_EXTENSION), ..., &fdo);
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
pdx->DeviceObject = fdo;
pdx->Pdo = pdo;
IoInitializeRemoveLock(&pdx->RemoveLock, ...);
pdx->devstate = STOPPED;
pdx->devpower = PowerDeviceD0;
pdx->syspower = PowerSystemWorking;
IoRegisterDeviceInterface(..., &pdx->ifname);
pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(...);
}

In this code snippet, STOPPED and DEVICE_EXTENSION are things I defined in one of my own header files.

Initializing the Default DPC Object


Many devices signal completion of operations by means of an interrupt. As you’ll learn when I discuss interrupt handling in
Chapter 7, there are strict limits on what your interrupt service routine (ISR) can do. In particular, an ISR isn’t allowed to call
the routine (IoCompleteRequest) that signals completion of an IRP, but that’s exactly one of the steps you’re likely to want to
take. You utilize a deferred procedure call (DPC) to get around the limitations. Your device object contains a subsidiary DPC
object that can be used for scheduling your particular DPC routine, and you need to initialize it shortly after creating the device
object:

NTSTATUS AddDevice(...)
{
IoCreateDevice(...);
IoInitializeDpcRequest(fdo, DpcForIsr)
}

Setting the Buffer Alignment Mask


Devices that perform DMA transfers work directly with data buffers in memory. The HAL might require that buffers used for

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.5 The AddDevice Routine - 41 -

DMA be aligned to some particular boundary, and your device might require still more stringent alignment. The
AlignmentRequirement field of the device object expresses the restriction—it’s a bit mask equal to 1 less than the required
address boundary. You can round an arbitrary address down to this boundary with this statement:

PVOID address = ...;


SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) ((SIZE_T) address & ~ar);

You round an arbitrary address up to the next alignment boundary like this:

PVOID address = ...;


SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) (((SIZE_T) address + ar) & ~ar);

In these two code fragments, I used SIZE_T casts to transform the pointer (which may be 32 bits or 64 bits wide, depending on
the platform for which you’re compiling) into an integer wide enough to span the same range as the pointer.
IoCreateDevice sets the AlignmentRequirement field of the new device object equal to whatever the HAL requires. For
example, the HAL for Intel x86 chips has no alignment requirement, so AlignmentRequirement is 0 initially. If your device
requires a more stringent alignment for the data buffers it works with (say, because you have bus-mastering DMA capability
with a special alignment requirement), you want to override the default setting. For example:

if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement)


fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1;

I’ve assumed here that elsewhere in your driver is a manifest constant named MYDEVICE_ALIGNMENT that equals a power
of 2 and represents the required alignment of your device’s data buffers.

Miscellaneous Objects
Your device might well use other objects that need to be initialized during AddDevice. Such objects might include various
synchronization objects, linked list anchors, scatter/gather list buffers, and so on. I’ll discuss these objects, and the fact that
initialization during AddDevice would be appropriate, in various other parts of this book.

Initializing the Device Flags


Two of the flag bits in your device object need to be initialized during AddDevice and never changed thereafter: the
DO_BUFFERED_IO and DO_DIRECT_IO flags. You can set one (but only one) of these bits to declare once and for all how
you want to handle memory buffers coming from user mode as part of read and write requests. (I’ll explain in Chapter 7 what
the difference between these two buffering methods is and why you’d want to pick one or the other.) The reason you have to
make this important choice during AddDevice is that any upper filter drivers that load afterwards will be copying your flag
settings, and it’s the setting of the bits in the topmost device object that’s actually important. Were you to change your mind
after the filter drivers loaded, they probably wouldn’t know about the change.
Two of the flag bits in the device object pertain to power management. In contrast with the two buffering flags, these two can
be changed at any time. I’ll discuss them in greater detail in Chapter 8, but here’s a preview. DO_POWER_PAGABLE means
that the Power Manager must send you IRP_MJ_POWER requests at interrupt request level (IRQL) PASSIVE_LEVEL. (If you
don’t understand all of the concepts in the preceding sentence, don’t worry—I’ll completely explain all of them in later
chapters.) DO_POWER_INRUSH means that your device draws a large amount of current when powering on, so the Power
Manager should make sure that no other inrush device is powering up simultaneously.

Building the Device Stack


Each filter and function driver has the responsibility of building up the stack of device objects, starting from the PDO and
working upward. You accomplish your part of this work with a call to IoAttachDeviceToDeviceStack:

NTSTATUS AddDevice(..., PDEVICE_OBJECT pdo)


{
PDEVICE_OBJECT fdo;
IoCreateDevice(..., &fdo);
pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo);
}

The first argument to IoAttachDeviceToDeviceStack (fdo) is the address of your own newly created device object. The second
argument is the address of the PDO. The second parameter to AddDevice is this address. The return value is the address of
whatever device object is immediately underneath yours, which can be the PDO or the address of some lower filter device
object. Figure 2-21 illustrates the situation when there are three lower filter drivers for your device. By the time your
AddDevice function executes, all three of their AddDevice functions have already been called. They have created their
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 42 - Basic Structure of a WDM Driver | Chapter 2

respective FiDOs and linked them into the stack rooted at the PDO. When you call IoAttachDeviceToDeviceStack, you get
back the address of the topmost FiDO.
IoAttachDeviceToDeviceStack might conceivably fail by returning a NULL pointer. For this to occur, someone would have to
remove the physical device from the system at just the point in time when your AddDevice function was doing its work, and
the PnP Manager would have to process the removal on another CPU. I’m not even sure these conditions are enough to trigger
a failure. (Or else the driver under you could have forgotten to clear DO_DEVICE_INITIALIZING, I suppose.) You would deal
with the failure by cleaning up and returning STATUS_DEVICE_REMOVED from your AddDevice function.

Figure 2-21. What IoAttachDeviceToDeviceStack returns.

Clear DO_DEVICE_INITIALIZING
Pretty much the last thing you do in AddDevice should be to clear the DO_DEVICE_INITIALIZING flag in your device object:

fdo->Flags &= ~DO_DEVICE_INITIALIZING;

While this flag is set, the I/O Manager will refuse to attach other device objects to yours or to open a handle to your device.
You have to clear the flag because your device object initially arrives in the world with the flag set. In previous releases of
Windows NT, most drivers created all of their device objects during DriverEntry. When DriverEntry returns, the I/O Manager
automatically traverses the list of device objects linked from the driver object and clears this flag. Since you’re creating your
device object long after DriverEntry returns, however, this automatic flag clearing won’t occur, and you must do it yourself.

2.5.4 Putting the Pieces Together


Here is a complete AddDevice function, presented without error checking or annotations and including all the pieces described
in the preceding sections:

NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)


{
PDEVICE_OBJECT fdo;
NTSTATUS status = IoCreateDevice(DriverObject,
sizeof(DEVICE_EXTENSION), NULL,
FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &fdo);

PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)


fdo->DeviceExtension;

IoRegisterDeviceInterface(pdo, &GUID_DEVINTERFACE_SIMPLE, NULL, &pdx->ifname);

pdx->DeviceObject = fdo;
pdx->Pdo = pdo;
IoInitializeRemoveLock(&pdx->RemoveLock, 0, 0, 0);
pdx->devstate = STOPPED;

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.6 Windows 98/Me Compatibility Notes - 43 -

pdx->devpower = PowerDeviceD0;
pdx->syspower = PowerSystemWorking;

IoInitializeDpcRequest(fdo, DpcForIsr);

if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement)


fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1;

KeInitializeSpinLock(&pdx->SomeSpinLock);
KeInitializeEvent(&pdx->SomeEvent, NotificationEvent, FALSE);
InitializeListHead(&pdx->SomeListAnchor);

fdo->Flags │= DO_BUFFERED_IO │ DO_POWER_PAGABLE;

pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo);

fdo->Flags &= ~DO_DEVICE_INITIALIZING;


return STATUS_SUCCESS;
}

2.6 Windows 98/Me Compatibility Notes


Windows 98/Me handles some of the details surrounding device object creation and driver loading differently than Windows
XP. This section explains the differences that might affect your driver. I’ve already mentioned a few of these, but repetition
can’t hurt.

2.6.1 Differences in DriverEntry Call


As I indicated earlier, the DriverEntry routine receives a UNICODE_STRING argument naming the service key for the driver.
In Windows XP, the string is a full registry path of the form \Registry\Machine\System\CurrentControlSet\Services\xxx (where
xxx is the name of the service entry for your driver). In Windows 98/Me, however, the string is of the form
System\CurrentControlSet\Services\<classname >\<instance#> (where <classname> is the class name of your device and
<instance#> is an instance number such as 0000 indicating which device of that class you happen to be). You can open the key
in either environment by calling ZwOpenKey, however.

2.6.2 DriverUnload
Windows 98/Me will call DriverUnload within a call to IoDeleteDevice that occurs within DriverEntry. You care about this
only if (1) your DriverEntry function calls IoCreateDevice and then (2) decides to return an error status, whereupon it (3)
cleans up by calling IoDeleteDevice.

2.6.3 The \GLOBAL?? Directory


Windows 98/Me doesn’t understand the directory name \GLOBAL??. Consequently, you need to put symbolic link names in
the \DosDevices directory. You can use \DosDevices in Windows XP also because it’s a symbolic link to the \?? directory,
whose (virtual) contents include \GLOBAL??.

2.6.4 Unimplemented Device Types


Windows 98 didn’t support creating device objects for mass storage devices. These are devices with types
FILE_DEVICE_DISK, FILE_DEVICE_TAPE, FILE_DEVICE_CD_ROM, and FILE_DEVICE_VIRTUAL_DISK. You can call
IoCreateDevice, and it will even return with a status code of STATUS_SUCCESS, but it won’t have actually created a device
object or modified the PDEVICE_OBJECT variable whose address you gave as the last argument.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 45 -

Chapter 3
3 Basic Programming Techniques

Writing a WDM driver is fundamentally an exercise in software engineering. Whatever the requirements of your particular
hardware, you will combine various elements to form a program. In the preceding chapter, I described the basic structure of a
WDM driver, and I showed you two of its elements—DriverEntry and AddDevice—in detail. In this chapter, I’ll focus on the
even more basic topic of how you call upon the large body of kernel-mode support routines that the operating system exposes
for your use. I’ll discuss error handling, memory and data structure management, registry and file access, and a few other
topics. I’ll round out the chapter with a short discussion of the steps you can take to help debug your driver.

3.1 The Kernel-Mode Programming Environment


Figure 3-1 illustrates some of the components that make up the Microsoft Windows XP operating system. Each component
exports service functions whose names begin with a particular two-letter or three-letter prefix:

Figure 3-1. Overview of kernel-mode support routines


„ The I/O Manager (prefix Io) contains many service functions that drivers use, and I’ll be discussing them all throughout
this book.
„ The Process Structure module (prefix Ps) creates and manages kernel-mode threads. An ordinary WDM driver might use
an independent thread to repeatedly poll a device incapable of generating interrupts, and for other purposes.
„ The Memory Manager (prefix Mm) controls the page tables that define the mapping of virtual addresses onto physical
memory.
„ The executive (prefix Ex) supplies heap management and synchronization services. I’ll discuss the heap management
service functions in this chapter. The next chapter covers the synchronization services.
„ The Object Manager (prefix Ob) provides centralized control over the many data objects with which Windows XP works.
WDM drivers rely on the Object Manager for keeping a reference count that prevents an object from disappearing while
someone is still using it and to convert object handles to pointers to the objects the handles represent.
„ The Security Reference Monitor (prefix Se) allows file system drivers to perform security checks. Someone else has
usually dealt with security concerns by the time an I/O request reaches a WDM driver, so I won’t be discussing these
functions in this book.
„ The so-called run-time library component (prefix Rtl) contains utility routines, such as list and string-management
routines, that kernel-mode drivers can use instead of regular ANSI-standard library routines. For the most part, the
operation of these functions is obvious from their names, and you would pretty much know how to use them in a program
if you just were aware of them. I’ll describe a few of them in this chapter.
„ Windows XP implements the native API for kernel-mode callers using routine names that begin with the prefix Zw. The
DDK documents just a few of the ZwXxx functions, namely the ones that pertain to registry and file access. I’ll discuss
those functions in this chapter.
- 46 - Basic Programming Techniques | Chapter 3

„ The Windows XP kernel (prefix Ke) is where all the low-level synchronization of activities between threads and
processors occurs. I’ll discuss the KeXxx functions in the next chapter.
„ The very bottom layer of the operating system, on which the support sandwich rests, is the hardware abstraction layer (or
HAL, prefix Hal). All the operating system’s knowledge of how the computer is actually wired together reposes in the
HAL. The HAL understands how interrupts work on a particular platform, how to address I/O and memory-mapped
devices, and so on. Instead of talking directly to their hardware, WDM drivers call functions in the HAL to do it. The
driver ends up being platform-independent and bus-independent.

3.1.1 Using Standard Run-Time Library Functions


Historically, the Windows NT architects preferred that drivers not use the run-time libraries supplied by vendors of C
compilers. In part, the initial disapproval arose from simple timing. Windows NT was designed at a time when there was no
ANSI standard for what functions belonged in a standard library and when many compiler vendors existed, each with its own
idea of what might be cool to include and its own unique quality standards. Another factor is that standard run-time library
routines sometimes rely on initialization that can happen only in a user-mode application and are sometimes implemented in a
thread-unsafe or multiprocessor-unsafe way.
I suggested in the first edition that it would be OK to use a number of “standard” runtime library functions for string
processing. That was probably bad advice, though, because most of us (including me!) have a hard time using them
safely. It’s true (at least at the time I’m writing this paragraph) that the kernel exports standard string functions such as
strcpy, wcscmp, and strncpy. Since these functions work with null-terminated strings, though, it’s just too easy to make
mistakes with them. Are you sure you’ve provided a large enough target buffer for strcpy? Are you sure that both of the strings
you’re comparing with wcscmp have a null terminator before they tail off into a not-present page? Were you aware that strncpy
can fail to null-terminate the target string if the source string is longer than the target?
Because of all the potential problems with run-time library functions, Microsoft now recommends using the set of “safe” string
functions declared in NtStrsafe.h. I’ll discuss these functions, and the very few standard string and byte functions that it’s safe
to use in a driver, later in this chapter.

3.1.2 A Caution About Side Effects


Many of the support “functions” that you use in a driver are defined as macros in the DDK header files. We were all taught to
avoid using expressions that have side effects (that is, expressions that alter the state of the computer in some persistent way)
as arguments to macros for the obvious reason that the macro can invoke the argument more or less than exactly once.
Consider, for example, the following code:

int a = 2, b = 42, c;
c = min(a++, b);

What’s the value of a afterward? (For that matter, what’s the value of c?) Take a look at a plausible implementation of min as a
macro:

#define min(x,y) (((x) < (y)) ? (x) : (y))

If you substitute a++ for x, you can see that a will equal 4 because the expression a++ gets executed twice. The value of the
“function” min will be 3 instead of the expected 2 because the second invocation of a++ delivers the value.
You basically can’t tell when the DDK will use a macro and when it will declare a real external function. Sometimes a
particular service function will be a macro for some platforms and a function call for other platforms. Furthermore, Microsoft
is free to change its mind in the future. Consequently, you should follow this rule when programming a WDM driver:
Never use an expression that has side effects as an argument to a kernel-mode service function.

3.2 Error Handling


To err is human; to recover is part of software engineering. Exceptional conditions are always arising in programs. Some of
them start with program bugs, either in our own code or in the user-mode applications that invoke our code. Some of them
relate to system load or the instantaneous state of hardware. Whatever the cause, unusual circumstances demand a flexible
response from our code. In this section, I’ll describe three aspects of error handling: status codes, structured exception handling,
and bug checks. In general, kernel-mode support routines report unexpected errors by returning a status code, whereas they
report expected variations in normal flow by returning a Boolean or numeric value other than a formal status code. Structured
exception handling offers a standardized way to clean up after really unexpected events, such as dereferencing an invalid
user-mode pointer, or to avoid the system crash that normally ensues after such events. A bug check is the internal name for a
catastrophic failure for which a system shutdown is the only cure.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.2 Error Handling - 47 -

3.2.1 Status Codes


Kernel-mode support routines (and your code too, for that matter) indicate success or failure by returning a status code to their
caller. An NTSTATUS value is a 32-bit integer composed of several subfields, as illustrated in Figure 3-2. The high-order 2 bits
denote the severity of the condition being reported—success, information, warning, or error. I’ll explain the impact of the
customer flag shortly. The facility code indicates which system component originated the message and basically serves to
decouple development groups from each other when it comes to assigning numbers to codes. The remainder of the status
code—16 bits’ worth—indicates the exact condition being reported.

Figure 3-2. Format of an NTSTATUS code.


You should always check the status returns from routines that provide them. I’m going to break this rule frequently in some of
the code fragments I show you because including all the necessary error handling code often obscures the expository purpose
of the fragment. But don’t you emulate this sloppy practice!
If the high-order bit of a status code is 0, any number of the remaining bits could be set and the code would still indicate
success. Consequently, never just compare status codes with 0 to see whether you’re dealing with success—instead, use the
NT_SUCCESS macro:

NTSTATUS status = SomeFunction(...);


if (!NT_SUCCESS(status))
{
//handle error

Not only do you want to test the status codes you receive from routines you call, but you also want to return status codes to the
routines that call you. In the preceding chapter, I dealt with two driver subroutines—DriverEntry and AddDevice—that are
both defined as returning NTSTATUS codes. As I discussed, you want to return STATUS_SUCCESS as the success indicator
from these routines. If something goes wrong, you often want to return an appropriate status code, which is sometimes the
same value that a routine returned to you.
As an example, here are some initial steps in the AddDevice function, with all the error checking left in:

NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)


{
NTSTATUS status;
PDEVICE_OBJECT fdo;
status = IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION),
NULL, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE,
&fdo);

if (!NT_SUCCESS(status))
{

KdPrint(("IoCreateDevice failed - %X\n", status));


return status;
}
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
pdx->DeviceObject = fdo;
pdx->Pdo = pdo;
pdx->state = STOPPED;

IoInitializeRemoveLock(&pdx->RemoveLock, 0, 0, 0);

status = IoRegisterDeviceInterface(pdo, &GUID_SIMPLE, NULL,


&pdx->ifname);
if (!NT_SUCCESS(status))
{
KdPrint(("IoRegisterDeviceInterface failed - %X\n", status));
IoDeleteDevice(fdo);

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 48 - Basic Programming Techniques | Chapter 3

return status;
}

1. If IoCreateDevice fails, we’ll simply return the same status code it gave us. Note the use of the NT_SUCCESS macro as
described in the text.
2. It’s sometimes a good idea, especially while debugging a driver, to print any error status you discover. I’ll discuss the
exact usage of KdPrint later in this chapter (in the “Making Debugging Easier” section).
3. IoInitializeRemoveLock, discussed in Chapter 6, cannot fail. Consequently, there’s no need to check a status code.
Generally speaking, most functions declared with type VOID are in the same “cannot fail” category. A few VOID
functions can fail by raising an exception, but the DDK documents that behavior very clearly.
4. Should IoRegisterDeviceInterface fail, we have some cleanup to do before we return to our caller; namely, we must call
IoDeleteDevice to destroy the device object we just created.
You don’t always have to fail calls that lead to errors in the routines you call, of course. Sometimes you can ignore an error.
For example, in Chapter 8, I’ll tell you about a power management I/O request with the subtype
IRP_MN_POWER_SEQUENCE that you can use as an optimization to avoid unnecessary state restoration during a power-up
operation. Not only is it optional whether you use this request, but it’s also optional for the bus driver to implement it.
Therefore, if that request should fail, you should just go about your business. Similarly, you can ignore an error from
IoAllocateErrorLogEntry because the inability to add an entry to the error log isn’t at all critical.
Completing an IRP with an error status—driver programmers call this failing the IRP—usually leads to a failure indication in
the return from a Win32 API function in an application. The application can call GetLastError to determine the cause of the
failure. If you fail the IRP with a status code containing the customer flag, GetLastError will return exactly that status code. If
you fail the IRP with a status code in which the customer flag is 0 (which is the case for every standard status code defined by
Microsoft), GetLastError returns a value drawn from WINERROR.H in the Platform SDK. Knowledge Base article Q113996,
“Mapping NT Status Error Codes to Win32 Error Codes,” documents the correspondence between GetLastError return values
and kernel status codes. Table 3-1 shows the correspondence for the most important status codes.

Kernel-Mode Status Code User-Mode Error Code


STATUS_SUCCESS NO_ERROR (0)
STATUS_INVALID_PARAMETER ERROR_INVALID_PARAMETER
STATUS_NO_SUCH_FILE ERROR_FILE_NOT_FOUND
STATUS_ACCESS_DENIED ERROR_ACCESS_DENIED
STATUS_INVALID_DEVICE_REQUEST ERROR_INVALID_FUNCTION
ERROR_BUFFER_TOO_SMALL ERROR_INSUFFICIENT_BUFFER
STATUS_DATA_ERROR ERROR_CRC

Table 3-1. Correspondence Between Common Kernel-Mode and User-Mode Status Codes
The difference between an error and a warning can be significant. For example, failing a METHOD_BUFFERED control
operation (see Chapter 9) with STATUS_BUFFER_OVERFLOW—a warning—causes the I/O Manager to copy data to the
user-mode buffer. Failing the same operation with STATUS_BUFFER_TOO_SMALL—an error—causes the I/O Manager to
not copy any data.

3.2.2 Structured Exception Handling


The Windows family of operating systems provides a method of handling exceptional conditions that helps you avoid potential
system crashes. Closely integrated with the compiler’s code generator, structured exception handling lets you easily place a
guard on sections of your code and invoke exception handlers when something goes wrong in the guarded section. Structured
exception handling also lets you easily provide cleanup statements that you can be sure will always execute no matter how
control leaves a guarded section of code.
Very few of my seminar students have been familiar with structured exceptions, so I’m going to explain some of the basics
here. You can write better, more bulletproof code if you use these facilities. In many situations, the parameters that you receive
in a WDM driver have been thoroughly vetted by other code and won’t cause you to generate inadvertent exceptions. Good
taste may, therefore, be the only impetus for you to use the stuff I’m describing in this section. As a general rule, though, you
always want to protect direct references to user-mode virtual memory with a structured exception frame. Such references occur
when you directly reference memory and when you call MmProbeAndLockPages, ProbeForRead, and ProbeForWrite, and
perhaps at other times.

Sample Code
The SEHTEST sample driver illustrates the mechanics of structured exceptions in a WDM driver.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.2 Error Handling - 49 -

Which Exceptions Can Be Trapped


Gary Nebbett researched the question of which exceptions can be trapped with the structured exception
mechanism and reported his results in a newsgroup post several years ago. The SEHTEST sample incorporates
what he learned. In summary, the following exceptions will be caught when they occur at IRQL less than or
equal to DISPATCH_LEVEL (note that some of these are specific to the Intel x86 processor):

„ Anything signaled by ExRaiseStatus and related functions

„ Attempt to dereference invalid pointer to user-mode memory

„ Debug or breakpoint exception

„ Integer overflow (INTO instruction)

„ Invalid opcode

Note that a reference to an invalid kernel-mode pointer leads directly to a bug check and can’t be trapped.
Likewise, a divide-by-zero exception or a BOUND instruction exception leads to a bug check.

Kernel-mode programs use structured exceptions by establishing exception frames on the same stack that’s used for argument
passing, subroutine calling, and automatic variables. A dedicated processor register points to the current exception frame. Each
frame points to the preceding frame. Whenever an exception occurs, the kernel searches the list of exception frames for an
exception handler. It will always find one because there is an exception frame at the very top of the stack that will handle any
otherwise unhandled exception. Once the kernel locates an exception handler, it unwinds the execution and exception frame
stacks in parallel, calling cleanup handlers along the way. Then it gives control to the exception handler.
When you use the Microsoft compiler, you can use Microsoft extensions to the C/C++ language that hide some of the
complexities of working with the raw operating system primitives. You use the __try statement to designate a compound
statement as the guarded body for an exception frame, and you use either the __finally statement to establish a termination
handler or the __except statement to establish an exception handler.

NOTE
It’s better to always spell the words __try, __finally, and __except with leading underscores. In C compilation
units, the DDK header file WARNING.H defines macros spelled try, finally, and except to be the words with
underscores. DDK sample programs use those macro names rather than the underscored names. The problem
this can create for you is that in a C++ compilation unit, try is a statement verb that pairs with catch to invoke
a completely different exception mechanism that’s part of the C++ language. C++ exceptions don’t work in a
driver unless you manage to duplicate some infrastructure from the run-time library. Microsoft would prefer you
not do that because of the increased size of your driver and the memory pool overhead associated with handling
the throw verb.

Try-Finally Blocks
It’s easiest to begin explaining structured exception handling by describing the try-finally block, which you can use to provide
cleanup code:

__try
{
<guarded body>
}
__finally
{
<termination handler>
}

In this fragment of pseudocode, the guarded body is a series of statements and subroutine calls that expresses some main idea
in your program. In general, these statements have side effects. If there are no side effects, there’s no particular point to using a
try-finally block because there’s nothing to clean up. The termination handler contains statements that undo some or all of the
side effects that the guarded body might leave behind.
Semantically, the try-finally block works as follows: First the computer executes the guarded body. When control leaves the
guarded body for any reason, the computer executes the termination handler. See Figure 3-3.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 50 - Basic Programming Techniques | Chapter 3

Figure 3-3. Flow of control in a try-finally block.


Here’s one simple illustration:

LONG counter = 0;
__try
{
++counter;
}
__finally
{
--counter;
}
KdPrint(("%d\n", counter));

First the guarded body executes and increments the counter variable from 0 to 1. When control “drops through” the right brace
at the end of the guarded body, the termination handler executes and decrements counter back to 0. The value printed will
therefore be 0.
Here’s a slightly more complicated variation:

VOID RandomFunction(PLONG pcounter)


{
__try
{
++*pcounter;
return;
}
__finally
{
--*pcounter;
}
}

The net result of this function is no change to the integer at the end of the pcounter pointer: whenever control leaves the
guarded body for any reason, including a return statement or a goto, the termination handler executes. Here the guarded body
increments the counter and performs a return. Next the cleanup code executes and decrements the counter. Then the subroutine
actually returns.
One final example should cement the idea of a try-finally block:

static LONG counter = 0;


__try
{
++counter;
BadActor();
}
__finally
{
--counter;
}

Here I’m supposing that we call a function, BadActor, that will raise some sort of exception that triggers a stack unwind. As
part of the process of unwinding the execution and exception stacks, the operating system will invoke our cleanup code to
restore the counter to its previous value. The system then continues unwinding the stack, so whatever code we have after the
__finally block won’t get executed.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.2 Error Handling - 51 -

Try-Except Blocks
The other way to use structured exception handling involves a try-except block:

__try
{
<guarded body>
}
__except(<filter expression>)
{
<exception handler>
}

The guarded body in a try-except block is code that might fail by generating an exception. Perhaps you’re going to call a
kernel-mode service function such as MmProbeAndLockPages that uses pointers derived from user mode without explicit
validity checking. Perhaps you have other reasons. In any case, if you manage to get all the way through the guarded body
without an error, control continues after the exception handler code. You’ll think of this case as being the normal one. If an
exception arises in your code or in any of the subroutines you call, however, the operating system will unwind the execution
stack, evaluating the filter expressions in __except statements. These expressions yield one of the following values:
„ EXCEPTION_EXECUTE_HANDLER is numerically equal to 1 and tells the operating system to transfer control to your
exception handler. If your handler falls through the ending right brace, control continues within your program at the
statement immediately following that right brace. (I’ve seen Platform SDK documentation to the effect that control
returns to the point of the exception, but that’s not correct.)
„ EXCEPTION_CONTINUE_SEARCH is numerically equal to 0 and tells the operating system that you can’t handle the
exception. The system keeps scanning up the stack looking for another handler. If no one has provided a handler for the
exception, a system crash will occur.
„ EXCEPTION_CONTINUE_EXECUTION is numerically equal to -1 and tells the operating system to return to the point
where the exception was raised. I’ll have a bit more to say about this expression value a little further on.
Take a look at Figure 3-4 for the possible control paths within and around a try-except block.

Figure 3-4. Flow of control in a try-except block.


For example, you can protect yourself from receiving an invalid pointer by using code like the following. (See the SEHTEST
sample in the companion content.)

PVOID p = (PVOID) 1;
__try
{
KdPrint(("About to generate exception\n"));
ProbeForWrite(p, 4, 4);
KdPrint(("You shouldn't see this message\n"));
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
KdPrint(("Exception was caught\n"));
}
KdPrint(("Program kept control after exception\n"));

ProbeForWrite tests a data area for validity. In this example, it will raise an exception because the pointer argument we supply
isn’t aligned to a 4-byte boundary. The exception handler gains control. Control then flows to the next statement after the
exception handler and continues within your program.
In the preceding example, had you returned the value EXCEPTION_CONTINUE_SEARCH, the operating system would have
continued unwinding the stack looking for an exception handler. Neither your exception handler code nor the code following it
would have been executed: either the system would have crashed or some higher-level handler would have taken over.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 52 - Basic Programming Techniques | Chapter 3

You should not return EXCEPTION_CONTINUE_EXECUTION in kernel mode because you have no way to alter the
conditions that caused the exception in order to allow a retry to occur.
Note that you cannot trap arithmetic exceptions, or page faults due to referencing an invalid kernel-mode pointer, by using
structured exceptions. You just have to write your code so as not to generate such exceptions. It’s pretty obvious how to avoid
dividing by 0—just check, as in this example:

ULONG numerator, denominator; // <== numbers someone gives you


ULONG quotient;
if (!denominator)
<handle error>else
quotient = numerator / denominator;

But what about a pointer that comes to you from some other part of the kernel? There is no function that you can use to check
the validity of a kernel-mode pointer. You just need to follow this rule:
Usually, trust values that a kernel-mode component gives you.
I don’t mean by this that you shouldn’t liberally sprinkle your code with ASSERT statements—you should because you may
not initially understand all the ins and outs of how other kernel components work. I just mean that you don’t need to burden
your own driver with excessive defenses against mistakes in other, well-tested, parts of the system unless you need to work
around a bug.

More About NULL Pointers


While we’re on the subject of invalid pointers, note that a NULL pointer is (a) an invalid user-mode pointer in
Windows XP and (b) a perfectly valid pointer in Windows 98/Me. If you use a NULL pointer directly, as in *p, or
indirectly, as in p->StructureMember, you’ll be trying to reference something in the first few bytes of virtual
memory. Doing so in Windows XP will cause a trappable access violation.

Dereferencing a NULL pointer in Windows 98/Me will not, of itself, cause any immediately observable problem.
I once spent several days tracking down a bug that resulted from overstoring location 0x0000000C in a
Windows 95 system. That location is the real-mode vector for the breakpoint (INT 3) interrupt. The wild store
didn’t show up until some infrequently used application did an INT 3 that wasn’t caught by a debugger. The
system reflected the interrupt to real mode. The invalid interrupt vector pointed to memory containing a bunch
of technically valid but nonsensical instructions followed by an invalid one. The system halted with an invalid
operation exception. As you can see, the eventual symptom was very far removed in space and time from the
wild store.

To debug a different problem in Windows 98, I once installed a debugging driver to catch alterations to the first
16 bytes of virtual memory. I had to remove it because so many VxD drivers (including some belonging to
Microsoft) were getting caught.

The moral of these anecdotes is that you should always test pointers for NULL before using them if there is any
possibility that the pointer could be NULL. To learn whether the possibility exists, read documentation and
specifications very carefully.

Exception Filter Expressions


You might be wondering how to perform any sort of involved error detection or correction when all you’re allowed to do is
evaluate an expression that yields one of three integer values. You could use the C/C++ comma operator to string expressions
together:

__except(expr-1, ... EXCEPTION_CONTINUE_SEARCH){}

The comma operator basically discards whatever value is on its left side and evaluates its right side. The value that’s left over
after this computational game of musical chairs (with just one chair!) is the value of the expression.
You could use the C/C++ conditional operator to perform a more involved calculation:

__except(<some-expr>
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH)

If the some_expr expression is TRUE, you execute your own handler. Otherwise, you tell the operating system to keep looking
for another handler above you in the stack.
Finally, it should be obvious that you could just write a subroutine whose return value is one of the EXCEPTION_Xxx values:

LONG EvaluateException()

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.2 Error Handling - 53 -

{
if (<some-expr>)
return EXCEPTION_EXECUTE_HANDLER;
else
return EXCEPTION_CONTINUE_SEARCH;
}

__except(EvaluateException())

For any of these expression formats to do you any good, you need access to more information about the exception. You can
call two functions when evaluating an __except expression that will supply the information you need. Both functions actually
have intrinsic implementations in the Microsoft compiler and can be used only at the specific times indicated:
„ GetExceptionCode() returns the numeric code for the current exception. This value is an NTSTATUS value that you can
compare with manifest constants in ntstatus.h if you want to. This function is available in an __except expression and
within the exception handler code that follows the __except clause.
„ GetExceptionInformation() returns the address of an EXCEPTION_POINTERS structure that, in turn, allows you to learn
all the details about the exception, such as where it occurred, what the machine registers contained at the time, and so on.
This function is available only within an __except expression.

NOTE
The scope rules for names that appear in try-except and try-finally blocks are the same as elsewhere in the
C/C++ language. In particular, if you declare variables within the scope of the compound statement that follows
__try, those names aren’t visible in a filter expression, an exception handler, or a termination handler.
Documentation to the contrary that you might have seen in the Platform SDK or on MSDN is incorrect. For what
it’s worth, the stack frame containing any local variables declared within the scope of the guarded body still
exists at the time the filter expression is evaluated. So if you had a pointer (presumably declared at some outer
scope) to a variable declared within the guarded body, you could safely dereference it in a filter expression.

Because of the restrictions on how you can use these two expressions in your program, you’ll probably want to use them in a
function call to some filter function, like this:

LONG EvaluateException(NTSTATUS status, PEXCEPTION_POINTERS xp)


{

__except(EvaluateException(GetExceptionCode(),
GetExceptionInformation()))

Raising Exceptions
Program bugs are one way you can (inadvertently) raise exceptions that invoke the structured exception handling mechanism.
Application programmers are familiar with the Win32 API function RaiseException, which allows you to generate an arbitrary
exception on your own. In WDM drivers, you can call the routines listed in Table 3-2. I’m not going to give you a specific
example of calling these functions because of the following rule:
Raise an exception only in a nonarbitrary thread context, when you know there’s an exception handler above you, and when
you really know what you’re doing.
In particular, raising exceptions is not a good way to tell your callers information that you discover in the ordinary course of
executing. It’s far better to return a status code, even though that leads to apparently more unreadable code. You should avoid
exceptions because the stack-unwinding mechanism is very expensive. Even the cost of establishing exception frames is
significant and something to avoid when you can.

Service Function Description


ExRaiseStatus Raise exception with specified status code
ExRaiseAccessViolation Raise STATUS_ACCESS_VIOLATION
ExRaiseDatatypeMisalignment Raise STATUS_DATATYPE_MISALIGNMENT

Table 3-2. Service Functions for Raising Exceptions

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 54 - Basic Programming Techniques | Chapter 3

Real-World Examples
Notwithstanding the expense of setting up and tearing down exception frames, you have to use structured exception
syntax in an ordinary driver in particular situations.
One of the times you must set up an exception handler is when you call MmProbeAndLockPages to lock the pages for a
memory descriptor list (MDL) you’ve created:

PMDL mdl = MmCreateMdl(...);


__try
{
MmProbeAndLockPages(mdl, ...);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
NTSTATUS status = GetExceptionCode();
IoFreeMdl(mdl);
return CompleteRequest(Irp, status, 0);
}

(CompleteRequest is a helper function I use to handle the mechanics of completing I/O requests. Chapter 5 explains all about
I/O requests and what it means to complete one.)
Another time to use an exception handler is when you want to access user-mode memory using a pointer from an untrusted
source. In the following example, suppose you obtained the pointer p from a user-mode program and believe it points to an
integer:

PLONG p; // from user-mode


__try
{
ProbeForRead(p, 4, 4);
LONG x = *p;

}
__except(EXCEPTION_EXECUTE_HANDLER)
{
NTSTATUS status = GetExceptionCode();

3.2.3 Bug Checks


Unrecoverable errors in kernel mode can manifest themselves in the so-called blue screen of death (BSOD) that’s all too
familiar to driver programmers. Figure 3-5 is an example (hand-painted because no screen capture software is running when
one of these occurs!). Internally, these errors are called bug checks, after the service function you use to diagnose their
occurrence: KeBugCheckEx. The main feature of a bug check is that the system shuts itself down in as orderly a way as
possible and presents the BSOD. Once the BSOD appears, the system is dead and must be rebooted.
You call KeBugCheckEx like this:

KeBugCheckEx(bugcode, info1, info2, info3, info4);

where bugcode is a numeric value identifying the cause of the error and info1, info2, and so on are integer parameters that will
appear in the BSOD display to help a programmer understand the details of the error. This function does not return (!).
As a developer, you don’t get much information from the Blue Screen. If you’re lucky, the information will include the offset
of an instruction within your driver. Later on, you can examine this location in a kernel debugger and, perhaps, deduce a
possible cause for the bug check. Microsoft’s own bug-check codes appear in bugcodes.h (one of the DDK headers); a fuller
explanation of the codes and their various parameters can be found in Knowledge Base article Q103059, “Descriptions of Bug
Codes for Windows NT,” which is available on MSDN, among other places.

Sample Code
The BUGCHECK sample driver illustrates how to call KeBugCheckEx. I used it to generate the screen shot for
Figure 3-5.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.3 Memory Management - 55 -

Figure 3-5. The blue screen of death.


You can certainly create your own bug-check codes if you want. The Microsoft values are simple integers beginning with 1
(APC_INDEX_MISMATCH) and (currently) extending through 0xF6 (PCI_VERIFIER_DETECTED_VIOLATION), along with
a few others. To create your own bug-check code, define an integer constant as if it were STATUS_SEVERITY_SUCCESS
status code, but supply either the customer flag or a nonzero facility code. For example:

#define MY_BUGCHECK_CODE 0x002A0001

KeBugCheckEx(MY_BUGCHECK_CODE, 0, 0, 0, 0);

You use a nonzero facility code (42 in this example) or the customer flag (which I left 0 in this example) so that you can tell
your own codes from the ones Microsoft uses.
Now that I’ve told you how to generate your own BSOD, let me tell you when to do it: never. Or at most, in the checked build
of your driver for use during your own internal debugging. You and I are unlikely to write a driver that will discover an error so
serious that taking down the system is the only solution. It would be far better to log the error (using the error-logging facilities
I’ll describe in Chapter 14) and return a status code.
Note that the end user can configure the behavior of KeBugCheckEx in the advanced settings for My Computer. The user can
choose to automatically restart the machine or to generate the BSOD. The end user can likewise choose several levels of detail
(including none) for a dump file and whether to log an event in the system event log.

3.3 Memory Management


In this section, I’ll discuss the topic of memory management. Windows XP divides the available virtual address space in
several ways. One division—a very firm one based on security and integrity concerns—is between user-mode addresses and
kernel-mode addresses. Another division, which is almost but not quite coextensive with the first, is between paged and
nonpaged memory. All user-mode addresses and some kernel-mode addresses reference page frames that the Memory Manager
swaps to and from the disk over time, while some kernel-mode addresses always reference page frames in physical memory.
Since Windows XP allows portions of drivers to be paged, I’ll explain how you control the pageability of your driver at the
time you build your driver and at run time.
Windows XP provides several methods for managing memory. I’ll describe two basic service
functions—ExAllocatePoolWithTag and ExFreePool—that you use for allocating and releasing randomly sized blocks from a
heap. I’ll also describe the primitives that you use for organizing memory blocks into linked lists of structures. Finally I’ll
describe the concept of a lookaside list, which allows you to efficiently allocate and release blocks that are all the same size.

3.3.1 User-Mode and Kernel-Mode Address Spaces


Windows XP and Microsoft Windows 98/Me run on computers that support a virtual address space, wherein virtual addresses
are mapped either to physical memory or (conceptually, anyway) to page frames within a swap file on disk. To grossly simplify
matters, you can think of the virtual address space as being divided into two parts: a kernel-mode part and a user-mode part.
See Figure 3-6.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 56 - Basic Programming Techniques | Chapter 3

Figure 3-6. User-mode and kernel-mode portions of the address space.


Each user-mode process has its own address context, which maps the user-mode virtual addresses to a unique collection of
physical page frames. In other words, the meaning of any particular virtual address changes from one moment to the next as
the Windows XP scheduler switches from a thread in one process to a thread in another process. Part of the work in switching
threads is to change the page tables used by a processor so that they refer to the incoming thread’s process context.
It’s generally unlikely that a WDM driver will execute in the same thread context as the initiator of the I/O requests it handles.
We say that we’re running in arbitrary thread context if we don’t know for sure to which process the current user-mode
address context belongs. In arbitrary thread context, we simply can’t use a virtual address that belongs to user mode because
we can’t have any idea to what physical memory it might point. In view of this uncertainty, we generally obey the following
rule inside a driver program:
Never (well, hardly ever) directly reference user-mode memory.
In other words, don’t take an address that a user-mode application provides and treat that address as a pointer that we can
directly dereference. I’ll discuss in later chapters a few techniques for accessing data buffers that originate in user mode. All
we need to know right now, though, is that we’re (nearly) always going to be using kernel-mode virtual addresses whenever we
want to access the computer’s memory.

How Big Is a Page?


In a virtual memory system, the operating system organizes physical memory and the swap file into like-size page frames. In a
WDM driver, you can use the manifest constant PAGE_SIZE to tell you how big a page is. In some Windows XP computers, a
page is 4096 bytes long; in others, it’s 8192 bytes long. A related constant named PAGE_SHIFT equals the page size as a
power of 2. That is:

PAGE_SIZE == 1 << PAGE_SHIFT

For your convenience, you can use a few preprocessor macros in your code when you’re working with the size of a page:
„ ROUND_TO_PAGES rounds a size in bytes to the next-higher page boundary. For example, ROUND_TO_PAGES(1) is
4096 on a 4-KB-page computer.
„ BYTES_TO_PAGES determines how many pages are required to hold a given number of bytes beginning at the start of a
page. For example, BYTES_TO_PAGES(42) would be 1 on all platforms, and BYTES_TO_PAGES(5000) would be 2 on
some platforms and 1 on others.
„ BYTE_OFFSET returns the byte offset portion of a virtual address. That is, it calculates the starting offset within some
page frame of a given address. On a 4-KB-page computer, BYTE_OFFSET(0x12345678) would be 0x678.
„ PAGE_ALIGN rounds a virtual address down to a page boundary. On a 4-KB-page computer, PAGE_ALIGN(0x12345678)
would be 0x12345000.
„ ADDRESS_AND_SIZE_TO_SPAN_PAGES returns the number of page frames occupied by a specified number of bytes
beginning at a specified virtual address. For example, the statement
ADDRESS_AND_SIZE_TO_SPAN_PAGES(0x12345FFF, 2) is 2 on a 4-KB-page machine because the 2 bytes span a
page boundary.

Paged and Nonpaged Memory


The whole point of a virtual memory system is that you can have a virtual address space that’s much bigger than the amount of
physical memory on the computer. To accomplish this feat, the Memory Manager needs to swap page frames in and out of
physical memory. Certain parts of the operating system can’t be paged, though, because they’re needed to support the Memory
Manager itself. The most obvious example of something that must always be resident in memory is the code that handles page
faults (the exceptions that occur when a page frame isn’t physically present when needed) and the data structures used by the
page fault handler.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.3 Memory Management - 57 -

The category of “must be resident” stuff is much broader than just the page fault handlers. Windows XP allows hardware
interrupts to occur at nearly any time, including while a page fault is being serviced. If this weren’t so, the page fault handler
wouldn’t be able to read or write pages from a device that uses an interrupt. Thus, every hardware interrupt service routine
must be in nonpaged memory. The designers of Windows NT decided to include even more routines in the nonpaged category
by using a simple rule:
Code executing at or above interrupt request level (IRQL) DISPATCH_LEVEL cannot cause page faults.
I’ll elaborate on this rule in the next chapter.
You can use the PAGED_CODE preprocessor macro (declared in wdm.h) to help you discover violations of this rule in the
checked build of your driver. For example:

NTSTATUS DispatchPower(PDEVICE_OBJECT fdo, PIRP Irp)


{
PAGED_CODE()

PAGED_CODE contains conditional compilation. In the checked-build environment, it prints a message and generates an
assertion failure if the current IRQL is too high. In the free-build environment, it doesn’t do anything. To understand why
PAGED_CODE is useful, imagine that DispatchPower needs for some reason to be in nonpaged memory but that you have
misplaced it in paged memory. If the system happens to call DispatchPower at a time when the page containing it isn’t present,
a page fault will occur, followed by a bug check. The bug check code will be pretty uninformative
(IRQL_NOT_LESS_OR_EQUAL or DRIVER_IRQL_NOT_LESS_OR_EQUAL), but at least you’ll find out that you have a
problem. If you test your driver in a situation in which the page containing DispatchPower happens fortuitously to be in
memory, though, there won’t be a page fault. PAGED_CODE will detect the problem even so.
Setting the Driver Verifier “Force IRQL Checking” option will greatly increase the chances of discovering that you’ve
broken the rule about paging and IRQL. The option forces pageable pages out of memory whenever verified drivers
raise the IRQL to DISPATCH_LEVEL or beyond.

Compile-Time Control of Pageability


Given that some parts of your driver must always be resident and some parts can be paged, you need a way to control the
assignment of your code and data to the paged and nonpaged pools. You accomplish part of this job by instructing the compiler
how to apportion your code and data among various sections. The run-time loader uses the names of the sections to put parts of
your driver in the places you intend. You can also accomplish parts of this job at run time by calling various Memory Manager
routines that I’ll discuss in the next section.

NOTE
Win32 executable files, including kernel-mode drivers, are internally composed of one or more sections. A
section can contain code or data and, generally speaking, has additional attributes such as being readable,
writable, shareable, executable, and so on. A section is also the smallest unit that you can designate when
you’re specifying pageability. When loading a driver image, the system puts sections whose literal names begin
with PAGE or .EDA (the start of .EDATA) into the paged pool unless the DisablePagingExecutive value in the
HKLM\System\CurrentControlSet\Control\Session Manager\Memory Management key happens to be set (in
which case no driver paging occurs). Note that these names are case sensitive! In one of the little twists of fate
that affect us all from time to time, running Soft-Ice/W on Windows XP requires you to disable kernel paging in
this way. This certainly makes it harder to find bugs caused by misplacement of driver code or data into the
paged pool! If you use this debugger, I recommend that you religiously use the PAGED_CODE macro and the
Driver Verifier.

The traditional way of telling the compiler to put code into a particular section is to use the alloc_text pragma. Since not every
compiler will necessarily support the pragma, the DDK headers either define or don’t define the constant ALLOC_PRAGMA
to tell you whether to use the pragma. You can then invoke the pragma to specify the section placement of individual
subroutines in your driver, as follows:

#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGE, AddDevice)
#pragma alloc_text(PAGE, DispatchPnp)

#endif

These statements serve to place the AddDevice and DispatchPnp functions into the paged pool.
The Microsoft C/C++ compiler places two annoying restrictions on using alloc_text:
„ The pragma must follow the declaration of a function but precede the definition. One way to obey this rule is to declare
all the functions in your driver in a standard header file and invoke alloc_text at the start of the source file that contains a

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 58 - Basic Programming Techniques | Chapter 3

given function but after you include that header.


„ The pragma can be used only with functions that have C-linkage. In other words, it won’t work for class member
functions or for functions in a C++ source file that you didn’t declare using extern “C”.
To control the placement of data variables, you use a different pragma under the control of a different preprocessor macro
symbol:

#ifdef ALLOC_DATA_PRAGMA
#pragma data_seg("PAGEDATA")
#endif

The data_seg pragma causes all static data variables declared in a source module after the appearance of the pragma to go into
the paged pool. You’ll notice that this pragma differs in a fundamental way from alloc_text. A pageable section starts where
#pragma data_seg(“PAGEDATA”) appears and ends where a countervailing #pragma data_seg() appears. Alloc_text, on the
other hand, applies to a specific function.

More About Section Placement


In general, I find it more convenient to specify the section placement of whole blocks of code by using the
Microsoft code_seg pragma, which works the same way as data_seg, only for code. That is, you can tell the
Microsoft compiler to start putting functions into the paged pool like this:

#pragma code_seg("PAGE")
NTSTATUS AddDevice(...){...}
NTSTATUS DispatchPnp(...){...}

The AddDevice and DispatchPnp functions would both end up in the paged pool. You can check to see whether
you’re compiling with the Microsoft compiler by testing the existence of the predefined preprocessor macro
_MSC_VER.

To revert to the default code section, just code #pragma code_seg with no argument:

#pragma code_seg()

Similarly, to revert to the regular nonpaged data section, code #pragma data_seg with no argument:

#pragma data_seg()

This sidebar is also the logical place to mention that you can also direct code into the INIT section if it’s not
needed once your driver finishes initializing. For example:

#pragma alloc_text(INIT, DriverEntry)

This statement forces the DriverEntry function into the INIT section. The system will release the memory it
occupies when it returns. This small savings isn’t very important in the grand scheme of things because a WDM
driver’s DriverEntry function isn’t very big. Previous Windows NT drivers had large DriverEntry functions that
had to create device objects, locate resources, configure devices, and so on. For them, using this feature
offered significant memory savings.

Notwithstanding the low utility of putting DriverEntry in the INIT section in a WDM driver, I was in the habit of
doing so until quite recently. Because of a bug in Windows 98/Me, I had a situation in which a WDM driver wasn’t
being completely removed from memory after I unplugged my hardware. One part of the system didn’t
understand this and tried to call DriverEntry when I replugged the hardware. The memory that had originally
contained DriverEntry had long since been overwritten by INIT code belonging to other drivers, and a crash
resulted. This was very difficult to debug! I now prefer to place DriverEntry in a paged section.

You can use the DUMPBIN utility that comes with Microsoft Visual C++ .NET to easily see how much of your
driver is initially pageable. Your marketing department might even want to crow about how much less nonpaged
memory you use than your competitors.

Run-Time Control of Pageability


Table 3-3 lists the service functions you can use at run time to fine-tune the pageability of your driver in various situations. The
purpose of these routines is to let you release the physical memory that would otherwise be tied up by your code and data
during periods when it won’t be needed. In Chapter 8, for example, I’ll discuss how you can put your device into a low power
state during periods of inactivity. Powering down might be a good time to release your locked pages.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.3 Memory Management - 59 -

Service Function Description


MmLockPagableCodeSection Locks a code section given an address inside it
MmLockPagableDataSection Locks a data section given an address inside it
Locks a code section by using a handle from a previous
MmLockPagableSectionByHandle
MmLockPagableCodeSection call (Windows 2000 and Windows XP only)
MmPageEntireDriver Unlocks all pages belonging to driver
MmResetDriverPaging Restores compile-time pageability attributes for entire driver
MmUnlockPagableImageSection Unlocks a locked code or data section

Table 3-3. Routines for Dynamically Locking and Unlocking Driver Pages
I’m going to describe one way to use these functions to control the pageability of code in your driver. You might want to read
the DDK descriptions to learn about other ways to use them. First distribute subroutines in your driver into separately named
code sections, like this:

#pragma alloc_text(PAGEIDLE, DispatchRead)


#pragma alloc_text(PAGEIDLE, DispatchWrite)

That is, define a section name beginning with PAGE and ending in any four-character suffix you please. Then use the
alloc_text pragma to place some group of your own routines in that special section. You can have as many special pageable
sections as you want, but your logistical problems will grow as you subdivide your driver in this way.
During initialization (say, in DriverEntry), lock your pageable sections like this:

PVOID hPageIdleSection;
NTSTATUS DriverEntry(...)
{
hPageIdleSection = MmLockPagableCodeSection((PVOID) DispatchRead);
}

When you call MmLockPagableCodeSection, you specify any address at all within the section you’re trying to lock. The real
purpose of making this call during DriverEntry is to obtain the handle value it returns, which I’ve shown you saving in a global
variable named hPageIdleSection. You’ll use that handle much later on, when you decide you don’t need a particular section in
memory for a while:

MmUnlockPagableImageSection(hPageIdleSection);

This call will unlock the pages containing the PAGEIDLE section and allow them to move in and out of memory on demand. If
you later discover that you need those pages back again, you make this call:

MmLockPagableSectionByHandle(hPageIdleSection);

Following this call, the PAGEIDLE section will once again be in nonpaged memory (but not necessarily the same physical
memory as previously). Note that this function call is available to you only in Windows 2000 and Windows XP, and then only
if you’ve included ntddk.h instead of wdm.h. In other situations, you will have to call MmLockPagableCodeSection again.
You can do something similar to place data objects into pageable sections:

PVOID hPageDataSection;

#pragma data_seg("PAGE")
ULONG ulSomething;
#pragma data_seg()

hPageDataSection = MmLockPagableDataSection((PVOID)&ulSomething);

MmUnlockPagableImageSection(hPageDataSection);

MmLockPagableSectionByHandle(hPageDataSection);

I’ve played fast and loose with my syntax here—these statements would appear in widely separated parts of your driver.
The key idea behind the Memory Manager service functions I just described is that you initially lock a section containing one
or more pages and obtain a handle for use in subsequent calls. You can then unlock the pages in a particular section by calling
MmUnlockPagableImageSection and passing the corresponding handle. Relocking the section later on requires a call to
MmLockPagableSectionByHandle.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 60 - Basic Programming Techniques | Chapter 3

A quick shortcut is available if you’re sure that no part of your driver will need to be resident for a while.
MmPageEntireDriver will mark all the sections in a driver’s image as being pageable. Conversely, MmResetDriverPaging will
restore the compile-time pageability attributes for the entire driver. To call these routines, you just need the address of some
piece of code or data in the driver. For example:

MmPageEntireDriver((PVOID) DriverEntry);

MmResetDriverPaging((PVOID) DriverEntry);

You need to exercise care when using any of the Memory Manager routines I’ve just described if your device uses an
interrupt. If you page your entire driver, the system will also page your interrupt service routine (ISR). If your device or
any device with which you share an interrupt vector should interrupt, the system will try to call your ISR. Even if you
think your interrupt isn’t shared and you’ve inhibited your device from generating an interrupt, bear in mind that spurious
interrupts have been known to occur. If the ISR isn’t present, the system will crash. You avoid this problem by disconnecting
your interrupt before allowing the ISR to be paged.

3.3.2 Heap Allocator


The basic heap allocation service function in kernel mode is ExAllocatePoolWithTag. You call it like this:

PVOID p = ExAllocatePoolWithTag(type, nbytes, tag);

The type argument is one of the POOL_TYPE enumeration constants described in Table 3-4, and nbytes is the number of bytes
you want to allocate. The tag argument is an arbitrary 32-bit value. The return value is a kernel-mode virtual address pointer to
the allocated memory block.
In most drivers, including the samples in this book and in the DDK, you’ll see calls to an older function named
ExAllocatePool:

PVOID p = ExAllocatePool(type, nbytes);

ExAllocatePool was the heap allocation function in the earliest versions of Windows NT. In the Windows XP DDK,
ExAllocatePool is actually a macro that invokes ExAllocatePoolWithTag using the tag value ’ mdW’ (’Wdm’ plus a trailing
space after byte reversal).

Pool Type Description


NonPagedPool Allocates from the nonpaged pool of memory
PagedPool Allocates from the paged pool of memory
Allocates from the nonpaged pool and ensures that memory is aligned with the
NonPagedPoolCacheAligned
CPU cache
Allocates from the paged pool of memory and ensures that memory is aligned
PagedPoolCacheAligned
with the CPU cache
Table 3-4. Pool Type Arguments for ExAllocatePool
The most basic decision you must make when you call ExAllocatePoolWithTag is whether the allocated memory block can be
swapped out of memory. That choice depends simply on which parts of your driver will need to access the memory block. If
you’ll be using a memory block at or above DISPATCH_LEVEL, you must allocate it from the nonpaged pool. If you’ll always
use the memory block below DISPATCH_LEVEL, you can allocate from the paged or nonpaged pool as you choose.
Allocations from the PagedPool must occur at an IRQL less than DISPATCH_LEVEL. Allocations from the
NonPagedPool must occur at an IRQL less than or equal to DISPATCH_LEVEL. The Driver Verifier bug checks
whether you violate either of these rules.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.3 Memory Management - 61 -

Limits on Pool Allocations


A frequently asked question is, “How much memory can I allocate with one call to ExAllocatePoolWithTag?”
Unfortunately, there’s no simple answer to the question. A starting point is to determine the maximum sizes of
the paged and non-paged pools. You can consult Knowledge Base article Q126402 and Chapter 7 of Inside
Windows 2000 (Microsoft Press, 2000) for (probably) more information than you’ll ever want to know about this
topic. By way of example, on a 512 MB machine, I ended up with a maximum nonpaged pool size of 128 MB and
an actual paged pool size of 168 MB.

Knowing the pool sizes is not the end of the story, though. I wouldn’t expect to be able to allocate anywhere
close to 128 MB of nonpaged memory on this 512-MB computer in one call to ExAllocatePoolWithTag. For one
thing, other parts of the system will have used up significant amounts of nonpaged memory by the time my
driver gets a chance to try, and the system would probably run very poorly if I took all that was left over. For
another thing, the virtual address space is likely to be fragmented once the system has been running for a
while, so the heap manager wouldn’t be able to find an extremely large contiguous range of unused virtual
addresses.

In actual, not-very-scientific tests, using the MEMTEST sample from the companion content, I was able to
allocate about 129 MB of paged memory and 100 MB of nonpaged memory in a single call.

Sample Code
The MEMTEST sample uses ExAllocatePoolWithTagPriority to determine the largest contiguous allocations
possible from the paged and nonpaged pools.

When you use ExAllocatePoolWithTag, the system allocates 4 more bytes of memory than you asked for and returns you a
pointer that’s 4 bytes into that block. The tag occupies the initial 4 bytes and therefore precedes the pointer you receive. The
tag will be visible to you when you examine memory blocks while debugging or while poring over a crash dump, and it can
help you identify the source of a memory block that’s involved in some problem or another. For example:

#define DRIVERTAG 'KNUJ'

PVOID p = ExAllocatePoolWithTag(PagedPool, 42, DRIVERTAG);

Here I used a 32-bit integer constant as the tag value. On a little-endian computer such as an x86, the bytes that compose this
value will be reversed in memory to spell out a common word in the English language. Several features of the Driver Verifier
relate to specific memory tags, by the way, so you can do yourself a favor in the debugging department by using one more
unique tags in your allocation calls.
Do not specify zero or “ GIB” (BIG with a space at the end, after byte reversal) as a tag value. Zero-tagged blocks can’t
be tracked, and the system internally uses the BIG tag for its own purposes. Do not request zero bytes. This restriction
could be a special concern if you’re writing your own C or C++ runtime support, since malloc and operator new allow requests
for zero bytes.

More About Pool Tags


Several diagnostic mechanisms inside the kernel depend on pool tagging, and you can help yourself analyze
your driver’s performance by picking a unique set of tags. You must also explicitly enable kernel pool tagging in
the retail release of the system (it’s enabled by default in the checked build) by using the GFLAGS.EXE utility.
GFLAGS is part of the platform SDK and other components.

Having done both of these things—using unique tags in your driver and enabling pool tagging in the kernel—you
can profitably use a few tools. POOLMON and POOLTAG in the DDK tools directory report on memory usage by
tag value. You can also ask GFLAGS to make one of your pools “special” in order to check for overwrites.

The pointer you receive will be aligned with at least an 8-byte boundary. If you place an instance of some structure in the
allocated memory, members to which the compiler assigns an offset divisible by 4 or 8 will therefore occupy an address
divisible by 4 or 8 too. On some RISC platforms, of course, you must have doubleword and quadword values aligned in this
way. For performance reasons, you might want to be sure that the memory block will fit in the fewest possible number of
processor cache lines. You can specify one of the XxxCacheAligned type codes to achieve that result. If you ask for less than a
page’s worth of memory, the block will be contained in a single page. If you ask for at least a page’s worth of memory, the
block will start on a page boundary.

NOTE
Asking for PAGE_SIZE + 1 bytes of memory is about the worst thing you can do to the heap allocator: the
system reserves two pages, of which nearly half will ultimately be wasted.

It should go without saying that you need to be extra careful when accessing memory you’ve allocated from the free storage
pools in kernel mode. Since driver code executes in the most privileged mode possible for the processor, there’s almost no
protection from wild stores.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 62 - Basic Programming Techniques | Chapter 3

Using GFLAGS or the Driver Verifier’s Special Pool option allows you to find memory overwrite errors more easily.
With this option, allocations from the “special” pool lie at the end of a page that’s followed in virtual memory by a
not-present page. Trying to touch memory past the end of your allocated block will earn you an immediate page fault. In
addition, the allocator fills the rest of the page with a known pattern. When you eventually release the memory, the system
checks to see whether you overwrote the pattern. In combination, these checks make it much easier to detect bugs that result
from “coloring outside the lines” of your allocated memory. You can also ask to have allocations at the start of a page preceded
by a not-present page, by the way. Refer to Knowledge Base article Q192486 for more information about the special pool.

Handling Low-Memory Situations


If there isn’t enough memory to satisfy your request, the pool allocator returns a NULL pointer. You should always test
the return value and do something reasonable. For example:

PMYSTUFF p = (PMYSTUFF) ExAllocatePool(PagedPool, sizeof(MYSTUFF));


if (!p)
return STATUS_INSUFFICIENT_RESOURCES;

Additional pool types include the concept must succeed. If there isn’t enough heap memory to satisify a request from the
must-succeed pool, the system bug checks. Drivers should not allocate memory using one of the must-succeed specifiers. This
is because a driver can nearly always fail whatever operation is under way. Causing a system crash in a low-memory situation
is not something a driver should do. Furthermore, only a limited pool of must-succeed memory exists in the entire system, and
the operating system might not be able to allocate memory needed to keep the computer running if drivers tie up some. In fact,
Microsoft wishes it had never documented the must-succeed options in the DDK to begin with.
The Driver Verifier will bug check whether a driver specifies one of the must-succeed pool types in an allocation
request. In addition, if you turn on the low-resource-simulation option in the Driver Verifier, your allocations will begin
randomly failing after the system has been up seven or eight minutes. Every five minutes or so, the system will fail all your
allocations for a burst of 10 seconds.
In some situations, you might want to use a technique that’s commonly used in file system drivers. If you OR the value
POOL_RAISE_IF_ALLOCATION_FAILURE (0x00000010) into the pool type code, the heap allocator will raise a
STATUS_INSUFFICIENT_RESOURCES exception instead of returning NULL if there isn’t enough memory. You should use a
structured exception frame to catch such an exception. For example:

#ifndef POOL_RAISE_IF_ALLOCATION_FAILURE
#define POOL_RAISE_IF_ALLOCATION_FAILURE 16
#endif

#define PagedPoolRaiseException (POOL_TYPE) \


(PagedPool │ POOL_RAISE_IF_ALLOCATION_FAILURE)
#define NonPagedPoolRaiseException (POOL_TYPE) \
(NonPagedPool │ POOL_RAISE_IF_ALLOCATION_FAILURE)

NTSTATUS SomeFunction()
{
NTSTATUS status;
__try
{

PMYSTUFF p = (PMYSTUFF)
ExAllocatePoolWithTag(PagedPoolRaiseException,
sizeof(MYSTUFF), DRIVERTAG);
<Code that uses "p" without checking it for NULL>
status = STATUS_SUCCESS;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
status = GetExceptionCode();
}
return status;
}

NOTE
POOL_RAISE_IF_ALLOCATION_FAILURE is defined in NTIFS.H, a header file that’s available only as part of the
extra-cost Installable File System kit. Doing memory allocations with this flag set is so common in file system
drivers, though, that I thought you should know about it.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.3 Memory Management - 63 -

Incidentally, I suggest that you not go crazy trying to diagnose or recover from failures to allocate small blocks of memory. As
a practical matter, an allocation request for, say, 32 bytes is never going to fail. If memory were that tight, the system would be
running so sluggishly that someone would surely reboot the machine. You must not be the cause of a system crash in this
situation, though, because you would thereby be the potential source of a denial-of-service exploit. But since the error won’t
arise in real life, there’s no point in putting elaborate code into your driver to log errors, signal WMI events, print debugging
messages, execute alternative algorithms, and so on. Indeed, the extra code needed to do all that might be the reason the system
didn’t have the extra 32 bytes to give you in the first place! Thus, I recommend testing the return value from every call to
ExAllocatePoolWithTag. If it discloses an error, do any required cleanup and return a status code. Period.

Releasing a Memory Block


To release a memory block you previously allocated with ExAllocatePoolWithTag, you call ExFreePool:

ExFreePool((PVOID) p);

You do need to keep track somehow of the memory you’ve allocated from the pool in order to release it when it’s no longer
needed. No one else will do that for you. You must sometimes closely read the DDK documentation of the functions you call
with an eye toward memory ownership. For example, in the AddDevice function I showed you in the previous chapter, there’s a
call to IoRegisterDeviceInterface. That function has a side effect: it allocates a memory block to hold the string that names the
interface. You are responsible for releasing that memory later on.
The Driver Verifier checks at DriverUnload time to ensure a verified driver has released all the memory it allocated. In
addition, the verifier sanity-checks all calls to ExFreePool to make sure they refer to a complete block of memory
allocated from a pool consistent with the current IRQL.
The DDK headers declare an undocumented function named ExFreePoolWithTag. This function was intended for internal use
only in order to make sure that system components didn’t inadvertently release memory belonging to other components. The
function was politely called a “waste of time” by one of the Microsoft developers, which pretty much tells us that we needn’t
worry about what it does or how to use it. (Hint: you need to do some other undocumented things in order to use it
successfully.)

Two More Functions


Although ExAllocatePoolWithTag is the function you should use for heap allocation, you can use two other pool allocation
functions in special circumstances: ExAllocatePoolWithQuotaTag (and a macro named ExAllocatePoolWithQuota that supplies
a default tag) and ExAllocatePoolWithTagPriority. ExAllocatePoolWithQuotaTag allocates a memory block and charges the
current thread’s scheduling quota. This function is for use by file system drivers and other drivers running in a nonarbitrary
thread context for allocating memory that belongs to the current thread. A driver wouldn’t ordinarily use this function because
a quota violation causes the system to raise an exception.
ExAllocatePoolWithTagPriority, which is new with Windows XP, allows you to specify how important you consider it to be
that a memory allocation request succeed:

PVOID p = ExAllocatePoolWithTagPriority(type, nbytes, tag, priority);

The arguments are the same as we’ve been studying except that you also supply an additional priority indicator. See Table 3-5.

Priority Argument Description


LowPoolPriority System may fail request if low on resources. Driver can easily cope with failure.
NormalPoolPriority System may fail request if low on resources.
HighPoolPriority System should not fail request unless completely out of resources.

Table 3-5. Pool Priority Arguments for ExAllocatePoolWithTagPriority


The DDK indicates that most drivers should specify NormalPoolPriority when calling this function. HighPoolPriority should
be reserved for situations in which success is critically important to the continued working of the system.
You can lexigraphically append the phrases SpecialPoolOverrun and SpecialPoolUnderrun to the names given in Table
3-5 (for example, LowPoolPrioritySpecialPoolOverrun, and so on). If an allocation would use the special pool, the
overrun and underrun flags override the default placement of blocks.
At the time I’m writing this, ExAllocatePoolWithTagPriority turns into a simple call to ExAllocatePoolWithTag if you are
asking for paged memory at high priority or nonpaged memory at any priority. The extra resource checking happens only with
requests for paged memory at low or normal priority. This behavior could change in service packs or later versions of the
operating system.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 64 - Basic Programming Techniques | Chapter 3

3.3.3 Linked Lists


Windows XP makes extensive use of linked lists as a way of organizing collections of similar data structures. In this chapter,
I’ll discuss the basic service functions you use to manage doubly-linked and singly-linked lists. Separate service functions
allow you to share linked lists between threads and across multiple processors; I’ll describe those functions in the next chapter
after I’ve explained the synchronization primitives on which they depend.
Whether you organize data structures into a doubly-linked or a singly-linked list, you normally embed a linking
substructure—either a LIST_ENTRY or a SINGLE_LIST_ENTRY—into your own data structure. You also reserve a list head
element somewhere that uses the same structure as the linking element. For example:

typedef struct _TWOWAY


{

LIST_ENTRY linkfield;

} TWOWAY, *PTWOWAY;

LIST_ENTRY DoubleHead;

typedef struct _ONEWAY


{

SINGLE_LIST_ENTRY linkfield;

} ONEWAY, *PONEWAY;

SINGLE_LIST_ENTRY SingleHead;

When you call one of the list-management service functions, you always work with the linking field or the list head—never
directly with the containing structures themselves. So suppose you have a pointer (pdElement) to one of your TWOWAY
structures. To put that structure on a list, you’d reference the embedded linking field like this:

InsertTailList(&DoubleHead, &pdElement->linkfield);

Similarly, when you retrieve an element from a list, you’re really getting the address of the embedded linking field. To recover
the address of the containing structure, you can use the CONTAINING_RECORD macro. (See Figure 3-7.)

Figure 3-7. The CONTAINING_RECORD macro.


So if you wanted to process and discard all the elements in a singly-linked list, your code would look something like this:

PSINGLE_LIST_ENTRY psLink = PopEntryList(&SingleHead);


while (psLink)
{
PONEWAY psElement = CONTAINING_RECORD(psLink, ONEWAY, linkfield);

ExFreePool(psElement);
psLink = PopEntryList(&SingleHead);
}

Just before the start of this loop, and again after every iteration, you retrieve the current first element of the list by calling
PopEntryList. PopEntryList returns the address of the linking field within a ONEWAY structure, or else it returns NULL to
signify that the list is empty. Don’t just indiscriminately use CONTAINING_RECORD to develop an element address that you
then test for NULL—you need to test the link field address that PopEntryList returns!

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.3 Memory Management - 65 -

Doubly-Linked Lists
A doubly-linked list links its elements both backward and forward in a circular fashion. See Figure 3-8. That is, starting with
any element, you can proceed forward or backward in a circle and get back to the same element. The key feature of a
doubly-linked list is that you can add or remove elements anywhere in the list.

Figure 3-8. Topology of a doubly-linked list.


Table 3-6 lists the service functions you use to manage a doubly-linked list.

Service Function or Macro Description


InitializeListHead Initializes the LIST_ENTRY at the head of the list
InsertHeadList Inserts element at the beginning
InsertTailList Inserts element at the end
IsListEmpty Determines whether list is empty
RemoveEntryList Removes element
RemoveHeadList Removes first element
RemoveTailList Removes last element

Table 3-6. Service Functions for Use with Doubly-Linked List


Here is a fragment of a fictitious program to illustrate how to use some of these functions:

typedef struct _TWOWAY {

LIST_ENTRY linkfield;

} TWOWAY, *PTWOWAY;

LIST_ENTRY DoubleHead;

InitializeListHead(&DoubleHead);
ASSERT(IsListEmpty(&DoubleHead));

PTWOWAY pdElement = (PTWOWAY) ExAllocatePool(PagedPool,sizeof(TWOWAY));

InsertTailList(&DoubleHead, &pdElement->linkfield);

if (!IsListEmpty(&DoubleHead))
{

PLIST_ENTRY pdLink = RemoveHeadList(&DoubleHead);


pdElement = CONTAINING_RECORD(pdLink, TWOWAY, linkfield);

ExFreePool(pdElement);
}

1. InitializeListHead initializes a LIST_ENTRY to point (both backward and forward) to itself. That configuration indicates
that the list is empty.
2. InsertTailList puts an element at the end of the list. Notice that you specify the address of the embedded linking field
instead of your own TWOWAY structure. You could call InsertHeadList to put the element at the beginning of the list

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 66 - Basic Programming Techniques | Chapter 3

instead of the end. By supplying the address of the link field in some existing TWOWAY structure, you could put the new
element either just after or just before the existing one.

PTWOWAY prev;
InsertHeadList(&prev->linkfield, &pdElement->linkfield);

PTWOWAY next;
InsertTailList(&next->linkfield, &pdElement->linkfield);

3. Recall that an empty doubly-linked list has the list head pointing to itself, both backward and forward. Use IsListEmpty to
simplify making this check. The return value from RemoveXxxList will never be NULL!
4. RemoveHeadList removes the element at the head of the list and gives you back the address of the linking field inside it.
RemoveTailList does the same thing, just with the element at the end of the list instead.
It’s important to know the exact way RemoveHeadList and RemoveTailList are implemented if you want to avoid errors. For
example, consider the following innocent-looking statement:

if (<some-expr>)
pdLink = RemoveHeadList(&DoubleHead);

What I obviously intended with this construction was to conditionally extract the first element from a list. C’est raisonnable,
n’est-ce pas? But no, when you debug this later on, you find that elements keep mysteriously disappearing from the list. You
discover that pdLink gets updated only when the if expression is TRUE but that RemoveHeadList seems to get called even
when the expression is FALSE.
Mon dieu! What’s going on here? Well, RemoveHeadList is really a macro that expands into multiple statements. Here’s what
the compiler really sees in the above statement:

if (<some-expr>)
pdLink = (&DoubleHead)->Flink;
{{
PLIST_ENTRY _EX_Blink;
PLIST_ENTRY _EX_Flink;
_EX_Flink = ((&DoubleHead)->Flink)->Flink;
_EX_Blink = ((&DoubleHead)->Flink)->Blink;
_EX_Blink->Flink = _EX_Flink;
_EX_Flink->Blink = _EX_Blink;
}}

Aha! Now the reason for the mysterious disappearance of list elements becomes clear. The TRUE branch of the if statement
consists of just the single statement pdLink = (&DoubleHead)->Flink, which stores a pointer to the first element. The logic
that removes a list element stands alone outside the scope of the if statement and is therefore always executed. Both
RemoveHeadList and RemoveTailList amount to an expression plus a compound statement, and you dare not use either of them
in a spot where the syntax requires an expression or a statement alone. Zut alors!
The other list-manipulation macros don’t have this problem, by the way. The difficulty with RemoveHeadList and
RemoveTailList arises because they have to return a value and do some list manipulation. The other macros do only one or the
other, and they’re syntactically safe when used as intended.

Singly-Linked Lists
A singly-linked list links its elements in only one direction, as illustrated in Figure 3-9. Windows XP uses singly-linked lists to
implement pushdown stacks, as suggested by the names of the service routines in Table 3-7. Just as was true for doubly-linked
lists, these “functions” are actually implemented as macros in wdm.h, and similar cautions apply. PushEntryList and
PopEntryList generate multiple statements, so you can use them only on the right side of an equal sign in a context in which
the compiler is expecting multiple statements.

Service Function or Macro Description


PushEntryList Adds element to top of list
PopEntryList Removes topmost element
Table 3-7. Service Functions for Use with Singly-Linked Lists

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.3 Memory Management - 67 -

Figure 3-9. Topology of a singly-linked list.


The following pseudofunction illustrates how to manipulate a singly-linked list:

typedef struct _ONEWAY {

SINGLE_LIST_ENTRY linkfield;
} ONEWAY, *PONEWAY;

SINGLE_LIST_ENTRY SingleHead;

SingleHead.Next = NULL;

PONEWAY psElement = (PONEWAY) ExAllocatePool(PagedPool,


sizeof(ONEWAY));

PushEntryList(&SingleHead, &psElement->linkfield);

SINGLE_LIST_ENTRY psLink = PopEntryList(&SingleHead);


if (psLink)
{
psElement = CONTAINING_RECORD(psLink, ONEWAY, linkfield);

ExFreePool(psElement);
}

1. Instead of invoking a service function to initialize the head of a singly-linked list, just set the Next field to NULL. Note
also the absence of a service function for testing whether this list is empty; just test Next yourself.
2. PushEntryList puts an element at the head of the list, which is the only part of the list that’s directly accessible. Notice
that you specify the address of the embedded linking field instead of your own ONEWAY structure.
3. PopEntryList removes the first entry from the list and gives you back a pointer to the link field inside it. In contrast with
doubly-linked lists, a NULL value indicates that the list is empty. In fact, there’s no counterpart to IsListEmpty for use
with a singly-linked list.

3.3.4 Lookaside Lists


Even employing the best possible algorithms, a heap manager that deals with randomly sized blocks of memory will require
some scarce processor time to coalesce adjacent free blocks from time to time. Figure 3-10 illustrates how, when something
returns block B to the heap at a time when blocks A and C are already free, the heap manager can combine blocks A, B, and C
to form a single large block. The large block is then available to satisfy some later request for a block bigger than any of the
original three components.

Figure 3-10. Coalescing adjacent free blocks in a heap.


If you know you’re always going to be working with fixed-size blocks of memory, you can craft a much more efficient scheme
for managing a heap. You can, for example, preallocate a large block of memory that you subdivide into pieces of the given
fixed size. Then you can devise some scheme for knowing which blocks are free and which are in use, as suggested by Figure
3-11. Returning a block to such a heap merely involves marking it as free—you don’t need to coalesce it with adjacent blocks

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 68 - Basic Programming Techniques | Chapter 3

because you never need to satisfy randomly sized requests.


Merely allocating a large block that you subdivide might not be the best way to implement a fixed-size heap, though. In
general, it’s hard to guess how much memory to preallocate. If you guess too high, you’ll be wasting memory. If you guess too
low, your algorithm will either fail when it runs out (bad!) or make too-frequent trips to a surrounding random heap manager to
get space for more blocks (better). Microsoft has created the lookaside list object and a set of adaptive algorithms to deal with
these shortcomings.

Figure 3-11. A heap containing fixed-size blocks.


Figure 3-12 illustrates the concept of a lookaside list. Imagine that you had a glass that you could (somehow—the laws of
physics don’t exactly make this easy!) balance upright in a swimming pool. The glass represents the lookaside list object.
When you initialize the object, you tell the system how big the memory blocks (water drops, in this analogy) are that you’ll be
working with. In earlier versions of Windows NT, you could also specify the capacity of the glass, but the operating system
now determines that adaptively. To allocate a memory block, the system first tries to remove one from the list (remove a water
drop from the glass). If there are no more, the system dips into the surrounding memory pool. Conversely, to return a memory
block, the system first tries to put it back on the list (add a water drop to the glass). But if the list is full, the block goes back
into the pool using the regular heap manager routine (the drop slops over into the swimming pool).

Figure 3-12. Lookaside lists.


The system periodically adjusts the depths of all lookaside lists based on actual usage. The details of the algorithm aren’t really
important, and they’re subject to change in any case. Basically (in the current release, anyway), the system will reduce the
depth of lookaside lists that haven’t been accessed recently or that aren’t forcing pool access at least 5 percent of the time. The
depth never goes below 4, however, which is also the initial depth of a new list.
When the Driver Verifier is running, all lookaside lists are set to a depth of zero, which forces all allocation and free
calls to go directly to the pool. This action makes it more likely that driver problems involving memory corruption can
be caught. Just bear this fact in mind when you’re debugging your driver with the Driver Verifier engaged.
Table 3-8 lists the eight service functions that you use when you work with a lookaside list. There are really two sets of four
functions, one set for a lookaside list that manages paged memory (the ExXxxPagedLookasideList set) and another for a
lookaside list that manages nonpaged memory (the ExXxxNPagedLookasideList set). The first thing you must do is reserve
nonpaged memory for a PAGED_LOOKASIDE_LIST or an NPAGED_LOOKASIDE_LIST object. Even the paged variety of
object needs to be in nonpaged memory because the system will access the list object itself at an elevated IRQL.

Service Function Description


ExInitializeNPagedLookasideList ExInitializePagedLookasideList Initialize a lookaside list
ExAllocateFromNPagedLookasideList ExAllocateFromPagedLookasideList Allocate a fixed-size block
Release a block back to a
ExFreeToNPagedLookasideList ExFreeToPagedLookasideList
lookaside list
ExDeleteNPagedLookasideList ExDeletePagedLookasideList Destroy a lookaside list
Table 3-8. Service Functions for Lookaside List

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.4 String Handling - 69 -

After reserving storage for the lookaside list object somewhere, you call the appropriate initialization routine:

PPAGED_LOOKASIDE_LIST pagedlist;
PNPAGED_LOOKASIDE_LIST nonpagedlist;

ExInitializePagedLookasideList(pagedlist, Allocate, Free, 0, blocksize, tag, 0);


ExInitializeNPagedLookasideList(nonpagedlist, Allocate, Free,
0, blocksize, tag, 0);

(The only difference between the two examples is the spelling of the function name and the first argument.)
The first argument to either of these functions points to the [N]PAGED_LOOKASIDE_LIST object for which you’ve already
reserved space. Allocate and Free are pointers to routines you can write to allocate or release memory from a random heap.
You can use NULL for either or both of these parameters, in which case ExAllocatePoolWithTag and ExFreePool will be used,
respectively. The blocksize parameter is the size of the memory blocks you will be allocating from the list, and tag is the 32-bit
tag value you want placed in front of each such block. The two zero arguments are placeholders for values that you supplied in
previous versions of Windows NT but that the system now determines on its own; these values are flags to control the type of
allocation and the depth of the lookaside list.
To allocate a memory block from the list, call the appropriate AllocateFrom function:

PVOID p = ExAllocateFromPagedLookasideList(pagedlist);
PVOID q = ExAllocateFromNPagedLookasideList(nonpagedlist);

To put a block back onto the list, call the appropriate FreeTo function:

ExFreeToPagedLookasideList(pagedlist, p);
ExFreeToNPagedLookasideList(nonpagedlist, q);

Finally, to destroy a list, call the appropriate Delete function:

ExDeletePagedLookasidelist(pagedlist);
ExDeleteNPagedLookasideList(nonpagedlist);

It is very important for you to explicitly delete a lookaside list before allowing the list object to pass out of scope. I’m
told that a common programming mistake is to place a lookaside list object in a device extension and then forget to
delete the object before calling IoDeleteDevice. If you make this mistake, the next time the system runs through its list
of lookaside lists to tune their depths, it will put its foot down on the spot where your list object used to be, probably with bad
results.

3.4 String Handling


WDM drivers can work with string data in any of four formats:

„ A Unicode string, normally described by a UNICODE_STRING structure, contains 16-bit characters. Unicode has
sufficient code points to accommodate the language scripts used on this planet. A whimsical attempt to standardize code
points for the Klingon language, reported in the first edition, has been rejected. A reader of the first edition sent me the
following e-mail comment about this:

I suspect this is rude, and possibly obscene.


„ An ANSI string, normally described by an ANSI_STRING structure, contains 8-bit characters. A variant is an
OEM_STRING, which also describes a string of 8-bit characters. The difference between the two is that an OEM string
has characters whose graphic depends on the current code page, whereas an ANSI string has characters whose graphic is
independent of code page. WDM drivers won’t normally deal with OEM strings because they would have to originate in
user mode, and some other kernel-mode component will have already translated them into Unicode strings by the time
the driver sees them.
„ A null-terminated string of characters. You can express constants using normal C syntax, such as “Hello, world!” Strings
employ 8-bit characters of type CHAR, which are assumed to be from the ANSI character set. The characters in string
constants originate in whatever editor you used to create your source code. If you use an editor that relies on the
then-current code page to display graphics in the editing window, be aware that some characters might have a different
meaning when treated as part of the Windows ANSI character set.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 70 - Basic Programming Techniques | Chapter 3

„ A null-terminated string of wide characters (type WCHAR). You can express wide string constants using normal C syntax,
such as L"Goodbye, cruel world!" Such strings look like Unicode constants, but, being ultimately derived from some text
editor or another, actually use only the ASCII and Latin1 code points (0020-007F and 00A0-00FF) that correspond to the
Windows ANSI set.

The UNICODE_STRING and ANSI_STRING data structures both have the layout depicted in Figure 3-13. The Buffer field of
either structure points to a data area elsewhere in memory that contains the string data. MaximumLength gives the length of the
buffer area, and Length provides the (current) length of the string without regard to any null terminator that might be present.
Both length fields are in bytes, even for the UNICODE_STRING structure.

Figure 3-13. The UNICODE_STRING and ANSI_STRING structures.

The kernel defines three categories of functions for working with Unicode and ANSI strings. One category has names
beginning with Rtl (for run-time library). Another category includes most of the functions that are in a standard C library for
managing null-terminated strings. The third category includes the safe string functions from strsafe.h, which will hopefully be
packaged in a DDK header named NtStrsafe.h by the time you read this. I can’t add any value to the DDK documentation by
repeating what it says about the RtlXxx functions. I have, however, distilled in Table 3-9 a list of now-deprecated standard C
string functions and the recommended alternatives from NtStrsafe.h.

Standard function
Safe UNICODE Alternative Safe ANSI Alternative
(deprecated)
strcpy, wcscpy, RtlStringCbCopyA,
RtlStringCbCopyW, RtlStringCchCopyW
strncpy, wcsncpy RtlStringCchCopyA
strcat, wcscat, RtlStringCbCatA,
RtlStringCbCatW, RtlStringCchCatW
strncat, wcsncat RtlStringCchCatA
sprintf, swprintf, RtlStringCbPrintfA,
RtlStringCbPrintfW, RtlStringCchPrintfW
_snprintf, _snwprintf RtlStringCchPrintfA
vsprintf, vswprintf, RtlStringCbVPrintfA,
RtlStringCbVPrintfW, RtlStringCchVPrintfW
vsnprintf, _vsnwprintf RtlStringCchVPrintfA
RtlStringCbLengthA,
strlen, wcslen RtlStringCbLengthW, RtlStringCchLengthW
RtlStringCchLengthA
Table 3-9. Safe Functions for String Manipulation

NOTE
I based the contents of Table 3-9 on a description of how one of the kernel developers planned to craft
NtStrsafe.h from an existing user-mode header named strsafe.h. Don’t trust me—trust the contents of the
DDK!

It’s also okay, but not idiomatic, to use memcpy, memmove, memcmp, and memset in a driver. Nonetheless, most driver
programmers use these RtlXxx functions in preference:

„ RtlCopyMemory or RtlCopyBytes instead of memcpy to copy a “blob” of bytes from one place to another. These functions
are actually identical in the current Windows XP DDK. Furthermore, for Intel 32-bit targets, both are macro’ed to
memcpy, and memcpy is the subject of a #pragma intrinsic, so the compiler generates inline code to perform it.
„ RtlZeroMemory instead of memset to zero an area of memory. RtlZeroMemory is macro’ed to memset for Intel 32-bit
targets, and memset is mentioned in a #pragmaintrinsic.

You should use the safe string functions in preference to standard run-time routines such as strcpy and the like. As I
mentioned at the outset of this chapter, the standard string functions are available, but they’re often too hard to use
safely. Consider these points in choosing which string functions you’ll use in your driver:
„ The uncounted forms strcpy, strcat, sprintf, and vsprintf (and their Unicode equivalents) don’t protect you against
overrunning the target buffer. Neither does strncat (and its Unicode equivalent), wherein the length argument applies to
the source string.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.5 Miscellaneous Programming Techniques - 71 -

„ The strncpy and wcsncpy functions will fail to append a null terminator to the target if the source is at least as long as the
specified length. In addition, these functions have the possibly expensive feature of filling any leftover portion of the
target buffer with nulls.
„ Any of the deprecated functions has the potential to walk off the end of a memory page looking in vain for a null
terminator. This trait makes them especially dangerous when dealing with string data coming to you from user mode.
„ As I write this, NtStrsafe.h doesn’t currently define any comparison functions (strcmp, etc.). Keep your eye on the DDK
for such functions. Note that case-insensitive comparisons of ANSI strings are tricky because they depend on localization
settings that can vary from one session to another on the same computer.

Allocating and Releasing String Buffers


You often define UNICODE_STRING (or ANSI_STRING) structures as automatic variables or as parts of your own device
extension. The string buffers to which these structures point usually occupy dynamically allocated memory, but you’ll
sometimes want to work with string constants too. Keeping track of who owns the memory to which a particular
UNICODE_STRING or ANSI_STRING structure points can be a bit of a problem. Consider the following fragment of a
function:

UNICODE_STRING foo;
if (bArriving)
RtlInitUnicodeString(&foo, L"Hello, world!");
else
{
ANSI_STRING bar;
RtlInitAnsiString(&bar, "Goodbye, cruel world!");
RtlAnsiStringToUnicodeString(&foo, &bar, TRUE);
}

RtlFreeUnicodeString(&foo); // <== don't do this!

In one case, we initialize foo.Length, foo.MaximumLength, and foo.Buffer to describe a wide character string constant in our
driver. In another case, we ask the system (by means of the TRUE third argument to RtlAnsiStringToUnicodeString) to allocate
memory for the Unicode translation of an ANSI string. In the first case, it’s a mistake to call RtlFreeUnicodeString because it
will unconditionally try to release a memory block that’s part of our code or data. In the second case, it’s mandatory to call
RtlFreeUnicodeString eventually if we want to avoid a memory leak.

The moral of the preceding example is that you have to know where the memory comes from in any UNICODE_STRING
structures you use so that you can release the memory only when necessary.

3.5 Miscellaneous Programming Techniques


In the remainder of this chapter, I’m going to discuss some miscellaneous topics that might be useful in various parts of your
driver. I’ll begin by describing how you access the registry database, which is where you can find various configuration and
control information that might affect your code or your hardware. I’ll go on to describe how you access disk files and other
named devices. A few words will suffice to describe how you can perform floating-point calculations in a WDM driver. Finally
I’ll describe a few of the features you can embed in your code to make it easier to debug your driver in the unlikely event it
shouldn’t work correctly the first time you try it out.

3.5.1 Accessing the Registry


Windows XP and Windows 98/Me record configuration and other important information in a database called the registry.
WDM drivers can call the functions listed in Table 3-10 to access the registry. If you’ve done user-mode programming
involving registry access, you might be able to guess how to use these functions in a driver. I found the kernel-mode support
functions sufficiently different, however, that I think it’s worth describing how you might use them.
In this section, I’ll discuss, among other things, the ZwXxx family of routines and RtlDeleteRegistryValue, which provide the
basic registry functionality that suffices for most WDM drivers.

Opening a Registry Key


Before you can interrogate values in the registry, you need to open the key that contains them. You use ZwOpenKey to open an
existing key. You use ZwCreateKey either to open an existing key or to create a new key. Either function requires you to first
initialize an OBJECT_ATTRIBUTES structure with the name of the key and (perhaps) other information. The
OBJECT_ATTRIBUTES structure has the following declaration:

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 72 - Basic Programming Techniques | Chapter 3

typedef struct _OBJECT_ATTRIBUTES {


ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES;

Service Function Description


IoOpenDeviceRegistryKey Opens special key associated with a physical device object (PDO)
IoOpenDeviceInterfaceRegistryKey Opens a registry key associated with a registered device interface
RtlDeleteRegistryValue Deletes a registry value
RtlQueryRegistryValues Reads several values from the registry
RtlWriteRegistryValue Writes a value to the registry
ZwClose Closes handle to a registry key
ZwCreateKey Creates a registry key
ZwDeleteKey Deletes a registry key
ZwDeleteValueKey Deletes a value (Windows 2000 and later)
ZwEnumerateKey Enumerates subkeys
ZwEnumerateValueKey Enumerates values within a registry key
ZwFlushKey Commits registry changes to disk
ZwOpenKey Opens a registry key
ZwQueryKey Gets information about a registry key
ZwQueryValueKey Gets a value within a registry key
ZwSetValueKey Sets a value within a registry key
Table 3-10. Service Functions for Registry Access
Rather than initialize an instance of this structure by hand, it’s easiest to use the macro InitializeObjectAttributes, which I’m
about to show you.
Suppose, for example, that we wanted to open the service key for our driver. The I/O Manager gives us the name of this key as
a parameter to DriverEntry. So we could write code like the following:

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)


{

OBJECT_ATTRIBUTES oa;

InitializeObjectAttributes(&oa, RegistryPath, OBJ_KERNEL_HANDLE │


OBJ_CASE_INSENSITIVE, NULL, NULL);
HANDLE hkey;

status = ZwOpenKey(&hkey, KEY_READ, &oa);


if (NT_SUCCESS(status))
{

ZwClose(hkey);
}

1. We’re initializing the object attributes structure with the registry pathname supplied to us by the I/O Manager and with a
NULL security descriptor. ZwOpenKey will ignore the security descriptor anyway—you can specify security attributes
only when you create a key for the first time.
2. ZwOpenKey will open the key for reading and store the resulting handle in our hkey variable.
3. ZwClose is a generic routine for closing a handle to a kernel-mode object. Here we use it to close the handle we have to
the registry key.
The OBJ_KERNEL_HANDLE flag, shown in the preceding code sample, is important for system integrity. If you’re
running in the context of a user thread when you call ZwOpenKey, and if you don’t supply this flag bit, the handle you
get will be available to the user-mode process. It might even happen that user-mode code will close the handle and open

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.5 Miscellaneous Programming Techniques - 73 -

a new object, receiving back the same numeric value. All of a sudden, your calls to registry functions will be dealing with the
wrong kind of handle.
Even though we often refer to the registry as being a database, it doesn’t have all of the attributes that have come to be
associated with real databases. It doesn’t allow for committing or rolling back changes, for example. Furthermore, the access
rights you specify when you open a key (KEY_READ in the preceding example) are for security checking rather than for the
prevention of incompatible sharing. That is, two different processes can have the same key open after specifying write access
(for example). The system does guard against destructive writes that occur simultaneously with reads, however, and it does
guarantee that a key won’t be deleted while someone has an open handle to it.

Other Ways to Open Registry Keys


In addition to ZwOpenKey, Windows XP provides two other functions for opening registry keys.
IoOpenDeviceRegistryKey allows you to open one of the special registry keys associated with a device object:

HANDLE hkey;
Status = IoOpenDeviceRegistryKey(pdo, flag, access, &hkey);

where pdo is the address of the physical device object (PDO) at the bottom of your particular driver stack, flag is an indicator
for which special key you want to open (see Table 3-11), and access is an access mask such as KEY_READ.

Flag Value Selected Registry Key


PLUGPLAY_REGKEY_DEVICE The hardware (instance) subkey of the Enum key
PLUGPLAY_REGKEY_DRIVER The driver subkey of the class key

Table 3-11. Registry Key Codes for IoOpenDeviceRegistryKey


I use IoOpenDeviceRegistryKey with the PLUGPLAY_REGKEY_DEVICE flag very often in my own drivers. In Windows XP,
this function opens the Device Parameters subkey of the hardware key for the device. In Windows 98/Me, it opens the
hardware key itself. These keys are the right place to store parameter information about the hardware. I’ll discuss this key more
fully in Chapter 15 in connection with installing and distributing a driver.
IoOpenDeviceInterfaceRegistryKey opens the key associated with an instance of a registered device interface:

HANDLE hkey;
status = IoOpenDeviceInterfaceRegistryKey(linkname, access, &hkey);

where linkname is the symbolic link name of the registered interface and access is an access mask such as KEY_READ.
The interface registry key is a subkey of HKLM\System\CurrentControlSet\Control\DeviceClasses that persists from one
session to the next. It’s a good place to store parameter information that you want to share with user-mode programs because
user-mode code can call SetupDiOpenDeviceInterfaceRegKey to gain access to the same key.

Getting and Setting Values


Usually, you open a registry key because you want to retrieve a value from the database. The basic function you use for that
purpose is ZwQueryValueKey. For example, to retrieve the ImagePath value in the driver’s service key—I don’t actually know
why you’d want to know this, but that’s not my department—you could use the following code:

UNICODE_STRING valname;
RtlInitUnicodeString(&valname, L"ImagePath");
size = 0;
status = ZwQueryValueKey(hkey, &valname, KeyValuePartialInformation,
NULL, 0, &size);
if (status == STATUS_OBJECT_NAME_NOT_FOUND ││ size == 0)
<handle error>;
size = min(size, PAGE_SIZE);
PKEY_VALUE_PARTIAL_INFORMATION vpip =
PKEY_VALUE_PARTIAL_INFORMATION) ExAllocatePool(PagedPool, size);
if (!vpip)
<handle error>;
status = ZwQueryValueKey(hkey, &valname, KeyValuePartialInformation,
vpip, size, &size);
if (!NT_SUCCESS(status))
<handle error>;
<do something with vpip->Data>ExFreePool(vpip);

Here we make two calls to ZwQueryValueKey. The purpose of the first call is to determine how much space we need to allocate

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 74 - Basic Programming Techniques | Chapter 3

for the KEY_VALUE_PARTIAL_INFORMATION structure we’re trying to retrieve. The second call retrieves the information. I
left the error checking in this code fragment because the errors didn’t work out in practice the way I expected them to. In
particular, I initially guessed that the first call to ZwQueryValueKey would return STATUS_BUFFER_TOO_SMALL (since I
passed it a zero-length buffer). It didn’t do that, though. The important failure code is
STATUS_OBJECT_NAME_NOT_FOUND, which indicates that the value doesn’t actually exist. Hence, I test for that value
only. If there’s some other error that prevents ZwQueryValueKey from working, the second call will uncover it.

NOTE
The reason for trimming the size to PAGE_SIZE is to impose a reasonableness limit on the amount of
memory you allocate. If a malicious user were able to gain access to the key from which you’re reading, he or
she could replace the ImagePath value with an arbitrary amount of data. Your driver could then become an
unwitting accomplice to a denial of service attack by consuming mass quantities of memory. Now, drivers
ordinarily deal with registry keys that only administrators can modify, and an administrator has many other
ways of attacking the system. It’s nonetheless good to provide a defense in depth against all forms of attack.

The so-called “partial” information structure you retrieve in this way contains the value’s data and a description of its data
type:

typedef struct _KEY_VALUE_PARTIAL_INFORMATION {


ULONG TitleIndex;
ULONG Type;
ULONG DataLength;
UCHAR Data[1];
} KEY_VALUE_PARTIAL_INFORMATION, *PKEY_VALUE_PARTIAL_INFORMATION;

Type is one of the registry data types listed in Table 3-12. (Additional data types are possible but not interesting to device
drivers.) DataLength is the length of the data value, and Data is the data itself. TitleIndex has no relevance to drivers. Here are
some useful facts to know about the various data types:
„ REG_DWORD is a 32-bit unsigned integer in whatever format (big-endian or little-endian) is natural for the platform.
„ REG_SZ describes a null-terminated Unicode string value. The null terminator is included in the DataLength count.
„ To expand a REG_EXPAND_SZ value by substituting environment variables, you should use RtlQueryRegistryValues as
your method of interrogating the registry. The internal routines for accessing environment variables aren’t documented or
exposed for use by drivers.
„ RtlQueryRegistryValues is also a good way to interrogate REG_MULTI_SZ values in that it will call your designated
callback routine once for each of the potentially many strings.

NOTE
Notwithstanding the apparent utility of RtlQueryRegistryValues, I’ve avoided using it ever since it caused a
crash in one of my drivers. Apparently, the value I was reading required the function to call a helper function
that was placed in the initialization section of the kernel and that was, therefore, no longer present.

Data TypeConstant Description


REG_BINARY Variable-length binary data
REG_DWORD Unsigned long integer in natural format for the platform
Null-terminated Unicode string containing %-escapes for environment variable
REG_EXPAND_SZ
names
REG_MULTI_SZ One or more null-terminated Unicode strings, followed by an extra null
REG_SZ Null-terminated Unicode string

Table 3-12. Types of Registry Values Useful to WDM Drivers


To set a registry value, you must have KEY_SET_VALUE access to the parent key. I used KEY_READ earlier, which wouldn’t
give you such access. You could use KEY_WRITE or KEY_ALL_ACCESS, although you thereby gain more than the necessary
permission. Then call ZwSetValueKey. For example:

RtlInitUnicodeString(&valname, L"TheAnswer");
ULONG value = 42;
ZwSetValueKey(hkey, &valname, 0, REG_DWORD, &value, sizeof(value));

Deleting Subkeys or Values


To delete a value in an open key, you can use RtlDeleteRegistryValue in the following special way:

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.5 Miscellaneous Programming Techniques - 75 -

RtlDeleteRegistryValue(RTL_REGISTRY_HANDLE, (PCWSTR) hkey, L"TheAnswer");

RtlDeleteRegistryValue is a general service function whose first argument can designate one of several special places in the
registry. When you use RTL_REGISTRY_HANDLE, as I did in this example, you indicate that you already have an open handle
to the key within which you want to delete a value. You specify the key (with a cast to make the compiler happy) as the second
argument. The third and final argument is the null-terminated Unicode name of the value you want to delete. This is one time
when you don’t have to create a UNICODE_STRING structure to describe the string.
In Windows 2000 and later, you can use ZwDeleteValueKey to delete a value (it’s an oversight that this function isn’t
documented in the DDK):

UNICODE_STRING valname;
RtlInitUnicodeString(&valname, L"TheAnswer");
ZwDeleteValueKey(hkey, &valname);

You can delete only those keys that you’ve opened with at least DELETE permission (which you get with KEY_ALL_ACCESS).
You call ZwDeleteKey:

ZwDeleteKey(hkey);

The key lives on until all handles are closed, but subsequent attempts to open a new handle to the key or to access the key by
using any currently open handle will fail with STATUS_KEY_DELETED. Since you have an open handle at this point, you
must be sure to call ZwClose sometime. (The DDK documentation entry for ZwDeleteKey says the handle becomes invalid. It
doesn’t—you must still close it by calling ZwClose.)

Enumerating Subkeys or Values


A complicated activity you can carry out with an open registry key is to enumerate the elements (subkeys and values) that the
key contains. To do this, you’ll first call ZwQueryKey to determine a few facts about the subkeys and values, such as their
number, the length of the largest name, and so on. ZwQueryKey has an argument that indicates which of three types of
information you want to retrieve about the key. These types are named basic, node, and full. To prepare for an enumeration,
you’d be interested first in the full information:

typedef struct _KEY_FULL_INFORMATION {


LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG ClassOffset;
ULONG ClassLength;
ULONG SubKeys;
ULONG MaxNameLen;
ULONG MaxClassLen;
ULONG Values;
ULONG MaxValueNameLen;
ULONG MaxValueDataLen;
WCHAR Class[1];
} KEY_FULL_INFORMATION, *PKEY_FULL_INFORMATION;

This structure is actually of variable length since Class[0] is just the first character of the class name. It’s customary to make
one call to find out how big a buffer you need to allocate and a second call to get the data, as follows:

ULONG size;
ZwQueryKey(hkey, KeyFullInformation, NULL, 0, &size);
size = min(size, PAGE_SIZE);
PKEY_FULL_INFORMATION fip = (PKEY_FULL_INFORMATION)
ExAllocatePool(PagedPool, size);
ZwQueryKey(hkey, KeyFullInformation, fip, size, &size);

Were you now interested in the subkeys of your registry key, you could perform the following loop calling ZwEnumerateKey:

for (ULONG i = 0; i < fip->SubKeys; ++i)


{
ZwEnumerateKey(hkey, i, KeyBasicInformation, NULL, 0, &size);
size = min(size, PAGE_SIZE);
PKEY_BASIC_INFORMATION bip = (PKEY_BASIC_INFORMATION)
ExAllocatePool(PagedPool, size);
ZwEnumerateKey(hkey, i, KeyBasicInformation, bip, size, &size);
<do something with bip->Name>

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 76 - Basic Programming Techniques | Chapter 3

ExFreePool(bip);
}

The key fact you discover about each subkey is its name, which shows up as a counted Unicode string in the
KEY_BASIC_INFORMATION structure you retrieve inside the loop:

typedef struct _KEY_BASIC_INFORMATION {


LARGE_INTEGER LastWriteTime;
ULONG Type;
ULONG NameLength;
WCHAR Name[1];
} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;

The name isn’t null-terminated; you must use the NameLength member of the structure to determine its length. Don’t forget
that the length is in bytes! The name isn’t the full registry path either; it’s just the name of the subkey within whatever key
contains it. This is actually lucky because you can easily open a subkey given its name and an open handle to its parent key.
To accomplish an enumeration of the values in an open key, employ the following method:

ULONG maxlen = fip->MaxValueNameLen + sizeof(KEY_VALUE_BASIC_INFORMATION);


maxlen = min(maxlen, PAGE_SIZE);
PKEY_VALUE_BASIC_INFORMATION vip = (PKEY_VALUE_BASIC_INFORMATION)
ExAllocatePool(PagedPool, maxlen);
for (ULONG i = 0; i < fip->Values; ++i)
{
ZwEnumerateValueKey(hkey, i, KeyValueBasicInformation, vip, maxlen, &size);
<do something with vip->Name>
}
ExFreePool(vip);

Allocate space for the largest possible KEY_VALUE_BASIC_INFORMATION structure that you’ll ever retrieve based on the
MaxValueNameLen member of the KEY_FULL_INFORMATION structure. Inside the loop, you’ll want to do something with
the name of the value, which comes to you as a counted Unicode string in this structure:

typedef struct _KEY_VALUE_BASIC_INFORMATION {


ULONG TitleIndex;
ULONG Type;
ULONG NameLength;
WCHAR Name[1];
} KEY_VALUE_BASIC_INFORMATION, *PKEY_VALUE_BASIC_INFORMATION;

Once again, having the name of the value and an open handle to its parent key is just what you need to retrieve the value, as
shown in the preceding section.
There are variations on ZwQueryKey and on these two enumeration functions that I haven’t discussed. You can, for example,
obtain full information about a subkey when you call ZwEnumerateKey. I showed you only how to get the basic information
that includes the name. You can retrieve data values only, or names plus data values, from ZwEnumerateValueKey. I showed
you only how to get the name of a value.

3.5.2 Accessing Files


It’s sometimes useful to be able to read and write regular disk files from inside a WDM driver. Perhaps you need to download a
large amount of microcode to your hardware, or perhaps you need to create your own extensive log of information for some
purpose. There’s a set of ZwXxx routines to help you do these things.
File access via the ZwXxx routines require you be running at PASSIVE_LEVEL (see the next chapter) in a thread that can
safely be suspended. In practice, the latter requirement means that you must not have disabled Asynchronous Procedure Calls
(APCs) by calling KeEnterCriticalRegion. You’ll read in the next chapter that some synchronization primitives require you to
raise the IRQL above PASSIVE_LEVEL or to disable APCs. Just bear in mind that those synchronization primitives and file
access are incompatible.
The first step in accessing a disk file is to open a handle by calling ZwCreateFile. The full description of this function in the
DDK is relatively complex because of all the ways in which it can be used. I’m going to show you two simple scenarios,
however, that are useful if you just want to read or write a file whose name you already know.

Sample Code
The FILEIO sample driver in the companion content illustrates calls to some of the ZwXxx functions discussed
in this section. This particular sample is valuable because it provides workarounds for the platform

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.5 Miscellaneous Programming Techniques - 77 -

incompatibilities mentioned at the end of this chapter.

Opening an Existing File for Reading


To open an existing file so that you can read it, follow this example:

NTSTATUS status;
OBJECT_ATTRIBUTES oa;
IO_STATUS_BLOCK iostatus;
HANDLE hfile; // the output from this process
PUNICODE_STRING pathname; // you've been given this

InitializeObjectAttributes(&oa, pathname,
OBJ_CASE_INSENSITIVE │ OBJ_KERNEL_HANDLE, NULL, NULL);
status = ZwCreateFile(&hfile, GENERIC_READ, &oa, &iostatus,
NULL, 0, FILE_SHARE_READ, FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);

Creating or Rewriting a File


To create a new file, or to open and truncate to zero length an existing file, replace the call to ZwCreateFile in the preceding
fragment with this one:

status = ZwCreateFile(&hfile, GENERIC_WRITE, &oa, &iostatus,


NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OVERWRITE_IF,
FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);

In these fragments, we set up an OBJECT_ATTRIBUTES structure whose main purpose is to point to the full pathname of the
file we’re about to open. We specify the OBJ_CASE_INSENSITIVE attribute because the Win32 file system model doesn’t
treat case as significant in a pathname. We specify OBJ_KERNEL_HANDLE for the same reason we did so in the registry
example shown earlier in this chapter. Then we call ZwCreateFile to open the handle.
The first argument to ZwCreateFile(&hfile) is the address of the HANDLE variable where ZwCreateFile will return the handle
it creates. The second argument (GENERIC_READ or GENERIC_WRITE) specifies the access we need to the handle to
perform either reading or writing. The third argument (&oa) is the address of the OBJECT_ATTRIBUTES structure containing
the name of the file. The fourth argument points to an IO_STATUS_BLOCK that will receive a disposition code indicating how
ZwCreateFile actually implemented the operation we asked it to perform. When we open a read-only handle to an existing file,
we expect the Status field of this structure to end up equal to FILE_OPENED. When we open a write-only handle, we expect it
to end up equal to FILE_OVERWRITTEN or FILE_CREATED, depending on whether the file did or didn’t already exist. The
fifth argument (NULL) can be a pointer to a 64-bit integer that specifies the initial allocation size for the file. This argument
matters only when you create or overwrite a file, and omitting it as I did here means that the file grows from zero length as you
write data. The sixth argument (0 or FILE_ATTRIBUTE_NORMAL) specifies file attribute flags for any new file that you
happen to create. The seventh argument (FILE_SHARE_READ or 0) specifies how the file can be shared by other threads. If
you’re opening for input, you can probably tolerate having other threads read the file simultaneously. If you’re opening for
sequential output, you probably don’t want other threads trying to access the file at all.
The eighth argument (FILE_OPEN or FILE_OVERWRITE_IF) indicates how to proceed if the file either already exists or
doesn’t. In the read-only case, I specified FILE_OPEN because I expected to open an existing file and wanted a failure if the
file didn’t exist. In the write-only case, I specified FILE_OVERWRITE_IF because I wanted to overwrite any existing file by
the same name or create a brand-new file as necessary. The ninth argument (FILE_SYNCHRONOUS_IO_NONALERT)
specifies additional flag bits to govern the open operation and the subsequent use of the handle. In this case, I indicated that
I’m going to be doing synchronous I/O operations (wherein I expect the read or write function not to return until the I/O is
complete). The tenth and eleventh arguments (NULL and 0) are, respectively, an optional pointer to a buffer for extended
attributes and the length of that buffer.
You expect ZwCreateFile to return STATUS_SUCCESS and to set the handle variable. You can then carry out whatever read or
write operations you please by calling ZwReadFile or ZwWriteFile, and then you close the handle by calling ZwClose:

ZwClose(hfile);

You can perform synchronous or asynchronous reads and writes, depending on the flags you specified to ZwCreateFile. In the
simple scenarios I’ve outlined, you would do synchronous operations that don’t return until they’ve completed. For example:

PVOID buffer;
ULONG bufsize;
status = ZwReadFile(hfile, NULL, NULL, NULL, &iostatus, buffer,
bufsize, NULL, NULL);

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 78 - Basic Programming Techniques | Chapter 3

-or-
status = ZwWriteFile(hfile, NULL, NULL, NULL, &iostatus, buffer,
bufsize, NULL, NULL);

These calls are analogous to a nonoverlapped ReadFile or WriteFile call from user mode. When the function returns, you might
be interested in iostatus.Information, which will hold the number of bytes transferred by the operation.

Scope of Handles
Each process has a private handle table that associates numeric handles with pointers to kernel objects. When
you open a handle using ZwCreateFile, or NtCreateFile, that handle belongs to the then-current process, unless
you use the OBJ_KERNEL_HANDLE flag. A process-specific handle will go away if the process terminates.
Moreover, if you use the handle in a different process context, you’ll be indirectly referencing whatever object
(if any) that that handle corresponds to in the other process. A kernel handle, on the other hand, is kept in a
global table that doesn’t disappear until the operating system shuts down and can be used without ambiguity
in any process.

If you plan to read an entire file into a memory buffer, you’ll probably want to call ZwQueryInformationFile to determine the
total length of the file:

FILE_STANDARD_INFORMATION si;
ZwQueryInformationFile(hfile, &iostatus, &si, sizeof(si),
FileStandardInformation);
ULONG length = si.EndOfFile.LowPart;

Timing of File Operations


You’ll be likely to want to read a disk file in a WDM driver while you’re initializing your device in response to an
IRP_MN_START_DEVICE request. (See Chapter 6.) Depending on where your device falls in the initialization
sequence, you might or might not have access to files using normal pathnames like \??\C:\dir\file.ext. To be
safe, put your data files into some directory below the system root directory and use a filename like
\SystemRoot\dir\file.ext. The SystemRoot branch of the namespace is always accessible since the operating
system has to be able to read disk files to start up.

Sample Code
The RESOURCE sample combines several of the ideas discussed in this chapter to illustrate how to access data
in a standard resource script from within a driver. This is not especially easy to do, as you’ll see if you examine
the sample code.

3.5.3 Floating-Point Calculations


There are times when integer arithmetic just isn’t sufficient to get your job done and you need to perform floating-point
calculations. On an Intel processor, the math coprocessor is also where Multimedia Extensions (MMX) instructions execute.
Historically, there have been two problems with drivers carrying out floating-point calculations. The operating system will
emulate a missing coprocessor, but the emulation is expensive and normally requires a processor exception to trigger it.
Handling exceptions, especially at elevated IRQLs, can be difficult in kernel mode. Additionally, on computers that have
hardware coprocessors, the CPU architecture might require a separate expensive operation to save and restore the coprocessor
state during context switches. Therefore, conventional wisdom has forbidden kernel-mode drivers from using floating-point
calculations.
Windows 2000 and later systems provide a way around past difficulties. First of all, a system thread—see Chapter 14—running
at or below DISPATCH_LEVEL is free to use the math coprocessor all it wants. In addition, a driver running in an arbitrary
thread context at or below DISPATCH_LEVEL can use these two system calls to bracket its use of the math coprocessor:

ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);


KFLOATING_SAVE FloatSave;
NTSTATUS status = KeSaveFloatingPointState(&FloatSave);
if (NT_SUCCESS(status))
{

KeRestoreFloatingPointState(&FloatSave);
}

These calls, which must be paired as shown here, save and restore the “nonvolatile” state of the math coprocessor for the
current CPU—that is, all the state information that persists beyond a single operation. This state information includes registers,
control words, and so on. In some CPU architectures, no actual work might occur because the architecture inherently allows
any process to perform floating-point operations. In other architectures, the work involved in saving and restoring state
information can be quite substantial. For this reason, Microsoft recommends that you avoid using floating-point calculations in
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.5 Miscellaneous Programming Techniques - 79 -

a kernel-mode driver unless necessary.

Sample Code
The FPUTEST sample illustrates one way to use floating-point and MMX instructions in a WDM driver.

What happens when you call KeSaveFloatingPointState depends, as I said, on the CPU architecture. To give you an idea, on an
Intel-architecture processor, this function saves the entire floating-point state by executing an FSAVE instruction. It can save
the state information either in a context block associated with the current thread or in an area of dynamically allocated memory.
It uses the opaque FloatSave area to record meta-information about the saved state to allow KeRestoreFloatingPointState to
correctly restore the state later.
KeSaveFloatingPointState will fail with STATUS_ILLEGAL_FLOAT_CONTEXT if no real coprocessor is present. (All CPUs
of a multi-CPU computer must have coprocessors, or else none of them can, by the way.) Your driver will therefore need
alternative code to carry out whatever calculations you had in mind, or else you’ll want to decline to load (by failing
DriverEntry) if the computer doesn’t have a coprocessor.

NOTE
You can call ExIsProcessorFeaturePresent to check up on various floating-point capabilities. Since this function
isn’t present in Windows 98/Me, you’ll also need to ship WDMSTUB with your driver. Consult Appendix A for
more information about this and related system incompatibilities.

3.5.4 Making Debugging Easier


My drivers always have bugs. Maybe you’re as unlucky as I am. If so, you’ll find yourself spending lots of time with a
debugger trying to figure out what your code is doing or not doing correctly or incorrectly. I won’t discuss the potentially
divisive subject of which debugger is best or the noncontroversial but artistic subject of how to debug a driver. But you can do
some things in your driver code that will make your life easier.

Checked and Free Builds


When you build your driver, you select either the checked or the free build environment. In the checked build environment, the
preprocessor symbol DBG equals 1, whereas it equals 0 in the free build environment. So one of the things you can do in your
own code is to provide additional code that will take effect only in the checked build:

#if DBG
<extra debugging code>
#endif

The KdPrint macro


One of the most useful debugging techniques ever invented is to simply print messages from time to time. I used to do this
when I was first learning to program (in FORTRAN on a computer made out of vacuum tubes, no less), and I still do it today.
DbgPrint is a kernel-mode service routine you can call to display a formatted message in whatever output window your
debugger provides. Another way to see the output from DbgPrint calls is to download the DebugView utility from
http://www.sysinternals.com. Instead of directly referencing DbgPrint in your code, it’s often easier to use the macro named
KdPrint, which calls DbgPrint if DBG is true and generates no code at all if DBG is false:

KdPrint((DRIVERNAME " - KeReadProgrammersMind failed - %X\n", status));

You use two sets of parentheses with KdPrint because of the way it’s defined. The first argument is a string with %-escapes
where you want to substitute values. The second, third, and following arguments provide the values to go with the %-escapes.
The macro expands into a call to DbgPrint, which internally uses the standard run-time library routine _vsnprintf to format the
string. You can, therefore, use the same set of %-escape codes that are available to application programs that call this routine
but not the escapes for floating-point numbers.
In all of my drivers, I define a constant named DRIVERNAME like this:

#define DRIVERNAME "xxx"

where xxx is the name of the driver. Recall that the compiler sees two adjacent string constants as a single constant. Using this
particular trick allows me to cut and paste entire subroutines, including their KdPrint calls, from one driver to another without
needing to make source changes.

The ASSERT macro


Another useful debugging technique relies on the ASSERT macro:

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 80 - Basic Programming Techniques | Chapter 3

ASSERT(1 + 1 == 2);

In the checked build of your driver, ASSERT generates code to evaluate the Boolean expression. If the expression is false,
ASSERT will try to halt execution in the debugger so that you can see what’s going on. If the expression is true, your program
continues executing normally. Kernel debuggers will halt when ASSERT failures happen, even in the retail build of the
operating system, by the way.

IMPORTANT
An ASSERT failure in a retail build of the operating system with no kernel debugger running generates a bug
check.

The Driver Verifier


The Driver Verifier is part of the checked and free builds of the operating system and is fast becoming one of
Microsoft’s major tools for checking driver quality. You can launch Driver Verifier from the Start Menu, whereupon
you’ll be presented with a series of wizard pages. Here is a bit of a road map to guide you through these pages the first time.
Figure 3-14 illustrates the initial wizard page. I recommend checking the Create Custom Settings (For Code Developers)
option. This choice will allow you to specify in detail which Driver Verifier options you want to engage.

Figure 3-14. Initial Driver Verifier wizard page


After making my recommended choice from the first page, you’ll be presented with a second page (see Figure 3-15). Here, I
recommend checking the Select Individual Settings From A Full List option.

Figure 3-15. Second Driver Verifier wizard page


The next wizard page (see Figure 3-16) allows you to specify the verifier settings you desire. The specified checks are in
addition to a number of checks that the Driver Verifier makes automatically.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.5 Miscellaneous Programming Techniques - 81 -

Figure 3-16. Driver Verifier’s custom settings wizard page


The choices available at the time I’m writing this are as follows:
„ Special Pool forces all memory allocations from verified drivers to be made from the special pool. As described earlier in
this chapter, such allocations are at the end (or start) of a page, so that storing after (or before) the allocated memory leads
to an immediate bug check.
„ Pool Tracking causes the Driver Verifier to track memory allocations made by verified drivers. You can see statistics
about memory usage as it changes from time to time. The Driver Verifier also ensures that all allocations are freed when
the verified drivers unload to help you catch memory leaks.
„ Force IRQL Checking essentially causes paged memory to be flushed whenever a verified driver raises IRQL to
DISPATCH_LEVEL or above. This action helps find places where the driver is incorrectly accessing paged memory. The
system runs quite slowly when this option is turned on.
„ I/O Verification causes the Driver Verifier to make basic checks on how a driver handles IRPs that it creates or forwards
to other drivers.
„ Enhanced I/O Verification attempts to flush out driver errors in boundary cases, such as completing PnP and Power IRPs
incorrectly, making assumptions about the order in which the PnP Manager loads drivers, and so on. Some of these tests
occur when the driver initially starts, by the way, which can prevent the system from starting.
„ Deadlock Detection creates a graph of the locking hierarchy for spin locks, mutexes, and fast mutexes used by verified
drivers in order to detect potential deadlocks.
„ DMA Checking ensures that verified drivers perform DMA using the methods prescribed by the DDK.
„ Low Resources Simulation involves randomly failing pool allocations from verified drivers, beginning seven minutes
after the system starts. The purpose of these failures is to ensure that drivers test the return value from pool allocation
calls.
You can use a special procedure, described in the DDK, to activate checks on a SCSI miniport driver.

NOTE
There can be interactions between the options you specify. At the present time, for example, asking for DMA
checking or deadlock detection turns off enhanced I/O verification.

Note too that the Driver Verifier is evolving rapidly even as we speak. Consult the DDK you happen to be
working with for up-to-date information.

After you select the verifier options you want, you will see one final wizard page (see Figure 3-17). This page allows you to
specify which drivers you want verified by checking boxes in a list. After making that selection, you’ll need to restart the
computer because many of the Driver Verifier checks require boot-time initialization. When I’m debugging one of my own
drivers, I find it most convenient to not have my driver loaded when the restart occurs. My driver therefore won’t already be in
the list, and I’ll have to add it via the Add Currently Not Loaded Driver(s) To The List button.
Driver Verifier failures are bug checks, by the way. You will need to be running a kernel debugger or to analyze the crash dump
after the fact to isolate the cause of the failure.

Don’t Ship the Checked Version!


Incidentally, I and every other user of kernel debuggers would greatly prefer that you not ship the debug version of your driver.
It will probably contain a bunch of ASSERT statements that will go off while we’re looking for our own bugs, and it will
probably also print a lot of messages that will obscure the messages from our drivers. I recall a vendor who shipped a debug

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 82 - Basic Programming Techniques | Chapter 3

driver for a popular network card. This particular driver logs every packet it receives over the wire. Nowadays I carry with me
to consulting gigs a little driver that patches their driver to eliminate the spew. And I buy other vendors’ network cards.

Figure 3-17. Driver selection page for Driver Verifier.

3.6 Windows 98/Me Compatibility Notes

3.6.1 File I/O


The ZwXxx routines for accessing disk files don’t work well in Windows 98/Me because of two basic problems—one from the
architecture of Windows and the other from what looks like an ordinary bug in the original release of Windows 98.
The first problem with file access has to do with the order in which Windows 98/Me initializes various virtual device drivers.
The Configuration Manager (CONFIGMG.VXD) initializes before the Installable File System Manager (IFSMGR.VXD).
WDM drivers for devices that exist at start-up time receive their IRP_MN_START_DEVICE requests during CONFIGMG’s
initialization phase. But since IFSMGR hasn’t initialized at that point, it’s not possible to perform file I/O operations by using
ZwCreateFile and the other functions discussed earlier in this chapter. Furthermore, there’s no way for a WDM driver to defer
handling IRP_MN_START_DEVICE until file system functionality becomes available. If you don’t have a debugger such as
Soft-Ice/W running, the symptom you will see is a blue screen complaining of a Windows Protection error while initializing
CONFIGMG.
A second and more crippling problem existed in the July 1998 retail release of Windows 98. The problem had to do with the
validity checking that ZwReadFile, ZwWriteFile, and ZwQueryInformationFile perform on their arguments. If you supply an
IO_STATUS_BLOCK in kernel-mode memory (and there’s basically no way to do anything else), these functions probe a
virtual address that doesn’t exist. In this original release, the resulting page fault was caught by a structured exception handler
and resulted in you getting back STATUS_ACCESS_VIOLATION even when you did everything right. I don’t know of any
workaround for this except by using the technique in the FILEIO sample. The problem was fixed in Windows 98, Second
Edition, by the way.
The FILEIO sample in the companion content illustrates a way past these Windows 98/Me difficulties. FILEIO makes a
run-time decision whether to call the ZwXxx functions or instead to call VxD services to perform file operations.

3.6.2 Floating Point


Floating-point operations are permissible in Windows 98/Me WDM drivers, but with some important restrictions relative to the
situation in Windows XP:
„ You can do floating-point (including MMX) operations in a WDM driver only in a system thread. System threads include
those you create yourself by calling PsCreateSystemThread and system worker threads. Note that work item callbacks
occur in a system worker thread, so you can do floating point in such a callback.
„ You should do floating-point operations only at PASSIVE_LEVEL. (DISPATCH_LEVEL corresponds to VxD hardware
interrupt handling in Windows 98/Me.)
The DDK cautions that you should not attempt to circumvent these rules. For example, you might be tempted to use
KeSaveFloatingPointState and KeRestoreFloatingPointState in an IOCTL handler despite the explicit prohibition on doing so,
or to manually save and restore the FPU state. What can happen to you is this: if an exception is pending in the coprocessor
when you initially save the floating-point state, that exception will be taken when you restore the state. The kernel cannot
correctly handle that exception. There is no workaround for this problem, which is inherent in the Intel processor design and
the way VMCPD.VXD works.
Note that the FPUTEST sample program obeys these rules by refusing to work in Windows 98/Me.

Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney

You might also like