Programmers Reference - The Windows NT Device Driver Cookbook (1)
The Windows NT Device Driver Cookbook
A introduction to writing a device driver for the ConSys System.
The intention of this cook book is to give a introduction to the task of writing a device driver for the Windows NT system. This documentation relays heavily on code definitions made especially for writing the PC-DOCT device driver.
Overview:
There is two major problems when writing a device driver for Windows NT. When writing the first device driver, the learning curve is very steep indeed. This cookbook is written to try to lessen the curve somewhat. Before proceeding which this cookbook it is recommended that the reader reads the introduction in the Microsoft DDK help manual.
Using
the drvDEF.h definitions:
Because the definition of a device driver reassembles a class, I have chosen to take a object orientated approach when creating my driver libraries. To handle a device driver in a simple and consistent way, the functionality of the driver is based on two basic classes: The CDeviceDriver and the CQueuedDeviceDriver classes. For adding functionality to these classes, a number of macros has been defined.
The
basics:
When using the ConSys driver library there are a number of basic steps to be taken. It is crucial for development, that a empty driver with no functionality is created and tested. Afterwards additional functionality can be added. To write a empty device a number of steps has to be taken.
Define a driver class: Declare a empty driver class as descendant of either the CDeviceDriver for the CQueuedDeviceDriver classes. A queued driver uses a StartIo function for handling io, and the CDeviceDriver class relays on direct IO.
Declare the driver: In the main file of the driver the DriverEntry function must be declared. This is done by three macros as shown below:
DECLARE_DRIVER_ENTRY;
#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT,DriverEntry)
#end
The driverEntry function must also be implemented. This is done by the following statements:
BEGIN_IMPLEMENT_DRIVER_ENTRY
IMPLEMENT_DRIVER_CLASS(driverClass,
driverType, exclusive)
END_IMPLEMENT_DRIVER_ENTRY
where the driverType is the type of the driver to implement and exclusive is a Boolean defining the access for clients. See IoCreateDevice in the DDK for documentation on this. If the driver implements more than one device type, additional IMPLEMENT_DRIVER_CLASS statements can be added. For driver initialization a entry point map for each driver class is needed. When no functionality is implemented these maps should be empty:
BEGIN_ENTRY_POINT_MAP(driverClass)
END_ENTRY_POINT_MAP;
One small but important function, that must be important method that must be implemented is the GetinstanceName. This method must return the name that this intense want to be registered under. Typically the name would be something like \Device\driverClass_X, where X is the instance number.
Now compile, install and debug the driver as documented in the DDK.
Helper
classes.
When programming a device driver, there is two different kinds of memory. Allocated memory can either be from paged poll or from non paged pool. As some routines (ex. interrupts) must have data allocated in non paged pool, witch is expensive in RAM, a new new operator is implemented in the class CDrvClass. This new operator takes an extra argument, to specify the memory type. When declaring classes in a device driver please use this class as the base class. The class also contains a member variable m_status containing the status of the class.
There has also been defined two extra classes. One for handling Unicode strings (CUString), and one for handling access to the registry (CRegistry). These two classes are not very user friendly, but better than nothing.
Driver
initialization.
Initialization on of the class instance is done in two steps. First the driver allocated in non paged pool. Because the class is allocated in non paged pool, caution must be taken when defining member variables. Do not allocate large string, or other large object, but use pointers instead. When the driver has been allocated, and the constructor called, the CreateDevice method is called. It is in this method most of the driver initialization takes place. As long as the CreateDevice method returns TRUE, new instances of the driver class will bee initialized. Upon return from the CreateDriver method the m_status member is checked. If the m_status is different from STATUS_SUCCESS, the default value, no more instances of the driver class will be allocated, and the DriverEntry will exit with the m_status as return parameter.
The CreateDevice usually allocated hardware and other resources needed for this instance of the driver class. Depending on the CreateDevice may also initialize hardware.
Entry
point maps.
To further add functionality to the driver, IRP functions must be added. Below is a example of adding write capability to the device. The first thing to do is to declare the entry in the driver:
DECLARE_IRP_MJ_WRITE;
and then add a entry to the entry point map:
BEGIN_ENTRY_POINT_MAP(driverClass)
DECLARE_ENTRY_IRP_MJ_WRITE
END_ENTRY_POINT_MAP;
To implement the functionality, add a
NTSTATUS OnWrite(IN PDEVIE_OBJECT pDeviceObject, IN PIRP pIrp);
to the driver class. Implement in this method the functionality of the IRP function, as descried in the DDK.
At the moment the predefined macros for declaring IPR's is:
Entry map declaration macros | Scope entry macros | driverClass::XXX |
DECLARE_ENTRY_IRP_MJ_CLOSE | DECLARE_IRP_MJ_CLOSE | OnClose |
DECLARE_ENTRY_IRP_MJ_CREATE | DECLARE_IRP_MJ_CREATE | OnCreate |
DECLARE_ENTRY_IRP_MJ_READ | DECLARE_IRP_MJ_READ | OnRead |
DECLARE_ENTRY_IRP_MJ_WRITE | DECLARE_IRP_MJ_WRITE | OnWrite |
DECLARE_ENTRY_IRP_MJ_QUERY_INFORMATION | DECLARE_IRP_MJ_QUERY_INFORMATION | OnQueryInformation |
DECLARE_ENTRY_IRP_MJ_SET_INFORMATION | DECLARE_IRP_MJ_SET_INFORMATION | OnSetInformation |
DECLARE_ENTRY_IRP_MJ_CLEANUP | DECLARE_IRP_MJ_CLEANUP | OnCleanup |
Below is an example of the definition of the OnRead macros. To define macros for other IRP's use the same method:
#define DECLARE_IRP_MJ_READ(driverClass)\
_DECLARE_SCOPE_ENTRY(driverClass,
OnRead, IRP_MJ_READ)
#define DECLARE_ENTRY_IRP_MJ_READ \
DECLARE_ENTRY_POINT(IRP_MJ_READ,
IRP_MJ_READ)
Conclusion:
This is only a very brief introduction to the art of writing a device driver. The best way to write a driver is by example. It is recommended to look in the source of the PC-DOCT device driver for further information, and ideas-
References:
Last Modified 11 January 2019