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

Writing a Virtual Storport Miniport Driver (Part II)

This article is one in a series on writing virtual Storport miniport drivers for Windows. Please find links to each article in the series here:Part I Part II Part III

 

Storport is a welcome relief to storage driver writers wishing to develop a driver that exports a virtual device. This article expands on the development of a Virtual Storport Miniport Driver that we started in an earlier issue of The NT Insider. In this article, Part II, we'll discuss how we've decided to implement our Virtual Storport Miniport Driver, and then talk about the data structures and routines that we are implementing to make this driver work.

 

We're sure that there are a lot of you chomping at the bit to see real working code and while we'll whet your appetite here, we're not ready to reveal all the secrets yet (we want you to read the next issue of The NT Insider). So be patient, we're getting there....

 

Implementation of a Virtual Storport Miniport Driver

Before going further into our discussion of a Virtual Storport Miniport Driver, it would be a good idea to talk about the implementation strategy that we used to create the driver. We decided to break up the implementation into 2 distinct pieces. The "Virtual Storport Miniport Processing" part of the driver is the upperedge of the driver and is responsible for handling all the operations coming from Windows that any Storport Miniport would have to handle. The "SCSI Adapter/Device Handling" part of the driver implements the lower-interface, the specifics of our virtual adapter, and devices that the miniport is going to export. Why the distinction?

This implementation allows us to very easily change the underlying behavior of adapter/device handling without having to change the Virtual Storport Miniport Processing code. When a request comes into the Virtual Storport Miniport Processing code, it decides how to handle that request, and if needed, will call into the SCSI Adapter/Device Handling code via defined interfaces in order to retrieve any device/adapter specific information needed. If the SCSI Adapter/Device Handling part of the code needs to communicate with the upper layer, there are also defined interfaces so that the lower layer is isolated from knowing how the upper layer communicates with Storport. We want to emphasize that this implementation strategy was just a choice we made. You don't need to follow this same approach if, for some reason, it doesn't fit your needs or preferences.

 

The sample driver to be provided with this article is written to allow SCSI devices to come and go dynamically through the use of IOCTLs sent from a user mode application. This application communicates with the OSR Virtual Storport Miniport via Storport Service Requests and allows the user to add, delete, and list virtual SCSI file backed CDROMS or file backed disks.

 

Now that we've discussed how we're implementing our solution, let's dig into the code.

DriverEntry

As we dive deeper into implementing a virtual miniport, the first routine we encounter is the miniport driver's DriverEntry routine. As stated in the previous article, this is where the miniport will register itself with Storport, export the routines needed by Storport to interact with the miniport, and initialize any global data structures that be needed.

 

The miniport's implementation of DriverEntry is shown in Figure 1. Notice that it has exported its entry points and set up the sizes of the DevicExtension, LuExtension, and SrbExtentions.

NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
 NTSTATUS status = STATUS_SUCCESS;
 
    // Save away the valuable global data
    OsrDriverObject = DriverObject;
    if (RegistryPath->MaximumLength > 0) {
        if(RegistryPath->MaximumLength > sizeof(OsrRegistryPathBuffer)) {
            return STATUS_INSUFFICIENT_RESOURCES;
        }
        OsrRegistryPath.MaximumLength = RegistryPath->MaximumLength;
        RtlCopyUnicodeString(&OsrRegistryPath, RegistryPath);
    }
 
    // Set up information for StorPortInitialize.
    RtlZeroMemory(&OsrHwInitData, sizeof(VIRTUAL_HW_INITIALIZATION_DATA));
    OsrHwInitData.HwInitializationDataSize = sizeof(VIRTUAL_HW_INITIALIZATION_DATA);
    OsrHwInitData.HwInitialize             = OsrHwInitialize;       // Required.
    OsrHwInitData.HwStartIo                = OsrHwStartIo;          // Required.
    OsrHwInitData.HwFindAdapter            = OsrHwFindAdapter;      // Required.
    OsrHwInitData.HwResetBus               = OsrHwResetBus;         // Required.
    OsrHwInitData.HwAdapterControl         = OsrHwAdapterControl;   // Required.
    OsrHwInitData.HwFreeAdapterResources   = OsrHwFreeAdapterResources;
    OsrHwInitData.HwProcessServiceRequest  = OsrHwProcessServiceRequest;
    OsrHwInitData.HwCompleteServiceIrp     = OsrHwCompleteServiceRequest;
    OsrHwInitData.AdapterInterfaceType     = Internal;
    OsrHwInitData.MultipleRequestPerLu    = TRUE;
    OsrHwInitData.PortVersionFlags     = 0;
    OsrHwInitData.DeviceExtensionSize      = sizeof(OSR_DEVICE_EXTENSION);
    OsrHwInitData.SpecificLuExtensionSize  = sizeof(OSR_LU_EXTENSION);
    OsrHwInitData.SrbExtensionSize         = sizeof(OSR_SRB_EXTENSION) +
                 OsrUserGetSrbExtensionSize();
 
    status =  StorPortInitialize(DriverObject,
                                 RegistryPath,
                                 (PHW_INITIALIZATION_DATA)&OsrHwInitData,
                                 NULL);
 
    if (STATUS_SUCCESS!=status) {// Port driver said not OK?
         return status;
    }
    return status;
}

 

