OSRLogo
OSRLogoOSRLogoOSRLogo x Seminar Ad
OSRLogo
x

Everything Windows Driver Development

x
x
x
GoToHomePage xLoginx
 
 

    Thu, 14 Mar 2019     118020 members

   Login
   Join


 
 
Contents
  Online Dump Analyzer
OSR Dev Blog
The NT Insider
The Basics
File Systems
Downloads
ListServer / Forum
  Express Links
  · The NT Insider Digital Edition - May-June 2016 Now Available!
  · Windows 8.1 Update: VS Express Now Supported
  · HCK Client install on Windows N versions
  · There's a WDFSTRING?
  · When CAN You Call WdfIoQueueP...ously

I Wanna Be A Bus Driver Baby -- Writing Windows Bus Drivers Part II


NOTE:
It is assumed that the reader has some familiarity with Windows XP, PNP, the concept of Function Device Objects (FDOs) and Physical Device Objects (PDOs), and of “.INF” files.

 

This article is the second in a series on how to write a driver for Windows that implements the functionality of a bus driver.  This article will deal with the following issues:

 

  1. Distinguishing a FDO request from a PDO request
  2. DriverEntry and AddDevice
  3. IoRegisterDeviceInterface

 

 

Distinguishing a FDO request from a PDO request

One of the first big issues that the bus driver writer must address is the creation and management of two different types of device objects.   On the one hand, we have the Functional Device Object (FDO), which may or may not be receiving Non-PNP IRPs, depending on the design and purpose of your driver.   On the other hand, we have Physical Device Objects (PDOs), which have been created to represent devices that have been found on our “bus” (and could also be responsible for processing Non-PNP IRPs).   So how do we distinguish between the two types of objects?    The simplest solution is to have a common structure at the start of both the FDO and PDO device extensions that a dispatch routine could look at in order to determine which object the IRP was targeted to.   That common structure may look something like that shown in Figure 1a.

 

Typedef struct _COMMON_DEV_EXT {

       BOOLEAN IsFdo;

} COMMON_DEV_EXT, *PCOMMON_DEV_EXT;

 

Figure 1a – Common Structure for FDO and PDO Device Extensions

 

In which case our driver dispatch routines need only look like Figure 1b.

 

NTSTATUS FdoDispatchRoutine(PDEVICE_OBJECT PDevObject, PIRP PIrp) {

 

  PDEV_EXT devExt = (PDEV_EXT) PDevObject->DeviceExtenstion;

 

  If(devExt->CommonDevExt.IsFdo) {

  return FdoDispatchRoutine(PDevObject,PIrp);

 }

 

 // Do Pdo functionality…….

 

 return status;

}

 

Figure 1b – Dispatch Routine

 

While this is a workable solution, it is not the most flexible.  It is conceivable that the bus driver writer may want to have different processing routines for the different types of PDOs that may be found on the bus.   Consider our example in Part I of this article concerning the “FredBus” that could have PDOs presenting either the “Fred Toaster” or “FredOven” device.   If I want different processing routines for the Toaster and the Oven devices, my PdoDispatch routine is going to have to have some additional code to determine which type of PDO is targeted so that it can handle it accordingly. 

 

A better solution may be to use the following common device extension (Figure 2a) at the start of all PDO and FDO device extensions.

 

Typedef struct _COMMON_DEV_EXT {

       BOOLEAN IsFDO;

PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];

} COMMON_DEV_EXT, *PCOMMON_DEV_EXT;

 

Figure 2a – Common Structure for FDO and PDO Device Extension (Revised)

 

 

In which case our driver dispatch routines only have to do what is shown in Figure 2b.

 

NTSTATUS FdoDispatchRoutine(PDEVICE_OBJECT PDevObject, PIRP PIrp) {

 

  PDEV_EXT devExt = (PDEV_EXT) PDevObject->DeviceExtension;

  PIO_STACK_LOCATION curStack = IoGetCurrentIrpStackLocation(PIrp);

 

  return *(devExt->CommonDevExt.MajorFunction[curStack->MajorFunction])(PDevObject,PIrp);

}

 

