Introduction to Device Drivers

Kernel 101 - writing a simple Windows NT device driver

Introduction to Device Drivers

Seeing as I've been spending alot of time doing kernel-level programming recently I thought it was time I wrote a few articles on the subject. This first article (and the rest in this "kernel" series) will cover the steps needed to build, install and start a Windows NT device-driver. Now, although the term "device driver" may seem intimidating to anyone who has never written such software, in reality this term is really a generic label for any software module which acts as a core operating system service. By following this tutorial you will see how simple it is to create your own device driver from scratch.

There are many confusing technologies associated with Windows kernel-programming. Legacy NT4-style device drivers, WDM (Windows Driver Model) device drivers, filesystem filter drivers, bus-drivers, miniport-drivers - the list goes on and on. Don't be mislead by all these terms though. All types of device-driver share the same underlying image format - the Microsoft Portable Executable. The only real difference is the libraries and header files used to build the drivers - and once built, the drivers all work in very similar ways.

At the very heart of a kernel device-driver is a single file very similar in concept to a DLL - called a SYS file. The only difference between a driver and a DLL is what libraries the driver links against - Windows NT drivers (SYS files) link against ntoskrnl.exe and HAL.DLL, Win32 PEs (DLL/EXE) link against kernel32.dll and ntdll.dll.

This tutorial will show you how to create an NT4-style "legacy" driver. This type of driver will not only function perfectly well on Windows 2000 and XP - but will also work on Windows NT4. Because we don't need the features that newer driver-models support we can make our lives much simpler by using this older style of driver.

Getting Started

Writing a device-driver is very simple because Microsoft gives you all the tools you need to get started. And you don't need to spend lots of money or use complicated IDEs, because the official Windows Device Driver Development Kit (Windows DDK) can be obtained from the following location:

http://www.microsoft.com/whdc/devtools/ddk/default.mspx

You only need to pay for the shipping cost of the CDs - and if you have an MSDN subscription you will have the DDK CDs anyway. If you want to target other Windows OS's other than XP then don't worry: the XP-DDK can be used to build "older" drivers, as long as you don't use newer kernel functions that only exist in XP.

Once the DDK is installed simply use the Start menu to open up the command-prompt build environment:

Start Menu -> Windows DDK -> Windows XP Checked Build Environment

Everything can done via the command-line with Windows Drivers. The command-line build environment (shown above) helps you out because all the correct environment-variables are already set, although the only one that is really necessary is "BASEDIR" which must point to the DDK install directory.

All your source-editing can be done using your favourite text-editor (e.g. Visual Studio is generally what people use). However it is quite difficult at first to configure Visual Studio to compile a driver project because Visual-Studio is designed to use the Platform SDK and the Visual C++ compiler. The DDK comes with its own compiler which must be used instead of the Visual Studio compiler (this is all that Microsoft supports - obviously with some trickery you could use the normal compiler, but that's not the point). So to start with we will use the DDK build environment and not Visual Studio. Maybe a later tutorial can address using Visual Studio for driver projects.

One other thing I should mention is the OSR (Open System Resources) website, www.osronline.com. This is by far the best resource on the Internet for device-driver development, so head over there and subscribe to their forums.

Hello World, Kernel Style

Writing a device-driver in Windows is incredibly simple - but first create an empty directory to store your driver project in. Use a simple path with no spaces in it, just because it is easier to navigate to in the command-prompt:

C:\DRIVERS\HELLO\

Create an empty text file in this directory and call it "hello.c", then type the following code into it:

#include <ntddk.h> 

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) 
{
    DbgPrint("Hello, World\n");
    return STATUS_SUCCESS; 
}

Pretty simple, right? The DriverEntry is a function required by the DDK - it is similar in concept to DllMain because it is called when your driver first loads - however your driver remains loaded after you return back to the kernel.

The two parameters are worth mentioning. DriverObject is a pointer to a DRIVER_OBJECT structure, a kernel data structure used to represent the loaded device driver. RegistryPath is a unicode string which holds the corresponding service entry location in the registry.