Figure 1 - Virtual Miniport Driver Entry

We've already talked about the purposes of each of the exported routines in our previous article, so let's talk about the defined extensions and their use in our implementation.

 

Storport Virtual Extensions

When declaring the driver as a Storport Virtual Miniport, the VIRTUAL_HW_INITIALIZATION_DATA structure (which is defined by Storport) allows the miniport to define the sizes of 3 different types of extensions: the device extension, logical unit extension, and the srb extension. Each of these extensions has a specific purpose and is defined in the following 3 sections.

 

OSR_DEVICE_EXTENSION

The device extension is similar to the device extension that a Windows driver would request when calling IoCreateDevice. This extension (defined by the implementer), is the appropriate place for the miniport to store its "adapter" specific information. Since the driver supports a virtual adapter, it doesn't have any hardware specific information to store here. However, this would be the place to store information needed by a driver in order to manage a SCSI adapter and devices that the driver will export in the future. For our implementation, the OSR_DEVICE_EXTENSION is defined as shown in Figure 2.

typedef struct _OSR_DEVICE_EXTENSION {
    ULONG                     MagicNumber;
    SCSI_ADAPTER_CONTROL_TYPE adapterState;
    SCSI_WMILIB_CONTEXT       WmiLibCtx;
    IO_SCSI_CAPABILITIES      Capabilities;
    PVOID                     PUserGlobalInformation;
    UNICODE_STRING            DeviceInterface;
    ULONG                     NodeNumber;
    LIST_ENTRY DeviceList;
    KSPIN_LOCK DeviceListLock;
    KEVENT     DeleteDevicesThreadStartEvent;
    KEVENT     DeleteDevicesThreadKillEvent;
    KEVENT     DeleteDevicesThreadDeadEvent;
    KEVENT     DeleteDevicesThreadWorkEvent;
    HANDLE     DeleteDevicesThreadHandle;
    BOOLEAN    KillThread;
} OSR_DEVICE_EXTENSION, *POSR_DEVICE_EXTENSION;

 

Figure 2 - OSR_DEVICE_EXTENSION

Where:

This structure will be allocated for the driver by Storport and passed into all the driver's functions that Storport calls via the routines exported via the driver's VIRTUAL_HW_ INITIALIZATION_DATA structure.

  • MagicNumber is a number that uniquely identifies the structure (used for debugging purposes)
  • AdapterState - is the current state of the SCSI adapter
  • WmiLibCtx - used for WMI processing (not currently implemented)
  • PUserGlobalInformation - a pointer to a data structure created by the SCSI adapter/device processing part of the implementation to maintain its context
  • DeviceInterface - pointer to device interface string
  • NodeNumber - not used
  • DeviceList,DeviceListLock used to maintain the list of SCSI devices currently exported by the Virtual Storport Miniport
  • DeleteDevicesThreadXXX - information used to maintain a thread used to clean up SCSI device data structures after a SCSI device has been removed from the system
  • KillThread - flag used to notify the DeleteDeviceThread that it is time to exit

 

The logical unit extension is used keep track of a SCSI device that the driver exports. As with any data structure its layout is defined by the implementer. For our implementation, the layout would be as shown in Figure 3.

typedef struct _OSR_LU_EXTENSION {
    UCHAR    DeviceType;
    ULONG....PathId;
    ULONG....TargetId;
    ULONG....Lun;
    BOOLEAN..Missing;
    PVOID....PDevExt;
    struct _OSR_VM_DEVICE*  OsrVmDevice;
} OSR_LU_EXTENSION, *POSR_LU_EXTENSION;

Figure 3 - OSR_LU_EXTENSION

Where:

DeviceType - is the type of SCSI device that this OSR_LU_EXTENSION defines

 

  • PathId - is the SCSI Bus number that this device resides on
  • TargetId - is the SCSI Target identifier uniquely describing this device on the SCSI bus
  • Lun - is the SCSI Logical Unit identifier uniquely describing this unit on the SCSI Target
  • Missing - is a flag indicating whether or not this device has been reported as missing to Storport
  • PDevExt - pointer to the OSR_DEVICE_ EXTENSION to which this device is attached
  • OSRVmDevice - pointer to an OSR_VM_DEVICE structure

