52964.fb2 Writing Windows WDM Device Drivers - читать онлайн бесплатно полную версию книги . Страница 19

Writing Windows WDM Device Drivers - читать онлайн бесплатно полную версию книги . Страница 19

Chapter 18NT Hardware

This chapter looks at how to find, allocate, and use hardware resources in non-WDM drivers that do not support Plug and Play. I call these "NT style" drivers because they run in NT 3.51, NT 4, and Windows 2000. These drivers sometimes also work in Windows 98, slightly to my surprise.

I have already shown how to use the translated hardware resource assignments in the last two chapters. The WdmIo and PHDIo drivers use MmMapIoSpace to map I/O ports into memory and IoConnectInterrupt to install an interrupt handler.

An NT style driver has three jobs to do to obtain its translated hardware resources.

1. Find its device's raw resource requirements.

2. Allocate these raw resources: check for conflicts with existing devices and reserve the resources so that other new devices cannot use them.

3. Translate the raw resource information.

The PHDIo driver receives its resource requirements from the Win32 application. The filename passed in the Create IRP specifies the I/O port details and optionally the Interrupt IRQ number. PHDIo can currently only accept ISA resource details.

This chapter shows how PHDIo allocates its raw resources and translates them. At the end of the chapter, I look at how NT and W2000 find various devices and make the resource information available to drivers.

First, I look at how to build and structure an NT style driver.

NT Style Driver Construction

An NT style driver is built in a slightly different way from a WDM driver. It also has none of the Plug and Play infrastructure to support.

DDK Issues

You must build an NT style driver in Windows 2000 or NT 4. The Windows 2000 DDK or NT 4 DDK must be installed as appropriate. If you alter an NT style driver, you must reboot Windows 98 to use the changed driver.

It is possible that crucial kernel structures have been changed between NT 4 and W2000, so it is safest to have one NT 4/NT 3.51 version of your driver and one W2000 version. In practice, it seems as though a driver compiled in W2000 using the W2000 DDK works in the NT platforms and Windows 98. I compared the free build driver made by the Windows 2000 Beta DDK with the same build using the NT 4 DDK. Although the files were both the same size, they were not identical.

An NT style driver can support Power Management and Windows Management Instrumentation (WMI), as long as it is only run on Windows 2000 or Windows 98. The major function code for the Power Management IRP, IRP_MJ_POWER, is defined as 0x16 in the W2000 DDK NTDDK.H. IRP_MJ_POWER is not defined in the NT 4 DDK. Instead, the IRP major function code 0x16 is defined as IRP_MJ_QUERY_POWER in NT 4. I am not sure whether this IRP is issued in NT 4. However, it is best if you do not handle IRP_MJ_POWER or IRP_MJ_SYSTEM_CONTROL in a driver installed in NT 4.

Compile Environment

An NT style driver must use NTDDK.H as its main header file, rather than WDM.H. In general, this gives the driver access to more facilities than would be available to a WDM device driver. In the SOURCES build file, remove the line that says DRIVERTYPE=WDM.

NT Style Driver Structure

An NT style driver like PHDIo creates devices in a different way from WDM device drivers. A WDM device driver has an AddDevice routine and receives Plug and Play IRP notifications. PHDIo does not handle the PnP IRP and, therefore, loses its Pnp.cpp file[44]. It also does not handle Power Management and WMI IRPs.

As a consequence of not using Plug and Play, PHDIo does not deal with PnP device stacks. The device extension does not need to have fields for the Physical Device Object (PDO) or NextStackDevice. Similarly, there is no need for the GotResources, Paused, IoDisabled, and OpenHandleCount fields. The StartDevice and RetrieveResources routines in DeviceIo.cpp have been removed, as there are no PnP Start Device IRPs to process.

Instead, PHDIo creates one device in its DriverEntry routine. This is not a Functional Device Object (FDO) in the PnP sense. Instead, it is just called a "device object". The PHDIo device extension now has a phddo field that contains a pointer this device object. phddo is used in exactly the same way as the fdo field in all the previous drivers.

Most NT style drivers that need resources find and allocate them in the DriverEntry call. However, PHDIo only receives its resource requirements when a handle is opened to its device. Later in the chapter, I will look at various techniques for finding a driver's resource requirements.