Figure 2b – Dispatch Routine (Revised)

 

 

So for the bus driver that we will be writing in this series of articles, we will be using the following common device extension (Figure 3) that will precede the specific portions of the device extensions to be used for the FDO and PDOs that we will create.

 

Typedef struct _COMMON_DEV_EXT {

  // Magic Number used to validate structure…

  ULONG   MagicNumber;

 

  // A backpointer to the device object for which this is the extension

  PDEVICE_OBJECT      Self;

 

  // This flag helps distinguish between PDO and FDO

  BOOLEAN             IsFDO;

 

  // We track the state of the device with every PnP irp

  // that affects the device through this variable.

  DEVICE_PNP_STATE    DevicePnPState;

   

  // Stores the current system power state

  SYSTEM_POWER_STATE  SystemPowerState;

 

  // Stores current device power state

  DEVICE_POWER_STATE  DevicePowerState;

 

  // Function Dispatch Table

  PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION+1];

 

} COMMON_DEV_EXT, *PCOMMON_DEV_EXT;

 

Figure 3– Our Bus Driver Common Device Extension

 

 

 

Also notice that we have used the COMMON_DEV_EXT to contain some information that is necessary for all PnP devices, e.g., SYSTEM_POWER_STATE, DEVICE_POWER_STATE, and our DEVICE_PNP_STATE which is an enumeration that I have created to keep track of our devices PnP state.  It is defined in Figure 4.

 

typedef enum _DEVICE_PNP_STATE {

 

    NotStarted = 0,         // Not started yet

    Started,                // Device has received the START_DEVICE IRP

    StopPending,            // Device has received the QUERY_STOP IRP

    Stopped,                // Device has received the STOP_DEVICE IRP

    RemovePending,          // Device has received the QUERY_REMOVE IRP

    SurpriseRemovePending,  // Device has received the SURPRISE_REMOVE IRP

    Deleted                 // Device has received the REMOVE_DEVICE IRP

 

} DEVICE_PNP_STATE;

 

When we get around to talking about our PnP state machine, you will see that all the enums defined here will match the PnP states that our state machine will contain.

 

Driver Entry and AddDevice

Now that we have decided about how we’re going to distinguish between our FDO and PDOs, it is time to start laying the foundation for our driver.  Naturally, the two functions that we’re going to have to add for our driver are DriverEntry and AddDevice.  As all driver writers know, DriverEntry is the first routine called when the driver is loaded and its purpose is to export all the necessary entry points that the driver needs in order to accomplish its goals.  It must also do any driver wide initialization that may be necessary, for example allocating memory to store the received RegistryPath for later use.  For our driver, DriverEntry is shown in Figure 5.

 

NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath)

{

 

#ifdef WPP_DRVTRACING

    WPP_INIT_TRACING(DriverObject, RegistryPath);

#endif WPP_DRVTRACING

 

    DbgPrint("\nOSRToasterOvenDriver Driver -- Compiled %s %s\n",__DATE__, __TIME__);

    OsrKdPrint_Def (OSRDBG_SS_INFO, ("Driver Entry\n"));

 

#if DBG

    DbgBreakPoint();

#endif

 

    //

    // Save the RegistryPath for WMI.

    //

 

    GRegistryPath.MaximumLength = RegistryPath->Length +

                                          sizeof(UNICODE_NULL);

    GRegistryPath.Length = RegistryPath->Length;

    GRegistryPath.Buffer = (PWCHAR) ExAllocatePoolWithTag(

                                       PagedPool,

                                       GRegistryPath.MaximumLength

                                       ‘gerO’);    

 

    if (!GRegistryPath.Buffer) {

 

       OsrKdPrint_Def(OSRDBG_SS_ERROR,("DriverEntry. STATUS_INSUFFICIENT_RESOURCES\n"));

        return STATUS_INSUFFICIENT_RESOURCES;

    }

   

    RtlCopyUnicodeString(&GRegistryPath, RegistryPath);

 

    //

    // Set entry points into the driver.

    //

 

    DriverObject->MajorFunction[IRP_MJ_CREATE] = OsrCreate;

    DriverObject->MajorFunction[IRP_MJ_CLOSE] = OsrClose;

    DriverObject->MajorFunction[IRP_MJ_PNP] = OsrPnP;

    DriverObject->MajorFunction[IRP_MJ_POWER] = OsrPower;

    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = OsrIoCtl;

    DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = OsrSystemControl;

    DriverObject->DriverUnload = OsrDriverUnload;

    DriverObject->DriverExtension->AddDevice = OsrAddDevice;

 

    return STATUS_SUCCESS;

}

 