The driver creates this structure to represent a SCSI device that the Storport Virtual Miniport exports. This would be, in our sample, either a CDROM or a Disk.

 

The OSR_LU_EXTENSION is allocated by Storport for all targets and luns, on the buses that the driver declared as being capable of supporting when we filled in the PORT_ CONFIGURATION_INFORMATION MaximumNumber OfTargets, NumberOfBuses, and MaximumNumberOf LogicalUnits fields in the miniports OsrHwFindAdapter callback. So if the miniport exported 1 bus with 8 targets, each supporting 1 lun, the total number of OSR_LU_EXTENSIONs pre-allocated by Storport would be 1x8X1, or a total of 8 OSR_LU_EXTENSIONs. While that number doesn't seem excessive, it could be if the miniport supported 8 buses, with 256 targets each of which supported 256 luns. So for this implementation, we keep the structure small and dynamically allocate OSR_VM_DEVICE structures as devices become available.

 

One thing to keep in mind is that even though Storport has allocated these structures on the driver's behalf, it doesn't necessarily mean that these devices are currently being exported by the driver (i.e. they have not been programmatically connected yet). Thus, when the driver is called at its OsrHwStartIo callback to handle a SRB_ FUNCTION_EXECUTE_SCSI request, it'll need to determine if the request is for a bus, target and LUN that's supported and presently exported by the miniport. We'll discuss this further when we describe SRB_FUNCTION_EXECUTE_SCSI request processing.

 

OSR_SRB_EXTENSION

Finally, the SRB (SCSI Request Block) extension is a structure allocated by Storport, and passed to the miniport in the SCSI_REQUEST_BLOCK SrbExtension field of a SRB whenever an SRB is delivered to the miniport. Its purpose is to allow it to store request-specific information while the SRB is being processed. The example driver's OSR_SRB_EXTENSION is defined as shown in Figure 4.

typedef struct _OSR_SRB_EXTENSION_VM {
....SCSIWMI_REQUEST_CONTEXT WmiRequestContext;
} OSR_SRB_EXTENSION_VM, *POSR_SRB_EXTENSION_VM;
 
typedef struct _OSR_SRB_EXTENSION {
....//
....// Used to queue the SRB to a worker thread for execution.
....//
....OSR_SRB_EXTENSION_VM  VMExtension;
 
....//
....// Start of User VM data;
....//
    UCHAR    UserData[1];
} OSR_SRB_EXTENSION, *POSR_SRB_EXTENSION;

 

Figure 4 - OSR_SRB_EXTENSION

Where:

 

 

  • VmExtension - is the Virtual Storport Miniport specific part of the structure and currently holds the WmiRequestContext. This part of the structure is currently unused.
  • UserData - marks the start of the SCSI Adapter/Device Processing specific part of the structure. If you take a look at Figure 1 you will notice that when we set the SrbExtensionSize field in the VIRTUAL_HW_INITIALIZATION_DATA block, we set it to "sizeof(OSR_SRB_EXTENSION) + OsrUserGetSrbExtensionSize()". This allows the SCSI Adapter/Device Processing specific part of the code to define additional space and that allocated space will begin at the field UserData.

Now that we've defined the main data structures that the driver uses, let's move on to the next step in getting the miniport driver initialized. That next step is the HwFindAdapter routine.

 

HwFindAdapter

HwFindAdapter is called after the driver has successfully registered with Storport in DriverEntry. The purpose of this function in a typical Storport driver is for the Storport Miniport to find and initialize its adapter. The Virtual Storport Miniport's job in this routine is particularly easy. Finding the adapter is certainly no problem. The only reason this miniport was loaded was because the PnP Manager found a PDO identified (thru the INF file provided), as an adapter that this miniport supports. As for performing the initialization, the driver is required to initialize the passed-in PORT_CONFIGURATION_INFORMATION structure with the information necessary to describe the adapter to Storport. Our driver's code for the HwFindAdapter routine is shown in Figure 5.