The one PHDIo device is deleted using PHDIoUnload when the driver is removed.

Device Creation and Deletion

The PHDIo DriverEntry routine delegates its device object creation to PHDIoCreateDevice, also in Init.cpp.

Listing 18.1 shows how PHDIo creates its device in PHDIoCreateDevice. Its kernel device name is \Device\PHDIo and the Win32 symbolic link name is \??\PHDIo. This is used by Win32 programs with a CreateFile filename of \\.\PHDIo.

PHDIoCreateDevice first sets up UNICODE_STRINGs for the kernel and symbolic link names. It then calls IoCreateDevice in the same way as before. This creates the phddo device object and its associated device extension. The IoCreateDevice Exclusive parameter is TRUE indicating that only one Win32 application can open this device at a time.

All the appropriate fields are set up in the device extension, including the phddo field. The device timer is initialized with IoInitializeTimer and the device Deferred Procedure Call is initialized with IoInitializeDpcRequest. Finally, an explicit symbolic link is set up using IoCreateSymbolicLink. Device interfaces are not used.

Listing 18.1 NT style device creation

PDEVICE_OBJECT phddo = NULL;

#define NT_DEVICE_NAME L"\\Device\\PHDIo"

#define SYM_LINK_NAME L"\\DosDevices\\PHDIo"

