The Windows NT Kernel-Mode Driver Cookbook, Featuring Visual C++

Ruediger R. Asche
Microsoft Developer Network Technology Group

January 27, 1995

Click to open or copy the files in the CPPDRV sample application for this technical article.

Abstract

This article is the first part of a two-part series that describes how to write Windows NT™ kernel-mode drivers in C++ using the Microsoft® Visual C++™ version 2.0 development system. This article focuses on the pragmatics of writing the driver—building the make file, setting up the debugging environment, installing a driver, using symbols, and source-level debugging the driver—while the second article, "Writing Windows NT Kernel-Mode Drivers in C++," focuses on the C++ aspects of developing kernel-mode drivers.

Introduction

Before I go into anything technical, I would like to let you, dear reader, know that I am very proud of this article. Why? Well, after I finished the associated sample, my colleague Dale Rogerson dropped by my office, and I said unto him, "Dale, you must check this out!" And so I popped up Microsoft® Visual C++™, went through the source code, looked up symbols, built a call graph, and did all the cool things you can do with Visual C++. Dale wasn't impressed. Then I popped up WinDbg, double-clicked a line in a source file, and typed something on the other machine. The highlighted line in the WinDbg source file changed color, and with repeated F10 keystrokes, I single-stepped through the code. Source-level debugging!

Dale still wasn't impressed. "So?" he said. "Well, Dale," I explained, "This is a KERNEL-MODE DEVICE DRIVER." Dale cracked up. "Ruediger, this time you have out-nerded me." If that's not a reason to be proud, I don't know what is.

This article serves two purposes: First, it introduces you to the pragmatics of writing Windows NT™ kernel-mode device drivers (tools you need, coding, symbolic debugging, testing), and second, it explains how to write a driver using the Microsoft Visual C++ environment. You can exploit the powerful features of Visual C++, such as the ability to browse source files or display class hierarchies—these features come in very handy when you're performing a complex task such as writing a device driver. By modifying the Visual C++ project appropriately, you can generate debug versions of your drivers that can be source-level debugged with the WinDbg utility.

The first section of this article, "The Basics," provides "one-stop shopping" for learning the mechanics of writing Windows NT kernel-mode device drivers. If you are already familiar with writing device drivers and are eager to learn how to use Visual C++, you can safely start with the third section, "Switching to Visual C++."

You cannot use Visual C++ version 2.0 to debug your driver, because you need kernel debug capabilities (which are not provided by Visual C++) to do that. It's sort of a bummer that you need different tools to build and debug your driver, but that's life. If you are used to debugging with Visual C++, you will find that the WinDbg interface is very similar to the Visual C++ IDE, only much more powerful.

To prevent this discussion from becoming too theoretical, I have taken one driver from the Windows NT Device Driver Kit (DDK) and modified the driver project files to demonstrate what we are talking about. To be more precise, I have taken two drivers—the mouse and keyboard class drivers—and merged them into one because the underlying code is almost identical.

The "Writing Windows NT Kernel-Mode Drivers in C++" article in the Development Library complements this article; it describes how you can exploit the features of C++ to understand and write device drivers more easily.

I assume that you have at least a rudimentary knowledge of Windows NT kernel-mode device drivers before you attack this article. For those of you who don't (in particular, those who start writing drivers for Windows NT from scratch), I begin this article by explaining the basics of device drivers. Please refer also to the article "The Little Device Driver Writer" in the Development Library for more information on Windows NT device drivers.

The Basics

A Windows NT kernel-mode device driver implements the "lower end" of the operating system's I/O system. That is, Windows NT accepts an I/O request from an application, packs it up in a well-defined form, and passes the request to a driver. The driver must implement a predefined set of entry points. The driver executes trusted code that might crash the entire system if it fails. Thus, the driver must handle all possible failures in a well-defined and predictable way.

Kernel drivers implement a maximum of around 10 entry points that can be called by the I/O system in Windows NT, including driver opened, driver closed, device opened, device closed, read from device, write to device, device I/O control, and interrupt service routine.

