Êíèãà: Writing Windows WDM Device Drivers

DebugPrint Driver

DebugPrint Driver

Referring to Figure 14.1, you can see that the job of the DebugPrint driver is to store trace events from all the drivers under test and make them available to the DebugPrint Monitor user mode application. As the figure illustrates, the test drivers only write to the DebugPrint driver, while the Monitor only reads.

This section will not look at all the DebugPrint driver code. The Plug and Play and Initialization code is largely the same as earlier WDM drivers. You should install just one DebugPrint device in the Other Devices category.

The interesting code is in the dispatch routines in Dispatch.cpp, along with a main header file DebugPrint.h. These files and the other source files can be found on the book's software disk.

Design

The DebugPrint driver uses a similar technique of storing all written trace events in a doubly-linked list called EventList. When the DebugPrint Monitor program starts, it reads all the available events. It then leaves one read request outstanding. When a new trace event is written by a test driver, the Monitor read request is satisfied straightaway. This design ensures that trace events get to the Monitor application as soon as possible.

This means that the DebugPrint driver has to be able to queue up incoming read requests. In fact, it only allows one read request to be queued. Any further read requests are rejected. As DebugPrint has an IRP queue, it must be prepared to cancel IRPs. The DbpCancelIrp routine described later does this job.

The DbpCreate and DbpClose routines simply complete their IRPs successfully. Note that these IRPs are issued from both test drivers and the Monitor application.

The DebugPrint driver uses Buffered I/O.

DebugPrint Devices

As mentioned previously, the DebugPrint driver uses both a device interface and a Windows 2000 device name. The DebugPrint Monitor application identifies a DebugPrint device using the device interface. The test drivers identify the DebugPrint device using the kernel name.

Listing 14.5 shows how a named Functional Device Object is created in DbpAddDevice in Pnp.cpp. The DebugPrintName variable is a UNICODE_STRING that is initialized with the desired kernel device name, DevicePHDDebugPrint. This is passed to the IoCreateDevice call.

Later in DbpAddDevice, the DebugPrint device interface is registered in the same way as before. The device interface GUID is defined in DbgPrtGUID.h as {ED6026A2-6813-11d2-AE43-00C0DFE4C1F3}. This header file is also included by the DebugPrint Monitor project.

Listing 14.5 DebugPrint driver device creation

UNICODE_STRING DebugPrintName;
RtlInitUnicodeString(&DebugPrintName, L"DevicePHDDebugPrint");
// Create our Functional Device Object in fdo
status = IoCreateDevice(DriverObject,
 sizeof(DEBUGPRINT_DEVICE_EXTENSION),
 &DebugPrintName,
 FILE_DEVICE_UNKNOWN,
 0,
 FALSE, // Not exclusive
 &fdo);

If you try to create a second DebugPrint device, the same kernel device name is passed to IoCreateDevice. As this device name is already being used, the call will fail. This problem does not occur if you pass NULL for the device name, as in earlier examples. If you definitely need more than one named kernel device, you must keep a (zero-based) count of devices and append it as a device number to the kernel device name.

No special processing is required when you remove a device with a kernel device name.

Read Queue

The Read IRP queue is implemented using variables in the device extension, shown in Listing 14.6. If ReadIrp is NULL, no IRP is queued. Otherwise, it contains a pointer to the queued IRP. A spin lock called ReadIrpLock is used to protect access to the read queue. These fields are initialized as follows in the DbpAddDevice routine in Pnp.cpp.

// Initialise "read queue"
KeInitializeSpinLock(&dx->ReadIrpLock);
dx->ReadIrp = NULL;

Listing 14.6 DebugPrint driver device extension

typedef struct _DEBUGPRINT_DEVICE_EXTENSION {
 PDEVICE_OBJECT fdo;
 PDEVICE_OBJECT NextStackDevice;
 UNICODE_STRING ifSymLinkName;
 bool GotResources; // Not stopped
 bool Paused; // Stop or remove pending
 bool IODisabled; // Paused or stopped
 LONG OpenHandleCount; // Count of open handles
 LONG UsageCount; // Pending I/O Count
 bool Stopping; // In process of stopping
 KEVENT StoppingEvent; // Set when all pending I/O complete
 PIRP ReadIrp; // "Read queue" of 1 IRP
 KSPIN_LOCK ReadIrpLock; // Spin lock to guard access to ReadIrp
 LIST_ENTRY EventList; // Doubly-linked list of written Events
 KSPIN_LOCK EventListLock; // Spin lock to guard access to EventList
} DEBUGPRINT_DEVICE_EXTENSION, *PDEBUGPRINT_DEVICE_EXTENSION;