NTSTATUS PHDIoCreateDevice(IN PDRIVER_OBJECT DriverObject {

 NTSTATUS status = STATUS_SUCCESS;

 // Initialise NT and Symbolic link names

 UNICODE_STRING deviceName, linkName;

 RtlInitUnicodeString(&deviceName, NT_DEVICE_NAME);

 RtlInitUnicodeString(&linkName, SYM_LINK_NAME);

 // Create our device

 DebugPrint("Creating device %T",&deviceName);

 status = IoCreateDevice(

  DriverObject,

  sizeof(PHDIO_DEVICE_EXTENSION),

  &deviceName,

  FILE_DEVICE_UNKNOWN,

  0,

  TRUE, // Exclusive

  &phddo);

 if (!NT_SUCCESS(status)) {

  DebugPrintMsg("Could not create device");

  return status;

 }

 phddo->Flags |= DO_BUFFEREO_IO;

 // Initialise device extension

 PPHDIO_DEVICE_EXTENSION dx = (PPHDIO_DEVICE_EXTENSION)phddo->DeviceExtension;

 dx->phddo = phddo;

 dx->UsageCount = 1;

 KeInitializeEvent(&dx->StoppingEvent, NotificationEvent, FALSE);

 dx->Stopping = false;

 dx->GotPortOrMemory = false;

 dx->GotInterrupt = false;

 dx->ConnectedToInterrupt = false;

 dx->SetTimeout = 10;

 dx->Timeout = –1;

 dx->StopTimer = false;

 dx->WriteCmds = NULL;

 dx->ReadCmds = NULL;

 dx->StartReadCmds = NULL;

 // Initialise timer for this device (but do not start)

 status = IoInitializeTimer(phddo, (PIO_TIMER_ROUTINE)Timeout1s, dx);

 if (!NT_SUCCESS(status)) {

  DebugPrintMsg("Could not initialise timer");

  IoDeleteDevice(phddo);

  return status;

 }

 // Create a symbolic link so our device is visible to Win32…

 DebugPrint("Creating symbolic link %T", &linkName);

 status = IoCreateSymbolicLink(&linkName, &deviceName);

 if (!NT_SUCCESS(status)) {

  DebugPrintMsg("Could not create symbolic link");

  IoDeleteDevice(phddo);

  return status;

 }

 // Initialise our DPC for IRQ completion processing

 IoInitializeDpcRequest(phddo, PHDIoDpcForIsr);

 return status;

}

When PHDIo is unloaded, its PHDIoUnload routine is called. This runs PHDIoDeleteDevice to stop the PHDIo device, remove its symbolic link, and delete its device object.

If a driver makes a variable number of devices, the unload routine could use the following technique to find all the devices to remove. The kernel sets the driver object DeviceObject field to point to the first device that belongs to the driver. The NextDevice field in each device object points to the next device. It is, therefore, a simple task to traverse this chain of device objects and delete them. The only catch is that you still have to generate the correct symbolic link name for each device so the symbolic link can be removed.

Claiming Resources

This section looks at how to allocate resources. The crucial kernel function is IoReportResourceUsage. This checks to see if any other devices are using the resources. If the resources are free, they are reserved so no other device can use them.

The PHDIo driver only finds out which resources are needed when the user calls CreateFile, passing the resource description in the filename string. The Create IRP arrives in the PHDIoCreate routine. All the previous Create IRP handlers have not done very much. However, PHDIoCreate has these jobs to do.

1. Get the resource details from the filename.

2. Check for resource conflicts and reserve the resources.

3. Translate and map the resources.

Getting the resource details is handled by the GetResourcesFromFilename routine. I will not go into the details of this code here, apart from saying that it uses three support routines that work with the UNICODE_STRING structure: usStrCmpN, usGetHex, and usGetDec. In the end, the GotPortOrMemory device extension field is true if an I/O port specifier has been found, GotInterrupt is true if an interrupt has been found and ResourceOverride is true if the \override specifier was used.

PHDIoCreate checks that an I/O port was specified. It then calls ClaimResources to check for resource conflicts and reserve the resources. Finally, TranslateAndMapResources is used to translate resource information and map memory. PHDIoCreate carefully ensures that all the resource bool fields are reset to false at the fail label if any error occurs.

Listing 18.2 PHDIoCreate routine

NTSTATUS PHDIoCreate(IN PDEVICE_OBJECT phddo, IN PIRP Irp) {

 PPHDIO_DEVICE_EXTENSION dx = (PPHDIO_DEVICE_EXTENSION)phddo->DeviceExtension;

 PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);

 DebugPrint("Create File is %T", &(IrpStack->FileObject->FileName));

 dx->GotPortOrMemory = false;

 dx->GotInterrupt = false;

 dx->PortNeedsMapping = false;

 dx->ConnectedToInterrupt = false;

 dx->ResourceOverride = FALSE;

 // Get resources from filename string

 PUNICODE_STRING usfilename = &(IrpStack->FileObject->FileName);

 NTSTATUS status = *(usfilename,dx);

 if (!NT_SUCCESS(status)) goto fail;

 // We must have IO port resource

 if (!dx->GotPortOrMemory) {

  DebugPrintMsg("No IO Port resource in filename");

  status = STATUS_INVALID_PARAMETER:

  goto fail;

 }

 // Claim resources

 status = ClaimResources(phddo);

 if (!NT_SUCCESS(status)) {

  DebugPrintMsg("Could not ClaimResources");

  goto fail;

 }

 // Translate and map resources

 status = TranslateAndMapResources(phddo);

 if (!NT_SUCCESS(status)) {

  UnclaimResources(phddo);

  goto fail;

 }

 // Complete

 return CompleteIrp(Irp,status);

 // On error, make sure everything's off fail:

 dx->GotPortOrMemory = false;

 dx->GotInterrupt = false;

 dx->PortNeedsMapping = false;

 dx->ConnectedToInterrupt = false;

 return CompleteIrp(Irp,status);

}

Claiming resources means working with Full and Partial Resource Descriptors. Chapter 9 showed that a device's resource assignments are given in these structures when the Plug and Play Start Device is received. The WdmIo driver obtains its resource assignments in this way.

NT style drivers have to build a resource list of these descriptors to pass to the IoReportResourceUsage routine. Note carefully that the raw resource details must be passed to IoReportResourceUsage, not the translated values. The kernel resource list structures are sufficiently intricate that it is worth showing them in Listing 1 8.3.

A resource list consists of one Full Resource Descriptor for each bus instance. Note carefully that this is an expandable structure. Although it is declared with only one Full Resource Descriptor, it may in fact contain one or more such descriptors.

A Full Resource Descriptor specifies the bus type and instance number. It also contains a Partial Resource List structure.

A Partial Resource List primarily contains an array of Partial Resource Descriptors. Again, note that a Partial Resource List is a structure that expands as more Partial Resource Descriptors are used.

A Partial Resource Descriptor finally contains the details of an individual resource. Table 9.2 in Chapter 9 gives full details of Partial Resource Descriptors.

