Windows Driver Development Environment
Even for experienced developers, getting started with Microsoft® Windows® device drivers can be difficult. You have a new programming model to learn and a new set of tools for building and debugging. Opening the documentation, you find thousands of pages of information.
This paper presents an overview of the tools used in creating a Windows device driver. Additionally, the paper discusses debugging and testing tools that can help you with your driver. In particular, the paper examines ways to find and fix bugs early in development, to help you produce a high-quality device driver.
Do You Need a Driver?
Before you start development of your new device and its driver, consider whether you need a driver at all: Kernel-mode drivers are significant software projects that require careful development to ensure the stability of the system. Developing kernel-mode drivers requires a different set of expertise than developing user-mode applications. In addition, the cost of supporting a kernel-mode driver can be many times the cost of its development. Given the complexities of kernel development, it can be desirable to avoid developing a driver.
Here are some questions to ask before assuming you need a kernel-mode driver:
Can you use a Microsoft-supplied driver? With modern peripheral buses such as USB and IEEE 1394, devices in many classes must comply with industry standard interfaces. The host controllers for these buses also must comply with industry standard interfaces for compatibility with Windows. If your device complies with these standards, it can use the drivers Microsoft provides.
Is the driver for a standard device class with a unique hardware design? If your device is in a standard device class such as a storage device, but it has a unique hardware design, you will need a driver. Similarly, if your device is accessed through Win32 API, a driver is required.
Can you move all or part of the driver into user mode? If your driver does not fit the above description, examine moving all or part of the driver into user mode. Not only is the user-mode environment easier to program, but a driver failure in user mode will affect only your device and not crash the system.
Is your device accessed through a user-mode API by only a few applications? A driver might not be needed if your device connects to the system through a bus driver with a user mode API and is to be used only by a limited number of applications. A device connected to the system by a serial link or an Ethernet protocol is not likely to need a driver. Additionally, devices connected to a storage bus such as SCSI or ATAPI can use the pass-through capabilities in the bus controller device driver to support sending and receiving requests from devices connected to the bus.
Is your driver a software-only driver? A software-only kernel-mode driver is another type of driver that sometimes is not needed. A user-mode service running under the system account can replace a kernel-mode driver in many cases.
Can you move some development to a user-mode DLL? Finally, if you conclude that your device does need a driver, consider using a user mode DLL as part of the effort to reduce the amount of kernel development needed for your project.
The Many Types of Drivers
When you say you want a device driver, you should think about the type of driver you need. Windows supports a wide variety of devices, and as this support has grown, the environment for these drivers has diverged. Understanding the different driver types is important for a number of reasons:
· The kernel programming interfaces can vary from type to type.
· The rules for what the driver can do varies by type.
· If you ask someone for help, they will need to know the type of driver you are working on.
Windows device drivers can be classified in many ways. For this paper, we will look at some of the major types. The following list is only one approach to classifying the many types of Windows drivers:
· Legacy drivers. These drivers were the generic model for Windows NT. With the advent of Plug and Play on Windows 2000, these drivers became relegated to some limited categories. Their primary use these days is for kernel services, that is, pure software drivers that provide interfaces and services that need to execute in the kernel. This type of driver should not be used for a hardware device.
· Windows (Plug and Play) drivers. When referring to a Windows driver, most people think of the current model of a driver with Plug and Play and power management. Windows Driver Model (WDM) drivers are a subset of these drivers. WDM was introduced to provide a common model for Plug and Play capabilities between Windows 98 (and later Windows ME) and Windows 2000 and later versions of Windows.
· File system drivers. File systems are in a special category of legacy drivers. These drivers keep track of files on a device or provide access to files across the network. File system drivers require additional headers and libraries beyond the standard tools. To create a file system driver, you need the Windows Installable File System Kit (IFS kit). See Resources at the end of this paper for availability of this kit.
· Storage miniport drivers. Drivers for storage controllers such as SCSI and IDE have their own programming interface. This interface limits what a driver of this type can do. In addition, to provide backward compatibility with Windows 98, the kernel calls are unique. For many of these functions you will find an exact equivalent in WDM.
· Network drivers. Like storage drivers, network drivers have their own programming interfaces. Unlike storage drivers, network drivers use standard Windows interfaces in many cases.
· Printer drivers. This is a special category of drivers. Printer drivers run in user mode and do not conform to the programming model for other drivers.
· Graphics drivers. Another special category of drivers, graphics drivers have their own rules and programming interface. Graphics drivers are some of the most complex projects in Windows.
· Kernel streaming drivers. This is another unique category of driver. Like a number of the other categories, kernel streaming drivers have unique interfaces for programming. In addition, this is the only class of drivers typically programmed in C++.
· Bus drivers. A bus driver enumerates devices on a bus and arbitrates access to the bus. Bus drivers are Plug and Play drivers and must support special requests beyond what other Windows drivers do.
· Filter drivers. A filter driver layers above or below an existing driver to provide special services or change the behavior of a standard driver. A common example of a filter driver is a virus scanner that sits above the file system and checks files for viruses. Filter drivers typically follow the rules of the driver they are filtering.
What You Need for Driver Development
For driver development, you need a system for development and a system on which to debug your driver. Any reasonably sized desktop system can provide the hardware for development. The only special thing it must have is a free serial or IEEE 1394 port for the debugger. We will look at what you need for the test machine later in this paper. For now we will concentrate on the development system software.
The Windows Device Driver Kit (DDK) provides the build environment for developing a device driver for Windows. The DDK includes the approved build tools, compiler, and linker for creating a driver. The DDK also has thousands of pages of documentation about developing drivers and numerous sample drivers. In fact, the only software required to build a driver besides the DDK is a good editing tool.
A common misconception among driver developers is that you need the DDK named for the particular version of Windows you are targeting with your driver. In fact, when developing a driver, you need the latest DDK. This DDK has the latest tools, samples and documentation for developing drivers. At present, the kits are named for the latest operating system at the time of their release. This means that the Windows 2003 DDK is not just for creating drivers for that version of Windows, but is the best choice for creating drivers for all versions of Windows back to Windows 2000. For a guide to choosing the proper DDK, see Resources at the end of this paper.
In addition to the DDK, you need the WinDbg debugger. This debugger is part of the Debugging Tools for Windows and can be downloaded from Microsoft. Along with the debugger, you need the debugging symbols for the version of Windows you are testing. The symbols can be accessed either through the Microsoft symbol server or by downloading the symbol files.. For availability of the debugger and symbols, see Resources at the end of this paper.
The final software purchase to consider is a code coverage tool. This tool can help in debugging and validate that you are testing your driver well. A full discussion of code coverage is provided later in this paper. See Resources at the end of this paper for companies that offer code coverage tools for Windows drivers.
Your Test Machine
Many developers and their managers are surprised that a separate test machine is required, but there are many good reasons for this. Debugging an operating system is a challenge because the debugger affects its normal operation. To limit this impact, Windows requires a separate machine for use with the WinDbg debugger. A separate machine is needed for a number of other reasons:
· Although your development machine typically will run a desktop version of Windows, for testing you need a variety of Windows versions, from server to desktop.
· Crashes are likely in developing a driver. At best, a crash requires rebooting the system to fix the bug, and then rebooting the system again to install and run the driver, a slow process. At worst, you can corrupt the disk and have to rebuild your development system and recover your work.
On the test system, you should install multiple versions of Windows. Even if you are targeting a particular version of Windows, having the latest version will provide you with the best validation tools. One of the versions running on the test system should be the checked build of Windows. The checked build is one of the most powerful tools for debugging and testing your driver. Using the checked build to improve your drivers will be discussed later in this paper. The checked build is only available as part of Microsoft Developer Network (MSDN). See Resources at the end of this paper for availability of the checked build and other developer resources on MSDN.
Now that you know the reasons for having a separate test system, be sure to get a good one:
· At a minimum, it needs a serial or IEEE 1394 port for the debugger.
· The most critical need is for a multiprocessor or at least a hyper-threaded system. You cannot debug multiprocessor issues without a multiprocessor machine. Now that even low-end systems support hyper-threading, this is a requirement.
· Nowadays you should consider x86-64 support in your driver. A multiprocessor x86-64 system makes an excellent test machine because it can support both 32-bit and 64-bit versions of Windows.
· Having pluggable disks on your test system makes it easy to test against multiple versions of Windows.
· If your device supports DMA, consider getting a test machine with more than 4GB of main memory. This amount might seem excessive, but it can be useful in tracking down problems in addressing extended memory. Finding such problems is difficult when your system does not have enough memory to test for them.
Why Find Bugs Early?
Driver debugging is difficult, so if you find and fix bugs early you can reduce development time. For many developers this is foreign to their current practice. The rest of this paper looks at the environment and tools for building Windows device drivers. Much of the discussion involves enabling as many checks as possible.
If you find a bug at compilation time, it takes seconds to fix the code and resubmit the module for compilation. If the bug goes undetected until found by the runtime verification tools, it can take at least five minutes just to rebuild and install the driver to fix the bug. If the bug is not found by the runtime checks, the five minutes stretches to hours or days to find the problem.
Once quality assurance (QA) gets involved, the costs escalate. QA has to find the bug, try to reproduce it, and put it in a bug report. Then management looks at the bug report and discusses it with the developer. Finally, the developer tries to reproduce the problem, and then debug it. Overall, this process can take person-days. The process takes even longer if a customer reports the bug, and although the effort may not increase as rapidly, the time until the customer has the bug fix still grows enormously.
Therefore, the little extra time it takes to enable the warning messages and fix the problems they report is inconsequential compared to the time it can save by finding even a single bug up front.
Installing the Software
Before you install the DDK and the debugger, you need to know several things:
· Not all of the tools in the DDK work if the directory path for the DDK contains a space. So do not install the DDK at some location like “C:\Program Files\DDK”. If the path contains a space, you will not get a clean error message. Instead, the build of your driver may fail in mysterious ways.
· The DDK has tools for building drivers for a number of versions of Windows. Before you think you need only the tools for a particular version, consider the following guideline:
Build your driver for the earliest version of Windows you wish to support, but be sure to have the tools to build for the latest version for testing.
You should load the support files for the earliest version of Windows you support and for the latest version of Windows you plan to test. In many cases, the simplest approach is to load the support files for all versions of Windows.
Additionally, you should load the compiler and build tools for a 64-bit target as well as for the 32-bit systems most people need. Even if you do not support 64-bit systems now, using the 64-bit build tools can find bugs in your code.
Finally, you should load most of the samples. Even if you are specializing in a particular type of driver, the rest of the samples can be valuable in showing approaches to coding in the Windows kernel.
· For the WinDbg debugger, you need either the 32-bit or the 64-bit version of the package, depending on your development system. This confuses some people. It is not the system you are testing but the system the debugger will be running on that determines which version you need.
· Another choice to make is whether to use the Beta version or the released version of the debugger. The Beta versions have been so stable that most developers use the Beta to get the latest commands.
· Finally, when you are actually installing the debugger, choose to install the debugger SDK. The SDK allows you to add your own useful tools to WinDbg.
This section explores the development environment needed to compile and link Windows device drivers. The actual process of compiling and linking the driver is controlled by the Build utility. The rest of this section provides an overview of the Build utility and the input file for the tool. At the end of the section we will look at an approach to running Build from the Visual Studio Integrated Development Environment.
Build Utility
To create a driver in the Windows DDK you use the Build utility. This utility invokes the correct tools with the appropriate options to build your driver. The Build utility is a wrapper around the Microsoft NMAKE utility, so it handles issues like checking dependencies to ensure that the correct files are rebuilt after a change.
If you have not worked with Build before, you might want to try building one of the samples from the DDK:
1. Copy the files under src\general\<sample>\sys (where <sample> is the particular sample’s name) to a test directory.
2. From the Start menu of your computer, go to Programs | Development Kits | Windows DDK version | Build Environments.
3. Choose one of the build environments. The build environment will open a command window.
4. Type build to create the driver.
For most drivers setting up the Build tool environment is simple:
1. Copy the standard makefile from a DDK sample project.
2. Create a sources file in the directory where your code resides.
Note that the makefile should not be changed. All commands are set up by the sources file or makefile.inc, discussed later in this section. An extremely simple sources file is shown here:
TARGETNAME=test
TARGETPATH=obj
TARGETTYPE=DRIVER
SOURCES=test.c
This file sets four macros that instruct the Build utility to create a driver named test.sys from the source file test.c.
Build Environments
Earlier in this paper we told you to choose one of the build environments, but what is a build environment? The current DDK has build environments for the three targeted versions of Windows (2000, XP, and Server 2003). Under each version are environments for checked and free builds and for each CPU target supported for that version:
· The checked build creates a driver with debugging enabled. Many of the optimizations in the compiler are disabled and conditional code for debugging is included. While working to get your driver functional, you build your driver with the checked build.
· The free build creates a production driver in which the code is optimized and debug code is disabled. This is the driver you use for performance testing and to ship to customers. Do not make the mistake that some developers have done of shipping the checked build driver. Your customers deserve the best performing driver.
The Build utility names several files and directories based on the environment chosen. These names include the construct Type_Version_Cpu:
Type |
Fre for the free build and Chk for the checked build. |
Version |
w2k for Windows 2000, wxp for Windows XP, or wnet for Windows Server 2003. |
Cpu |
x86, amd64, or ia64 depending on the processor. |
As stated earlier, building and testing for the latest version of Windows provides additional checks for your driver. Building for the earliest version of Windows you plan to support creates a binary of your driver compatible with all versions of Windows from that version forward.
Sources File
The example at the beginning of this section had a very simple sources file, but the recommended file is more complex. The sources file has a large number of macros and environment variables, all of which are documented in the DDK. The following table lists items for the sources file used in this paper.
C_DEFINES |
Specifies switches to pass to the C compiler and the resource compiler |
INCLUDES |
Specifies directories in addition to the source directory to search for include files. |
MSC_WARNING_LEVEL |
Sets the warning level for the C compiler. The default is /W3. Recommended is /Wall /WX. See "Compile Time Checking" later in this paper for details. |
NTTARGETFILE0 |
Causes the inclusion in the build process of makefile.inc. Typically used to support Windows Management Instrumentation (WMI). |
SOURCES |
Specifies the files to be compiled. |
TARGETNAME |
Specifies the name of the binary to be built. This name does not include the file name extension. |
TARGETLIBS |
Specifies additional libraries and object files to link to the product. |
TARGETPATH |
Specifies the destination directory for all build products. In almost every case you should set TARGETPATH=obj. |
TARGETTYPE |
Specifies the type of product being built. Common values are DRIVER (kernel-mode driver), PROGRAM (user-mode program) and DRIVER_LIBRARY (kernel-mode library) |
USER_C_FLAGS |
Specifies switches to pass to the C compiler only. |
VERIFIER_DDK_EXTENSIONS |
Activates the Call Usage Verifier (CUV) tool when set to non-zero. |
For a number of the capabilities specified later in this paper it is valuable to know about the following construct in the sources file. Sources has conditional tests, in particular:
!if !$(FREEBUILD)
...
!endif
This specifies that the actions between !if and !endif occur only in the checked build of the driver.
Makefile.inc File
Makefile.inc provides additional information to the Build utility. This file allows you to add commands and dependencies to the makefile, typically for pre- and post-processing of files related to the project. A common use of this in driver development is to compile a Managed Object Format (MOF) file needed for Windows Management Instrumentation (WMI) and produce an include file containing data structures for the driver.
Build Command Line
As part of finding bugs early, consider some options for the Build command line. At a minimum consider adding –bew to the build command:
b |
Enables full error messages. |
e |
Generates the log, error, and warning files that can help when something goes wrong with Build. |
w |
Displays the warning messages to the command window, so you do not miss them. |
Every so often and before you ship your driver you should use the –c option to rebuild all the files. The –c option deletes all object files.
The Build utility has a large number of options, which are all documented in the DDK. As a minimum, consider using the ones mentioned here to validate your work.
Multi-Target Build Projects
Building most driver projects is relatively simple: You put the files you need in a single directory and build the driver. Many developers encounter difficulties when the project becomes more complex.
If your project has multiple components, each should be in its own directory with a dirs file to identify the individual directories. The dirs file typically contains a single definition of the form:
DIRS=dir1 dir2 dir3
Dir1, dir2 and dir3 are subdirectories for components of the directory that contains the dirs file. These components can contain subcomponents; in fact, all of the examples in the DDK can be built as a single project. The simple model of a driver and perhaps a couple of support components such as an application and a DLL represent most projects.
If you have a multi-component project, sharing files is the most likely problem. The Build utility allows you to use source files from the current directory, its parent directory, and the platform-specific subdirectory. The common approach places the shared files in the parent directory and the unique files in the current directory as described below:
Current directory |
This directory contains all the files for the project except those source files that need to be shared with other components. |
Parent directory |
This directory contains source files shared between two or more of the subdirectories of this directory. |
Platform-specific directory |
This directory contains any files specific to this particular platform. |
Why Use Build?
The Build utility is the only utility you should use to compile and link a device driver. The Build utility is a wrapper program for NMAKE (the Windows make program) and is provided with the DDK to control compilation and linking of device-driver and related code. By using Build, you ensure that the driver follows the conventions and settings that Windows requires. Build handles all of the target-dependent problems, including different paths to compilers and include files for specific platforms. Additionally, Build supports different target directories based on both the target platform and whether debugging is turned on.
Using tools other than the Build utility creates three types of problems:
· First, many DDK tools utilize the build environment. If you do not use Build, you cannot use these tools.
· Second, if you believe you have found a bug in the DDK, Microsoft support will require code built with the Build utility to give you customer support.
· Finally, if you do not use the Build utility, you can easily have the wrong settings for Windows drivers and create subtle bugs that can take weeks to find.
Using Build with Microsoft Visual Studio
One of the main reasons developers try to bypass Build is to use Microsoft Visual Studio. The answer here is to use Visual Studio as an editor and integrated development environment, but to have it invoke the Build utility using a batch command file.
Fortunately you do not have to worry about the details yourself, because two versions of a batch command called ddkbuild are free to download. See Resources at the end of this paper for sources of this batch command.
If you choose to use this approach, it is advisable to test that your project can still be built in the DDK build environments. This testing guarantees that nothing unusual is happening because of the batch command.
A principle of good software engineering is to find bugs early. Studies have indicated that the cost of finding and fixing a defect increases ten times for every step of the development process. So a bug found at compile time can be fixed at 1% of the cost of a bug that makes it to testing.
Developers should follow careful software engineering principles, but in the pressure of product development schedules, this can be hard to do. Additionally, many developers do not have formal training in these best-practice methodologies. Fortunately, the Windows DDK provides a number of capabilities to find bugs early. Utilizing these tools does not require the developer to have formal training to find bugs early.
One thing to note here is that the same problem might be found by more than one of the techniques described in this section. Cleaning up all the reported problems from one tool makes it easier to run the next tool. It is still important to use all of the techniques because each can find unique problems.
Finally, a number of the techniques can create false positives, where a warning or error reported can never happen. Tools that try to find errors need to err on the side of caution. It is still important for the developer to clean up all of these warnings. Fortunately, eliminating most of the warnings is easy. View the effort to suppress the warnings as a "mini code review." You need to check whether the warning is valid anyway, so look at the code around the warning and validate that no other errors are present. These checks will take you through your code in a different manner than you usually would look at it, so use this fresh perspective to look for problems.
Compile with /Wall /WX
A simple way to improve your code is to compile with all warnings enabled and treated as errors. The compiler option /Wall enables all warnings. The /WX option treats warnings as errors.
Unfortunately, at present, the include files from the DDK do not compile cleanly with all warnings enabled. (Microsoft is working to fix this.) To overcome this problem, at present a number of warnings have to be disabled. Most of these warnings are related to Microsoft extensions to the standard language and have no impact on reliability.
For example, following line in a sources file enables warnings without requiring changes:
MSC_WARNING_LEVEL=/Wall /W4 /WX /wd4115 /wd4127
/wd4200 /wd4201 /wd4214 /wd4255 /wd4514 /wd4619 /wd4668 /wd4820
This line instructs the compiler to enable all warnings but exclude those warnings that the DDK include files generate. A better approach is to disable only the warnings for the Microsoft include files and then enable them for the driver code.
MSC_WARNING_LEVEL=/Wall /WX
Then wrap the Microsoft-supplied include files with:
#pragma warning (disable: 4115 4127 4200 4201 4214 4255 4619 4668 4820)
...
#pragma warning (default: 4115 4200 4214 4255 4619 4668 4820)
In this example, two of the warnings (4127 and 4201) are not enabled for the driver. These remain disabled because they flag constructs that occur in macros defined in the Microsoft include files. Note that the specific warnings can vary with the particular version of the DDK. These examples should be considered only as guidance.
These warnings add simple checks that can point out potential problems. The following are some of the common warnings that are likely to appear for your driver.
C4057 'operator' : 'identifier1' indirection to slightly different base types from 'identifier2'
Typically, this warning appears because of two similar but different types, such as a signed versus unsigned value or a short versus long value. To fix this, change the base type where possible to the correct form, and otherwise use a C cast to convert the item.
C4100 'identifier' : unreferenced formal parameter
If this warning occurs in a function you designed, ask yourself the reason for passing this parameter. This warning mainly occurs where the DDK dictates the function prototype. In this case, use the UNREFERENCED_PARAMETER macro to eliminate the error. Careful! If your driver later starts using the parameter, be sure to delete the UNREFERENCED_PARAMETER macro for that parameter.
C4101 'identifier' : unreferenced local variable
The simple solution here is to delete the unused variable. Before you do so, ask yourself why it was there in the first place. Many times this indicates that something was forgotten in the function, so check your code and design before deleting the variable.
C4244 'variable' : conversion from 'type' to 'type', possible loss of data
This warning occurs when you convert an integer value to a smaller type. This can reflect a real error such as forcing a 64-bit value to 32 bits. Runtime checks can be enabled for this problem, so when fixing this error, consider ANDing the value with a mask to a legal size and then casting the value to the correct type.
Use the C++ compiler
C++ has been described as “a better C.” This is not a recommendation to use C++ language features in the kernel. Using C++ in the kernel presents a number of challenges, so unless you are comfortable with Windows driver development you should avoid using C++. The paper "C++ for Kernel Mode Drivers: Pros and Cons" provides information for you to make your own decision. See Resources at the end of this paper for a link.
Although using the full C++ language in the kernel can cause a number of problems, using the C++ compiler can help you to identify potential improvements in your driver, because it does more validation and checking. .The challenge here is to be sure that you are still programming in C while compiling with C++.
One approach is to use the compiler's /TP switch to treat files with a .c extension as C++ code. Using this option in the sources file only for the checked build allows you to catch any C++ constructs in your code. For example:
!if !$(FREEBUILD)
USER_C_FLAGS= /TP
!endif
This example builds a driver using C++ for the checked build. If you do this, be sure to change your DriverEntry routine also, as follows:
#ifdef __cplusplus
extern "C"
#endif
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath);
You might also need to use extern "C" to wrap the include file for WPP software tracing (described later in this paper). This technique requires building both the checked and the free build of the driver. If you use Visual Studio with ddkbuild, use the batch build command to build both versions at the same time.
Most of the errors found using the C++ compiler involve type checks and certain constructs not allowed in C++. As with /Wall, cleaning these up these warnings can reduce the number of actual errors in your driver.
PREfast
PREfast is a tool that Microsoft developed to find a number of problems that are hard for a compiler to locate. PREfast performs a static analysis of each function in your code to locate problems. The Windows Server 2003 DDK provides a version of PREfast with driver-specific rules that every driver writer should use. This version of PREfast can detect a number of common problems for drivers through explicit knowledge of kernel-mode functions and structures.
Running PREfast
PREfast is not as easy to find as other tools in the DDK because it is not listed on the Tools menu. Instead, go to Programs | Development Kits | Windows DDK version | Help | PREfast to find out about PREfast.
You invoke PREfast as part of your Build utility command line, for example:
prefast /list build –cefbw
This line builds the driver, runs PREfast on the sources, and displays the defect log on the console. See the PREfast help file for a description of all of the command options.
Common PREfast Reports
PREfast has too many checks to describe in this document; the PREfast documentation in the DDK describes all of the warnings. Some of the common warnings for driver writers are listed below along with notes about how to eliminate them.
1 - Using uninitialized memory <variable>.
This warning could be noise or it could indicate a real problem. In many cases, initializing the variable to a safe value at the beginning of the routine is the best solution. Just be sure not to mask a real problem with the initialization.
11 - Dereferencing NULL pointer <pointer>.
A common reason for this warning is that your driver has not tested to see whether a memory allocation returned NULL.
14 - Leaking memory <pointer>.
Typically, this warning is caused by not freeing memory allocated in a function. Note that if you have a function that allocates memory for your driver, PREfast may generate this error as noise.
8120–8124: Various reports about IRQL
PREfast can catch many cases where the Interrupt Request Level (IRQL) is incorrect for a function that is being called.
8128: An access to <field name> has been made directly; it should be made by <API name>
The DDK contains a number of structures with fields that should not be accessed directly. This is a common bug for developers, and PREfast will catch many of these.
8134: The type of a pool tag should be integral, not a string or string pointer
The DDK allows you to tag memory, typically with a 4-character tag. A common mistake is to use a string for the tag.
8143: A dispatch routine that calls IoMarkIrpPending must also return STATUS_PENDING
This is a common mistake in drivers, which PREfast can catch for you.
Define DEPRECATE_DDK_FUNCTIONS
The DDK has evolved over the years. With the changes, a number of functions are no longer recommended and supported. The WHQL Test Specifications define a set of functions that should no longer be used. The DDK provides a way to check for functions that should not be used by defining DEPRECATE_DDK_FUNCTIONS in sources as part of a C_DEFINES line. When DEPRECATE_DDK_FUNCTIONS is defined, using these functions generates errors at compile time.
However, DEPRECATE_DDK_FUNCTIONS does not flag every function in the DDK that should not be used. In some cases, these functions are correct for an older version of Windows but have been supplemented by a new routine. For this reason, you should check the DDK documentation when using a function for the first time or when a newer DDK becomes available, to see if you are using the best routine.
If you encounter a situation where a function is obsolete in later versions, you have two choices:
· Typically, the function will be supported even after it is obsolete. One approach is to use the function for all versions of Windows, even though it is obsolete in some of them. The problem is that at some point the function might be removed and your driver would stop working.
· A better approach is to use MmGetSystemRoutineAddress to check for the presence of the functions. Be sure to check for both the old and the new function, and fail with an error if neither is present. Your driver might last a long time, and both functions could be obsolete someday.
Using Preprocessor Tests and C_ASSERT
Many developers know about runtime assertions, but how many think about assertions at compile time? Many conditions are testable at compile time. As pointed out earlier in this paper, finding bugs early saves time.
You can approach compile time assertions in two ways. Either approach works; the important thing is to test at compile time.
C_ASSERT. One of the most overlooked capabilities in the DDK is the C_ASSERT macro. C_ASSERT checks assertions at compile-time. The expression in C_ASSERT must be determinable at compile time. The checks can include testing use of the sizeof operator on type declarations and use of compile-time constants. An assertion failure results in error C2118: negative subscript. A couple of common examples of C_ASSERT follow:
typedef union {
// Warning: Size of elements must be the same
TYPE_1 element1;
TYPE_2 element2;
} UNION_1;
C_ASSERT ( sizeof(TYPE_1) == sizeof(TYPE_2) );
// Warning: Tables need the same number of entries
PCHAR table1[] = {“a”, ... };
PCHAR table2[] = {“AA”, ... };
C_ASSERT ( RTL_NUMBER_OF(table1) == RTL_NUMBER_OF(table2) );
Typically, drivers will have a comment noting that TYPE_1 and TYPE_2 must be the same size or that two arrays must have the same number of elements. These comments should not be easy for a developer to miss. Fail the compilation if the requirements are not met!
Conditional tests. One problem with compile-time asserts is that you get the negative subscript error but you get no useful data until you look at the code. So you might want to consider changing your C_ASSERTs that use only preprocessor data to do conditional tests. The compiler provides a #error statement that will generate an error. The error is reported as a message on the line following the #error. A couple of common examples are:
#define BLK_SHIFT 9
#define BLK_SIZE 512 // Must be 1 << BLK_SHIFT
#if !(BLK_SIZE = (1 << BLK_SHIFT))
#error !(BLK_SIZE == (1 << BLK_SHIFT))
#endif
// Need version 5 or module should be changed
#include “driver_include.h”
#if !(DRIVER_INCLUDE_VERSION == DRIVER_INCLUDE_V5)
#error !(DDRIVER_INCLUDE_VERSION == DRIVER_INCLUDE_V5)
#endif
The advantage to this approach is that the error message you see matches the assertion so that you can avoid checking the code.
Build for 32-bit and 64-bit
All of the above approaches help you find bugs at compile time, but only for the targets your driver is compiled for. Compiling for both x86 and x64 can find problems in your code. In particular, compiling for 64-bit can find problems in three major areas:
· Pointer issues. The most common problems migrating to 64-bit are pointer problems. These include everything from errors in pointer arithmetic to trying to store a pointer in a 32-bit value, or having a structure change in size because of the pointers it contains.
· Inline assembly language. You should not use inline assembly language (or assembly language at all) in your driver, but sometimes it happens. You might not even realize it; you might be using an include file or a piece of code from another developer who used assembly language. This is the time to catch and fix this code.
· Compiler differences. A number of subtle differences in the compiler are described in the DDK under “Code Compatibility for x64”. Although it is not likely you will encounter these differences, it is possible.
Be aware that you cannot find all the compatibility problems for the driver without testing on both platforms. Compiling cleanly for both just means you have taken a good first step.
Run ChkINF Before You Install
ChkINF is a commonly overlooked tool. Some developers view it as a WHQL test rather than a development tool. ChkINF is one of the best ways to get your driver installation to work the first time. No INF file is ready to use until ChkINF has validated it and all of the possible errors and warnings have been eliminated.
ChkINF resides in the \tools directory of the DDK. ChkINF is a PERL script, so you will need one of the Perl interpreters identified in the DDK documentation for the tool. The DDK also documents all of the command line arguments for the tool. The following is a common approach to running ChkINF.
ChkINF test.inf /B
The above validates test.inf and invokes the browser to display an HTML file with the results. The report consists of a section with errors and warnings, and then an annotated copy of the INF file showing the problems. Fix all of the problems that the tool reports. Many of the warning messages point to problems that in some cases stop the driver from loading properly.
As with compile-time checking, taking advantage of the multitude of runtime checks can speed your driver development and make your driver more reliable. Runtime checking catches many problems that appear in drivers. Conversely, because runtime checking affects the performance of the system, these checks can hide some problems related to timing in drivers. So be sure to test your driver with runtime checks both on and off.
The tools listed here all should be used. The errors detected by the following techniques have much less overlap, unlike the some of the compile-time checking techniques discussed earlier. Additionally, very few of these tools show false positives. If the problem is reported, it is a real bug.
Some of these techniques cause the code of the driver to change. It is recommended that you follow these steps in debugging and checking your driver:
1. Activate all of the checks described below for your driver, and work on your driver until it runs cleanly in this environment.
2. Disable the various compile-time modifications and run the driver in your debugging environment again.
3. Run your driver on the target system (that is, with no debugging enabled) to verify that it works and to use performance profiling against your driver.
Kernel Debuggers
Microsoft provides two debuggers for working with the Windows kernel:
· KD is a command-line only debugger.
· WinDbg provides the same command line as KD, but includes a graphical user interface.
The rest of this section discusses WinDbg. Most of the techniques are also applicable to KD.
Setting up the debugger
The debuggers can use two possible interconnects: serial or IEEE 1394. Both interconnects have special considerations.
Serial. Debugging over a serial link requires a null-modem cable to connect your test machine to your development system. On the test system, the serial port must be a legacy port, since the debugger directly manages the port. A non-legacy serial port that requires a device driver does not work for the system under test. These restrictions do not apply for the development system. To set up the link:
1. Connect the null-modem cable to the desired serial ports on the two systems.
2. Test the link with Hyperterminal:
Open a Hyperterminal session to the port on each system with the same baud rate.
Use a high baud rate to improve response time for the debugger.
Type in each Hyperterminal session and verify that coherent output appears on the other machine.
3. Edit the boot.ini file on the test machine (described later) to add /debugport=comX /baudrate=y, where comX is the port you tested on the system, and y is the baud rate of your Hyperterminal test.
4. Start WinDbg and choose Kernel Debug from the File menu.
5. Choose the Serial tab and set the baud rate and the port to the values used in the Hyperterminal test.
IEEE 1394. For debugging over IEEE 1394, you need a standard IEEE 1394 interface on each system and a cable to connect the ports. You cannot use the IEEE 1394 controller for anything but debugging on the test machine. Your systems must be running Windows XP or later to use IEEE 1394. To set up the link:
1. Connect the IEEE 1394 cable to the ports on the two systems.
2. Right click My Computer, choose Manage, and open the Device Manager on the test system.
3. In the Device Manager, disable the IEEE 1394 controller completely by right-clicking the controller and choosing Properties. Set the controller to disabled.
4. Edit the boot.ini file on the test machine (described later) to add /debugport=1394 /channel=y, where y is the channel you have chosen for the link.
5. On the development system, repeat steps 2 and 3, but instead of disabling the IEEE 1394 controller, disable the IEEE 1394 network adapter.
6. Start WinDbg and choose Kernel Debug from the File menu.
7. Choose the IEEE 1394 tab and set the channel to the same value you used on the test system.
Editing boot.ini. To edit boot.ini, use the bootcfg command on XP and later systems. On Windows 2000, you will need to find boot.ini in the root of the boot drive, change its attributes from read-only to read-write, and then edit it with a plain text editor like Notepad. In either case, you need to duplicate the operating system line to create a separate entry with additional switches for debugging. Typically, the debugger line should be first so that it becomes the default boot for your test system. The Windows DDK describes how to edit the boot.ini file.
Finding symbols. After the debugger is connected, you also must specify where to find the symbols for the test system and the source code for your driver. As mentioned earlier in “What You Need for Driver Development,” you can get the symbols for the system in multiple ways.
If possible, use the Microsoft symbol server. This requires a link to the Internet, but it eliminates most of the problems in finding the right symbols. Using the Microsoft symbol server is the only way to get the correct symbols for some hot-fixes and Beta versions of Windows. To set up the symbols, choose Symbol File Path from the File menu of WinDbg, and then enter one or more directory paths separated by semicolons. For example:
SRV*e:\SymStore*http://msdl.microsoft.com/download/symbols;e:\development\mydriver\objchk_wnet_x86\i386
This path instructs WinDbg to get symbols from the symbol server and cache them in e:\SymStore and, if the symbols are not found, to use the checked build directory for my driver.
If you have problems getting the symbols, use the command !sym noisy to see the what is happening. Set up the source directories in a similar manner; choose Source File Path from the File menu of WinDbg and then enter one or more directory paths separated by semicolons.
Using Driver Mapping
One very powerful capability of the debugger is mapping driver files. The debugger can intercept the load of your driver on the test system and load the driver from the development system to the test machine. This eliminates the all-too-common mistake of forgetting to put the new driver onto the test system.
The simplest way to set this up is to open the System Control Panel, choose the Advanced tab and click the Environment Variables button. Under User Variables, select New, then enter _NT_KD_FILES for the name of the variable name and a full path to a map file you will create. Next, create the map file you specified in the environment variable with contents in the format:
map
\Systemroot\system32\drivers\mydriver.sys
e:\development\mydriver\objchk_wnet_x86\i386\mydriver.sys
The second line must match the path to the file in the test system's registry. The third line is a pointer to the driver on the development system. Note that using the path to the build directory guarantees that you have the latest driver. You can repeat these three lines for additional drivers. For more information about this technique, see “Mapping Driver Files” in the debugger documentation.
WinDBG Display Windows
The WinDbg user interface provides a number of display windows. The following lists the commonly used windows and tricks for using them. Note that windows that display data based on the current scope show valid information only when the test system has stopped:
· Command window. This window displays diagnostics and information from commands you enter. It is possible to search this window using the Find command from Edit menu, which can help when the window contains lots of data. In addition, you can log the data from the command window to a file by using the Open/Close Log File command from the Edit menu.
· Sources windows. These windows display the source code for the driver. First, be sure to set your tab stops to match your editor by using Options from the View menu. You can quickly set or clear a breakpoint by setting the cursor to the line you are interested in, then pressing F9 or the breakpoint toolbar button.
· Calls window. This window shows the call stack on the test machine. Double-clicking an entry in the call stack will cause the source window to display the location of the call and the locals windows to show the local variables for the call location.
· Locals window. This window shows local variables for the current scope. You can change the value of a local variable by editing the Value field. Variables are displayed based on their types. Setting the Typecast button shows the type of each variable. The type in the column can be edited to force the item to be displayed differently.
· Watch window. This window shows variables for the current scope including global variables. Note that, for the global variable to be displayed, it must be accessible from the current scope. The watch window behaves like the locals window.
Useful Commands
The debuggers have three general types of commands:
· Regular commands. These commands start with a letter and are the core functionality for debugging a driver.
· Dot commands. These commands start with a period and control the operation of the debugger.
· Debugger extensions. These are custom commands that have been created for the debugger. All extension commands start with an exclamation point. Microsoft provides many of these to help you debug drivers. In addition, you can create your own extensions specific for your driver debugging requirements.
The following list contains some useful debugger commands for debugging drivers. Note that this list does not contain the common commands such as setting a breakpoint or displaying data. Those commands are expected and easy to find. This list provides commands you might not find easily.
!error value
This command displays data about the error code specified by value.
!analyze –v
This command displays abundant information about a bug check. Whenever the system crashes, you need to issue this command to help make sense of the problem. Get to know this command!
.reboot
This command reboots your test system after a crash and the analysis.
lm
This command lists the loaded modules (drivers) in the system. This list can tell you if your driver was loaded on the test machine.
!devnode 0 1
This command displays all the device nodes. When you cannot figure out why your driver did not start, this command is for you.
!irp addr
This command displays the I/O request packet at the location specified by addr.
!poolfind tag
This command displays memory allocated with the given tag (see “Pool Tagging” later in this paper).
.reload [/u] [module]
This command reloads the symbols for the specified module or for all modules if none is specified by name. The /u option unloads the symbols, which is required before rebuilding the driver. If the debugger has the symbols open the linker cannot write a new file and the build fails.
This is only a small subset of the commands; see the debugger documentation for more.
Checked Build of Windows
The checked build of Windows is one of the most powerful diagnostic tools available to a driver developer. Some people confuse the checked build of their driver with the checked build of Windows. The checked build of Windows is a version of the operating system that was built with checking turned on, in the same way you can build your driver with the checked build. A wise developer does not ship a driver and support code unless the product runs cleanly on the checked build of Windows. The checked build provides a number of useful features for the developer:
· Extensive assertions, particularly to check parameters to functions and the internal state of Windows.
· Significant diagnostic output that can be enabled for specific features.
· Most optimizations turned off. This makes it easier to debug into a call to the kernel and changes the timing of routines, which can help find timing-dependent problems in your driver.
Assertions
The checked build uses the same ASSERT macro that driver writers use. The macro reports into the debugger the expression that failed and the file and line number at which the failed expression appears in the source code. The following is an example report:
*** Assertion failed: Irp->IoStatus.Status != 0xffffffff
*** Source File: D:\nt\private\ntos\io\iosubs.c, line 3305
0:Break, Ignore, Terminate Process or Terminate Thread (bipt)? b
0:Execute '!cxr BD94B918' to dump context
Break instruction exception - code 80000003 (first chance)
ntkrnlmp!DbgBreakPoint:
804a3ce4 cc int 3
In this example, the user entered a “b” to stop at a breakpoint. This allows you to check the stack and see where your driver is calling into the kernel. This is the likely location of the problem.
Thousands of ASSERT statements in Windows are active in the checked build. In general, they fall into three major categories:
· Parameter checks. These can be simple range checks for a value passed to a kernel routine. Another parameter check uses the type data that many kernel data structures contain to verify that a pointer to the correct type of data is passed.
· Timing actions with recommended limits. These useful ASSERT statements check the timing of actions for which Microsoft has recommended limits. For instance, holding a spin lock more than the recommended time triggers an assertion failure.
· Checks on internal data structures. These checks verify internal Windows data structures. These failures are tough to find, because the assertion is rarely triggered until long after the actual error.
Diagnostic Output
The checked build of Windows provides support for diagnostic output to the debugger. The Knowledge Base article "How to: Enable Verbose Debug Tracing in Various Drivers and Subsystems" shows how to get data that will help diagnose many problems for a few of the possible categories of output. Look up DPFLTR_TYPE in wdm.h or ntddk.h for all of the categories. See Resources at the end of this paper for a link to this KB article.
Most of this output must be enabled. For example:
ed NT!Kd_NTOSPNP_Mask 0xFFFFFFFF
ed NT!Kd_PNPMGR_Mask 0xFFFFFFFF
These debugger commands enable kernel and user mode Plug and Play manager diagnostics. Enabling these diagnostics can help track down many driver problems.
Installation of the Checked Build
You can install the checked build of Windows in either of two ways:
· Install parts of the build on a standard Windows system.
· Install the complete checked build in its own disk partition.
Each installation has challenges and advantages.
The partial installation is documented in the DDK under Installing Just the Checked Operating System and HAL. This technique installs just the checked versions of the kernel and the HAL with the free versions of other system components. This has the advantage of running much faster than the full checked build, but limits the checks and diagnostics. In particular, many of the subsystem outputs described in the KB article about verbose debug tracing require the drivers from the checked build. Additionally, many of the drivers supplied with the checked build have assertions that can help you find problems in your driver.
The full installation requires its own disk partition. The full checked build of Windows is installed like any other version of the operating system. The full checked build runs significantly slower, but provides many more assertions and additional diagnostic output. One problem many people face installing the full checked build is that it may assert during installation! The reason is that a driver in the distribution has a bug. If you encounter this assertion, you need to install the checked build with the debugger running, so you can ignore the assertion and continue. If you do encounter this assertion, you must always boot the checked build with the debugger running.
When you install a Windows Server 2003 system, you can press F8 as installation first starts. Press F8 when you are prompted for F6. This will activate the debugger over the COM2 port with a baud rate of 19200.
If you are not installing Windows Server 2003, or if you want a different setup for the debugger, you must install the system from a running version of Windows. Invoke the installation program with the /noreboot option. Once the GUI installation program finishes, find the temp directory where the files were installed (usually C:\ ) and edit the file txtsetup.sif. In the file, find the line:
OsLoadOptions = "/fastdetect /noguiboot
/nodebug"
Edit this line, removing the /nodebug option and adding the debugger options you would use in the boot.ini file. When you reboot to continue the installation, the message “Starting Windows” appears in the white line at the bottom of the screen, and the system starts in the debugger.
Driver Verifier
Driver Verifier, like the checked build of Windows, is a powerful tool for finding driver problems. Whereas the checked build is passive, only adding checks to the normal operation of the kernel, Driver Verifier is an active checking tool that can modify the environment to test a driver. Additionally, Driver Verifier provides monitoring capabilities for the drivers under its control. Driver Verifier ships as part of the Windows operating system and can check the drivers you choose.
Driver Verifier is controlled by the Verifier utility. This utility has too many options to explain in detail in this paper. All of the options are documented in the DDK under Driver Verifier Manager (verifier.exe). For most drivers you should always enable the following options:
· Automatic checks. These standard checks always apply to any driver being verified and cannot be disabled.
· Special memory pool. This option allocates memory from a special pool to check for buffer overruns and related problems.
· Forcing IRQL checking. This option checks memory references and finds attempts to reference memory at the wrong IRQL.
· Memory pool tracking. This option tracks your driver's memory usage and insures that the driver frees all memory.
· I/O Verification. This option monitors your driver's handling of I/O requests.
· DMA Verification. This option monitors your driver's handling of DMA operations.
· Deadlock detection. This option finds many potential deadlocks by making sure your driver uses a consistent locking model.
Other options are available for specific types of drivers. The important thing is to enable Driver Verifier for your driver before you ever load your driver. This way, you start catching bugs early in the development process.
Driver Verifier also maintains a number of counters that you can monitor. These counters provide information about memory allocation and spin locks. Both the Verifier utility and the !verifier debugger extension can display these counters.
Driver Verifier has one option you should not enable when you first test your driver. This option is Low Resource Simulation, which injects failures into your driver’s requests for resources. Testing with this option is important because your driver should not crash the system if it cannot allocate a resource such as memory. However, this option should be used for testing after your driver is running well using the techniques described elsewhere in this paper.
Call Usage Verifier
Call Usage Verifier (CUV) adds checks beyond those of the previously discussed techniques. Unlike the previous techniques, using CUV requires you to modify your sources file and recompile your driver. To modify the sources file add the following line:
VERIFIER_DDK_EXTENSIONS=1
CUV requires rebuilding the driver because it replaces calls to routines and macros in the DDK headers with instrumented versions for checking.
CUV supports four types of checking:
· Initialization checks. These typically verify that your driver initialized a kernel type appropriately before using it.
· Consistency checks. These verify that your driver used the kernel functions in a consistent manner on a data structure.
· Paged memory checks. These validate that your driver does not call a routine that expects non-paged memory with paged memory.
· IRP stack checks. These validate the driver’s use of the I/O Request Packet (IRP) stack.
CUV works only for WDM and legacy drivers. CUV produces a false report on rare occasions, so check its results carefully. When your driver starts, CUV reports that the driver was built with CUV When an error occurs, CUV reports into the debugger; for example:
DDK+ Driver Error: Calling InitializeSpinLock(...) at File
c:\projects\linklist.c, Line 225
The Spin lock specified as parameter 1 [0x811abe78]
has been previously initialized and used as
a Listhead for Interlocked operations by this driver.
Break, Ignore, Zap, Remove, Disable all, H for help (bizrdh)?
Pool Tagging
Pool tagging allows your driver to associate a four-character alphanumeric tag with each block of memory that you allocate. The usual approach for using pool tagging is to create a unique tag for each class of data structure you allocate. Be aware that none of the DDK tools check to ensure that the tag you create is unique for the system. You can see the tags Windows knows about by viewing the file PoolTag.txt. This file is in the DDK under \tools\other\i386 and is shipped with the debugger in the directory \triage. Try to make your tags distinct from common tags such as irp or ddk.
The actual tagging of memory is enabled according to some interesting rules. Tagging is never enabled if the Driver Verifier “special pool” option is being used. Windows 2000 and Windows XP enable tagging by default only for the checked build of Windows. For the free build, you can enable tagging using the command line gflags /r +ptg. Tagging is always enabled for all builds of Windows Server 2003.
These tags and their allocations can be viewed in three ways:
· In the debugger
· With the Poolmon utility supplied with the system
· With the Pooltag utility supplied with the DDK.
Using pool tagging allows you to track the memory allocations of your driver for specific data structures. Tracking is especially valuable when Driver Verifier reports that you have not freed all the memory you allocated when your driver unloads. Looking at the tags from the memory being reported makes it easier to determine what you forgot to clean up.
Starting with Windows XP, you can not only allocate memory with unique tags, you can supply that tag when you free memory using ExFreePoolWithTag. If you free memory with ExFreePoolWithTag, the tag supplied by the call is checked against the tag of the memory you are freeing. Mismatched tags cause the system to bugcheck.
Using allocation and free with tags provides type checking for memory. Verifying that the memory being freed is of the type intended can catch errors created by freeing the wrong memory. Bugs from incorrect freeing of memory can be some of the hardest to find.
Normally the memory can still be freed without specifying the tag. Drivers can require the use of ExFreePoolWithTag by or’ing the tag with the PROTECTED_POOL flag. If your driver is for Windows XP and later, you should use ExFreePoolWithTag with PROTECTED_POOL tags except when you allocate memory that the system will free; in those cases do not use a PROTECTED_POOL tag.
Code Coverage
Code coverage is a powerful tool for checking which parts of your driver are executed during testing. The simplest form of code coverage is line coverage, which reports which lines in your program were executed. More advanced tools show you which paths in a conditional statement have been exercised. The more of your code that is executed during testing, the more likely you are to find errors.
The best way to check code coverage is to run all your tests for a driver with code coverage enabled, and then see how much of your code was exercised. Do not be surprised if you run code coverage with all your tests and find that roughly two thirds of your driver code is not being executed. Increasing code coverage involves a number of approaches:
· Check that your tests make all valid requests. With code coverage, you see what code is actually run. It is easy to see valid requests to your driver that are never tested. Most test suites need improvements to increase coverage.
· Check that your tests supply all the incorrect input. Just as with valid requests, you are likely to find that your test code does not try all of the possible incorrect requests for your driver. When you are reviewing this code, be sure to check whether your driver handles all incorrect requests properly.
· Use low resource simulation in Driver Verifier. Another way to increase coverage is to use the low resource simulation option in Driver Verifier to exercise failure paths in the driver.
· Consider adding fault injection to your driver. Even after this work, you are likely to find a lot of code that is not exercised. Typical examples are failures in your hardware that are hard to create, or similar “impossible conditions.” For these you might need to create a macro to inject faults.
A simple approach is a macro that, for the debug version of your driver, calls a function that will randomly return TRUE. In the production version of your driver, the macro always returns FALSE. You then add code to your driver that, when the macro is true, modifies the behavior of the driver to execute the failure condition. For example:
devStatus = READ_REGISTER_ULONG( statusRegister );
devStatus = FaultInject ? DEV_ERROR_FLAG : 0;
if ( devStatus & DEV_ERROR_FLAG ) …
Adding the second line makes it possible to simulate the situation where the device returns the error flag and thus exercises the recovery code controlled by the third line.
Even after you do all of this, you still will not get complete coverage. The goal of code coverage should be for you to see that all of the reasonable cases are being tested. The code that is not exercised should be checked thoroughly to find problems.
Performance Profiling
KernRate is an often-overlooked tool in the DDK. This tool is primarily a profiler that periodically samples the instruction pointer. KernRate also collects statistics on a number of performance-related items in the kernel such as those in the performance monitor (Sysmon). Traditionally, this data is dumped to a text file, and the developer has to read through lots of text. Microsoft has added a new tool called KrView that uses Microsoft Excel to create a workbook to display the data. You can download a package that contains both KrView and the latest version of KernRate. See Resources at the end of this paper for availability of KrView and KernRate.
If you have not done performance profiling before, keep in mind that getting good data takes time. For good solid numbers, consider running a profile for hours. In addition, your numbers are worthless unless you are exercising your driver, so you need a good test that can exercise your driver for a long time. Besides time, the profile will take a lot of space; sample profilers look at the instruction pointer and put the data into a “bucket” that indicates the code being executed. KernRate allows you to adjust the size of the buckets, The smaller the bucket, the more accurate the sample.
You should use performance profiling with your driver in two ways:
· First, run a performance profile of the whole system while exercising your driver. Even with a significant load on your driver, the percentage of time spent in your driver should be relatively low.
· Second, run a performance profile on your driver alone. When you analyze the data from this profile, look for hot spots where your driver takes a lot of time. At each hot spot, look for either a problem with the code or a better algorithm that reduces the load. Many hot spots will be valid, but checking them can find bugs.
Kernel Logging
As part of Event Tracing for Windows (ETW), recent kernels can log a number of actions by the system. We will discuss considerations for ETW in your driver later in this paper, but for now let us look at the data logged by the system. The simplest way to log this data is with the TraceView utility provided in the DDK tools directory. TraceView can log nine different categories of data, including:
· Process creation and termination
· Thread creation and termination
· File I/O
· Disk I/O
· Image file (executables and DLL) load and unload
· Registry accesses
Enabling kernel logging while debugging your driver can help in finding bugs.
Installation Logging
Just as you used ChkINF to verify your INF file before installation, you should enable SetupAPI logging during your driver installation. Driver installations perform a number of actions. Logging provides a way to validate that your installation is correct. If your driver installation fails, the log is available to show you what happened. Consider setting the following registry entry(or creating it if needed) to the value 0x3000FFFF just before your installation of your device:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurentVersion\Setup
This setting enables verbose logging so that you see everything that happens during your device installation. Be sure to restore the value at the end of the installation because verbose logging produces a lot of output, which slows down the system.
The setup log resides in %systemroot%\setupapi.log. Take a look through this file to see what your device installation did. This log contains a lot of data. An excellent reference for understanding it is the paper "Troubleshooting Device Installation with the SetupAPI Log File." See Resources at the end of this paper for a link.
Miscellaneous Tools
Windows and the DDK include several other miscellaneous tools that many developers overlook. In addition to the tools listed below, many third-party tools are also available to driver writers:
· DriverQuery. DriverQuery is a command-line tool that ships with Windows. This tool lists all the drivers that are active on the system. Using the /v option with this tool expands the data to include the size of the driver, the path to the driver, and other information. Applying the /si option provides data about whether the driver is signed and what INF file installed the driver on the system. Use DriverQuery to see that your driver is loaded, and to verify that the INF file is what you think it is.
· DeviceTree. DeviceTree is a GUI-based tool that displays data about drivers and devices. In the Driver view, the tool lists all of the drivers, giving data on the driver, the calls it supports, and all of the devices it has created. Under the driver, DeviceTree displays data about each device, including the flags on the device object and its reference count. In the Plug and Play view, the device data appears in a tree that shows the relationships between devices. DeviceTree can help identify a number of problems when debugging your driver.
· DevCon. DevCon is a command-line tool that provides all of the capabilities of the Windows Device Manager. In addition, it provides many of the capabilities of the DriverQuery and DeviceTree tools. DevCon is useful for creating scripts to exercise Plug and Play control of your device. The source code for DevCon is supplied in the DDK in src\setup\devcon. Use this code as a guide to writing your own driver tools.
· RegEdt32. Installing drivers modifies the registry. Many drivers use data from the registry to control operation. RegEdt32 is the tool to use to inspect and edit the registry, including the access control for registry keys.
Diagnostics is often overlooked but is critical to a high-quality Windows device driver. It is the only area of coding that we will cover.
It is important to have a standard diagnostics model for all the drivers your company produces. The model should include common levels and flags to control the diagnostics. The model should also have a standard presentation format for diagnostics. This presentation format should consider everything from standard naming for event log entries and performance monitoring counters to the display of data for assertions, debug prints, and WPP tracing.
Assertions
We have already encountered assertions with the checked build of Windows. The use of assertions is a powerful debugging technique. You should be aggressive in using them. The assert statements both provide debugging capabilities and document the assumptions your code makes.
The DDK provides a couple of macros, ASSERT and ASSERTMSG, so that your driver can assert when it is built in the checked build environment:
ASSERT ( expression )
ASSERTMSG ( message, expression )
In each of these macros, the expression is checked and, if it is false, the assertion is fired in the debugger. ASSERTMSG allows an additional message to be printed as part of the diagnostics. When you use the assert macros, keep in mind that, in Windows 2000, the macros did not return a value. In later systems, these macros return the value of the expression. These macros produce code only in a checked build of your driver. They have no impact on your production code.
So when should you use assertions? The general answer is whenever you enter or exit a major block of code. In particular:
· On entry to and exit from a module of code with static data. While many developers do not think of modules in C programming, a file with static data is a module. On entering any non-static function, the static data of the module should be validated with asserts. On exit from a non-static function, the assertions on the static data should be performed to verify that the module did not corrupt the data.
· On entry to and exit from a procedure. The common use of assertions is to check the validity of input parameters to a function. What many developers forget is that you need to validate the data you touch on the exit of your function.
· On entry to and exit from a loop. Just as you perform checks around procedures, it might be appropriate to do this for a loop. Checking pre-loop and post-loop conditions can help to localize problems.
· Before data is referenced by a pointer or before using a global variable. Many developers check a pointer before referencing it, but you should also validate the data the pointer points to. In the same manner, the contents of global variables should be validated before being used.
Given the places you need to assert, what should your checks entail? Probably more than you think. Some common validations should be:
· Pointers are valid. A common assertion check is that a pointer is not equal to NULL. However, do you ever do more? For instance, many Windows kernel structures have a type field. Do you validate the type? For example:
ASSERTMSG ( “Invalid IRP pointer”,
irp != NULL && irp->Type == IO_TYPE_IRP)
You can go further and check whether the memory pointed to is writeable, for instance. The point is not to stop with the simple check for NULL.
· A variable in is range. Another common check is that a given value is in range. The goal here is to be sure to validate all of the variables you get from outside your code.
· Doubly linked list is consistent. Before you think this is excessive, consider the following assertion:
ASSERTMSG ( “Corrupted List”,
listPtr != NULL && listPtr->Flink != NULL &&
listPtr->Flink->Blink == ListPtr )
The additional checks can catch list corruption. Adding the assertions will not find errors as quickly as validating every element of the list, but it is simpler, with little overhead.
· String variables are valid. Many Windows strings are counted strings, in which the string is designated by a structure with a buffer pointer, length, and maximum length. When you validate a string variable, consider:
ASSERTMSG ( “Invalid Counted String”, string != NULL &&
string->MaxLength > string->Length &&
string != NULL ?
(string->Length != 0 ) : TRUE ))
The additional checks make sure you are not surprised with an invalid string.
· IRQL is valid for a function. This is a driver-specific check, and an important one. Many kernel service routines work at a limited range of IRQL. Therefore, you should ensure that your functions that call these routines check for correct IRQL.
Developers make two common mistakes when using assertions:
· Side effects that modify driver behavior. First, be sure that your assertions and any other checking code do not produce side effects that modify the behavior of your driver. It is easy to fall into the trap of writing code like the following:
ASSERT ( --CurrentCount > 0 )
This will work fine until you move to the free build and find that your counter is not being decremented.
· Confusing assertions with error checks. The second mistake is confusing assertions with error checks. Don’t use an assertion for a condition that might occur during normal operation, such as the following:
p = ExAllocatePoolWithTag ( NonPagedPool, BLK_SIZE,
BLK_TAG );
ASSERT ( p != NULL )
This usage is incorrect, because in normal operation your allocation might fail for lack of memory.
You can make many more validation checks in your code. Remember that you can make these validations complex, because these assertions only exist in the checked build of your driver. In fact, consider creating functions for validating complex data structures, and place these functions in conditional code so that they exist only in the checked build of your driver. This is another technique that can help you find bugs early.
Debug Print Statements
Debug print statements are the most common diagnostic in most drivers. The DDK describes four debug print statements:
KdPrint ( format, ... );
KdPrintEx ( componentId, level, format, ... );
DbgPrint ( format, ... );
DbgPrintEx ( componentId, level, format, ... );
All of these calls use the same format and following arguments as the printf statement. The first two calls are macros that produce code only in the checked build of your driver. The last two calls are the underlying calls that are always active. For the Ex versions of these calls, use the componentId and level to allow filtering of the debug output.
Your driver should not use the Dbg versions of these calls unless you are creating your own conditional macro for printing. Using debug print statements incurs significant overhead, and they should not appear in a production driver.
Even in the checked version of your driver, having all of the debug prints active can make your driver painfully slow to use, so consider creating your own macro to allow conditional debug prints. Before you decide to use only DbgPrintEx, you should be aware that a limited number of component IDs are available for driver developers, so it is better to create your own macro. For example:
#if DBG
#define MyDriverPrint ( level, flag, args ) \
if ( MyDriverLeven >= level && \
(flag) & MyDriverFlags ) \
{ \
DbgPrint args; \
}
#else
#define MyDriverPrint ( flag, level, args )
#endif
This creates a macro that prints only for the checked build of the driver. The debug flags and level are global variables that your driver can initialize to control its output. Using this macro matches the model of WPP tracing which is discussed later in this paper along with the advantages of keeping a constant model.
Normally, debug print data is monitored in WinDbg. Two tools are available to monitor debug messages on the system with your driver:
· DebugMon
· DebugView
See Resources at the end of this paper for availability of these tools.
Both of these tools provide a graphical user interface to monitor debug messages on a local system or over the network. Do not assume that these tools and debug prints are enough to debug a driver. As discussed in this paper, many verification tools are available, and you need the debugger to take full advantage of these tools.
Having looked at the debug print calls, what about the data they print? Consider some simple rules for your output:
· Use text whenever possible. For example, it is much easier to read "IRP_MJ_CLOSE" than to see 0x2 and remember that 0x2 indicates a close request. Several drivers in the DDK samples have example code for printing IRP major functions.
· Print information that describes a data structure, not just a pointer to the structure. For instance, print the major function of the IRP and the pointer to the IRP. Having the function code makes it easier to read and act on the data.
· Print the information in a standard format. All too often, debug statements vary widely, making the output hard to use while debugging.
· Consider providing a call trace, showing the entry to and exit from functions in your driver. A good approach is to use an indent level so that nested calls appear indented from their callers.
· Consider adding more data to your debug print macro. Having a standard summary line that shows the source file and line, the currently executing thread, and the IRQL can help debugging.
As previously noted, the format string follows the conventions of printf. Consider the following tricks and rules for format strings:
· If you are printing a pointer, use %p. This will work correctly on both 32-bit and 64-bit variables.
· If you are printing a counted string, use %Z (ANSI) or %wZ (Unicode). This will print the string correctly even if it is not null-terminated.
· If you are trying to print a Unicode value, remember that the driver must be running at an IRQL less than DISPATCH_LEVEL.
· When writing your debug statements, realize that all of these functions have a limit of 512 bytes per call.
WPP Software Tracing
WPP software tracing provides a low-overhead mechanism for logging data. WPP software tracing creates a binary data log that is post-processed into a set of text messages. A significant advantage of using WPP is that the data collected from your driver is not readable by your customers. This capability allows you to use messages that might otherwise be embarrassing such as “I guess the hardware can return this code after all!”
The simplest approach to using WPP tracing is to add the following line to the end of the SOURCES file:
RUN_WPP=$(SOURCES) -km
You then need to set up some definitions and include files in your driver. Finally, you add statements of the form:
DoTraceMessage(IN TraceFlag, IN Format, ...)
Format in this statement is the same as that for debug prints. DoTraceMessage is the default name of the trace statement. The WPP tracing tools allow you to define your own name, so for instance, you can convert debug print statements into trace statements. It is also possible to force WPP tracing output as debug print statements. To make WPP tracing also appear as debug print statements, use:
#define WPP_DEBUG(args) DbgPrint
args;
You might not want to use this, however, because WPP tracing supports a number of format strings that are not supported in debug prints. Some of the interesting ones are:
%!FILE! |
Displays the name of the source file. |
%!FLAGS! |
Displays the value of the trace flags. |
%!LEVEL! |
Displays the name of the trace level. |
%!LINE! |
Displays the line number of the line |
%!bool! |
Displays TRUE or FALSE. |
%!irql! |
Displays the name of the current IRQL. |
%!sid! |
Takes a pointer to a Security Identifier (SID) and displays the SID. |
%!SRB! |
Takes a pointer to SCSI request block (SRB) and displays the SRB. |
%!SENSEDATA! |
Takes a pointer to SCSI SENSE_DATA and displays the data. |
%!GUID! |
Take a pointer to a GUID and displays the GUID. |
%!NTSTATUS! |
Takes a status value and displays the string associated with a given status code. |
Additional formats plus the ability to display enumerations are described in the Software Tracing FAQ section of the DDK.
WPP tracing presents some challenges to the developer in designing output. In particular, WPP tracing achieves lower overhead by keeping data as binary. Some of the guidelines for debug print statements do not apply for WPP tracing. The recommendation to convert constants to text is an example. Putting a binary 0x2 in the message takes one instruction, whereas “IRP_MJ_CLOSE” requires a string copy. Also, including the summary data is unnecessary, because WPP tracing already includes the file and line data.
WPP tracing provides diagnostic support for its actions. To enable this support add the option –v4 to the RUN_WPP line in the SOURCES file. To display this data, define WppDebug to be a debug print statement. For example:
#define WppDebug(_level_,_msg_) KdPrint (_msg_)
This causes the state transitions of WPP tracing to appear as debug prints. This debug output allows you to monitor the operation of WPP tracing and see that your requests to enable and disable tracing are correct.
Now that we have talked about WPP tracing, you should be aware of a number of tools that are available for controlling and displaying tracing. The following are the tools:
Logmon |
This tool controls tracing (such as stopping and starting). The tool ships with Windows XP and later systems. |
Tracelog |
This tool controls tracing. The tool ships with the DDK, SDK and symbols. The source for the tool is available in the SDK so you can write a custom tool if desired. |
TraceFmt |
This tool translates a trace log to a readable format. The tool ships with the DDK, SDK, and symbols. |
TracePdb |
This tool extracts the trace format data from a symbol file. The tool ships with the DDK, SDK, and symbols. |
Traceview |
This is a general tool for control and display of trace data. The tool is shipped in the tools directory of the DDK. |
WmiTrace extensions |
The debuggers have an extension to provide control and display of trace data. |
WPP tracing is a powerful tool that should be used in every shipping device driver. The low performance impact of tracing provides a developer with a way to get detailed diagnostics from a customer without requiring a modified driver.
Event Log
So far, this paper has discussed diagnostic data designed for the developer. It is just as important for your users to know what is going on. The best way to provide this information is through the System Event Log.
An event log entry is a block of binary data that is interpreted by the event viewer. The actual size of an event log entry is small. Besides the fixed headers, an entry’s data is less than 256 bytes. The event viewer uses a message catalog to translate the error code in the entry into a locale-specific text message. The text message can include strings that are inserted from the log entry data.
When writing to the event log, think about the following:
· The size of the event log is limited. The administrator of the system determines its size and the rules for overwriting log entries. Given the limited size, make sure the messages you write to the event log are there for a reason. Administrators are not interested that your driver started and stopped correctly; when they check the log, they are looking for problems.
· APIs exist for reading the event log, and many system administrators have tools that scan for particular errors. Make your driver’s error codes unique and use a separate code for each error. Also, create only one event per error; an event log entry is not a print statement where multiple lines are fine.
· Finally, do not use the lazy approach of creating a single global error message, and inserting text about the actual error from a string.
The limited size of the event log entry influences your design of entries. Unlike internal diagnostics where text is recommended, event log entries are sometimes better with binary data. Use the unique error code and the text message to give your customer data about decoding the binary.
A final consideration when using the event log is where the message catalog will reside. The message catalog is compiled into a resource that is placed in an executable file such as the driver or a DLL. The examples in the DDK show how to include the catalog in your driver. This approach has the limitation that adding or changing messages requires that you rebuild the driver. The other approach is to put the messages into a dummy DLL, which allows you to update the messages without impacting the driver. This makes it possible to distribute the dummy DLL and the message catalog file so that the messages can be internationalized without driver source code.
Performance Monitoring
A good driver provides feedback to the user through the performance monitor (sysmon). For common classes of devices, drivers should provide the standard data. Even if your driver does not fit a common category; it should provide monitoring information.
The simplest way to provide the performance monitoring data is through Windows Management Instrumentation (WMI). WMI is a powerful instrumentation model that can do much more than support sysmon. It is not the intent of this paper to explain WMI; see the Windows Management Instrumentation section of the DDK for this.
When designing a driver, be sure it has a WMI class (the container for the data) that meets sysmon requirements. These requirements include the following items. For explanation of the individual terms see the DDK:
· The class must inherit from Win32_PerfRawData.
· The class must reside in the \root\cimv2 or \root\wmi namespace.
· The class qualifiers Dynamic, Provider(“WmiProv”) and HiPerf are required, and the qualifiers Abstract, GenericPerfCtr and Cooked are not allowed.
· Each data item must be a signed or unsigned integer of size 32-bit or 64-bit, with the qualifiers CounterType, DefaultScale and Perfdetail.
You also need to decide which data to provide in the performance monitoring class of your driver. Consider the counters available to sysmon and, in general, think about items such as:
· IRP counters. Provide a count of requests to your driver. Consider both a total and a breakdown by types such as reads, writes, and IOCTL’s.
· Data counters. Provide a count of the data your driver has processed.
· Failure rates. Provide the number or rate of failures. This category is important if your device reports failures. Include them in the log.
· Security counters. If your driver has its own security checks, provide a counter of the rejections that occur. You might also want to consider reporting failures of requests for invalid data.
The DDK has a sample showing WMI support of sysmon in the src\wdm\wmi\wmifilt directory.
Custom Dump Data
A good driver writer considers what is needed from a crash dump to help diagnose a problem. If your device maintains a lot of state information outside of the computer’s main memory, you might want to collect this data and add it to the crash dump.
Windows provides callbacks for reading device information and storing it in main memory for a dump. Unfortunately, the procedure for this has changed from Windows 2000 to later systems. On Windows 2000 you use KeRegisterBugCheckCallback to notify the system that it should call your driver to collect data. On Windows XP and later systems, KeRegisterBugCheckReasonCallback performs this task.
Even if your driver does not collect data from the device for a dump, consider how you will analyze the crash dump. Writing a debugger extension to aid in decoding your driver’s data can make crash dumps easier to analyze.
Version Block
One of the simplest ways to help customers report problems in you driver is to include a version information resource, also called a version block. To create the version information, you use a VERSIONINFO resource definition statement in a resource script file. The DDK sample drivers have an RC file with the VERSIONINFO, and so should your driver. The information in this block should include your company name, the product name and version, the filename and versions, a description of the file, and a copyright notice.
To display version information, in Windows Explorer, right click on a driver file, then select Properties. In the Properties dialog box, select the Version tab to see the version information for that driver.
Your driver should have unique version information for every build. This allows customers to identify which version of your product they are using when problems occur. They might be using an old version and you might already have fixed the bug.
A high-quality device driver is a well-tested driver. Microsoft provides a number of testing tools for drivers. These tools test many of the common problem areas for device drivers.
It is not sufficient to run just the Microsoft-supplied tests. Your driver needs additional testing specific to its implementation. This testing can be done either by writing custom test code to exercise the driver, or by creating scripts to invoke existing tools and utilities. Whichever you choose, consider the following guidelines when developing and running your tests:
· Make the testing easily reproducible. Testing that requires many manual steps or a specific system is not a viable test. The test should be easy to run and easy to move to additional test machines. If the test requires manual steps, provide a clearly written document that describes what to do.
· Test results should be easily verifiable. Many tests provide detailed logs of their runs. These logs should have a simple result line indicating whether the tests succeeded. It is not acceptable to have a large log that must be hand verified to determine whether the test ran correctly. At a minimum, create the log so that it can easily be compared to a previous successful log.
· Test on a variety of platforms. This paper has encouraged the use of the checked build of Windows, the checked build of the driver, Driver Verifier, code coverage, and various logging tools. You should run your tests in this environment, but not only in this environment. Using the retail build of Windows and both uniprocessor and multiprocessor systems is required for good testing. Use a number of systems with different Hardware Abstraction Layers (HAL) if possible.
· When you fix a bug, write a test. The one thing you should always do when testing is to make sure your customer never sees the bug again. When you debug a problem, create a regression test that can be integrated into future testing of the driver.
Device Path Exerciser
The Device Path Exerciser (dc2.exe) is a powerful testing tool for most drivers. This tool checks the stability of drivers for various types of device I/O control calls. In addition, the tool provides a number of options for opening and exercising a device. The tool is quite complex so it is easy to miss some of its testing capabilities. No driver should ship without a set of tests through the Device Path Exerciser.
To invoke the tool use one of the following:
dc2 [options] /dr driver [driver …]
dc2 [options] device
The first line is supported only for Windows XP and later. Up to ten drivers can be specified by their service names. The second line works on all systems and supports a single device. The options fall into three major categories: logging options, open options, and test options. These are described in the following sections:
Logging Options
Device Path Exerciser has four different logs. Two of these logs can produce large volumes of information depending on the tool's settings. Be careful, because full logs can make the test durations unbearably long. These logs are:
DC2.log. This is the general event log of the tool. This log is overwritten each time dc2 runs, so you have to rename the file to save it. The log is controlled by the /ltX option, where X is one of the following, in increasing order of verbosity:
n |
Disable the dc2.log |
c |
Write one entry for the start and end of each test category |
g |
Write one entry for the start and end of each test group |
a |
Write one entry for each individual test |
Diags.log. This is the results log of the tool. Device Path Exerciser appends data to this log each time it runs, so rename the log to keep the results of a session. If you are done with the data, delete the log to prevent it from getting too large. The log is controlled by the /ldX option where X is one of the following in increasing order of verbosity:
n |
Disable the diags.log |
f |
Write fatal errors only |
e |
Write errors and fatal errors |
w |
Write warnings, errors, and fatal errors |
a |
Write information, warnings, errors and fatal errors |
CrashN.log. This log is used to restart tests after a crash. Device Path Exerciser appends data to this log each time it runs. The log is controlled by the /lrX option where X is one of the following in increasing order of verbosity:
n |
Disable the crashN.log |
a |
Write information needed to restart tests |
Crash.log. This is a log of the tests that caused the system to crash. Device Path Exerciser appends data to this log only if you use the /c or /r switch to skip completed tests and tests that crash the system. No options are available to control this log.
Open Options
Device Path Exerciser attempts to open your device in a number of ways. Keep in mind that many of the tests are run each time the device is opened. Running all the open options is valuable, but you might not want to combine them with every device-control test. The common open options and the command-line switches that trigger them are:
· Basic opens. These are the default. Device Path Exerciser attempts to open the device in five different ways. For most drivers, only the standard open reaches the driver.
· Synchronous opens (/k). This option requires the /k switch. It opens the device with the FILE_SYNCHRONOUS_IO_ALERT option added for each of the five variants under the basic open.
· Direct device opens (/k /dd). This requires the /k and /dd switches and adds additional tests to synchronous open tests.
In addition to these options, specifying the /oc switch on the tool creates a test where several threads try to open and close the device thousands of times.
Test Options
Device Path Exerciser has a large number of tests. The common test options are:
· Miscellaneous tests (/m). These tests are triggered by the /m option. This is a set of tests on a number of operations for a driver. Most drivers will only see the read/write and cancel tests. The read/write tests specify operations with valid buffer pointers and varying size (including zero) and offsets.
· Zero-Length buffer tests (/in). This test will send DeviceIoControl requests to the driver with a length of zero and an invalid address.
· Random tests (/ir). This test will send DeviceIoControl requests to the driver with valid and invalid buffer pointers and varying size (including zero) and offsets.
The last two tests can be constrained to a set of device types using the /dl min and /du max switches to specify the minimum and maximum values. In the same manner, using /fl min and /fu max can specify the valid range of function codes.
Plug and Play Driver Test
The Plug and Play Driver Test (pnpdtest.exe) provides an easy way to test Plug and Play support in your device driver. To run it, you might have to reboot the system to load a filter driver above your driver. The test should run cleanly when applied directly to your driver and when invoked for any underlying hardware device in the driver stack. The test has four major options:
· Removal. This option attempts to remove the device from the system. Be sure to check that your driver does not just reject all remove requests, because the test will still pass in this case.
· Rebalance. This test stops and starts the driver to rebalance hardware resources. Note that this test will not run if your driver does not have hardware resources. If your driver loads on top of a driver for hardware, apply this test to the device that owns the hardware to test your driver.
· Surprise Removal. This test issues a surprise removal to the driver for the device. Note that on Windows 2000 you will need to reboot after the test to restore the device.
· Stress. This test combines rebalance and removal for five minutes, followed by a surprise remove.
Sleeper and ACPI Stress
One of the hardest areas to get right in a Windows device driver is power management. The Sleeper tool provides concise control of the power states of the system. Use Sleeper to put the system into various power states and make sure that your device acts as expected.
In addition to Sleeper, use the ACPI Stress tool (pmte.exe) to exercise your driver's power management. The ACPI stress tool works for devices in the following categories: disk, CD-ROM, floppy disk, sound, network, IrDA, serial port, modem, parallel port, and video.
Hardware Compatibility Tests
Hardware Compatibility Tests (HCTs) are a set of tests that hardware and software must pass to qualify for the Windows Logo Program. The HCTs are useful to the driver writer, but their primary goal is to test hardware. The classes of tests are:
Audio
Bus Controllers
Display
Anti-Virus/File System Filter Drivers
Imaging
Input and HID
Modems
Network Devices
Storage Controllers and Devices
Streaming Media and Broadcast
Systems
Unclassified
Each of these classes has individual types of drivers. The individual types and tests are explained in the HCT documentation. The individual tests required for a device reflect the certification goals of the HCT. Read the documentation carefully, because some tests require additional computers or devices. For example, the network device tests require multiple machines.
The Universal Device in the Unclassified class is for devices that do not meet a specific category of device in the HCT. Any driver should pass most of the tests in the category. In particular, if you look at the tests for Unclassified, you will see a number of test and validation programs already discussed in this paper, such as ACPI Stress, ChkINF, Device Path Exerciser, and Driver Verifier. You also see some new tests related to areas discussed in this paper such as Enable/Disable Device, Public Import, and x64 Calling Conventions.
Finally, the test list for the Unclassified class includes tests for a variety of devices such as ATA/ATAPI, NTFS file systems, and USB controllers. These different device category tests are present to exercise your device if it affects one of these areas, and to ensure that your device does not interfere with normal operation of the system. You should select only the relevant set of tests when running the HCT.
The HCT is usually controlled by the Test Manager. The Test Manager scans your hardware and drivers at startup, to check for changes to the configuration. The Test Manager will delete the logs if the configuration changes. As the Test Manager starts, you will be prompted to choose one or more devices for each category of tests you chose during installation. Choosing a minimal set of devices to test reduces the setup up time for testing.
After you have started the Test Manager, you can use it to select the devices to test and the tests to run on the devices. After the tests are complete, this tool displays the logs and provides details of the results.
Some of the tools and tests that the HCT installs can be run independently of the Test Manager. Take some time to explore the HCT directory, trying various tests. The following are two of the useful tools from the HCT.
Disabler |
This command-line tool disables or removes a device. It can be set to run a number of times and invoke a command between each run. Type disabler -? to display the options. |
Pcidump |
This tool dumps the configuration space for PCI or CardBus devices. Type pcidump -? to display the options. |
Driver development is a complex task, and producing a high-quality driver requires careful attention to detail. Windows provides many tools to identify problems throughout the development cycle. It is up to the developer to take the time to use these tools to find bugs early. Finding the bugs early is the best way to reduce the overall costs of the driver. In general, a developer should:
· Use the latest Windows DDK build environment. The latest DDK provides the newest samples, tools, and documentation. Using anything other than the standard DDK build environment can cause subtle problems and make it harder to use some tools.
· Find bugs at compile time. Using the compiler with /Wall, C++, and PREfast to find problems will make your debugging easier. Enabling DEPRECATE_DDK_FUNCTIONS and C_ASSERT will protect you from future surprises. Checking INF files with ChkINF is the easiest way to get a clean installation.
· Runtime checks and logging are debugging tools. Driver Verifier, Call Usage Verifier, and the checked build of Windows are debugging tools. Using pool tagging with all checking enabled helps find leaks and problems. Your driver should run cleanly with all of these tools. Profiling your driver for code coverage and performance should be a standard step in the development process. Monitoring the various logging capabilities of the system can help find otherwise overlooked problems in your driver.
· Planning diagnostics for maintenance makes development and support easier. Liberal use of diagnostics such as debug prints, WPP tracing, and assertions enables quick isolation of bugs in your code. Event logging, performance monitor support, and a version block help your customers to provide good feedback when problems occur.
· Take advantage of the tests provided by Microsoft. The HCT tests and the test tools in the DDK are a good base for your testing. Be sure to include your own tests to exercise your driver. No one can ever test too much.