Note: It is assumed that the reader has some familiarity with Windows 2000, PNP, the concept of Functional Device Objects (FDOs) and Physical Device Objects (PDOs), and of “.INF” files
This article is the first in a series of articles on how to write a driver for Windows 2000 that implements the functionality of a bus driver. A bus driver in Windows 2000 is a driver that manages a “bus” and its associated physical devices. Of course, this could be a real, honest to goodness, hardware bus. But what’s really cool about bus drivers is that the “bus” that they manage might not even be any sort of bus, in any conventional sense of the word. Rather, creating a bus driver will allow one driver to easily connect with and control the number of instances of a higher-level driver in the system.
Consider the Microsoft Disk Class driver. This driver actually acts as both a Function driver and a bus driver. In its role as a Function driver, it creates a Functional Device Object (FDO) for each disk on the system. It then proceeds to act as a bus driver, creating a Physical Device Object (PDO) for each partition on each disk.
So, bus drivers can really be very useful things in Win2K, when you think about them “right”. The bus and its devices can be actual physical hardware or virtual hardware, or some abstract software construct. In each case, the bus driver is responsible for enumerating the bus, handling bus related events (insertions, removals, etc.) and managing power state.
Of course, supporting a new hardware bus will require creating a bus driver. One of the significant changes in the Windows 2000 architecture was that the bus management functions were removed from the Hardware Abstraction Layer (HAL) and isolated into the bus drivers. Thus, support for new buses can be added to Win2K without having to change the HAL!
For this article, we are going to discuss creating a Virtual Bus driver that supports the mythical Fred Bus. Our Fred Bus is a virtual bus that supports two virtual devices, the Fred Toaster and Fred Oven devices. Figure 1 illustrates our bus.
Figure 1 – The Fred Bus
Our goal for this series of articles is to demonstrate the creation of a rudimentary bus driver that will give you enough information so that you can create your own bus driver should the need present itself.
The first thing that we’re going to have to do is create a driver for the Fred Bus. Our driver will be a WDM driver and have a standard “.INF” file. This INF file will ensure that Win2K loads our driver when it enumerates the root bus. This driver will be responsible for creating the FDO that is the instantiation of our Fred Bus driver and the PDO representing the Fred Toaster and the Fred Oven virtual devices (See Figure 2). A PDO is a Device Object that a bus driver creates to represent a device on the bus. In our case the Fred Toaster and Fred Oven devices. Once these PDOs are created, the Fred Bus driver must notify the Plug and Play Manager to enumerate the Fred Bus. This is done by calling IoInvalidateDeviceRelations(), which notifies the PnP Manager that the relations for a device have changed. The types of device relations include Bus Relations, Ejection Relations, Removal Relations, and the Target Device Relation. For our purposes, we will call IoInvalidateDeviceRelations() with Bus Relations, and the device object we pass to the PnP Manager is the PDO to which our Fred Bus driver is attached. This will result in the PnP Manager calling our driver with an IRP_MJ_PNP request that has a minor function of IRP_MN_QUERY_DEVICE_RELATIONS for our bus driver FDO. This request is described below in Figure 2.
Figure 2 – Device Object Relationships
The PnP Manager makes this call to query a device's BusRelations() (child devices) when the device is enumerated and at other times while the device is active, For example, this interface is called when a driver calls IoInvalidateDevice Relations() to indicate that a child device (PDO) has arrived or departed (as we did in the Fred Bus driver). The PnP Manager passes in the old set of device relations, which may or may not already exist The Fred Bus driver is expected to determine whether or not the list needs to be modified, because devices may have been added or removed. Once we are finished with the list, we are then expected to pass the request down to the Device Object below us (in the case of our Fred Bus driver, the PDO we are attached to) so that it may also have a chance to modify the list. For our example driver, we are going to tell the PnP Manager about the two PDOs we have created: one each for the Fred Toaster and Fred Oven devices.
Once all the drivers in the chain have had a chance to handle this request and our bus driver has completed it successfully, the PnP Manager will issue the following IRP_MJ_PNP requests to gather information about the child devices (i.e. PDOs) enumerated by this request:
Note that the Fred Bus driver will receive more IRP_MJ_PNP requests than are described in this article and will return to the appropriate status for the requests that it does not handle.
The following sections describe the requests that our Fred Bus driver will receive on behalf of its child PDOs. The Fred Bus driver is expected to return information that adequately describes the device to the PnP Manager. Unfortunately, knowing all of the information to return is not always easy if you are trying to imitate some standard device because it is frequently not documented in the DDK. In this case, you will be required to determine the standard behavior for the device, such as by using a filter driver over an existing implementation to observe its behavior and imitating this behavior in your driver. In our sample, the returned information is easy because we are not trying to imitate anything.
In addition, the Fred Bus driver must be able to distinguish which requests are to the FDO and which requests are to the PDOs. This can be done by having a common field at the same offset in the FDO and PDO device objects that indicates which type of Device Object this is. Then each of your dispatch routines can branch to the appropriate routine in order to service requests for the FDO or PDOs.
The PnP Manager calls the driver with this request when it wants to determine BusQueryDeviceID, BusQuery HardwareIDs, BusQueryCompatibleIDs, or the BusQuery InstanceID for a child device. Let’s talk about each one.
This query is how the driver tells the PnP Manager the Hardware IDs that identify the specific device. These IDs are used by Win2K to search for the correct entry in “.INF” files and the registry so that it knows what drivers to load to service the discovered hardware. In our example, we are going to return unique IDs for the Fred Toaster (L"FredBus\\FbToaster\0") and Fred Oven (L"FredBus\\FbOven\0") devices. When our Fred Bus driver gets loaded and creates the FbToaster and FbOven PDOs, the PnP Manager will look for drivers to service those devices.
This query is where you tell the PnP Manager the Device ID the unique string to identify your device. This can be the same as the hardware IDs (which requires a REG_MULTI_SZ). In a list of hardware IDs for a device, Device ID is the most specific and should be first in the list.
This query is how the driver tells the PnP Manager the Compatible Hardware IDs that identify the specific device device. These IDs are another thing that Win2K searches for in “.INF” files registry so that it knows what drivers to load to service the discovered hardware. In our example, we are going to return unique IDs for the Fred Toaster (L"FredBus\\CompatibleFbToaster\0") and Fred Oven (L"FredBus\\CompatibleFbOven\0") devices. Why would you need compatible IDs? Consider a device that supports some proprietary sound interface and a Sound Blaster interface, this device could list the Sound Blaster ID as a compatible ID and load a Sound Blaster driver if your device driver is not registered.
This query is how the driver tells the PnP Manager the unique Instance ID if you’re the specific device. In our example, we will just use a monotonically increasing number as our Instance ID.
The PnP Manager calls the driver with this request when it wants to get a PDO’s description (DeviceTextDescription()) or location (DeviceTextLocationInformation()) in the appropriate Localized format as selected by the input LCID value.
This query is where you tell the PnP Manager what your device is. The string returned from here is displayed in the "found new hardware" pop-up window if no INF match is found for the device. The strings that we will return will be L”FredBus_FbToaster_XX” and L”FredBus_FbOven_XX” where XX is the unique ID that we assigned to each instance of our devices.
This is a good one. This where you are supposed to tell the PnP Manager where your device is located. You could accomplish this by calling IoGetDeviceProperty() using the PDO to which the Fred Bus driver is attached in order to get the bus and slot number in which your device is located. However, for our example, the slot number isn’t going to mean much to the user considering there are more slot numbers on the bus that are for internal system components than are externally visible. Anyway, we can return some “English” information here just to be complete.
The PnP Manager sends this IRP to get the capabilities of a device, such as whether the device can be locked or ejected.
Function and filter drivers can handle this request if they alter the capabilities supported by the bus driver. Bus drivers must handle this request for their child devices.
One of the input parameters to this call is a pointer to a DEVICE_CAPABILITIES structure. Your driver may need to fill this in if appropriate for your particular device. Our example will demonstrate the information that can be returned. This will be done by supporting the Eject operation for toast, since the author hates burnt toast.
The PnP Manager calls the driver with this request when it wants to determine the devices instance and parent bus information. Your driver should return a pointer to a PNP_BUS_INFORMATION structure in the IoStatus.Information location of the IRP. (See example in Figure 3).
busInfo->BusTypeGuid = GUID_FRED_BUS_TYPE;
// Some buses have a specific INTERFACE_TYPE value,
// such as PCMCIABus, PCIBus, or PNPISABus.
// For other buses, especially newer buses like Fred Bus, the bus
// driver sets this member to PNPBus.
busInfo->LegacyBusType = PNPBus;
// This is a hypothetical bus
busInfo->BusNumber = 0;
Irp->IoStatus.Information = (ULONG_PTR)busInfo;
Figure 3 — IRP_MN_QUERY_BUS_INFORMATION
The PnP Manager uses this IRP to get a device's boot configuration resources. A bus driver must handle this request for their child devices that require hardware resources. If a bus driver returns a resource list in response to this IRP, it allocates a CM_RESOURCE_LIST from paged memory. The PnP Manager frees the buffer when it is no longer needed. If a device requires no hardware resources, the device's parent bus driver completes the IRP (IoCompleteRequest()) without modifying Irp->IoStatus.Status or Irp->IoStatus.Information. Since the Fred Bus driver devices require no resources, we just complete the request without modifying the Irp->IoStatus locations.
The PnP Manager uses this IRP to get a device's resource requirements list. The PnP Manager sends this IRP when a device is enumerated, prior to allocating resources to a device, and when a driver reports that its device's resource requirements have changed. A bus driver must handle this request for their child devices that require hardware resources. If a bus driver returns a resource list in response to this IRP, it allocates a CM_RESOURCE_LIST from paged memory. The PnP Manager frees the buffer when it is no longer needed. If a device requires no hardware resources, the device's parent bus driver completes the IRP (IoCompleteRequest()) without modifying Irp->IoStatus.Status or Irp->IoStatus.Information. Since the Fred Bus driver devices require no resources, we just complete the request without modifying the Irp->IoStatus locations.
The PnP Manager sends this IRP after the drivers for a device return success from the IRP_MN_START_DEVICE request. The PnP Manager also sends this IRP when a driver for the device calls IoInvalidateDeviceState(). Our sample bus driver that we are writing does not support this operation.
In the words of an OSR employee, “Here is something that I find particularly interesting”. The IRP_ MN_QUERY_INTERFACE request enables a driver to export a direct-call interface to other drivers. In the good old days, this type of interface exchange was done through a “roll your own” custom IOCTL between drivers, when they wanted to exchange function pointers.
A bus driver that exports an interface must handle this request for its child devices (child PDOs). Function and filter can optionally handle this request. An "interface" in this context consists of routine(s) and possibly data exported by a driver or set of drivers. An interface has a structure that describes its contents and a GUID that identifies its type. Since our Fred Bus driver doesn’t support a direct call interface, we will happily ignore this request.
The Basics, Continued
Now that we’ve discussed the IRPs that we are going to receive as part of being a bus driver, we need to discuss the IRPs that we’re not going to handle, but must do something with. The choices we have for these IRPs are to either succeed, ignore, or delegate them. Succeeding IRPs is easy as illustrated by example code in Figure 4.
NTSTATUS SuccessPnPRequest(PDEVICE_OBJECT Pdo, PIRP Irp)
Irp->IoStatus.Status = STATUS_SUCCESS;
Ignoring IRPs is simply also, as illustrated by the following example code:
NTSTATUS IgnorePnPRequest(PDEVICE_OBJECT Pdo,PIRP Irp)
NTSTATUS status = Irp->IoStatus.Status;
Figure 4 — Succeeding IRPs
Finally, Delegating a request is simple too, because all we are going to do is to forward the request to the driver below us, if necessary (See Figure 5).
NTSTATUS DelegatePnPRequest(PDEVICE_OBJECT Pdo,PIRP Irp)
PPDO_DEV_EXT pdoExt = (PPDO_DEV_EXT) Pdo->DeviceExtension;
PDEVICE_OBJECT fdo = pdoExt->FdoDeviceObject;
PFDO_DEV_EXT fdoExt = (PFDO_DEV_EXT) Fdo->DeviceExtension;
// We’re assuming here that the IRP that we have received has more than enough
// Stack locations in it, because when we created the PDO, we made sure it did.
irpStack = IoGetNextIrpStackLocation(Irp);
curIrpStack = IoGetCurrentIrpStackLocation(Irp);
*irpStack = *curIrpStack;
// Set up the fields in the IRP that need to be set up.
irpStack->DeviceObject = fdoExt ->LowerDeviceObject;
irpStack->FileObject = fdoExt ->LowerDeviceObject;
return IoCalldriver(fdoExt ->LowerDeviceObject,Irp);
Figure 5 — Delegating IRPs
Another thing to remember about implementing the Fred Bus driver is that we are going to be receiving requests for both our PDOs and FDO, so we need to determine which set up are Dispatch routines accordingly. Since we only have one Driver Object that contains all the Dispatch for the entire driver, it is necessary to have our dispatch routines determine whether or not the request is destined for a PDO or FDO. This can be accomplished simply by having a common area in the Device Extension of both the PDO and the FDO extensions that indicates what type of Device Object was input for the request. The user can then implement a simple if-then-else statement to jump to the correct handler for the FDO or PDO handler.
Summary for Part I
In this, the first article in a series of creating a Bus Driver for Windows 2000, we’ve touched upon several items of interest in understanding the basics. In our next article, we will start to actually develop the driver from scratch and create a “.INF” file that will allow us to load our driver.