Listing 18.3 Kernel resource list structures

typedef struct _CM_RESOURCE_LIST {

 ULONG Count;

 CM_FULL_RESOURCE_DESCRIPTOR List[1];

} CM_RESOURCE_LIST, *PCM_RESOURCE_LIST;

typedef struct _CM_FULL_RESOURCE_DESCRIPTOR {

 INTERFACE_TYPE InterfaceType; // unused for WDM

 ULONG BusNumber; // unused for WDM

 CM_PARTIAL_RESOURCE_LIST PartialResourceList;

} CM_FULL_RESOURCE_DESCRIPTOR, *PCM_FULL_RESOURCE_DESCRIPTOR;

typedef struct _CM_PARTIAL_RESOURCE_LIST {

 USHORT Version;

 USHORT Revision;

 ULONG Count;

 CM_PARTIAL_RESOURCE_DESCRIPTOR PartialDescriptors[1];

} CM_PARTIAL_RESOURCE_LIST, *PCM_PARTIAL_RESOURCE_LIST;

typedef struct _CM_PARTIAL_RESOURCE_DESCRIPTOR {

 UCHAR Type;

 UCHAR ShareDisposition;

 USHORT Flags;

 union {

  // …

 } u;

} CM_PARTIAL_RESOURCE_DESCRIPTOR, *PCM_PARTIAL_RESOURCE_DESCRIPTOR;

The PHDIo driver deals with only the first instance of the ISA bus, so it needs only one Full Resource Descriptor. It always has one Partial Resource Descriptor for the I/O port. It can also have a second Partial Resource Descriptor for the interrupt, if one was specified.

ClaimResources, in Listing 18.4, builds a resource list structure and passes it to IoReportResourceUsage.

As you can guess, it is quite a job building the resource list correctly. The correct size for the whole structure must be determined first. A suitably sized block of paged memory is allocated and zeroed.

ClaimResources gradually fills the resource list. The resource list Count is set to one, as there is only one Full Resource Descriptor. The Full Resource Descriptor InterfaceType field is set to Isa and the BusNumber is set to 0. The Partial Resource List Count is set to 1 or 2, depending on how many resources are declared. The Partial Resource Descriptor for the I/O port is generated, then the one for the interrupt, if required. Whew!

There is a final complication to calling IoReportResourceUsage. You must either specify the resource list as belonging to the whole driver, or associate the resource list with an individual device. PHDIo says that the resources belong to the whole driver. If PHDIo were enhanced to provide more than one device, it would make sense to allocate resources on a per-device basis. One call to IoReportResourceUsage per device would be needed in this case.

Listing 18.4 ClaimResources routine