Now that we have that done, we can move on to our AddDevice routine.    AddDevice is called when the PnP Manager has determined that a PDO, that it has been notified about, has been determined to be owned by the driver being called.   The PDO could be for an actual physical device, if we were servicing a physical adapter or it could be for a virtual physical device that the PnP Manager created for us if our “.INF” file said that we were “ROOT” enumerated.  The purpose of AddDevice (shown in Figure 6) is to create a FDO, representing the functionality that our device, and attach it to the PDO which represents the physical side of our device.   Together the FDO and PDO represent an “operational device” or a DEVNODE (Device Node).  Remember, because we are attached to the PDO, we are actually a filter driver of the PDO, therefore we will see all the PnP and Power IRPs that were targeted at the PDO.  Is this a good thing?   Sure it is.  The functionality of a device is affected by changes in its physical state.

 

NTSTATUS OsrAddDevice(PDRIVER_OBJECT DriverObject,PDEVICE_OBJECT PhysicalDeviceObject)

{

    NTSTATUS                    status;

    PDEVICE_OBJECT              deviceObject;

    PFDO_DEVEXT                 pFdoData;

 

    OsrKdPrint_Def (OSRDBG_PNP_INFO,("Add Device %d: 0x%x\n",OsrDeviceCount+1,

                    (unsigned int) PhysicalDeviceObject));

 

    status = IoCreateDevice (

                    DriverObject,               // our driver object

                    sizeof (FDO_DEVEXT),        // device object extension size

                    NULL,                       //

                    FILE_DEVICE_CONTROLLER,     // We are a controller

                    0,                          // No special characteristics

                    FALSE,                      // our FDO is not exclusive

                    &deviceObject);             // The device object created

 

    if (!NT_SUCCESS (status)) {

 

      OsrKdPrint_Def(OSRDBG_ERROR,("OsrAddDevice Exit IoCreateDevice status. %x\n",

                     status));

      return status;      

 

    }

 

    //

    // Clear out our device extension;

    //

 

    pFdoData = (PFDO_DEVEXT) deviceObject->DeviceExtension;

    RtlZeroMemory (pFdoData, sizeof (FDO_DEVEXT));

 

    //

    // We've successfully created our device, increment our device count.

    //

    InterlockedIncrement(&OsrDeviceCount);

 

    //

    // Indicate that we support Direct I/O for Read and Write.

    //

    deviceObject->Flags = DO_DIRECT_IO;

 

    //

    // Mark the initial state of the FDO

    //

    pFdoData->CommonDevExt.DevicePnPState = NotStarted;

    pFdoData->CommonDevExt.MagicNumber = FDO_DEVEXT_MAGIC_NUMBER;

    pFdoData->CommonDevExt.IsFDO = TRUE;

    pFdoData->CommonDevExt.Self = deviceObject;

 

    //

    // Set the initial powerstate of the FDO

    //

    pFdoData->CommonDevExt.DevicePowerState = PowerDeviceUnspecified;

    pFdoData->CommonDevExt.SystemPowerState = PowerSystemWorking;

 

    //

    // Initialize our FunctionDispatch Table.

    //

    for(ULONG fIndex = 0; fIndex < xxx; fIndex++) {

      pFdoData->CommonDevExt.MajorFunction[fIndex] = OsrDefaultDispatch;

    }

    pFdoData->CommonDevExt.MajorFunction[IRP_MJ_CREATE] = OsrFdoCreate;

    pFdoData->CommonDevExt.MajorFunction[IRP_MJ_CLOSE] = OsrFdoClose;

    pFdoData->CommonDevExt.MajorFunction[IRP_MJ_PNP] = OsrFdoPnP;

    pFdoData->CommonDevExt.MajorFunction[IRP_MJ_POWER] = OsrFdoPower;

    pFdoData->CommonDevExt.MajorFunction[IRP_MJ_DEVICE_CONTROL] = OsrFdoIoCtl;

    pFdoData->CommonDevExt.MajorFunction[IRP_MJ_SYSTEM_CONTROL] = OsrFdoSystemControl;

 

    //

    // Initialize our listhead and locks...

    //

    ExInitializeFastMutex (&pFdoData->Mutex);

    InitializeListHead (&pFdoData->PDOs);

 

    // Set the PDO for use with PlugPlay functions

    pFdoData->UnderlyingPDO = PhysicalDeviceObject;

 

    //

    // Biased to 1. Transition to zero during remove device

    // means IO is finished. Transition to 1 means the device

    // can be stopped.

    //

    pFdoData->OutstandingIO = 1;

 

    //

    // Initalize the remove event to Not-Signaled.  This event

    // will be set when the OutstandingIO will become 0.

    //

    KeInitializeEvent(&pFdoData->RemoveEvent,

                  SynchronizationEvent,

                  FALSE);

 

    // 

    // Initialize the stop event to Signaled:

    // there are no Irps that prevent the device from being

    // stopped. This event will be set when the OutstandingIO

    // will become 1.

    //

    KeInitializeEvent(&pFdoData->StopEvent,

                      SynchronizationEvent,

                      TRUE);

   

 

    //

    // Initialize IRP Queue.

    //

    InitializeListHead(&pFdoData->IrpQueue);

    KeInitializeSpinLock(&pFdoData->IrpQueueLock);

    KeInitializeSpinLock(&pFdoData->SpinLock);

 

    //

    // Tell the PlugPlay system that this device will need a

    // device interface.

    //

    status = IoRegisterDeviceInterface (

                PhysicalDeviceObject,

                (LPGUID) &GUID_OSRTOASTEROVEN _BUS_ENUMERATOR,

                NULL,

                &pFdoData->InterfaceName);

 

    if (!NT_SUCCESS (status)) {

        OsrKdPrint_Def(OSRDBG_ERROR,

                      ("AddDevice: IoRegisterDeviceInterface failed (%x)", status));

        InterlockedDecrement(&OsrDeviceCount);

        IoDeleteDevice (deviceObject);

        return status;

    }

 

    //

    // Attach our filter driver to the device stack.

    // The return value of IoAttachDeviceToDeviceStack is the top of the

    // attachment chain.  This is where all the IRPs should be routed.

    //

    pFdoData->NextLowerDriver = IoAttachDeviceToDeviceStack (

                                    deviceObject,

                                    PhysicalDeviceObject);

 

    //

    // Register with WMI

    //

    status = OsrWmiRegistration(pFdoData);

 

    //

    // Make sure that we successfully registered.   If we did then set the

    // rest of the needed bits and get out of here....

    //

    if (!NT_SUCCESS (status)) {

        OsrKdPrint_Def(OSRDBG_ERROR,

                      ("AddDevice: OsrWmiRegistration failed (%x)\n", status));

        InterlockedDecrement(&OsrDeviceCount);

        IoDetachDevice (pFdoData->NextLowerDriver);

        IoDeleteDevice (deviceObject);

 

    } else {

        deviceObject->Flags |= DO_POWER_PAGABLE;

        deviceObject->Flags &= ~DO_DEVICE_INITIALIZING;

 

        OsrKdPrint_Def (OSRDBG_ERROR,("AddDevice: OsrWmiRegistration Succeeded\n"));

    }

 

    return status;

}

 