Listing 14.7 shows the complete Read IRP handler. Its first job is to acquire the ReadIrpLock spin lock. If the ReadIrp field is not NULL, it means another Read IRP has been queued. The IRP is failed, not forgetting to release the spin lock first. Even though the Monitor program will never issue more than one read IRP, it is best to be on the safe side.

The ReadEvent routine is then called. If there are any trace events available straightaway, ReadEvent returns the event data in the IRP, completes the IRP, and returns true. If this happens, the read routine can just return straightaway without queuing the IRP.

If there are no trace events available, the IRP is queued. This simply means storing the IRP pointer in ReadIrp. The final job is to mark the IRP as pending using IoMarkIrpPending and set its cancel routine using IoSetCancelRoutine. The read routine must return STATUS_PENDING because it has queued its IRP.

IRPs that are not cancelled must remove their cancel routine using IoSetCancelRoutine before completing the IRP.

Listing 14.7 DebugPrint driver read dispatch routine

NTSTATUS DbpRead(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
 PDEBUGPRINT_DEVICE_EXTENSION dx = (PDEBUGPRINT_DEVICE_EXTENSION)fdo->DeviceExtension;
 if (!dx->IODisabled) return CompleteIrp(Irp, STATUS_DEVICE_NOT_CONNECTED, 0);
 if (!LockDevice(dx)) return CompleteIrp(Irp, STATUS_DELETE_PENDING, 0);
 // Get access to our Read IRP queue
 KIRQL irql;
 KeAcquireSpinLock(&dx->ReadIrpLock,&irql);
 // Only one listening read allowed at a time.
 if (dx->ReadIrp!=NULL) {
  KeReleaseSpinLock(&dx->ReadIrpLock,irql);
  UnlockDevice(dx);
  return CompleteIrp(Irp, STATUS_UNSUCCESSFUL, 0);
 }
 // See if there's data available
 if (ReadEvent(dx, Irp)) {
  KeReleaseSpinLock(&dx->ReadIrpLock,irql);
  UnlockDevice(dx);
  return STATUS_SUCCESS;
 }
 // No event is available, queue this read Irp
 dx->ReadIrp = Irp;
 KeReleaseSpinLock(&dx->ReadIrpLock,irql);
 // Mark Irp as pending and set Cancel routine
 Irp->IoStatus.Information = 0;
 IoMarkIrpPending(Irp);
 IoSetCancelRoutine(Irp.DbpCancelIrp);
 return STATUS_PENDING;
}

Cancelling IRPs

Any IRPs that are queued must have a cancel routine. A driver ought to also handle the IRP_MJ_CLEANUP Cleanup IRP.

Cancel and Cleanup Circumstances

First, let's be clear when IRP cancel routines are called and when the Cleanup IRP is sent.

Case 1 is a situation in which a user application calls the CancelIo Win32 function on a file handle. All IRPs with cancel routines have their cancel routine called. Only IRPs that have been issued by the current thread are effected.

Case 2 covers these three situations:

• a user mode program crashes with IRPs pending,

• it exits with overlapped I/O requests pending and without closing its file handle, and

• if Ctrl+C is pressed in console applications.

In this case, all IRPs with cancel routines have their cancel routines called first. If there are outstanding IRPs without cancel routines, the I/O Manager simply sets the IRPs' Cancel flag and waits until the IRPs complete. Finally, the Cleanup IRP is sent.

If an uncancellable IRP does not complete within five minutes, the IRP is forcibly detached from the user process so that it can terminate. However, the IRP is still left uncompleted. You will not be able to reinstall the driver, so a reboot will be necessary to try the fixed version of your driver.

Issuing the Cleanup IRP seems to perform no useful function in this case.