ULONG OsrHwFindAdapter(IN PVOID PDevExt,IN PVOID PHwContext,
   IN PVOID PBusInformation,   // Miniport's FDO.
   IN PVOID PLowerDO,          // Device Object beneath FDO.
   IN PCHAR PArgumentString,IN OUT PPORT_CONFIGURATION_INFORMATION PConfigInfo,
   IN PBOOLEAN PBAgain)
{
    POSR_DEVICE_EXTENSION    pDevExt = (POSR_DEVICE_EXTENSION) PDevExt;
    NTSTATUS    status;
 
    // Set up our base configuration information.
    PConfigInfo->VirtualDevice                  = TRUE;
    PConfigInfo->ScatterGather                  = TRUE;
    PConfigInfo->ResetTargetSupported           = TRUE;
    PConfigInfo->Master                         = TRUE;
    PConfigInfo->CachesData                     = FALSE;
    PConfigInfo->MaximumNumberOfTargets         = 8;
    PConfigInfo->NumberOfBuses                  = 1;
    PConfigInfo->Dma32BitAddresses              = FALSE;
    PConfigInfo->Dma64BitAddresses = SCSI_DMA64_MINIPORT_SUPPORTED;
    PConfigInfo->MaximumNumberOfLogicalUnits = SCSI_MAXIMUM_LOGICAL_UNITS;
    PConfigInfo->SynchronizationModel = StorSynchronizeFullDuplex;
    PConfigInfo->MapBuffers = STOR_MAP_ALL_BUFFERS;
 
    // Initialize our basic fields of the device extension.
    pDevExt->MagicNumber = OSR_DEVICE_EXTENSION_MAGIC;
    InitializeListHead(&pDevExt->DeviceList);
    KeInitializeSpinLock(&pDevExt->DeviceListLock);
    KeInitializeEvent(&pDevExt->DeleteDevicesThreadStartEvent,SynchronizationEvent,FALSE);
    KeInitializeEvent(&pDevExt->DeleteDevicesThreadKillEvent,SynchronizationEvent,FALSE);
    KeInitializeEvent(&pDevExt->DeleteDevicesThreadDeadEvent,SynchronizationEvent,FALSE);
    KeInitializeEvent(&pDevExt->DeleteDevicesThreadWorkEvent,SynchronizationEvent,FALSE);
 
    status = PsCreateSystemThread(&pDevExt->DeleteDevicesThreadHandle,
                     (ACCESS_MASK)0L, 0, 0, 0, DeleteDevicesThreadStart,
                     pDevExt);
 
    if(NT_SUCCESS(status)) {
        KeWaitForSingleObject(&pDevExt->DeleteDevicesThreadStartEvent,
                              Executive,KernelMode,FALSE,NULL);
    } else {
        pDevExt->KillThread = TRUE;
        return SP_RETURN_NOT_FOUND;
    }
 
    // Register our own device interface so that our user mode application
    // can connect to us.
    status = IoRegisterDeviceInterface((PDEVICE_OBJECT) PLowerDO,
        &GUID_OSR_VIRTUALMINIPORT,NULL,&pDevExt->DeviceInterface);
    if(!NT_SUCCESS(status)) {
       goto adapterNotFound;
    }
 
    // Our structures are not initialized.   Call Adapter/Device Layer to initialize
    // do its job.
    __try {
        status = OsrUserInitialize(PDevExt,(PDEVICE_OBJECT) PLowerDO,
           &pDevExt->PUserGlobalInformation,&pDevExt->NodeNumber);
 
        if(NT_SUCCESS(status)) {
            // Okay the user level is initialized.   Ask the user code to get the
            // scsi capabilities for the device.
            OsrUserGetScsiCapabilities(pDevExt->PUserGlobalInformation,
               &pDevExt->Capabilities);
        } else {
            RtlFreeUnicodeString(&pDevExt->DeviceInterface);
            goto adapterNotFound;
        }
        PConfigInfo->MaximumTransferLength = pDevExt->Capabilities.MaximumTransferLength;
        PConfigInfo->AlignmentMask = pDevExt->Capabilities.AlignmentMask;
        PConfigInfo->AdapterScansDown = pDevExt->Capabilities.AdapterScansDown;
    } __except(EXCEPTION_EXECUTE_HANDLER) {
        RtlFreeUnicodeString(&pDevExt->DeviceInterface);
        status = GetExceptionCode();
        goto adapterNotFound;
    }
 
    return SP_RETURN_FOUND;
 
adapterNotFound:
    pDevExt->KillThread = TRUE;
    KeSetEvent(&pDevExt->DeleteDevicesThreadKillEvent,IO_NO_INCREMENT,FALSE);
    return SP_RETURN_NOT_FOUND;
}

 

Figure 5 - HwFindAdpater Implementation

In the example code, the driver initializes the input PORT_CONFIGURATION structure (PConfigInfo) that defines the virtual adapter's capabilities to Storport. It initializes a listhead, a spin lock, and several events that are stored in the OSR_DEVICE_EXTENSION. Next, the driver registers a device interface that the user mode management application will use to find the driver. Next, the driver calls OSRUserInitialize and OsrUserGetScsiCapabilities routines in order to retrieve information from the lower-edge "SCSI Adapter/Device Handling part of the driver. . The function completes by returning SP_RETURN_FOUND to Storport to indicate that an adapter was "successfully discovered" by the function. . If an error occurs during initialization, the driver would return SP_RETURN_NOT_ FOUND.

 

Once this routine has completed successfully, the driver will next be called at its HwInitialize callback.

 