Unlike application code, drivers cannot link with the standard C run-time routines. If device drivers need support for tasks such as logging errors, outputting messages to the debugger, accessing character strings, or allocating memory, they need to call the operating system. The libraries and header files that provide the necessary entry points or prototypes are shipped with the Windows NT DDK; the information can be found in the Kernel-Mode Driver Reference in the Development Library (see Product Documentation, DDKs, Windows NT 3.5 Device Driver Kit, Kernel-mode Drivers, Reference).

You can build two versions of your driver: the "free" version (in Visual C++ terminology, this corresponds to the "retail" version) or the "checked" version (corresponding to the "debug" version).

Setting Up a Debugger

Note   Most of the information in this section can also be found in the Windows NT DDK (see Product Documentation, DDKs, in the Development Library), but I decided to include it here for the sake of "one-stop shopping."

If you wish to debug your driver, you will need to set up a debugging system, which consists of two machines running Windows NT. To debug your driver, you can use either the KD console debugger or WinDbg, which is a graphical user interface (GUI) debugger. In this article, I will describe how to use WinDbg to debug a driver. The debugger machine (also referred to as the "host") runs WinDbg, and the debuggee (also referred to as the "target") runs the system with the driver that you wish to debug.

You can optionally install the checked build of Windows NT on your target machine, but there is no need to do that—the symbols and everything will be there as long as you build the checked version of the driver. The checked version of Windows NT may be useful for obtaining additional debugging information on the Windows NT kernel.

You can connect the host and target machines either with a serial cable or through a network using a named pipe transport. If you have trouble getting the serial transport to work, connect the two Windows NT machines with a serial cable (pins 2 and 3 crossed). Run the Windows Terminal application on both machines, choose corresponding communication settings, and see if you can send information back and forth between the two machines. If you can, your hardware and communications are fine.

To set up your debugging system, follow the instructions below for the target and host machines.

Instructions for the target machine

Make sure that you have a debug option in the BOOT.INI file of the target machine. If you do, the target machine will display a multi-boot screen with a debug option when it starts up. If that is not the case, follow these steps:

Instructions for the host machine

Meanwhile, on the host machine:

Run WINDBG.EXE.

Make sure that the host machine has the following files:

You do not need to modify any environment variables on the host machine.

From the WinDbg Options menu, choose Kernel Debugger, and select the appropriate options for your transport.

From the WinDbg Program menu, choose Open, select the copy of NTOSKRNL.EXE, and choose Go from the Run menu.

The command window of WinDbg should now display something like the following:

Kernel debugger waiting to connect on com2 @ 9600 baud

As soon as you reboot the target machine, you should see the following displayed in the WinDbg command window:

Kernel Debugger connection established on com2 @ 9600 baud
Kernel Version 807 Free loaded @ 0x80100000