NTSTATUS ClaimResources(IN PDEVICE_OBJECT phddo) {

 PPHDIO_DEVICE_EXTENSION dx = (PPHDIO_DEVICE_EXTENSION)phddo->DeviceExtension;

 // Get resource count: either 1 (IOport) or 2 (IOport&IRQ)

 ULONG PartialResourceCount = 1;

 if (dx->GotInterrupt) PartialResourceCount++;

 // Get size of required CM_RESOURCE_LIST

 ULONG ListSize = FIELD_OFFSET(CM_RESOURCE_LIST, List[0]);

 ListSize += sizeof(CM_FULL_RESOURCE_DESCRIPTOR) + ((PartialResourceCount-1) * sizeof(CM_PARTIAL_RESOURCE_DESCRIPTOR));

 // Allocate CM_RESOURCE_LIST

 PCM_RESOURCE_LIST ResourceList = (PCM_RESOURCE_LIST)ExAllocatePool(PagedPool, ListSize);

 if (ResourceList==NULL) {

  DebugPrintMsg("Cannot allocate memory for ResourceList");

  return STATUS_INSUFFICIENT_RESOURCES;

 }

 RtlZeroMemory(ResourceList, ListSize);

 // Only one Full Resource Descriptor needed, for ISA

 ResourceList->Count = 1;

 // Initialise Full Resource Descriptor

 PCM_FULL_RESOURCE_DESCRIPTOR FullRD = &ResourceList->List[0];

 FullRD->InterfaceType = Isa;

 FullRD->BusNumber = 0;

 FullRD->PartialResourceList.Count = PartialResourceCount;

 // Initialise Partial Resource Descriptor for IO port

 PCM_PARTIAL_RESOURCE_DESCRIPTOR resource = &FullRD->PartialResourceList.PartialDescriptors[0];

 resource->Type = CmResourceTypePort;

 resource->ShareDisposition = CmResourceShareDriverExciusive;

 resource->Flags = CM_RESOURCE_PORT_IO;

 resource->u.Port.Start = dx->PortStartAddress;

 resource->u.Port.Length = dx->PortLength;

 // Initialise Partial Resource Descriptor for Interrupt

 if (dx->GotInterrupt) {

  resource++;

  resource->Type = CmResourceTypeInterrupt;

  resource->ShareDisposition = CmResourceShareDriverExclusive;

  if (dx->Mode==Latched) resource->Flags = CM_RESOURCE_INTERRUPT_LATCHED;

  else resource->F1ags = CM_RESOURCE_INTERRUPT_LEVEL_SENSITIVE;

  resource->u.Interrupt.Level = dx->Irql;

  resource->u.Interrupt.Vector = dx->Irql;

  resource->u.Interrupt.Affinity = 1;

 }

 // Ask for resources for the driver

 DebugPrint("Allocating %d resources", PartialResourceCount);

 DebugPrint("phddo->DriverObject %x", phddo->DriverObject);

 if (dx->ResourceOverride) DebugPrintMsg("Resource override conflict");

 BOOLEAN ConflictDetected;

 NTSTATUS status = IoReportResourceUsage(NULL,

  phddo->DriverObject, ResourceList, ListSize, // Driver resources

  NULL, NULL, 0, // Device resources

  dx->ResourceOverride, &ConflictDetected);

 // Cope (or override) if resource conflict found

 if (ConflictDetected) {

  DebugPrintMsg("ConflictDetected");

  if (dx->ResourceOverride) {

   DebugPrintMsg("Conflict detected and overridden");

   status = STATUS_SUCCESS;

  }

 }

 // Free allocated memory

 ExFreePool(ResourceList);

 return status;

}

Table 18.1 shows the parameters for IoReportResourceUsage. If you are allocating resources for the whole driver, use the DriverObject, DriverList, and DriverListSize parameters; otherwise, set these to NULL. Do the same for the per-device parameters, DeviceObject, DeviceList, and DeviceListSize.

In the NT and Windows 2000 platforms, the resource assignments end up in the registry in the HKLM\HARDWARE\RESOURCEMAP key[45]. If you specify a DriverClassName parameter, this string is used as a subkey to hold the resource assignments. Otherwise, in NT 4 and NT 3.51, the resource assignments end up in the "OtherDrivers" subkey. In W2000, the resource assignments are in the "PnP Manager" subkey. You can use RegEdt32 to inspect the raw and translated resource assignments. In NT 3.51 and NT 4 this is the only way to view the resources used by the system. In Windows 2000 and Windows 98, the Device Manager properties for a device shows its resource usage; in W2000, the PHDIo device can be found if you opt to show hidden devices.

IoReportResourceUsage checks for resource conflicts and, if none, assigns the new resources. The OverrideConflict parameter can be used to force the storage of the new resource list, even if there were conflicts. The output ConflictDetected BOOLEAN says whether a conflict was detected. If its ResourceOverride field is true, PHDIo ignores a resource conflict and forces a STATUS_SUCCESS return.

Table 18.1 IoReportResourceUsage function

NTSTATUS IoReportResourceUsage(IRQL==PASSIVE_LEVEL)
ParameterDescription
IN PUNICODE_STRING DriverClassNameOptional resource class name
IN PDRIVER_OBJECT DriverObjectDriver object pointer
IN PCM_RESOURCE_LIST DriverListResource list for driver
IN ULONG DriverListSizeDriver resource list size
IN PDEVICE_OBJECT DeviceObjectDevice object pointer
IN PCM_RESOURCE_LIST DeviceListResource list for device
IN ULONG DeviceListSizeDevice resource list size
IN BOOLEAN OverrideConflictIf TRUE, store resource list even if a conflict was detected
OUT PBOOLEAN ConflictDetectedBOOLEAN that is set TRUE if a conflict was detected