When we call IoCreateDevice to create the FDO, you can see that for the second parameter we pass in sizeof(FDO_DEVEXT).   This is the Device Extension for our FDO which contains the COMMON_DEV_EXT (located as the first element of the structure) that both our FDO and subsequent PDOs will contain.  In our FDO device extension we have added an OutstandingIO count that we set to “1”.  We use this count in conjunction with the StopEvent and RemoveEvent variables in order to allow us to be able to detect whether all our outstanding IO requests have completed.  This, in turn, allows us to properly handle IRP_MJ_PNP, IRP_MN_QUERY_STOP and IRP_MN_QUERY_REMOVE requests.   Whenever we have to pass a request to a driver below us, we increment the OutstandingIO by one and if the new count is 2 we reset the signal state of the Stop and RemoveEvents to non-signaled.   Whenever an outstanding IO completes we’ll decrement the OutstandingIO count.   Whenever the decremented count is 1, we set the StopEvent, and if the decremented count is 0 we set the RemoveEvent.

 

The last thing that I want to talk about in this article is IoRegisterDeviceInterface

 

IoRegisterDeviceInterface

Microsoft added IoRegisterDeviceInterface in Windows 2000 to correct a problem that the style of naming used in NTV4 could not address.   As you probably remember in NTV4, device driver writers used a convention whereby their device objects where named DevX where X is a number starting at 0 and going to N.   When a user wanted to find out how many devices there were, they had to start at Dev0 and continue enumerating devices until they tried to open up a device that did not exist.   With PnP, this style of naming does not work because it could be possible that Dev0 and Dev2 exist but Dev1 doesn’t exist because it was removed.  Also, with the old style of naming it was not easy for developers of devices that offered the same services to allow their users to easily switch their applications to use another vendor’s device without changing the names that the application used in order to open the device.     

 