followed by a genial exchange of messages between the target and the host (indicated by "SND" and "RCV" merrily flickering on the top line of the target machine's blue screen, and a number of messages in the WinDbg command window on the host machine).

If you have reached this point, your kernel debugger is set up correctly, and you are ready to rock and roll.

Building Drivers in the DDK Environment

When you install the Windows NT DDK, your environment is, by default, set up to work with the build tools from the DDK. If you have not built a driver with the DDK yet, you should first take the time to figure out if it works correctly—if it doesn't, you will definitely not be able to build the driver in Visual C++. Follow the steps below.

Do the Homework First . . .

The DDK installation program will have created a new program group called "Windows NT DDK" in Program Manager. In this program group, double-click the Checked Build icon to open up a command prompt, which will provide you with an environment for building checked versions of your driver. From the command prompt, switch to the directory that contains one of the sample drivers, for example:

cd c:\ddk\src\input\kbdclass

and type:

build

If everything is set up correctly, this command will start the build process for the kbdclass driver (or whatever you selected), and, if successful, will place the driver in the default target directory (for example, C:\DDK\LIB\I386\CHECKED). Note that the build process also generates a file called BUILD.LOG in the current directory—this file contains the complete output from the build process.

You can build the driver on the host machine or the target machine—it doesn't really matter. If you build the driver on the target machine, you must copy the source and binary files to the host machine first. If you build the driver on the host machine, you will need to copy the driver's binary file to the target machine. Thus, building the driver on the host is probably a little more efficient. In the remainder of this section, I will assume that you built the driver on the host machine.

If the driver you need to build is a file system or disk driver, it is a good idea to keep a copy (or the originals) of the source files for your driver somewhere on the host at all times—otherwise, a driver bug that wipes out your hard drive may provide you with an enlightening experience.

Did You Do Everything Right?

Next, you should check to see whether symbolic debugging works. Copy the binary of the checked version of the driver to the target machine—either to your %SYSTEMROOT%\SYSTEM32\DRIVERS directory, or to a custom directory. In the latter case, you need to modify the Registry to point to that directory in the HKEY_LOCAL_MACHINE\CurrentControlSet\Services\driver-name\ImagePath key. If you have trouble installing your driver, please refer to the next section, "Installing the Driver."

If the checked version of the driver has been built correctly, you should now be able to symbolically debug the driver. If the symbols for your driver have already been loaded, you will see a line similar to:

Module Load: drivername.sys (symbol load deferred)

in the WinDbg command window. From the WinDbg command prompt, you can now type:

ld drivername

WinDbg will display one of the following messages:

Installing the Driver

If you're rebuilding an existing driver, you do not need to do anything special to install the driver because Windows NT already knows about it. If you're building a custom driver, however, you must first tell Window NT to add the driver to the system. To do that, you must add a few entries to the Registry.

For the shipping version of the driver, you will want to write an installation program that uses the Registry functions to create the appropriate keys and values. For the development process, you can edit the Registry by hand using the REGEDT32 program. Note that adding device-driver entries to the system Registry requires special privileges. If REGEDT32 does not allow you to add the appropriate entries, log off the target machine and log on as an administrator, or manipulate the Registry from an administrator machine.

You will need to add a new key under HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services. Please see the Windows NT DDK, Programmer's Guide, Part 1, Chapter 2 (under Product Documentation, DDKs, Windows NT 3.5 Device Driver Kit, in the Microsoft Developer Network Library) for details.

Switching to Visual C++

Now that we have all the pieces we need to build a driver with the DDK, let's move on to Microsoft Visual C++. Once you have built the driver with Visual C++, why not switch to C++ to write your driver? If you're game, read the article "Writing Windows NT Kernel-Mode Drivers in C++" in the Development Library.

The main reason to exploit Visual C++ to build your driver is that the Visual C++ integrated development environment (IDE) offers a number of very powerful features that you can use to simplify the development process. For example, given a project it can understand, Visual C++ builds a browse table that lets you jump from a function call to the function's definition in no time flat, lets you search the entire project for definitions and references of symbols, lets you jump to a compilation error by double-clicking the error message line, and so on.

To exploit all of these features, you need to generate a project for your driver. Unfortunately, AppWizard in Visual C++ does not currently support device drivers, so you need to tweak the project yourself using the Settings dialog boxes from the Visual C++ Project menu. But let's start at the beginning, where it makes sense to begin, and generate a project.

All of the steps I describe are applicable to the Visual C++ version 2.0 (32-bit) IDE.

Step 1: Create a New Project

From the Visual C++ File menu, choose New, generate a new project, and add the source file(s) for your driver to the project. For the time being, select "Dynamic-Link Library" as the project type. You might also want to rename the targets as "NT Kernel-Mode Driver (Release)" and "NT Kernel-Mode Driver (Debug)," or something along those lines.

At this point, it is probably a good idea to choose Options from the Tools menu, select the Directories tab, and add the directories for the DDK libraries and include files to the appropriate search directories.

Step 2: Change the Compiler Options

The Windows NT DDK header files expect a certain number of defined constants in the preprocessor options. These constants may vary slightly from driver to driver—if you run into any preprocessor trouble, the safe bet is to proceed as follows: Build the driver using the DDK build process (as described earlier in this article), load the BUILD.LOG file into an editor, and compare the compiler options specified in the log files with the options set in your Visual C++ project—this is the exact process I used to port my project to Visual C++.

The preprocessor options that I determined would work for my driver were: _X86, _I386, STD_CALL, CONDITION_HANDLING, WIN32_LEAN_AND_MEAN, and NT_UP. I undefined the symbol NT_INST. In the debug (checked) version of the driver, I also defined DBG.

Note that most drivers in the DDK have local include files, the paths of which you need to add to the project as well.

For the code generation options, you should, at the minimum, select the __stdcall option (/G7) and ensure 64-bit alignment of structures (8 bytes). You can safely ignore the run-time library settings—these only add preprocessor DEFINE statements to the compile line, and those DEFINEs are pretty much bogus because we do not rely on any run-time libraries. Precompiled headers and C++ options can be ignored for the time being.

I don't see how the C language options would affect the driver, so play it by ear here. I decided to include the /Gf and /Gy options (check the boxes for enabling function-level linking and eliminating duplicate strings). For the optimization options, I chose disable optimization (/Od) for the checked version and full optimization (/Ox and /Oy–) for the free version.

Step 3: Select the Linker Options

The linker options are probably the trickiest ones to handle, because some of the options you must specify are not offered by the predefined interface.

First, delete all the default libraries shown in the Object/Library Modules text box, and insert NTOSKRNL.LIB and HAL.LIB instead. NTOSKRNL.LIB contains the entry points for almost all of the system services that a driver needs to access, and HAL.LIB contains the entry points for the routines that the hardware abstraction layer (HAL.DLL) provides. For some driver types, you might need additional libraries.

Note   For the checked and free versions of the driver, you need to link your driver with different versions of the libraries, so you should probably either override the library path in the free (or checked) linker option, or specify the full library path in the Object/Library Modules text box.

You will also want to select the Ignore All Default Libraries check box to prevent accidental linkage with one of the user-mode run-time libraries (which will cause all hell to break loose).

In the Project Options text box, change the entry:

/Subsystem: Windows

to:

/Subsystem: native

This entry will tell the linker to build a kernel-mode module. In the Output category, change the entry point symbol to DriverEntry@8. All other options depend on the selections you made for your driver.

For the free version of your driver, your work is complete. There is a small problem involving the checked version, however. For WinDbg to be able to find your symbols, your link line must contain the following two entries:

/DEBUG:full 

and:

/DEBUGTYPE: both

Unfortunately, Visual C++ does not understand the /DEBUG:full entry, and, even worse, will not allow you to set both options on the same link line. The only way I was able to link the driver with symbols that WinDbg could understand was to invoke the linker manually with both debug options set. One way to accomplish that would be to write a batch file that you invoke after Visual C++ finishes compiling, or to delete the executable after Visual C++ is done and run a custom, external make file that contains only the link line. My solution to the problem is both sleazy and pragmatic: First, I built a sample driver using the DDK, copied the resulting log file to a file with the .LNK extension, edited the .LNK file so it contained the link options that the DDK make file had set earlier, and added the project-specific input and output files. The MOUCLASS.LNK file looks like this:

-MERGE:.CRT=.data -MERGE:_PAGE=PAGE -MERGE:_TEXT=.text 
-SECTION:INIT,d 
-PDB:NONE 
-RELEASE 
-FORCE 
-INCREMENTAL:NO  
-NODEFAULTLIB 
-IGNORE:4037,4065  
-debug:FULL 
-debugtype:both 
-machine:i386 
-ENTRY:DriverEntry@8 
-align:0x20  
-subsystem:native  
-base:0x10000 
-out:C:\DDK\lib\i386\checked\mouclass.sys  
c:\ddk\lib\i386\checked\ntoskrnl.lib 
c:\ddk\lib\i386\checked\hal.lib 
windebug\inpclass.obj 
windebug\drvclass.obj 
windebug\mclasses.obj

Now all we have to do to build the driver is to run:

link @drivername.lnk

Note that you will need to do this "external linking" only for the checked (debug) version of the driver.

You should now select Build from the Project menu. As the build progresses in the Visual C++ build window, you can double-click error messages to pop up the appropriate source windows.

If the build is successful, you should now copy the driver binary to the target machine and begin debugging, following my instructions in the section "Did You Do Everything Right?" earlier in this article.

Summary

The powerful Visual C++ IDE provides a great advantage when you're building Windows NT kernel-mode drivers. Although Visual C++ does not currently allow you to debug a device driver, the combination of Visual C++ and WinDbg can significantly facilitate the development of a Windows NT kernel-mode device driver.