A driver releases its claim on any resources by calling IoReportResourceUsage again; this time to report that it uses no resources. Listing 18.5 shows how UnclaimResources does this job. UnclaimResources is called when the file handle is closed.

Listing 18.5 UnclaimResources routine

void UnclaimResources(IN PDEVICE_OBJECT phddo) {

 DebugPrintMsg("Freeing all allocated resources");

 // Release all driver's resources by declaring we have none.

 CM_RESOURCE_LIST ResourceList;

 ResourceList.Count = 0;

 BOOLEAN ConflictDetected;

 IoReportResourceUsage(NULL,

  phddo->DriverObject, &ResourceList, sizeof(ResourceList), // Driver

  NULL, NULL, 0, // Device resources

  FALSE, &ConflictDetected); // ignore return result

}

Translating Resources

At this point, PHDIo has found its raw resource requirements and claimed them for itself. The final stage is to translate the raw resource information into a form that can be used by the driver. Listing 18.6 shows the TranslateAndMapResources routine that does this job.

HalTranslateBusAddress is used to translate a bus address. The bus type and number are passed as the first parameters, followed by the raw address. The AddressSpace parameter is used as both an input and an output. If set to 1, the address is in I/O space. If AddressSpace is 0 on output, the output address in memory space must be mapped using MmMapIoSpace. The PortStartAddress device extension field receives the translated address.

The raw interrupt information must be translated using HalGetInterruptVector. The bus type and number as passed as before. For the ISA bus, specifying the IRQ number for the raw interrupt level and raw vector parameters seems to work. The translated interrupt Vector, Irql, and Affinity are stored in the PHDIo device extension. Do not forget that PHDIo still needs to connect to the interrupt to install its interrupt service routine.

The final step in TranslateAndMapResources is to get a usable pointer to the I/O port. If the port needs mapping, MmMapIoSpace is called to obtain this pointer. Otherwise, PHDIo can just use the low part of the translated port address.

Listing 18.6 TranslateAndMapResources routine

NTSTATUS TranslateAndMapResources(IN PDEVICE_OBJECT phddo) {

 PPHDIO_DEVICE_EXTENSION dx = (PPHDIO_DEVICE_EXTENSION)phddo->DeviceExtension;

 // Translate IO port values

 ULONG AddressSpace = 1; //IO space

 if (!HalTranslateBusAddress(Isa, 0, dx->PortStartAddress, &AddressSpace, &dx->PortStartAddress)) {

  DebugPrint("Create file: could not translate IO %x", dx->PortStartAddress.LowPart);

  return STATUS_INVALID_PARAMETER;

 }

 DebugPrint("IO trans %x,%d", dx->PortStartAddress.LowPart, dx->PortLength);

 dx->PortNeedsMapping = (AddressSpace==0);

 dx->PortInIOSpace = (AddressSpace==1);

 // Translate IRQ values

 if (dx->GotInterrupt) {

  ULONG irq = dx->Irql;

  dx->Vector = HalGetInterruptVector(Isa, 0, irq, irq, &dx->Irql, &dx->Affinity);

  if (dx->Vector==NULL) {

   DebugPrint("Create filename: Could not get interrupt vector for IRQ %d", irq);

    return STATUS_INVALID_PARAMETER;

  }

  DebugPrint("Interrupt vector %x IRQL %d Affinity %d Mode %d", dx->Vector, dx->Irql, dx->Affinity, dx->Mode);

 }

 // Map memory

 if (dx->PortNeedsMapping) {

  dx->PortBase = (PUCHAR)MmMapIoSpace(dx->PortStartAddress, dx->PortLength, MmNonCached);

  if (dx->PortBase==NULL) DebugPrintMsg("Cannot map IO port");

  return STATUS_NO_MEMORY;

 } else dx->PortBase = (PUCHAR)dx->PortStartAddress.LowPart;

 return STATUS_SUCCESS;

}

The FreeResources routine is used to unmap memory and disconnect from the interrupt, if necessary.

Well, that wraps up the discussion of PHDIo. The rest of its functionality is the same as WdmIo. The rest of the chapter backtracks to revisit the subject of finding the resources that a driver needs.

Finding Resources