So Microsoft took an idea from the Component Object Model (COM) and added it to device driver writing.   Essentially, with IoRegisterDeviceInterface, a device registers using a GUID (Globally Unique Identifier) of the type of device it represents.  It could be a common GUID like GUID_DEVINTERFACE_COMPORT or GUID_DEVINTERFACE_DISK or it could be a custom one like GUID_DEVINTERFACE_OSRTOASTER.    Devices that register using these interface GUIDS agree to support the functionality that they imply.  For example registering with GUID_DEVINTERFACE_COMPORT implies that the registrant supports all the functionality that a COM port must provide in Windows while registrants of GUID_DEVINTERACE_OSRTOASTER agree to support all the functionality that being an OSRTOASTER implies (We don’t let the toast get burnt or stuck in the toaster!).   Users that want to use a device that supports a specific type of interface can use the new SetUpDi interface in order to enumerate and open up all devices that register supporting a specific type of device interface.   What’s also nice is that unlike NTV4 style device naming, PnP device interfaces have state either “enabled” or “disabled”.   A PnP driver can enable the interface when it is ready to receive user requests and can disable it if goes into a start where it does not want to receive user requests.   Is that nice or what?

 

Summary

In this article we talked about continuing the development of our virtual bus driver by discussing: Distinguishing a FDO request from a PDO request, DriverEntry and AddDevice, and IoRegisterDeviceInterface.   All three of these topics are necessary to discuss in order to develop a proper and functional Bus Driver.

Related Articles
So You Wanna Be a Bus Driver? - Writing Bus Drivers for Windows 2000

User Comments
Rate this article and give us feedback. Do you find anything missing? Share your opinion with the community!
Post Your Comment

"Not enough and integrated"
In your first article, you said:

"Summary for Part I ... 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."

so, where is your ".INF" file description ? I want to know what is the difference between the bus driver ".INF" file and that of function driver. And We would like you provide us the whole source code of the article example. Thank you.

Rating:
11-Sep-05, hary wang


"Bus Driver FDO creation"
So my bus has created and enumerated PDO's for each child. Why is DriverEntry called for each PDO prior to calling AddDevice and is there a mechanism to simply call AddDevice for requesting each subsequent child PDO to create a FDO?

Rating:
20-May-03, Delmont Fredricks


Post Your Comments.
Print this article.
Email this article.
bottom nav links