52964.fb2
This chapter looks at a fully-fledged driver, DebugPrint. The DebugPrint software lets you use formatted print trace statements in your code and view the output in the DebugPrint Monitor Win32 program.
Along the way, I cover the following important device driver topics.
• System threads
• Dispatcher objects: events, Mutexes, and semaphores
• Linked lists
• File I/O in drivers
• Queuing IRPs
• Basic cancel routines
• Win32 overlapped I/O requests
The main design requirement for the DebugPrint software is that test drivers can do formatted prints that appear in a user mode application running on the same PC as the driver. The software must work under Windows 98 and Windows 2000. It should be easy for developers to include trace statements in their code. They should be able to use trace statements in most types of driver code.
This specification does not include source level debugging or breakpoints.
Figure 14.1 shows the design used in the DebugPrint software. The figure shows that more than one test driver can run at the same time. In each test driver, the trace output is first stored internally in an EventList doubly-linked list. A DebugPrint system thread in the driver code reads the EventList and writes the events to the DebugPrint driver.
The DebugPrint driver write routine stores the trace events in its own EventList. The DebugPrint Monitor application issues read requests to the DebugPrint driver to read the trace events. These are then displayed to the user in the DebugPrint Monitor window.
An alternative design might have removed the DebugPrint driver. The test driver DebugPrint system thread could then have written the trace events directly to a disk file. However, it is unclear whether it is possible to lock such a file so that "simultaneous" accesses by the test drivers and DebugPrint Monitor would be handled properly. Writing to a disk file would also be slower.
Figure 14.1 DebugPrint design
The DebugPrint software, therefore, consists of the following three different pieces of code.
• Code added to the test driver to produce events
• The DebugPrint driver
• The DebugPrint Monitor application
As explained in Chapter 6, a driver writer has to add DebugPrint.c[31] and DebugPrint.h source files to their driver project to support DebugPrint calls. These routines ensure that the trace statement output is sent to the DebugPrint driver. It should eventually be possible to put all the code in DebugPrint.c into a static library or DLL that is linked with test drivers.
The main job performed by the DebugPrint code in each test driver is to write events to the DebugPrint driver. The kernel provides several routines to call other drivers, including ZwCreateFile, ZwWriteFile, and ZwClose. The documentation for these functions says that these functions can only be called at PASSIVE_LEVEL IRQL. This means that they cannot be called directly from some sorts of driver code.
In my initial design, I ignored this issue. However, I soon ran into another problem that was more difficult to track down. Eventually, I worked out that calls to ZwWriteFile have to be made in the same process context as ZwCreateFile. The DDK documentation did not make this point prominently. In my initial design, I used ZwCreateFile in the test driver DriverEntry routine. However, the print routines using ZwWriteFile could naturally be called in dispatch routines, which are usually not running in the same process context.
Thread and process contexts are not normally a problem for most device drivers. When you process a normal user request, the IRP has all the information you need. The fact that you may be running in an arbitrary thread context does not effect the job you have to do. The Zw… file access functions are one case when the process context is significant. If you use "neither" Buffered I/O nor Direct I/O or use METHOD_NEITHER IOCTLs, the thread context is also important.
The solution to both these problems is to use a system thread. A driver can create a system thread (or threads) that runs in kernel mode. This thread runs at PASSIVE_LEVEL IRQL. If the DebugPrint system thread makes all the Zw… calls, the "same process" problem is fixed. A doubly-linked list is used to store the events for processing by the system thread. Inserting events into this list can be done safely at IRQL levels up to and including DISPATCH_LEVEL This means that most types of driver code can generate DebugPrint trace output.
The DebugPrintInit routine calls PsCreateSystemThread (at PASSIVE_LEVEL IRQL) to create its system thread as shown in the partial code in Listing 14.1. PsCreateSystemThread is passed the name of the function to run and a context to pass to it. The DebugPrint system thread function is defined as follows.
void DebugPrintSystemThread(IN PVOID Context)
DebugPrintInit passes just a NULL context to the thread function. Some drivers may wish to create one thread per device, and so will usually pass a pointer to the device extension as the context. A DebugPrint test driver has just one system thread for all its devices.
The DebugPrint system thread does not need a high priority. Therefore, DebugPrintSystemThread calls KeSetPriorityThread straightaway to set its priority to the lowest real-time priority. It uses KeGetCurrentThread to get a pointer to its own thread object.
If PsCreateSystemThread succeeds, it returns a handle to the thread. Later, I shall show that a driver can wait for certain objects to be set. It can wait for the completion of a system thread if it has a pointer to the system thread object, not a handle. Use ObReferenceObjectByHandle to retrieve the thread object pointer. If this succeeds, call ZwClose to close the thread handle. Technically speaking, the call to ZwClose reduces the reference count to the thread handle. When the thread completes, its handle reference count will be decremented to zero.
A system thread must terminate itself using PsTerminateSystemThread. I am not sure what happens if a system thread function simply returns without calling PsTerminateSystemThread. The main driver code cannot force a system thread to terminate. For this reason, a global Boolean variable called ExitNow is used. This is set true when the driver wants the system thread to exit.
DebugPrintClose waits for the ThreadExiting event to be set into the signalled state by the thread as it exits. This ensures that the thread has completed by the time DebugPrintClose exits. DebugPrintClose is commonly called from a driver unload routine. All driver code must have stopped running when the unload routine exits, as the driver address space may soon be deleted. Make sure that any other call-backs are disabled, e.g., interrupt handlers, Deferred Procedure Calls, and timers.
Listing 14.1 DebugPrint test driver thread and event handling
KEVENT ThreadEvent;
KEVENT ThreadExiting;
PVOID ThreadObjectPointer=NULL;
void DebugPrintInit(char* _DriverName) {
// …
ExitNow = false;
KeInitializeEvent(&ThreadEvent, SynchronizationEvent, FALSE);
KeInitia1izeEvent(&ThreadExiting, SynchronizationEvent, FALSE);
HANDLE threadHandle;
status = PsCreateSystemThread(&threadHandle, THREAD_ALL_ACCESS, NULL, NULL, NULL, DebugPrintSystemThread, NULL);
if (!NT_SUCCESS(status)) return;
status = ObReferenceObjectByHandle(threadHandle, THREAD_ALL_ACCESS, NULL, KernelMode, &ThreadObjectPointer, NULL);
if (NT_SUCCESS(status)) ZwClose(threadHandle);
// …
}
void DebugPrintClose() {
// …
ExitNow = true;
KeSetEvent(&ThreadEvent, 0, FALSE);
KeWaitForSingleObject(&ThreadExiting, Executive, KernelMode, FALSE, NULL);
// …
}
void DebugPrintSystemThread(IN PVOID Context) {
// Lower thread priority
KeSetPriorityThread(KeGetCurrentThread(), LOW_REALTIME_PRIORITY);
// Make One second relative timeout
LARGE_INTEGER OneSecondTimeout;
OneSecondTimeout.QuadPart = –1i64 * 1000000i64 * 10i64;
// Loop waiting for events or ExitNow
while (true) {
KeWaitForSingleObject(&ThreadEvent, Executive, KernelMode, FALSE, &0neSecondTimeout);
// Process events
// …
if (ExitNow) break;
}
// Tidy up
if (ThreadObjectPointer!=NULL) {
ObDereferenceObject(&ThreadObjectPointer);
ThreadObjectPointer = NULL;
}
DebugPrintStarted = FALSE;
ClearEvents();
KeSetEvent(&ThreadExiting, 0, FALSE);
PsTerminateSystemThread(STATUS_SUCCESS);
}
System Worker Threads
An alternative technique is available if you want to perform occasional short tasks at PASSIVE_LEVEL IRQL. However, this system worker thread method was not suitable for the DebugPrint software because the thread context may change between calls.
To use the system worker thread method, first allocate a WORK_QUEUE_ITEM structure from nonpaged memory. Call ExInitializeWorkItem passing this pointer, a callback routine, and a context for it. When you want your function to be run, call ExQueueWorkItem. In due course, your callback routine is called at PASSIVE_LEVEL in the context of a system thread. Do not forget to free the WORK_QUEUE_ITEM structure memory when finished with it. System worker threads have a lower priority than system threads running at the lowest real time priority, but higher than most user mode threads.
The WdmIo and PHDIo drivers, described in Chapters 15-18, show how to use a system worker thread.
In W2000, it is recommended that you use the IoAllocateWorkItem, IoQueueWorkItem, IoFreeWorkItem functions instead.
The main DebugPrint test driver code sets the ExitNow Boolean to true when it wants its system thread to terminate. However, it is not a good idea for the system thread to spin continuously waiting for this value to become true.
Instead, a kernel event called ThreadEvent signals when to check ExitNow. A KEVENT must be defined for the event in nonpaged memory. Kernel events are very similar to their user mode Win32 cousins.
Listing 14.1 shows how ThreadEvent is initialized using KeInitializeEvent, at PASSIVE_LEVEL IRQL. Two types of events are supported, SynchronizationEvent and NotificationEvent. The last parameter sets the initial state of the event, which is nonsignalled in this case.
When set, a Synchronization event only releases one waiting thread before reverting to the nonsignalled state. A Notification event stays signalled until explicitly reset.
DebugPrintClose uses KeSetEvent to set an event into the signalled state, after setting ExitNow to true. The third parameter to KeSetEvent specifies whether you are going to call one of the KeWait… routines straightaway. If not, you can call KeSetEvent at any IRQL up to and including DISPATCH_LEVEL. If waiting, you must be running at PASSIVE_LEVEL.
If you need to put an event into the nonsignalled state, call KeClearEvent or call KeResetEvent to determine the previous event state. You can use KeReadStateEvent to read the event state. All these routines can be called at DISPATCH_LEVEL or lower.
For NT and W2000 drivers you can use IoCreateNotificationEvent and IoCreateSynchronizationEvent to share an event between two or more drivers.
A thread running at PASSIVE_LEVEL can synchronize with other activities by waiting for dispatcher objects such as events, Mutex objects, and semaphores. You can wait for timer and thread objects. Finally, you can also wait on file objects if they have been opened in ZwCreateFile for overlapped I/O.
Although driver dispatch routines run at PASSIVE_LEVEL, they should not wait on kernel dispatcher objects, other than with a zero time-out. You can wait for inherently synchronous operations to complete using nonzero time-outs. Plug and Play handlers can wait on dispatcher objects. For example, the ForwardIrpAndWait routine described in Chapter 9 uses an event to signal when lower drivers have finished processing an IRP.
A thread waits for dispatcher objects to become signalled using KeWaitForSingleObject or KeWaitForMultipleObjects, which are similar to the Win32 equivalents. As Table 14.1 shows, KeWaitForSingleObject waits on just one dispatcher object, or until a time-out has expired. A negative timeout value is used for relative periods, as a LARGE_INTEGER in units of 100 nanoseconds. The DebugPrint system thread calls KeWaitForSingleObject with a relative time-out of one second. Positive time-out values represent an absolute system time, in 100-nanosecond units since January 1, 1601[32] in the GMT time zone.
The KeWaitForMultipleObjects routine works in a similar way, except that you can pass an array of dispatcher objects. You can opt to wait for just one of the objects to become signalled, or all of them.
Table 14.1 KeWaitForSingleObject function
NTSTATUS KeWaitForSingleObject | (IRQL==PASSIVE_LEVEL) or at DISPATCH_LEVEL if a zero time-out is given |
---|---|
Parameter | Description |
IN PVOID Object | Pointer to dispatcher object |
IN KWAIT_REASON WaitReason | Usually Executive for drivers, but can be UserRequest if running for user in a user thread. |
IN KPROCESSOR_MODE WaitMode | Kernel Mode for drivers |
IN BOOLEAN Alertable | FALSE for drivers |
IN PLARGE_INTEGER | Timeout NULL for an infinite time-out. Negative time-outs are relative. Positive time-outs are absolute. |
Returns | STATUS_SUCCESS STATUS_TIMEOUT |
Mutex Objects
A Mutex is a mutual exclusion dispatcher object that can only be owned by one thread at a time. Mutexes are sometimes called "mutants." Initialize a KMUTEX object in nonpaged memory using KeInitializeMutex; the Level parameter is used to ensure that multiprocessor Windows 2000 systems can acquire multiple Mutexes safely.
A Mutex object is in the signalled state when it is available. A thread requests ownership using one of the KeWaitFor… routines. If two or more threads are waiting for a Mutex, only one thread will wake up and become its owner. Call KeReleaseMutex to release ownership.
If you already own a Mutex and ask for it again, the KeWaitFor… routine will return immediately. An internal counter is incremented, so call KeReleaseMutex once for each time you requested ownership of the Mutex.
The kernel causes a bugcheck if you do not release a Mutex before returning control to the I/O Manager. KeInitializeMutex and KeReleaseMutex must be called at PASSIVE_LEVEL You can also inspect the Mutex state using KeReadStateMutex at an IRQL up to and including DISPATCH_LEVEL
A Fast Mutex is a variation on an ordinary Mutex that is faster because it does not permit multiple ownership requests. An Executive Resource is another similar synchronization object, available in W2000 only. See the DDK documentation for more details of these objects.
Semaphores
A semaphore is a dispatcher object that maintains a count. Call KeInitializeSemaphore at PASSIVE_LEVEL IRQL to initialize a KSEMAPHORE object in nonpaged memory. You must specify maximum and initial counts.
A semaphore is nonsignalled when zero and signalled with any count greater than zero. A thread that calls one of the KeWaitFor… routines and finds a signalled semaphore will decrement its count and the thread will proceed. If a semaphore's count is 2 and three threads simultaneously attempt to wait for the semaphore, only two will proceed. The semaphore count ends up as 0 with one thread still waiting.
Call KeReleaseSemaphore, at DISPATCH_LEVEL or lower, to add a value to a semaphore count. You can read the semaphore count at any IRQL using KeReadStateSemaphore.
Timer, Thread, and File Objects
A timer is a dispatcher object that becomes signalled when its timer expires. A file object becomes signalled when an overlapped I/O operation has completed. The file must have been opened in ZwCreateFile with the DesiredAccess SYNCHRONIZE flag set. You can also wait for thread completion.
DebugPrint System Thread Function
Listing 14.1 shows that the DebugPrint system thread for drivers under test primarily consists of a loop that waits for the ExitNow flag to become true or for trace events to arrive.
At the top of this main loop, the thread function calls KeWaitForSingleObject to wait for the ThreadEvent to become signalled. As stated previously, DebugPrintClose sets the ExitNow flag to true and sets the ThreadEvent into the signalled state. The thread function is released; if it finds ExitNow true, it exits its main loop, tidies up, and terminates.
The call to KeWaitForSingleObject includes a one-second time-out. This is used to let the thread function look for and process trace events in the EventList buffer, as described in the next sections.
The two formatted print functions, DebugPrint and DebugPrint2, eventually call DebugPrintMsg. I will not go into the details of how the formatted prints work. You can work it out for yourself by looking at the code in the Print… and DebugSprintf routines in DebugPrint.c. The only point to note is that the DebugPrint routines can accept a variable number of arguments. I have assumed — successfully so far — that the va_list macros defined in stdarg.h work satisfactorily in kernel mode drivers.
Listing 14.2 shows how DebugPrintMsg builds a trace event and puts it in a DEBUGPRINT_EVENT structure allocated from nonpaged memory. The DEBUGPRINT_EVENT structure is added into the EventList doubly-linked list. DebugPrintMsg is passed a NULL-terminated ANSI message string.
The event data consists of the following three items:
• the current system time in GMT,
• the driver name (specified in DebugPrintInit), and
• the message.
DebugPrintMsg first gets the current system time in GMT using KeQuerySystemTime. It converts the number of 100-nanosecond intervals since January 1, 1601 into a more recognizable TIME_FIELDS structure using RtlTimeToTimeFields. The ExSystemTimeToLocalTime function (which converts from GMT to local time) is only available in W2000, so it is not used here. The time is converted to the local timezone in the DebugPrint Monitor application.
It would reduce the event structure size if the LARGE_INTEGER output from KeQuerySystemTime were stored directly. However, there is no equivalent of the RtlTimeToTimeFields routine in Win32, so the event structure holds the time as time fields.
DebugPrintMsg now works out the size of the event data (i.e., the three previous data items, including the strings' terminating NULLs). It then determines the size of the DEBUGPRINT_EVENT structure that envelops the event data. It allocates some nonpaged memory for this structure and fills it in. It then uses ExInterlockedInsertTailList to insert the DEBUGPRINT_EVENT structure at the end of EventList.
Listing 14.2 DebugPrint test driver DebugPrintMsg function
void DebugPrintMsg(char* Msg) {
if (!DebugPrintStarted) return;
// Get current time
LARGE_INTEGER Now, NowLocal;
KeQuerySystemTime(&Now);
TIME_FIELDS NowTF;
RtlTimeToTimeFields(&Now, &NowTF);
// Get size of Msg and complete event
USHORT MsgLen = ANSIstrlen(Msg)+1;
ULONG EventDataLen = sizeof(TIME_FIELDS) + DriverNameLen + MsgLen;
ULONG len = sizeof(LIST_ENTRY)+sizeof(ULONG)+EventDataLen;
// Allocate event buffer
PDEBUGPRINT_EVENT pEvent = (PDEBUGPRINT_EVENT)ExAllocatePool(NonPagedPool.len);
if (pEvent!=NULL) {
PUCHAR buffer = (PUCHAR)pEvent->EventData;
// Copy event info to buffer
RtlCopyMemory(buffer, &NowTF, sizeof(TIME_FIELDS));
buffer += sizeof(TIME_FIELDS);
RtlCopyMemory( buffer, DriverName, DriverNameLen);
buffer += DriverNameLen;
RtlCopyMemory(buffer, Msg, MsgLen);
// Insert event into event list for processing by system thread
pEvent->len = EventDataLen;
ExInterlockedInsertTailList(&EventList, &pEvent->ListEntry, &EventListLock);
}
}
Doubly-Linked Lists
A doubly-linked list is a slightly complicated beast to use safely. First, you need to declare a LIST_ENTRY structure in nonpaged memory for the list head. Drivers that need one list per device declare the list head in the device extension. However, the DebugPrint test driver code declares just its EventList variable as a global, as it is available to all devices.
LIST_ENTRY EventList;
Next, define a structure that you want to put in your doubly-linked list. Include a LIST_ ENTRY field in this structure to provide the links in both directions of the list. The DebugPrint structure is called DEBUGPRINT_EVENT. EventData is a variable length field, as it is not always 1-byte long. The Len field gives its length.
typedef struct _DEBUGPRINT_EVENT {
LIST_ENTRY ListEntry;
ULONG Len;
UCHAR EventData[1];
} DEBUGPRINT_EVENT, *PDEBUGPRINT_EVENT;
Initialize a doubly-linked list using InitializeListHead, passing a pointer to the list head variable. You can now insert DEBUGPRINT_EVENT structures at the head or tail of the list using the InsertHeadList and InsertTailList routines. The corresponding RemoveHeadList and RemoveTailList routines remove entries from the list[33]. Find out if the list is empty first using IsListEmpty.
All well and good. However, it is important that attempts to access the list are carried out safely so that the links are not corrupted in a multiprocessor environment. The kernel provides interlocked versions of the add and remove routines that use a spin lock to guard access to the link structure. The DebugPrint test driver code uses a spin lock called EventListLock and initializes it as normal.
KeInitializeSpinLock(&EventListLock);
InitializeListHead(&EventList);
Listing 14.2 shows how to use one of the interlocked linked list routines, ExInterlockedInsertTailList. It is passed pointers to the list head, the LIST_ENTRY field in your structure, and the spin lock.
Listing 14.3 shows an extract from the DebugPrint test driver system thread function. This is the code that is run every second to see if any events have been produced by calls to DebugPrintMsg.
Listing 14.3 DebugPrint test driver system thread event processing
// Loop until all available events have been removed
while(true) {
PLIST_ENTRY pListEntry = ExInterlockedRemoveHeadList(&EventList, &EventListLock);
if (pListEntry==NULL) break;
// Get event as DEBUGPRINT_EVENT
PDEBUGPRINT_EVENT pEvent = CONTAINING_RECORD(pListEntry, DEBUGPRINT_EVENT, ListEntry);
// Get length of event data
ULONG EventDataLen = pEvent->Len;
// Send event to DebugPrint
NTSTATUS status = ZwWriteFile(DebugPrintDeviceHandle, NULL, NULL, NULL,
&IoStatus, pEvent->EventData, EventDataLen, &ByteOffset, NULL);
// Ignore error returns
// Free our event buffer
ExFreePool(pEvent);
}
The code loops until all the events in EventList have been removed and sent to the DebugPrint driver. It removes the first entry from the doubly-linked list using ExInterlockedRemoveHeadList, passing pointers to the list head and the guarding spin lock. The return value is NULL if there is nothing left in the list.
ExInterlockedRemoveHeadList returns a pointer to the ListEntry field in the DEBUGPRINT_ EVENT structure. What is really needed is not this, but a pointer to the DEBUGPRINT_EVENT structure itself. For this particular structure, a simple cast would suffice. However, there is a way to deal correctly with the general case in which the LIST_ENTRY variable is not at the beginning of the structure. The system header files provide the appropriate macro, CONTAINING_RECORD. Pass the LIST_ENTRY pointer, the data type of your structure and the name of its LIST_ENTRY field. The returned value is the pointer to the DEBUGPRINT_EVENT structure.
Having got the correct event pointer in pEvent, the system thread extracts the length of the event data and writes the event data itself to the DebugPrint driver using ZwWriteFile. Finally, it frees the memory that was allocated for the DEBUGPRINT_EVENT structure, before checking to see if any more events are available.
The ClearEvents routine is called to clear any remaining events when the system thread finishes or DebugPrintClose is called. ClearEvents removes any events using ExInterlockedRemoveHeadList and frees the event memory.
Singly-Linked Lists
There are kernel functions for singly-linked lists, which are really stacks. Declare a list head as a variable of type SINGLE_LIST_ENTRY. Initialize it by setting its Next field to NULL. PushEntryList puts an entry onto the front of the list, while PopEntryList removes an entry from the front of the list. You must use the same technique as before to get the correct pointer from a popped entry: include a SINGLE_LIST_ENTRY field in the structure you put on the list and use CONTAINING_RECORD.
There are also interlocked versions of these routines, using a spin lock as before to ensure accesses are carried out safely.
Device Queues
A device queue is an enhanced form of a doubly-linked list, usually used for storing IRPs. These are covered in glorious detail in Chapter 16.
Let's draw together the final pieces of the DebugPrint code for test drivers.
DebugPrintInit makes a copy of the driver name passed to it. Why does it bother to do this? Well, initially it did not. However, the call to DebugPrintInit usually comes from a DriverEntry routine. DriverEntry code is often marked as being discardable once the driver has been initialized. This meant that the code with the driver name string might be discarded. I found that this did indeed happen and so the original pointer referred to invalid memory. Watch out for this problem in your drivers.
Listing 14.4 shows how the system thread opens a connection to the DebugPrint driver using ZwCreateFile. It opens a connection to a DebugPrint driver device called \Device\PHDDebugPrint. The DebugPrint driver also uses a driver interface so that user mode programs, such as DebugPrint Monitor, can find this device.
While drivers can use Plug and Play Notification to find other drivers that support a device interface, this is a complicated approach. Instead, the one and only DebugPrint device is given a kernel name of PHDDebugPrint. Calls to ZwCreateFile do not use Win32 symbolic link names for devices. Instead, as in this example, they must use the full kernel device name.
The filename must be specified as a UNICODE_STRING. RtlInitUnicodeString is used to initialize the DebugPrintName variable. The filename is set into an OBJECT_ATTRIBUTES structure using InitializeObjectAttributes. A pointer to this structure is finally passed to ZwCreateFile. Like its Win32 equivalent, you must specify access and share parameters to ZwCreateFile. There are a host of other options, so consult the documentation for full details. Finally, you get a file HANDLE to use in further I/O requests.
As mentioned earlier, subsequent calls to ZwReadFile, ZwWriteFile, ZwQueryInformationFile, and ZwClose must take place in the same thread context as the call to ZwCreateFile. All these file I/O routines must be called at PASSIVE_LEVEL.
A typical call to ZwWriteFile is illustrated in Listing 14.3. As well as the file handle, simply specify the data pointer and a transfer count. The completion details can be found in an IO_STATUS_BLOCK structure. Specify a file pointer byte offset.
The DebugPrint test driver code eventually closes the file handle using ZwClose just before it terminates.
The system thread code tried to open its connection to the DebugPrint driver for 5 minutes. This ensures that the DebugPrint driver has started during system startup.
Listing 14.4 DebugPrint test driver system thread file opening
// Make appropriate ObjectAttributes for ZwCreateFile
UNICODE_STRING DebugPrintName;
RtlInitUnicodeString(&DebugPrintName, _"\\Device\\PHDDebugPrint");
OBJECT_ATTRIBUTES ObjectAttributes;
InitializeObjectAttributes(&ObjectAttributes, &DebugPrintName, OBJ_CASE_INSENSITIVE, NULL, NULL);
// Open handle to DebugPrint device
IO_STATUS_BLOCK IoStatus;
HANDLE DebugPrintDeviceHandle = NULL;
NTSTATUS status = ZwCreateFile(&DebugPrintDeviceHandle,
GENERIC_READ | GENERIC_WRITE,
&ObjectAttributes, &IoStatus,
0, // alloc size = none
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ|FILE_SHARE_WRITE,
FILE_OPEN,
0,
NULL, // eabuffer
0); // ealength
if (!NT_SUCCESS(status) || DebugPrintDeviceHandle==NULL) goto exit1;
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.
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.
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, \Device\PHDDebugPrint. 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"\\Device\\PHDDebugPrint");
// 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.
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;
}
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);
}
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);
}
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;
}
The DebugPrint Monitor is a user mode application that keeps reading events from the DebugPrint driver and displays them to the user. The Monitor is a standard MFC application, so this chapter will not discuss all the code in detail. The source files can be found on the book software disk.
The Monitor program saves its current screen position and column widths in the registry. Consult the code to find out how this is done.
The Monitor uses a single document interface. The view class CDebugPrintMonitorView is derived from CListView, which encapsulates a list view control. It is this list view control that stores the event information once it has been received by the Monitor. The corresponding document class CDebugPrintMonitorDoc does not store the event information. It simply implements the OnSaveDocument document method to save event information from the view to a .dpm file, and OnOpenDocument to load data.
The main work is carried out in a worker thread, which runs ListenThreadFunction in Listener.cpp. This thread chugs away in the background, reading any available events from the DebugPrint driver. It posts a message to the view class for each event, passing a pointer to the event data. The view class handles these messages in its OnDebugPrintEvent routine by inserting an appropriate event item at the end of the list control.
The DebugPrint Monitor application class calls StartListener in its InitInstance method to start the listen worker thread. The ExitInstance method calls StopListener to stop it.
The code uses a LISTENER_INFO structure to pass information to the worker thread. There is only one instance of this object called ListenerInfo.
typedef struct _LISTENER_INFO {
HANDLE DebugPrintDriver;
bool KeepGoing;
} LISTENER_INFO, *PLISTENER_INFO;
LISTENER_INFO ListenerInfo;
StartListener first stores a safe HWND handle to the view window, to be used later.
It then finds the first DebugPrint driver device and opens a handle to it using GetDeviceViaInterface01. This is a variant on GetDeviceViaInterface, shown in Chapter 5. GetDeviceViaInterface01 opens the device for overlapped I/O by specifying the FILE_FLAG_OVERLAPPED bit in the dwFlagsAndAttributes parameter to CreateFile. The DebugPrint device handle is stored in ListenerInfo.DebugPrintDriver.
The code uses the KeepGoing field in ListenerInfo to signal when the worker thread should stop. KeepGoing is therefore set to true before the thread is started. The thread is started using AfxBeginThread, passing a pointer to the function to run, ListenThreadFunction, and a context to pass to it.
ListenThreadFunction loops, waiting either for event data or for KeepGoing to return false. ListenThreadFunction is in the file Dispatch.cpp on the CD-ROM.
The DebugPrint_Event class shown in Listing 14.11 is used to communicate events to the view class. Each class instance has Driver and Message strings and an MFC CTime Timestamp. A static method called SendEvent is used to post a DebugPrint_Event message to the view class.
DebugPrint_Event::SendEvent( "Monitor", "Starting to listen", CTime::GetCurrentTime(), false);
Listing 14.11 DebugPrint driver DebugPrint_Event class
const UINT WM_DEBUGPRINTEVENT = (WM_USER+1);
class DebugPrint_Event {
public:
CString Driver;
CTime Timestamp;
CString Message;
bool SetModified; // false to reset document SetModifiedFlag.
static HWND ViewHwnd; // View Hwnd
// Generate and send an event
static void SendEvent(CString d, CString m, CTime t = 0, bool sm=true) {
if (ViewHwnd==NULL) return;
DebugPrint_Event* pEvent = new DebugPrint_Event;
pEvent->Driver = d;
if (t==0) t = CTime::GetCurrentTime();
pEvent->Timestamp = t;
pEvent->Message = m;
pEvent->SetModified = sm;
::PostMessage(ViewHwnd, WM_DEBUGPRINTEVENT, 0, (LPARAM)pEvent);
}
};
The code in ListenThreadFunction mainly deals with overlapped I/O to the DebugPrint device. It must check the state of the KeepGoing flag. Overlapped I/O lets us issue a read request and get on with other tasks.
To do overlapped I/O, a Win32 event and an OVERLAPPED structure are needed. CreateEvent is used to initialize the FileIOWaiter manual event into the nonsignalled state. The OVERLAPPED structure (ol) stores the file pointer offset and FileIOWaiter event handle.
A standard ReadFile call is used to initiate a read request. The read buffer is 1024 bytes, which should be large enough for any DebugPrint event. ReadFile is passed a pointer to the OVERLAPPED structure. Overlapped I/O does work with device files in Windows 98, but does not work on ordinary file I/O.
ReadFile returns true if the read request completes straightaway. The number of bytes transferred is stored in TxdBytes. If the read request is held in the DebugPrint read queue, ReadFile returns false and GetLastError returns ERROR_IO_PENDING. The code checks for a real error return from ReadFile. If the DebugPrint Monitor application is run twice, the second incarnation will get an error here when the DebugPrint driver read routine fails an attempt to queue a second Read IRP.
If the read request is pending, ListenThreadFunction loops calling WaitForSingleObject with a timeout of 100ms. If WaitForSingleObject times-out, ListenThreadFunction checks to see if KeepGoing has returned false. If so, it calls CancelIo to cancel the pending Read IRP and exits. Incidentally, calling CancelIo in another thread, such as the StopListener routine, does not work.
WaitForSingleObject detects when the read request has finished when the FileIOWaiter event becomes signalled. In Windows 2000, ListenThreadFunction could wait on the file object instead of an event. However, this does not work in Windows 98, so I use events that work in both operating systems. ListenThreadFunction calls GetOverlappedResult to retrieve the number of bytes that were received.
The remaining code extracts the event information from the read buffer and builds an event object to post to the Monitor view class. The event timestamp that was generated in the DebugPrintMsg routine is a time in GMT. The GMTtoLocalTime function does all the necessary grovelling around to convert this into a local time.
ListenThreadFunction finally closes the event and file handles using CloseHandle.
Listing 14.12 DebugPrint driver ListenThreadFunction function
UINT ListenThreadFunction(LPVOID pParam) {
PLISTENER_INFO pListenerInfo = (PLISTENER_INFO)pParam;
if (pListenerInfo==NULL) return –1;
CString StartMsg = "Starting to listen";
// …
DebugPrint_Event::SendEvent("Monitor", StartMsg, CTime::GetCurrentTime(), false);
// Buffer for events
const int MAX_EVENT_LEN = 1024;
char Event[MAX_EVENT_LEN+1];
// Create Overlapped read structure and event
HANDLE FileIOWaiter = CreateEvent(NULL, TRUE, FALSE, NULL);
if (FileIOWaiter==NULL) goto Exit2;
OVERLAPPED ol;
ol.Offset = 0;
ol.OffsetHigh = 0;
ol.hEvent = FileIOWaiter;
// Keep looping, waiting for events, until KeepGoing goes false
for(;;) {
// Initiate overlapped read
DWORD TxdBytes;
ResetEvent(FileIOWaiter);
memset(Event,0,MAX_EVENT_LEN+1);
if (!ReadFile(ListenerInfo.DebugPrintDriver, Event, MAX_EVENT_LEN, &TxdBytes, &ol)) {
// Check for read errors
if (GetLastError() !=ERROR_IO_PENDING) {
CString Msg;
Msg.Format("Read didn't return pending %d", GetLastError());
DebugPrint_Event::SendEvent("Monitor", Msg);
goto Exit;
}
// Wait for read to complete (check for KeepGoing
// going false every 100ms)
while (WaitForSingleObject(FileIOWaiter, 100)== WAIT_TIMEOUT) {
if (!ListenerInfo.KeepGoing) {
// Cancel the pending read
CancelIo(ListenerInfo.DebugPrintDriver);
goto Exit;
}
}
// Get read result, ie bytes transferred
if (!GetOverlappedResult(ListenerInfo.DebugPrintDriver, &ol, &TxdBytes, FALSE)) {
DebugPrint_Event::SendEvent("Monitor", "GetOverlappedResult failed");
continue;
}
}
// Check there's something there
if (TxdBytes < sizeof(TIME_FIELDS)+2) {
DebugPrint_Event::SendEvent("Monitor", "Short read msg");
continue;
}
// Extract Timestamp, Driver and Msg, and post to View
Event[MAX_EVENT_LEN] = '\0';
PTIME_FIELDS pTF = (PTIME_FIELDS)Event;
CTime gmtEventTime(pTF->Year, pTF->Month, pTF->Day, pTF->Hour, pTF->Minute, pTF->Second);
CTime EventTime = GMTtoLocalTime(gmtEventTime);
char* DriverName = Event+sizeof(TIME_FIELDS);
CString CSDriverName = DriverName;
CString CSDriverMsg = Event+sizeof(TIME_FIELDS)+strlen(DriverName)+1;
DebugPrint_Event::SendEvent(CSDriverName, CSDriverMsg, EventTime);
}
Exit:
CloseHandle(FileIOWaiter);
Exit2:
CloseHandle(ListenerInfo.DebugPrintDriver);
ListenerInfo.DebugPrintDriver = NULL;
DebugPrint_Event::SendEvent("Monitor", "Stopped listening");
return 0;
}
This chapter has built a full working driver, which can be used to generate debug trace prints that can be seen in a user application. It has covered system threads, dispatcher objects, linked lists, file I/O, a simple IRP queue, and IRP cancel routines.
Since writing this chapter, the test driver DebugPrint code has changed from C++ to C. The code on the book software disk is slightly different from the code printed in this chapter, though they are functionally identical.
I.e., soon after the Gregorian calendar was introduced in 1582.
The NT 4 DDK wrongly states that RemoveHeadList and RemoveTailList return NULL if the list is empty.