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.
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.
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.
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).
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.
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:
multi(0)disk(0)rdisk(0)partition(1)\winnt="Windows NT"
to a new line. Append the switches /DEBUGPORT=port (COM1 or COM2) and /BAUDRATE=data transfer speed to the new line, for example:
multi(0)disk(0)rdisk(0)partition(1)\winnt="Windows NT" /DebugPort=COM1
/BaudRate=9600
Save the BOOT.INI file, and reset the file attributes to hidden and read-only.
Windows NT [debugger enabled]
If you don't see "[debugger enabled]" on that line, the debugging switch was ignored. On some machines, the boot loader expects the debugging switch to be in a special position on the command line. If your command line has multiple switches, try rearranging the switches.
Kernel Debugger Using: COM2 (Port 0x2f8, Baud rate 9600),
and "SND" flashing in the upper-right corner of the screen.
If the kernel debugger wasn't started on the host machine, you will see "SND" flash a few more times, and the operating system will load as usual, with no debugger support. If you do not see the message above, the target machine has not been configured correctly.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.