Case 3 is a situation in which a user mode programs closes its file handle with overlapped I/O requests pending

In this case, IRPs with cancel routines do not have their cancel routines called. Instead, the Cleanup IRP is issued to cancel all pending IRPs.

The Implications

If you give each queued IRP a cancel routine, most normal cases are covered (i.e., the afore-mentioned Cases 1 and 2). To be thorough, however, you ought to provide a Cleanup handler to cover Case 3. As you well know, programmers are bound to forget to close a file handle sometime or other.

If you do not provide a cancel routine for IRPs but do provide a Cleanup handler, only Case 3 is handled correctly. This is an unsatisfactory solution. In addition, user mode applications will not be able to cancel IRPs with CancelIo.

Some drivers work by providing a cancel routine only for an IRP while it is queued. Once the IRP actually begins processing, the cancel routine is removed. If you use this technique, be sure to provide a reasonable timeout for real IRP processing. Otherwise, crashed programs will not be able to exit.

DebugPrint IRP Cancelling

Full Cancel and Cleanup support can be quite complicated. This is particularly the case for IRPs put in the device queue for processing in a StartIo routine. A full example for this case is given in Chapter 16.

The DebugPrint driver uses just a cancel routine for its one queued IRP. It does not handle the Cleanup IRP.

The DbpCancelIrp routine shown in Listing 14.8 is called if an IRP is cancelled. An I/O request is cancelled when a user mode application calls the Win32 CancelIO function. This technique is used by the DebugPrint Monitor application. The kernel will also cancel any outstanding IRPs if a process terminates unexpectedly or when the file handle is closed.

The I/O Manager uses its Cancel spin lock to ensure that cancel operations happen safely.

A cancel routine is always called at DISPATCH_LEVEL IRQL holding this Cancel spin lock. The DbpCancelIrp routine can simply release this straightaway.

DbpCancelIrp then goes on to acquire the DebugPrint device extension ReadIrpLock spin lock. It checks to see if the given IRP pointer matches the one in the queue. If it does, it clears the queue. Regardless of whether the given IRP matches the one in the list, DbpCancelIrp just cancels the IRP by calling CompleteIrp, passing a status of STATUS_CANCELLED.

Listing 14.8 DebugPrint driver cancel routine

VOID DbpCancelIrp(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
 PDEBUG_PRINT_DEVICE_EXTENSION dx = (PDEBUGPRINT_DEVICE_EXTENSION)fdo->DeviceExtension;
 IoReleaseCancelSpinLock(Irp->CancelIrql);
 // If this is our queued read, then unqueue it
 KIRQL irql ;
 KeAcquireSpinLock(&dx->ReadIrpLock,&irql);
 if (Irp==dx->ReadIrp) {
  UnlockDevice(dx);
  dx->ReadIrp = NULL;
 }
 KeReleaseSpinLock(&dx->ReadIrpLock,irql);
 // Whatever Irp it is, just cancel it
 CompleteIrp(Irp, STATUS_CANCELLED, 0);
}

Write Algorithm

The DebugPrint driver write routine DbpWrite, shown in Listing 14.9, at first sight looks pretty similar to the DebugPrintMsg routine described earlier. Its job is to insert the trace event data into an interlocked doubly-linked list. If there is a Read IRP queued up, DbpWrite goes on to satisfy this read request.

DbpWrite first gets the write parameters, and completes Write IRPs with a zero transfer length straightaway. The device file pointer is ignored.

As shown previously, the event list is stored in the device extension in field EventList, protected by spin lock EventListLock. Each event is stored in a DEBUGPRINT_EVENT structure.

DbpWrite determines the correct size for the DEBUGPRINT_EVENT structure and tries to allocate some memory for it from the nonpaged pool. It fails the IRP with STATUS_INSUFFICIENT_RESOURCES if no memory is available. Next, it copies the event data into the event and stores the data length, before calling ExInterlockedInsertTailList in the same way as before to insert the event safely into EventList.

DbpWrite now checks to see if there is a queued Read IRP. It must first grab the ReadIrpLock spin lock. If the ReadIrp field is not NULL, ReadEvent is called to complete the Read IRP and the ReadIrp field is reset to NULL.