Unlike win32 programs which use BOOL return types, and TRUE and FALSE, kernel drivers use NTSTATUS return types. This is a 32bit value which can hold many different error codes and severity levels. The value STATUS_SUCCESS (which is zero) is used to indicate a successful return value. Other STATUS_xxx codes can be found in the ntstatus.h header file. Note that, if we return anything other than STATUS_SUCCESS from DriverEntry, our driver will fail to load - so make sure you get it right!

Building a Driver

Assuming that you have created your project directory, you need to create two more files which the DDK build utility requires in order to build your project. The first file you need to create is called "makefile" - note that there is no extension on this filename. Be very careful about this last point - if you have "Hide Extensions for known file types" enabled in your Windows settings then Notepad will automatically append a ".txt" extension onto your files without you realising.

!INCLUDE $(NTMAKEENV)\makefile.def

makefile should contain a single line (shown above) and must never be edited in any way. Usually the DDK samples include such a file, so you can copy it straight from there.

The second file you need to create is your main project file, called "sources", again with no file extension. The contents of this file direct the DDK build utility to your sourcecode files.

TARGETNAME = hello
TARGETPATH = obj
TARGETTYPE = DRIVER

INCLUDES   = %BUILD%\inc
LIBS       = %BUILD%\lib
SOURCES    = hello.c

sources contains two important pieces of information - the name of your driver, and a list of source-files. If you have more than one source-file, then they should be listed one-after-the-other on the same line, separated by a space. You have to be careful with filenames that contain spaces (i.e. use quotes), so it is easiest just to use simple names.

Building a driver is very simple once you have created a driver project. At this point you should have three files in your project directory - hello.c, sources and makefile. Open up the DDK Checked Build Environment (the DDK command-prompt) and type "build ":

C:\DRIVERS\HELLO> build

That's it! Assuming you installed the DDK correctly the driver will compile with no problems will be placed into the objchk/i386 directory.

Drivers and Services

Installing and executing a driver is quite a bit different than running a "normal" program from the command line. There is no concept of running a new process because a driver is a kernel module which lives permanently inside the system. Two steps are required to get a driver installed - registering the driver as a system service, and then starting the driver.

This is usually the area that confuses people who are not used to working with drivers. A driver is treated by Windows as a regular service which can be started and stopped just like any other service. The Windows component that controls all this is called the Service Control Manager. This component exposes an API which can be used to register, unregister, and start & stop drivers and regular Win32 services.

Actually registering a driver and starting it can be a little confusing if you've not done it before. But even if you are familiar with these concepts I advise anyone who hasn't done so to visit www.osronline.com and download their Driver Loader tool from the downloads section. This handy tool allows you to register, start, stop and unregister drivers, all from a single GUI.

Installing a Driver

There are two ways to register a driver as a system service. The first (and easist) is to use the CreateService API which is documented in MSDN. But basically all this does is create a few values in the Registry on your behalf. The second method is to manually create these values in the following registry location:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\<driver name>

You need to make a new subkey with the name of your service (in this case the service is a kernel driver), so you could call it "hello" for instance. Inside this registry key you need supply the name, path and start-type of the driver, using the following registry values:

Registry Value Description Example
DisplayName Name of your driver as it appears in the service list
hello
ImagePath Full NT-style path to the driver (or just the filename if it lives in system32\drivers).
\??\C:\DRIVERS\HELLO\hello.sys
Start How the driver can be started. For testing the recommended value is Demand (3), which indicates that the driver can only be started manually.

The other start values - Boot (0), System (1) and Auto (2) instruct Windows to load the driver at various points during the system startup.

The last value - Disabled (4) - prevents the driver from loading at all.

3
Type The type of service. Basic kernel drivers must have a value of "1" here.
1

Be very careful what value you give to the Start item. For testing purposes you should only ever use Demand (or Manual as it is also known). All the other options (System, Boot and Automatic) cause the driver to be loaded during boot-time - which can be fatal if your driver has a bug in it because you may never recover your system.

Note that a driver only needs to be registered once (it can only be registered once) - and can be started and stopped as many times as you like after this. I would advise you at this stage to use the OSR Driver Loader to register your driver.

Starting a Driver