The PHDIo driver is told which resources to use in the filename passed with the Create IRP. However, most NT style drivers do not have this luxury. Instead, they must use one of the following techniques to find out what resources to use.

• Ask what resources the kernel has detected.

• Interrogate configurable buses.

• Save the resource requirements in the driver's registry key when it is installed.

• Poke around in memory to see if you can find your devices.

Calling IoGetConfigurationInformation returns a count of certain types of device.

Auto-Detected Hardware

NT and Windows 2000 attempt to identify all the hardware devices attached to the system when they boot. A driver can look for suitable devices using the IoQueryDeviceDescription kernel call. IoQueryDeviceDescription calls your configuration callback routine for each matching hardware element. I have not tried to find out if this call works in Windows 98.

The automatic detection process finds all the buses on the system, all recognized controllers on each bus, and. if possible, each peripheral attached to a controller. It starts by locating any standard serial and parallel ports and finds any attached mice or printers. Along the way, it finds any disk drivers, network cards, etc. The detected information is put in the registry in the HKLM\HARDWARE\DESCRIPTION key.

Table 18.2 shows the parameters for IoQueryDeviceDescription. You must supply a BusType parameter, and can optionally provide ControllerType and PeripheralType parameters. These parameters are pointers, so specifying NULL means that you do not want to find this type of hardware. You also pass a callback routine and a context to pass to it.

The possible bus, controller, and peripheral types are found in the INTERFACE_TYPE and CONFIGURATION_TYPE enumerations in NTDDK.H.

Table 18.2 IoQueryDeviceDescription function

NTSTATUS IoQueryDeviceDescription(IRQL==PASSIVE_LEVEL)
ParameterDescription
IN PINTERFACE_TYPE BusTypeBus type
IN PULONG BusNumberBus number, zero-based
IN PCONFIGURATION_TYPE ControllerTypeController type
IN PULONG ControllerNumberController number, zero-based
IN PCONFIGURATION_TYPE PeripheralTypePeripheral type
IN PULONG PeripheralNumberPeripheral number, zero-based
IN PIO_QUERY_DEVICE_ROUTINE CalloutRoutineConfiguration callback routine name
IN PVOID ContextContext for configuration callback
ReturnsSTATUS_OBJECT_NAME_NOT_FOUND if no match found, or status returned by callback

Listing 18.7 shows some code that finds any parallel ports that have been detected (i.e., where the ControllerType is ParallelController). The code can be found in the book software file PHDIo\autodetect.cpp. Note carefully that the code will find parallel ports that are on all the available buses, not just the ISA bus. At the moment, this code just prints out the basic raw resource details using DebugPrint. If you use this code, you will eventually have to claim these resources and translate them into usable values. Do not forget that you need a separate Full Resource Descriptor for each bus type in the resource list passed to IoReportResourceUsage.

FindParallelPort loops through all possible bus types. For each bus type, it keeps incrementing the bus number from zero and checking if the bus instance exists using IoReportResourceUsage. If the bus instance does exist, it calls IoRecortResourceUsage again to find all printers on the bus instance.

The AbcConfigCallback callback is called when a bus instance is found and when a parallel port is found. In the first case, the BusInfo parameter is valid. CtrlrInfo is valid when looking for parallel ports. If looking for a peripheral, PeripheralInfo is valid. If the relevant parameter is non-NULL, it points to some information obtained from the registry: a device identifier, configuration data, and information about its subcomponents. The configuration data has the detected raw resource assignments. Use the code in Listing 18.7 to enumerate these resources.

Listing 18.7 Finding any autodetected parallel ports

NTSTATUS FindParallelPort() {

 NTSTATUS status;

 for (int BusType=0; BusType<MaximumInterfaceType; BusType++) {

  INTERFACE_TYPE iBusType = (INTERFACE_TYPE)BusType;

  CONFIGURATION_TYPE CtrlType = ParallelController;

  ULONG BusNumber =0;

  while (true) {

   // See if this bus instance exists

   status = IoGueryDeviceDescription(&iBusType, &BusNumber, NULL, NULL, NULL, NULL, AbcConfigCallback, NULL);

   if (!NT_SUCCESS(status)) {

    if (status != STATUS_OBJECT_NAME_NOT_FOUND) return status;

    break;

   }

   // See what printers exist on this bus instance

   status = IoQueryDeviceDescription(&iBusType, &BusNumber, &CtrlrType, NULL, NULL, NULL, AbcConfigCallback, NULL);

   if (!NT_SUCCESS(status) && (status != STATUS_OBJECT_NAME_NOT_FOUND)) return status;

   BusNumber++;

  }

 }

 return status;

}

