Programming The Microsoft Windows Driver Model 2nd Edition (001-100)
Programming The Microsoft Windows Driver Model 2nd Edition (001-100)
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
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.
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.
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
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.)
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.
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-
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
-4- Beginning a Driver Project | Chapter 1
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.
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
-6- Beginning a Driver Project | Chapter 1
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
1.3 What Kind of Driver Do I Need? -7-
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.
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
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
1.3 What Kind of Driver Do I Need? -9-
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 10 - Beginning a Driver Project | Chapter 1
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.
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:\>
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.1 How Drivers Work - 13 -
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.
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.
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.
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.
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 18 - Basic Structure of a WDM Driver | Chapter 2
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.
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.
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
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.
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 22 - Basic Structure of a WDM Driver | Chapter 2
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
2.3 The Two Basic Data Structures - 23 -
} 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
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 26 - Basic Structure of a WDM Driver | Chapter 2
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.
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:
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.
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.
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;
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:
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.
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
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.
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:
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;
I’ll explain the NTSTATUS status codes and the NT_SUCCESS macro in the next chapter.
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:
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):
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
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.
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 -
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.
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
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.
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(...)
{
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.
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:
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()
{
for (devindex = 0;
SetupDiEnumDeviceInterfaces(info,NULL, &m_guid, devindex, &ifdata);
++devindex)
{
DWORD needed;
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;
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.
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.
NTSTATUS AddDevice(...)
{
IoCreateDevice(...);
IoInitializeDpcRequest(fdo, DpcForIsr)
}
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:
You round an arbitrary address up to the next alignment boundary like this:
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:
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.
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.
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:
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.
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);
KeInitializeSpinLock(&pdx->SomeSpinLock);
KeInitializeEvent(&pdx->SomeEvent, NotificationEvent, FALSE);
InitializeListHead(&pdx->SomeListAnchor);
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.
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.
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.
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:
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.2 Error Handling - 47 -
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:
if (!NT_SUCCESS(status))
{
IoInitializeRemoveLock(&pdx->RemoveLock, 0, 0, 0);
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.
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.
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 -
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
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:
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:
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.
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:
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.
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.
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:
__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.
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:
(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:
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
NTSTATUS status = GetExceptionCode();
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 -
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 56 - Basic Programming Techniques | Chapter 3
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.
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:
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.
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
#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.
#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:
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.3 Memory Management - 59 -
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:
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.
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:
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).
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.3 Memory Management - 61 -
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:
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.
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.
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
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.
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.)
The arguments are the same as we’ve been studying except that you also supply an additional priority indicator. See Table 3-5.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 64 - Basic Programming Techniques | Chapter 3
LIST_ENTRY linkfield;
} TWOWAY, *PTWOWAY;
LIST_ENTRY DoubleHead;
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.)
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.
LIST_ENTRY linkfield;
} TWOWAY, *PTWOWAY;
LIST_ENTRY DoubleHead;
InitializeListHead(&DoubleHead);
ASSERT(IsListEmpty(&DoubleHead));
InsertTailList(&DoubleHead, &pdElement->linkfield);
if (!IsListEmpty(&DoubleHead))
{
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.3 Memory Management - 67 -
SINGLE_LIST_ENTRY linkfield;
} ONEWAY, *PONEWAY;
SINGLE_LIST_ENTRY SingleHead;
SingleHead.Next = NULL;
PushEntryList(&SingleHead, &psElement->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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 68 - Basic Programming Techniques | Chapter 3
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;
(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);
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.
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:
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.
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.
UNICODE_STRING foo;
if (bArriving)
RtlInitUnicodeString(&foo, L"Hello, world!");
else
{
ANSI_STRING bar;
RtlInitAnsiString(&bar, "Goodbye, cruel world!");
RtlAnsiStringToUnicodeString(&foo, &bar, TRUE);
}
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
- 72 - Basic Programming Techniques | Chapter 3
OBJECT_ATTRIBUTES oa;
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.
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.
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.
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:
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.
RtlInitUnicodeString(&valname, L"TheAnswer");
ULONG value = 42;
ZwSetValueKey(hkey, &valname, 0, REG_DWORD, &value, sizeof(value));
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.5 Miscellaneous Programming Techniques - 75 -
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.)
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:
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:
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:
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:
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.
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 -
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);
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;
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.
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 -
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.
#if DBG
<extra debugging code>
#endif
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:
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.
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney
3.5 Miscellaneous Programming Techniques - 81 -
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.
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.
Programming The Microsoft Windows Driver Model 2nd Edition Copyright © 2003 by Walter Oney