HwInitialize

The final step in getting the virtual adapter initialized is successfully handling the Storport call to its HwInitialize handler. As the name implies, the purpose of this routine is to initialize the adapter hardware and find all devices that the miniport driver is going to export. As for initializing hardware, the driver doesn't have any hardware to initialize, so it's only responsibility is to perform any software initialization that may be required. If, however, the miniport was going to communicate over the network or work with a file on disk, it might need to perform that initialization in this routine. Keep in mind that if the driver is being loaded early in the boot cycle, the resources it needs may not be available and will need to be connected to later on.

 

In terms of finding all the devices that the adapter is going to export, how that is done is dependent upon the implementation. In our example driver implementation, devices are programmatically added and deleted, so in this case, there is really nothing for the miniport driver to do here. Our sample driver's implementation of HwInitialize is shown in Figure 6.

BOOLEAN OsrHwInitialize(IN PVOID PDevExt)
{
 POSR_DEVICE_EXTENSION pDevExt = (POSR_DEVICE_EXTENSION) PDevExt;
 NTSTATUS    status;
 BOOLEAN     ok = TRUE;
 
    __try {
        status = OsrUserAdapterStarted(pDevExt->PUserGlobalInformation);
    } __except(EXCEPTION_EXECUTE_HANDLER) {
        status = GetExceptionCode();
    }
 
    if(!NT_SUCCESS(status)) {
        ok = FALSE;
    }
 
    // Enable our own device interface so that user mode applications can
    // communicate with us.
    if(ok) {
        status = IoSetDeviceInterfaceState(&pDevExt->DeviceInterface,TRUE);
        if(!NT_SUCCESS(status)) {
            OsrTracePrint(TRACE_LEVEL_ERROR,OSRVMINIPT_DEBUG_ADAPTER,
           (__FUNCTION__": Error calling IoSetDeviceInterfaceState %x.\n",status));
        }
    }
    return ok;
}

 

Figure 6 - HwInitialize Implementation

After the HwInitialize function is complete, the driver is finally in a state where it can handle SRB requests from Storport. These SRBs will be received and processed in the driver's declared HwStartIo handler and this routine will be described next.

 

HwStartIo

This routine is called to handle each SRB targeted at a bus, target, and lun that the miniport has indicated that it handles. Remember that Storport knows all the buses, targets and luns that the driver is capable of supporting because the driver defined them in its HwFindAdapter callback.

 

One thing to keep in mind is that being capable of supporting a device and having a device present are two different things. Since the driver is implementing a Virtual Storport miniport which handles the dynamic addition and deletion of devices, the driver has to be able to determine whether or not the device that Storport is targeting is actually present. That is where the DeviceList that is defined in the OSR_DEVICE_EXTENSION comes into play. When the driver receives a request, it searches the DeviceList in order to determine whether or not the targeted device is present. If it is, it handles the request. If however the targeted device is not found, it completes the received SRB with a SRB_STATUS_NO_DEVICE error to indicate no device is currently present at that position on the bus. The driver is happy, Storport is happy, and life is good.

 

So the question becomes: how do devices get added to the DeviceList? SCSI devices are added to the DeviceList when a user requests the driver to "Create a Device". The code contained in the SCSI Adapter/Device Handling layer will define the device being created and call the Virtual Storport Miniport Processing layer to tell it to create a new SCSI device by calling OSRSpCreateScsiDevice. The final step is the SCSI Adapter/Device Handling layer calling the routine OSRSPAnnounceArrival, which announces to Storport that a bus change has been detected causing Storport to issue SRB_FUNCTION_EXECUTE_SCSI/SCSIOP_INQUIRY to the driver for all supported buses, targets and luns. We'll get more into the details of adding devices in another article.

 

So, what types of requests will we be required to handle in the HwStartIo callback? The requests that the driver handles in our example implementation are described below. As for request types that we haven't listed, you can look at the documentation and decide whether or not they are important to your specific implementation.

 

 

  • SRB_FUNCTION_EXECUTE_SCSI - This function is used to tell the adapter to execute a SCSI request on the specified SCSI device, target and lun.
  • SRB_FUNCTION_IO_CONTROL - This function is used allow a driver to perform driver-defined I/O control requests.
  • SRB_FUNCTION_RESET_LOGICAL_UNIT - This function is called to perform a reset on the specified logical unit.
  • SRB_FUNCTION_RESET_DEVICE - This function is called to perform a reset on the specified Target

For our example implementation the HwStartIo handler is shown in Figure 7.