NTSTATUS AbcConfigCallback(IN PVOID Context, IN PUNICODE_STRING PathName, IN INTERFACE_TYPE BusType, IN ULONG BusNumber, IN PKEY_VALUE_FULL_INFORMATION *BusInfo, IN CONFIGURATION_TYPE CtrlrType, IN ULONG CtrlrNumber, IN PKEY_VALUE_FULL_INFORMATION *CtrlrInfo, IN CONFIGURATION_TYPE Peripheral Type, IN ULONG Peripheral Number, IN PKEY_VALUE_FULL_INFORMATION *PeripheralInfo) {

 DebugPrint("ConfigCallback: Bus: %d,%d", BusType, BusNumber);

 DebugPrint("ConfigCallback: Controller: %d,%d", CtrlrType, CtrlrNumber);

 DebugPrint("ConfigCallback: Peripheral: %d,%d", PeripheralType, PeripheralNumber);

 if (CtrlrInfo!=NULL) {

  PCM_FULL_RESOURCE_DESCRIPTOR frd =

   (PCM_FULL_RESOURCE_DESCRIPTOR) (((PUCHAR)CtrlrInfo[IoQueryDeviceConfigurationData]) +

   CtrlrInfo[IoQueryDeviceConfigurationData]->DataOffset);

  for (ULONG i=0; i<frd->PartialResourceList.Count; i++) {

   PCM_PARTIAL_RESOURCE_DESCRIPTOR resource = &frd->PartialResourceList.PartialDescriptors[i];

   switch(resource->Type) {

   case CmResourceTypePort:

    DebugPrint("ConfigCallback: I/O port %x,%d", resource->u.Port.Start.LowPart, resource->u.Port.Length);

    break;

   case CmResourceTypeInterrupt:

    DebugPrint("ConfigCallback: Interrupt level %c vector %d", resource->u.Interrupt.Level, resource->u.Interrupt.Vector);

    break;

   default:

    DebugPrint("ConfigCallback: Resource type %d", resource->Type);

   }

  }

 }

 return STATUS_SUCCESS;

}

Interrogating Configurable Buses

A configurable bus is one that can be configured in software. Devices on these buses must have Plug and Play WDM device drivers. The PCI bus driver will retrieve a device's details and generate an appropriate Hardware ID. This Hardware ID will be used to identify the correct driver. Its AddDevice routine is called, etc.

In NT systems, you will need to use a different technique to get configurable bus data. This method still works in Windows 2000. The HalGetBusData and HalGetBusDataByOffset calls can be made to retrieve configuration data for one slot of a specified bus instance. For example, if you ask for PCIConfiguration bus data, the passed buffer is filled with PCI_COMMON_CONFIG data.

You can then claim the necessary resources using IoAssignResources, in a similar way to IoReportResourceUsage. As an alternative, drivers of PCI-type devices can call HalAssignSlotResources. HalSetBusData and HalSetBusDataByOffset can be used to write back configuration data.

I have not tried it, but it looks as though HalGetBusData and HalSetBusData can be used to access the BIOS CMOS data.

Final Resource Discovery Techniques

There are two final ways of obtaining a driver's resource requirements. The first is to store the relevant information in the driver's registry key, say in its Parameters subkey. This information will typically be set up when the device is installed.

Last and least, you can simply poke around in likely memory locations to see if your device is present. Dangerous.

Conclusion

This chapter has looked at how to write an NT style device driver such as PHDIo. As it does not use Plug and Play, it must use different techniques to find what hardware resources it needs, reserve them, and translate them into usable values.

The PHDIo driver receives its resource details for its controlling Win32 application. Other drivers must find out what devices the kernel has found or interrogate all the configurable buses itself.


  1. The LockDevice and UnlockDevice routines are still used and so have been moved from Pnp.cpp into Init.cpp. 

  2. I do not know where they go in Windows 98.