Finally, DbpWrite completes its own Write IRP, returning STATUS_SUCCESS.

Listing 14.9 DebugPrint driver write routine

NTSTATUS DbpWrite(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
 PDEBUGPRINT_DEVICE_EXTENSION dx = (PDEBUGPRINT_DEVICE_EXTENSION)fdo->DeviceExtension;
 if (!dx->IODisabled) return CompleteIrp(Irp, STATUS_DEVICE_NOT_CONNECTED, 0);
 if (!LockDevice(dx)) return CompleteIrp(Irp, STATUS_DELETE_PENDING, 0);
 PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
 ULONG BytesTxd = 0;
 // Check write len
 ULONG WriteLen = IrpStack->Parameters.Write.Length;
 if (WriteLen==0) {
  UnlockDevice(dx);
  return CompleteIrp(Irp, STATUS_SUCCESS, 0);
 }
 // Copy write data into an event
 ULONG Len = sizeof(LIST_ENTRY)+sizeof(ULONG)+WriteLen;
 PDEBUGPRINT_EVENT pEvent = (PDEBUGPRINT_EVENT)ExAllocatePool(NonPagedPool,Len);
 if (pEvent==NULL) {
  UnlockDevice(dx);
  return CompleteIrp(Irp,STATUS_INSUFFICIENT_RESOURCES,0);
 }
 pEvent->Len = WriteLen;
 RtlCopyMemory(pEvent->EventData, Irp->AssociatedIrp.SystemBuffer, WriteLen);
 // Insert event into event list
 ExInterlockedInsertTailList(&dx->EventList,&pEvent-> ListEntry,&dx->EventListLock);
 // If read pending, then read it
 KIRQL irql;
 KeAcquireSpinLock(&dx->ReadIrpLock,&irql);
 if (dx->ReadIrp!=NULL) if (ReadEvent(dx, dx->ReadIrp)) {
  UnlockDevice(dx);
  dx->ReadIrp = NULL;
 }
 KeReleaseSpinLock(&dx->ReadIrpLock,irql);
 // Complete IRP
 UnlockDevice(dx);
 return CompleteIrp(Irp, STATUS_SUCCESS, WriteLen);
}

Read Algorithm

The ReadEvent routine shown in Listing 14.10 is called by the read and write dispatch routines. It is called while holding the ReadIrpLock spin lock. ReadEvent returns true if a trace event was found.

ReadEvent tries to remove an entry from the event list using ExInterlockedRemoveHeadList. If it finds an entry, it obtains a pointer to the DEBUGPRINT_EVENT structure using CONTAINING_RECORD. It now checks the event data length against the size of the Read IRP buffer and shortens the transfer count, if necessary. The event data is copied to the Read I/O buffer and the Read IRP is completed. Finally, the event buffer memory is freed.

Listing 14.10 DebugPrint driver ReadEvent routine

bool ReadEvent(PDEBUGPRINT_DEVICE_EXTENSION dx, PIRP Irp) {
 // Try to remove Event from EventList
 PLIST_ENTRY pListEntry = ExInterlockedRemoveHeadList(&dx->EventList, &dx->EventListLock);
 if (pListEntry==NULL) return false;
 // Get event as DEBUGPRINT_EVENT
 PDEBUGPRINT_EVENT pEvent = CONTAINING_RECORD(pListEntry, DEBUGPRINT_EVENT, ListEntry);
 // Get length of event data
 ULONG EventDataLen = pEvent->Len;
 // Get max read length acceptible
 PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
 ULONG ReadLen = IrpStack->Parameters.Read.Length;
 // Shorten event length if necessary
 if(EventDataLen>ReadLen) EventDataLen = ReadLen;
 // Copy data to Irp and complete it
 RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, pEvent->EventData, EventDataLen);
 IoSetCancelRoutine(Irp, NULL);
 CompleteIrp(Irp, STATUS_SUCCESS, EventDataLen);
 // Free event memory
 ExFreePool(pEvent);
 return true;
}

Îãëàâëåíèå êíèãè


Ãåíåðàöèÿ: 0.892. Çàïðîñîâ Ê ÁÄ/Cache: 3 / 0
ïîäåëèòüñÿ
Ââåðõ Âíèç