BOOLEAN OsrHwStartIo(IN PVOID PDevExt,IN PSCSI_REQUEST_BLOCK  PSrb)
{
    POSR_DEVICE_EXTENSION pDevExt = (POSR_DEVICE_EXTENSION) PDevExt;
    UCHAR    srbStatus = SRB_STATUS_INVALID_REQUEST;
    BOOLEAN  bFlag;
    NTSTATUS status;
    BOOLEAN  bSrbCompleted = TRUE;
 
    switch (PSrb->Function) {
        case SRB_FUNCTION_EXECUTE_SCSI:
            srbStatus = OsrVmExecuteScsi(pDevExt, PSrb, &bSrbCompleted);
            break;
 
        case SRB_FUNCTION_IO_CONTROL:
            srbStatus = OsrVmIoControl(pDevExt, PSrb);
            break;
 
        case SRB_FUNCTION_RESET_LOGICAL_UNIT:
            StorPortCompleteRequest(PDevExt,
                                    PSrb->PathId,
                                    PSrb->TargetId,
                                    PSrb->Lun,
                                    SRB_STATUS_BUSY);
            srbStatus = SRB_STATUS_SUCCESS;
            break;

        case SRB_FUNCTION_RESET_DEVICE:
            StorPortCompleteRequest(PDevExt,
                                    PSrb->PathId,
                                    PSrb->TargetId,
                                    SP_UNTAGGED,
                                    SRB_STATUS_TIMEOUT);
            srbStatus = SRB_STATUS_SUCCESS;
            break;

        default:
            srbStatus = SRB_STATUS_INVALID_REQUEST;
            break;
    } // switch (pSrb->Function)
 
    // If the SRB completed, notify StorPort
    if (bSrbCompleted) {
        PSrb->SrbStatus = srbStatus;
        StorPortNotification(RequestComplete, PDevExt, PSrb);
    }
return TRUE;
}

 

Figure 7 - HwStartIo Implementation

The first thing you should notice is that there is no completion status from this routine, only a BOOLEAN return. The return status indicates whether or not the driver has accepted the request, not whether or not it executed it. We will talk more about this later.

 

As you can see, the only two functions that are handled internally by this routine are SRB_FUNCTION_RESET_ LOGICAL_UNIT and SRB_FUNCTION_RESET_ DEVICE. For this implementation, they really don't mean much since there isn't hardware to reset as there would be in a traditional Storport driver. In a different implementation, these request types might be used to reset a miniport's device's internal queues.

 

The two other functions that the driver does handle are SRB_FUNCTION_EXECUTE_SCSI and SRB_ FUNCTION_IO_CONTROL.

 

SRB_FUNCTION_IO_CONTROL

This function is used allow a driver to perform I/O control requests sent to the driver by an application or other driver. Some IOCTL control codes are predefined by Microsoft, while others can be defined by the miniport itself. At present there are 13 IOCTL_SCSI_MINIPORT_XXXX IOCTLs defined in "storport.h" and 2 defined in "NTDDSCSI.H". Of those 15, only 2, IOCTL_SCSI_MINIPORT and IOCTL_SCSI_MINIPORT_NVCACHE are documented in the WDK. The others, while not specifically documented, can be seen in use in the "src\storage\class\disk\disk.c" and "src\storage\class\disk\diskwmi.c" modules located in the WDK.

 

We disregard IOCTL_SCSI_MINIPORT_NVCACHE for this article because the driver we are designing is not implementing a device for NVCACHE. That leaves us with IOCTL_SCSI_MINIPORT. This IOCTL is intended to allow special control functions to be sent to a miniport driver. The functions to be performed can be either system defined or defined by the miniport. There is only one problem with using this as a means of communicating with a miniport - it is called at IRQL DISPATCH_LEVEL.

 

In our implementation, we decided against using IOCTL_SCSI_MINIPORT to send private commands to the miniport because Storport sends these requests at IRQL DISPATCH_LEVEL. This would complicate processing, because running at elevated IRQL prevent the driver from using "Zw" operations to interact with the file system. We'll discuss how we sent private commands to the example driver later.

 

SRB_FUNCTION_EXECUTE_SCSI

SRB_FUNCTION_EXECUTE_SCSI is the most important function that the miniport handles, because this is the function thru which SCSI commands are delivered to devices that the driver supports. The include file "SCSI.H" in the WDK lists SCSIOP_XXXXX functions 0x00 thru 0x9F. Some of the defined operations are generic SCSI operations codes while others are disk, tape, CDROM, or other SCSI device specific operations. The SCSI device our miniport will export determines which operations the driver needs to support. Since our driver implements both a disk and CDROM device it needs to handle the following:

 

 

  • SCSIOP_READ - read from the device
  • SCSIOP_WRITE - write to the device
  • SCSIOP_TEST_UNIT_READY - is the device ready
  • SCSIOP_FORMAT_UNIT - format the device
  • SCSIOP_INQUIRY - inquire as to what the device is
  • SCSIOP_MODE_SENSE - read the specified mode page from the device
  • SCSIOP_READ_CAPACITY - get the devices capacity
  • SCSIOP_VERIFY - verify the device is ready
  • SCSIOP_READ_TOC - read the table of contents (CDROM only)