Once a driver has been registered as a system-service, it can be loaded (and unloaded) using the Service Control Manager. You can start a driver programmatically using the StartService API call, but it is far easier to goto the command-prompt and type:

net start hello

The following output will then be displayed:

The hello service was started successfully.

Nothing else will appear to happen though because drivers don't (and can't) output any data to the console.

Note that at this point the driver has been loaded into kernel-space and your DriverEntry function has been executed. If you have gotten this far then you haven't blue-screened your computer - so congratulations! Maybe for the first time code that you have written is now in the area of system memory that is inaccessible to usermode programs (address range 0x80000000 and above).

Viewing Driver Output

For debugging purposes it is common for drivers to use the DbgPrint API to emit debug messages:

ULONG DbgPrint(const char *fmt, ...);

DbgPrint is very similar to the regular printf routine, the difference being that it can only be called from kernel mode. The debug messages, instead of displaying on the screen, are sent to the kernel debugger (usually WinDbg), assuming that one is attached.

However there are a variety of programs on the internet that can capture these kernel messages without having to use a debugger. One of the best is DebugView from SysInternals, shown above. Note that I am currently working on a version of my own (with complete source-code) which you will soon be able to download from this site.

Unloading a Driver

Stopping (and unloading) a driver is as simple as starting it:

net stop hello

However the driver we have built at this point will never unload, because we have omitted a tiny detail. The one thing we left out was the DriverUnload routine, which the service-control-manager calls when a driver is about to unload. This unload routine must be specified during DriverEntry if our driver is to be unloadable, and a pointer to the routine stored in the DriverObject:

#include <ntddk.h>

void DriverUnload(PDRIVER_OBJECT pDriverObject)
{
    DbgPrint("Driver unloading\n");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    DriverObject->DriverUnload = DriverUnload;
    DbgPrint("Hello, World\n");

    return STATUS_SUCCESS;
}

You will have to rebuild the driver and reboot your computer, but now you will be able to use "net start" and "net stop" as often as you like to start and stop the driver.

Catch22 Driver Loader

I have stated previously that it is possible to register and start a driver programmatically - i.e. from your own usermode application. The API is part of the Service Control Manager API - functions such as OpenSCManager, CreateService, StartService, StopService, CloseServiceHandle are all used together to achieve this goal.

The second sourcecode download at the top of this tutorial is a Win32 console application, which demonstrates how a program can extract a driver from it's own resources and drop it to disk. Once extracted it starts and then stops the driver before cleaning up afterwards - all using the Service Control Manager API.

This is a very useful little program which provides a good framework for usermode projects requiring the use of a device-driver, so take a look at the sources and give it a whirl - you should be able to replace the sample driver with your own version and carry on using the same program.

Related Reading

There are many books on Device Driver development for Windows. My favourite is "Windows NT Device Driver Development" by Peter Viscarola and W. Anthony Mason. Both of these authors work for Open System Resources and really know their stuff. Other books worth reading are listed below.

Programming the Microsoft Windows Driver Model 2nd Edition - by Walter Oney.Microsoft Windows Internals 4th Edition - by Mark Russinovich.Undocumented Windows 2000 Secrets - by Sven Schreiber.Undocumented Windows NT - by Prasad Dabak, Sandeep Phadke and Milind Borate.Windows NT/2000 Native API Reference - by Gary Nebbett.

All these books contain vital information for kernel programming and device-driver writers.

Conclusion

That pretty much covers the first steps necessary to build, install and start a device driver in Windows NT/2000/XP. Note that at no point have we needed to write complicated INF files (driver-installation files), nor have we needed to use driver signing wizards or complicated GUIs. These things are only necessary for more complex WDM drivers which have additional requirements for their installation. For our simple NT4 legacy driver we can avoid all this hassle and install the driver ourselves.

I strongly advise anyone doing driver development to invest in a separate debugging machine on which to test your drivers - it's just not worth risking your main development machine with a buggy driver. When a driver crashes, it really crashes - the infamous Blue Screen Of Death will become a very familiar site to you as you are learning device-driver programming - I guarantee it!

Well hopefully you have found this tutorial useful, if you have any feedback I'd love to hear it. Don't forget to check out the downloads at the top of this tutorial - they're very useful!