We will discuss the details of each of these functions in a future article in this series. However, as previously mentioned, one thing that the driver will have to do for every SRB_FUNCTION_EXECUTE_SCSI request that it receives is determine if the request is for a bus, target and LUN that's both supported and presently exported by the driver. Specifically, for each request, it needs to check:

 

 

  • Is this request for a bus, target, and lun that is supported by this miniport? This entails calling StorPortGetLogicalUnit, which will return the address of the Storport allocated OSR_LU_EXTENSION if present.
  • Is this request for a bus, target, and lun that is presently exported by this miniport? This entails looking through internal data structures to see if a "device" had been programmatically added to this location on the bus, target, and lun. For our implementation, the driver would look through the DeviceList in the OSR_DEVICE_EXTENSION for any device that has a matching bus, target, and lun. If it is found, the driver knows it can process the request for the device. If no device is present for the bus, target, and lun, at this time, the miniport indicates that the device is not currently present.

While any Storport miniport needs to determine if a request it receives is for bus/target/lun it supports, one thing that makes the our virtual Storport miniport a bit unique is that it supports dynamic device attachment and export. The process of this dynamic addition is described next.

 

Defining a SCSI Device

To enable Storport to determine which types of devices are connected to the SCSI bus exported by the miniport, Storport issues a SRB_FUNCTION_EXECUTE_SCSI request with the SCSI command SCSIOP_INQUIRY to each bus, target, and lun that the miniport declared as supported in its initialization of the PORT_CONFIGURATION_ INFORMATION structure (MaximumNumberOfTargets, MaximumNumberOfLogicalUnits, and NumberOfBuses) during its OsrHwFindAdapter callback. For each bus, target, and lun that the miniport decides is present (i.e. plugged in), at the time that the SCSIOP_INQUIRY was received, the miniport will respond to the request by filling in the input INQUIRYDATA block buffer as part of the request. A partial definition of the INQUIRYDATA block (a standard SCSI data structure, fully defined in SCSI.H) is shown in Figure 8.

typedef struct _INQUIRYDATA {
  UCHAR  DeviceType : 5;
  UCHAR  DeviceTypeQualifier : 3;
  UCHAR  DeviceTypeModifier : 7;
  UCHAR  RemovableMedia : 1;
  union {
    UCHAR Versions;
    struct {
      UCHAR  ANSIVersion : 3;
      UCHAR  ECMAVersion : 3;
      UCHAR  ISOVersion : 2;
    };
  };
  UCHAR  ResponseDataFormat : 4;
  UCHAR  CommandQueue : 1;
  UCHAR  TransferDisable : 1;
  UCHAR  LinkedCommands : 1;
  UCHAR  Synchronous : 1;
  UCHAR  Wide16Bit : 1;
  UCHAR  Wide32Bit : 1;
  UCHAR  RelativeAddressing : 1;
  UCHAR  VendorId[8];
  UCHAR  ProductId[16];
  UCHAR  ProductRevisionLevel[4];
  UCHAR  VendorSpecific[20];
  UCHAR  Reserved3[40];
} INQUIRYDATA, *PINQUIRYDATA;

 

Figure 8 - SCSI_INQUIRY Data Structure (Fields Omitted)

Here are a few fields that are interesting to our implementation:

 

 

  • DeviceType - This field describes the type of device being exported by the miniport. Valid values for this field are defined in "SCSI.H". The values that our driver uses are:
  • DIRECT_ACCESS_DEVICE (disks)
  • READ_ONLY_DIRECT_ACCESS_DEVICE (CDROMs)
  • DeviceTypeQualifier - This field indicates whether or not the device is present. The values for this field can be:
    • DEVICE_QUALIFIER_ACTIVE - the OS supports the device and it is present
    • DEVICE_QUALIFIER_NOT_ACTIVE - the OS supports the device but it is not present
    • DEVICE_QUALIFIER_NOT_SUPPORTED = the OS does not support this device
  • DeviceTypeModifier - This type modifier, if any, is defined by the SCSI specification; if no modifier exists, the value should be zero
  • RemovableMedia - This bit is set if the SCSI device supports removable media; for example, a floppy drive, tape, or CDROM device
  • Versions - This defines the SCSI version supported, values can be:
    • 0 - SCSI-1
    • 1 - CCS
    • 2 - SCSI-2
    • 3 - SCSI-3
  • ResponseDataFormat - According to the SCSI book referenced, this value should match the value specified in the Versions field above
  • LinkedCommands - TRUE if this device supports linked commands, FALSE otherwise
  • Synchronous - TRUE if this device supports synchronous transfers, FALSE otherwise
  • Wide16Bit - TRUE if this device supports 16-bit transfers, FALSE otherwise (i.e. if setting Wide32 to TRUE)
  • Wide32Bit - TRUE if this device supports 32-bit transfers, FALSE otherwise (i.e. if setting Wide16 to TRUE)
  • VendorId - Vendor name in ASCII. Data is left aligned and the unused bytes are filled in with ASCII blanks
  • ProductId - Product's name in ASCII. Data is left aligned and the unused bytes are filled in with ASCII blanks.
  • ProductRevisionLevel - Product's revision number in ASCII. Data is left aligned and the unused bytes are filled in with ASCII blanks.
  • VendorSpecific - Vendor specific data

For example, let's say that one of the devices that the driver exports is a CDROM. This would mean that it would initialize the INQUIRYDATA block as shown in Figure 9.

pInquiryData->DeviceType = READ_ONLY_DIRECT_ACCESS_DEVICE;
pInquiryData->DeviceTypeQualifier = DEVICE_QUALIFIER_ACTIVE;
pInquiryData->DeviceTypeModifier = 0;
pInquiryData->RemovableMedia = TRUE;
pInquiryData->Versions = 2;             // SCSI-2 support
pInquiryData->ResponseDataFormat = 2;   // Same as Version see SCSI book
pInquiryData->Wide32Bit = TRUE;         // 32 bit wide transfers
pInquiryData->Synchronous = TRUE;       // Synchronous commands
pInquiryData->CommandQueue = FALSE;     // Does not support tagged commands
pInquiryData->LinkedCommands = FALSE;   // No Linked Commands
RtlCopyMemory((PUCHAR) &pInquiryData>VendorId[0],OSR_INQUIRY_VENDOR_ID_CDROM,
    strlen(OSR_INQUIRY_VENDOR_ID_CDROM));
RtlCopyMemory((PUCHAR) &pInquiryData>ProductId[0],OSR_INQUIRY_PRODUCT_ID_CDROM,
    strlen(OSR_INQUIRY_PRODUCT_ID_CDROM));
RtlCopyMemory((PUCHAR) &pInquiryData->ProductRevisionLevel[0],OSR_INQUIRY_PRODUCT_REVISION,
    strlen(OSR_INQUIRY_PRODUCT_REVISION));
#if 0
RtlCopyMemory((PUCHAR) &pInquiryData->VendorSpecific[0],OSR_INQUIRY_VENDOR_SPECIFIC_CDROM,
    strlen(OSR_INQUIRY_VENDOR_SPECIFIC_CDROM));
#endif

Figure 9 - INQUIRYDATA for a CDROM Device

Whereas for a disk, the only field that would have to be different from the definition in Figure 9 is the DeviceType field, where would specify DIRECT_ACCESS_DEVICE.

 

As for what goes into the VendorId, ProductId, ProductRevisionLevel, and VendorSpecific fields, our example implementation used the values below, your implementation will vary:

 

#define OSR_INQUIRY_VENDOR_ID "OSRDisk"
#define OSR_INQUIRY_PRODUCT_ID "OSR_R_DISK"
#define OSR_INQUIRY_PRODUCT_REVISION "V2.0"
#define OSR_INQUIRY_VENDOR_SPECIFIC "OSR_DISK"
#define OSR_INQUIRY_VENDOR_ID_CDROM "OSRCdrm"
#define OSR_INQUIRY_PRODUCT_ID_CDROM "OSR_R_Cdrm"
#define OSR_INQUIRY_VENDOR_SPECIFIC_CDROM "OSR_Cdrm"

 

 

Now you may notice the "#if/#endif" in Figure 9 surrounding the calls to copy the VendorSpecific fields into the INQUIRYDATA block. It turns out that Windows SCSI drivers are only interested in the first 36 bytes of data (check out INQUIRYDATABUFFERSIZE in scsi.h). If the driver forgets this and gives back the vendor specific fields, it will get a nice Verifier bugcheck!

 

To summarize, Storport uses the SCSIOP request to determine which device is connected to a bus, target, and lun that the miniport defines as connected. Return of the proper information to Storport/Windows will result in building of the proper device stacks in the operating system to support the devices (i.e. disks, CDROMs) that we define.

 

Summary

We hope that you found this part of our continuing series on writing a Virtual Storport Miniport Driver helpful. In the next article, we'll describe the details of I/O request processing. And, yes... the whole series will culminate with presentation of the complete driver source code, which we'll make freely available via OSR Online. Now, get started on that Virtual Storport driver!

 

 

Related Articles
Writing a Virtual Storport Miniport Driver

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

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