52964.fb2
Before looking at other aspects of WDM drivers, it is worth while looking at how to test and debug drivers. This chapter explains how to use the DebugPrint tool that comes with the book software. This lets a driver "print" debug event messages to a Win32 application, the DebugPrint Monitor.
A later chapter looks in detail at how the DebugPrint software works.
Keep your driver as simple as possible to make it easier to test and debug.
Oh, and needless to say — but I will say it anyway — comment your driver very well. Document it well. Do not forget to update your comments and documentation as the driver develops.
Developers usually want to start debugging only when they suspect something has gone wrong with their software. However, it is especially important to start work early with device drivers. Even if you think a driver is doing its job, check it thoroughly. Do not get a first cut working and ship it to customers immediately.
Here is an example of a situation where I did not test code properly. When developing the DebugPrint software, I needed to implement a formatted print function. I could not use sprintf, as this function might use facilities that are not available to kernel mode drivers.
To make things simple, I developed the routines in a Win32 program and got them working. I copied the source over to the DebugPrint driver. A quick test seemed to indicate that it worked OK. In fact, I had changed something and so my handling of ellipsis arguments went wrong (passing a variable number of arguments to a function). By a complete fluke, one of the simple tests I inserted did get the correct information from the stack. It took a while to realize the error of my ways.
This section describes some of the testing issues that are particularly important for device drivers. The following debugging section highlights some of the typical ways that drivers fail. If you — and only you — are writing the user mode program that accesses your driver, you may be able to impose certain constraints on your driver. For example, you could dictate that only one request is ever issued at a time. This would make some of your routines slightly easier to implement. However, it is best to write your routines properly straightaway. It is quite possible for someone to later change the Win32 code without realizing that it might break your driver.
This may sounds obvious, but check that your driver works. This means writing a test program (or other driver) that exercises all of its functions.
As with any other program, check that all sorts of data work as expected. For example, check that invalid parameters are detected properly. Check that small and large amounts of data can be transferred. Check boundary conditions. For example, if you have a maximum request size of 1024 bytes, check that transfers of 0, 1, 1023, and 1024 bytes succeed and that transfers of 1025 bytes fail.
Check that all the features of the hardware you are driving are exercised. Do you have a strategy for coping with hardware updates? For example, leave some room for more function codes. Make sure that your new hardware firmware can be used by the old driver.
Check that a driver works in stress situations (e.g., in low memory computers or when being pummelled by lots of requests). Do this by repeatedly issuing I/O requests to your driver. Test with other processes doing the same job simultaneously. Another good test is to run your driver while copying a large file to a floppy disk; in Windows 98, this seems to stop most other activities dead in their tracks.
Check that a driver works when more than one of its devices are in use. Check that several user programs or threads can access the driver simultaneously.
Check that your driver works on a clean PC that has never seen your driver. Test on plain vanilla W2000 and W98 systems, without any of the development environment. Check that you installed any recent DLLs that are needed. For example, your user mode installation code may not run with an out-of-date version of SETUPAPI.DLL.
Do all your tests in Windows 2000 and Windows 98 if you are writing a driver that runs under both operating systems. If you provide a Dec Alpha W2000 version, check that it works as well.
In particular, check that a driver installs correctly in both operating systems. W2000 and W98 use different locations for some registry entries, though this should not effect most drivers.
Some facilities are only available in NT or W2000. For example, a driver can make calls to write events to the system log. In W98, these functions do nothing. Check that they work properly in NT and Windows 2000.
As long as you use the correct WDM.H header, you should not be making any kernel calls that work only in W2000. For example, I was going to use the ExSystemTimeToLocalTime kernel call in the DebugPrint driver. However, the documentation says that you must include NTDDK.H. This means that the call is not available in W98.
If possible, check that your driver works in a multiprocessor computer. Even if you do run it on such a computer, you may have difficulty deciding whether it has passed the test as the time-critical events may not happen very often. Stress testing the driver should force the issue.
Win32 programs can issue asynchronous "overlapped" I/O requests, meaning that they can issue a request and get on with another job while the I/O is happening. Windows 98 programs cannot issue such requests for file I/O, but they can for device I/O.
Initially, there seems to be no difference as far as a device driver is concerned. Both synchronous "blocking" I/O requests and asynchronous I/O requests arrive at the driver in the same way, through the driver dispatch entry points.
However, asynchronous I/O requests can be cancelled. This can happen either when the CancelIo function[13] is called, or if the file handle is closed with the I/O still pending.
Synchronous I/O requests are also cancelled if a process crashes. The operating system tries to cancel any requests and close any open file handles.
How a driver copes with cancelling I/O is covered in a later chapter. Ensure that your test program checks these situations.
Win32 user mode programs run within the protective arms of the operating system. For example, it stops them accessing memory that is not in a task's address space. Windows 98 programs have some of the kernel mapped into its upper addresses, so user mode programs could trample on the kernel. The protection offered by NT and Windows 2000 is better. If a user mode application runs into problems, it is usually possible to stop the program and carry on with other work.
A device driver has none of this protection because it is part of the kernel. Be especially careful to test your driver fully or you may lose data. Be prepared for the worst on your own development computer by making regular backups.
Although problems with a driver can be a real pain, it can eventually give you a greater understanding of what you are doing. You have to inspect each call or section of code carefully. Rather than just following the instructions given here, you may eventually really know what is going on. Or even, heaven forbid, why the system was designed to work the way it does. You may have suggestions for how drivers or the system might be improved.
Windows 98 and Windows 2000 sometimes react differently to errors in drivers.
Crashes
A fatal error in NT or W2000 causes the "blue screen of death", properly called a "bugcheck". You must reset the computer to continue. The blue screen gives an indication of the error, and usually a list of the kernel modules and a stack trace. More details of how to decode this information are given later. Annoying though these bug checks are, they do stop further damage being done to the operating system, such as corrupting disks.
NT and W2000 usually log a "Save Dump" event for a bugcheck, listing the most pertinent information. An event may not be logged if the bugcheck occurred at boot time, before the event log service has started.
In W98, a similar blue screen appears for fatal errors, again giving a brief description of what went wrong. W98 can usually carry on, but it may be best to restart the PC.
The two most common causes of fatal errors are
• Accessing nonexistent memory
• Accessing paged memory at or above DISPATCH_LEVEL IRL
Core Dumps
If NT or W2000 has a bugcheck, you can also get it to produce core dumps in a file on disk, called memory.dmp by default. You have to enable this option in the Control Panel System applet Advanced tab Startup and Recovery section. You can use the WinDbg and DumpExam tools to inspect the dump. However, I found that using WinDbg in this way was not particularly productive.
In NT and W2000, you can include information in the core dump by calling KeInitializeCallbackRecord and KeRegisterBugCheckCallback. In the event of a bugcheck, your callback routine is called to store any state information in the core dump.
Driver Will Not Start
When you update a driver like Wdm1, the Device Manager may state that you need to restart the system before the driver will run. This means that the driver returned an error while loading.
There are two possible causes for such errors. Suppose your driver will start when the system reboots. This means that when it unloads it must be leaving something around that stops it from starting again. A common problem is forgetting to delete your device or the symbolic link to your device.
If your driver will not start when the system reboots, it means that you must have changed your driver so that the DriverEntry routine fails. Trace through the code to work out what is going wrong.
Just to complicate matters, in some circumstances, Windows needs to juggle the revised system resources when a driver is reloaded. In this case, Windows will not even attempt to start your driver if a reboot is necessary to satisfy the resource allocation process.
Hang Ups
A user thread can hang up if you never complete an IRP. The thread will never be able to complete. Have you queued the IRP somewhere and never processed it?
A driver can also hang up if it cannot acquire a resource that it needs. The different resources are described later. If a resource is unavailable, it may mean that just one IRP cannot be processed, or it could mean that all IRP processing grinds to halt.
A "deadly embrace" occurs if two different pieces of code are trying to access the same resources. For example, code A might hold resource X and want resource Y. If code B holds Y and wants X, then a deadly embrace occurs, hanging the whole system. The simplest solution is to always acquire resources in the same order.
You can also get hang ups if you try to acquire a resource more than once, or if your interrupt service routine never returns. Continual interrupts can seriously degrade the system performance.
Resource Leaks
Resource leaks are less dire, but still important to fix. For example, if you forget to free some memory allocated during each read, the system will eventually run out of memory.
Some resources are more important than others. For example, nonpaged memory should be used conservatively.
Time Dependencies
Possibly the worst type of problem to sort out is related to timing.
At the simplest level, a timing problem can simply mean that you have not filled a buffer quickly enough. For example, if your hardware needs to output data regularly, you may not have provided it with the data. Or did you fill it too quickly? Check that your driver works on different speed computers.
Another possible problem is that your driver may not be reading data quickly enough. For example, an isochronous device, such as a microphone, may generate a regular number of samples per second. Check that you can keep reading data at this speed, even in a stressed system. Alternatively, have a strategy for skipping samples (e.g., a call to your device to drop samples).
An incremental approach makes debugging far easier. Get each stage working and tested before moving onto the next stage.
An incremental approach still means that you should do proper design work. Think very carefully about how your driver is going to work before starting any development. Having decided upon a design, plan a development path that allows you to implement your design in stages.
Driver development environments are usually set up to produce two different builds: an optimized retail release version, called the free build, and an unoptimised test debug build, called the checked build.
Put any debug or test code so that it appears in the checked build. The simplest way to do this is to check the value of the preprocessor variable DBG, which is set to 1 in the checked build and 0 in free build. Here is an example of how to use this technique.
#if DBG
DebugPrintInit("Wdm1 checked");
#else
DebugPrintInit("Wdm1 free");
#endif
I shall soon look at the different types of debug code that you can put in your checked version.
Running under the checked build version of NT or W2000 can also pick up driver errors as the operating system makes more internal checks.
One decision you will need to make is whether to work primarily in Windows 2000 or Windows 98. You could set up the development environment in one operating system, and only do test installs in the other. As stated previously, you ought to be testing your driver in both operating systems.
My advice is to use Windows 2000 for development. A few useful tools are only available in W2000 (and NT). And, significantly, W2000 is better at detecting driver errors. For example, W2000 will usually cause a bugcheck when you try to access a bad address, such as writing to a NULL pointer. Windows 98 may not detect this sort of error and your driver will continue, oblivious to the problem. The downside to W2000 is that it takes a bit longer to restart.
It is useful to be able to dual boot a computer with Windows 2000 and Windows 98. If a device fails during system boot, you can use the other operating system to remove the offending driver temporarily. For this technique to work, you need the Windows 2000 system drive to be accessible to Windows 98 (i.e., not formatted in NTFS).
Apart from using your brain, there are two basic techniques for debugging. The first is to include "print" statements of some sort in your driver. The second is to do source code level debugging.
One way of reporting data is to generate NT and Windows 2000 event messages. For a commercial driver, you should use this facility to report any abnormal events to system administrators. However, it is somewhat complicated to set up a connection between a message number and the string it represents. You can include strings and data values as a part of the event.
The NT and Windows 2000 event viewer does not have the best display in the world, and it does not automatically update itself. Windows 98 has no event viewer.
Generating Windows Management Instrumentation (WMI) events is theoretically a way for a driver to report events. While this has the advantage that you should be able to view the data across a network, it is not a serious proposition for driver development, as it is even harder work to see events.
There are better tools available to display driver "print" trace statements. The next section discusses the DebugPrint software. This lets a driver use formatted print statements in the code. The output appears in a user mode monitor application.
As mentioned previously, two similar tools are available commercially: Driver Monitor in the Compuware Numega DriverWorks software, and OSRTracer in the Open Systems Resources OSR DDK software.
Windows 2000 includes Driver Verify for catching some common types of driver error. Use the Driver Verifier Manager (Start+Programs+Development Kits+Windows 2000 DDK+Driver Verifier) to change the driver verify settings.
Drivers can corrupt memory by accessing outside their allocated buffers. W2000 can use a special pool, with inaccessible memory at either side of a buffer. If you try to write to the inaccessible memory, a bugcheck occurs. The facility can be turned on for all your driver's memory requests. Alternatively, you can use it selectively by calling ExAllocatePoolWithTag with an appropriate Tag parameter.
Another common mistake is to access paged memory at DISPATCH_LEVEL IRQL or higher. A driver may "get away with it" if the paged memory is resident. Driver Verify can flush all paged memory to disk before your driver is called at elevated IRQL.
Drivers must also cope well if a memory request fails. Another Driver Verify feature can forcibly fail a random number of memory allocation requests.
The final and most powerful tool is a debugger. Standard user mode debuggers like Visual Studio do not work with drivers.
Instead, you must use a debugger designed for the job. The Microsoft DDKs come with the WinDbg debugger for NT and Windows 2000 systems. To use this, you must have two PCs — one running a checked build of NT/W2000 and another the free build — connected together using a serial cable. A normal network connection will also be useful. You can insert trace statements and debug at source-level. There are instructions for WinDbg in the DDKs. It will not be covered in this book.
NuMega Compuware at www.numega.com sells a debugger called SoftICE, which lets you to debug at source-level on a single PC.
This book includes the DebugPrint software, which lets you use formatted print statements to trace the execution of a driver. Although not the ultimate debugging solution, it certainly gives a driver writer a useful tool for tracing what code a driver runs and what data is in variables. DebugPrint works in Windows 2000 and Windows 98, but not in earlier versions of these operating systems.
The source code of the DebugPrint driver and user mode monitor are included with the book. The DebugPrint driver serves to illustrate several driver development techniques. The DebugPrint source is described in Chapter 14.
DebugPrint is described in full on the web site www.phdcc.com/debugprint.
To use DebugPrint, you must first install the DebugPrint driver. Do this in a similar way to the Wdm1 driver. This time, browse to the DebugPrint\sys directory and install the free build driver called "DebugPrint driver debugging tool".
You can now use the DebugPrint Monitor application to listen for trace print events from drivers that you are testing. You will probably find it convenient to set up a shortcut to this application at DebugPrint\exe\Release\DebugPrintMonitor.exe.
The Wdm1 driver includes various calls to the DebugPrint software. However, these calls only appear in the checked build of the driver. (You can opt to include the DebugPrint output in free builds.)
If you now reinstall the Wdm1 driver, selecting the checked build, you should see various events in the DebugPrint Monitor window, as shown in Figure 6.1. The displayed output is described in the next chapter.
Note: A revised version of installation file DebugPrint\sys\DebugPrint.INF is available on the book's web site, www.phdcc.com/wdmbook. This updated version will install DebugPrint in W98.
Figure 6.1 Sample DebugPrint Monitor output
The DebugPrint Monitor program runs on the same computer as the drivers you are testing. It is a standard user mode MFC Win32 application that needs the shared MFC42.DLL library.
The Monitor is very easy to use. When started, it begins listening for DebugPrint trace events. If any events are buffered up, these are read and displayed first.
The Monitor displays one event per line. It has columns for the driver name, a timestamp, and the actual trace message. The latest event is always scrolled into view.
Use the Edit+Delete events menu to clear all the events from the display.
You can save the current list of events to a .dpm file. This is an ASCII text file with the columns separated by tab characters. You can reload .dpm files.
The Monitor remembers its position on the screen and the column widths. Printing events is not yet supported.
First, you must copy two standard source files into your driver project, DebugPrint.c and DebugPrint.h. Include DebugPrint.h in the driver's main header file. Amend the SOURCES file so that DebugPrint.c is built. These files are in the Wdm1 project source code.
Make calls to the DebugPrint functions in your driver code. You can only make these calls at DISPATCH_LEVEL IRQL or lower. This means that you can make DebugPrint calls in your DriverEntry, the main IRP dispatch routines, and in StartIo and Deferred Procedure Call (DPC) routines, but not in interrupt handling routines. The DebugPrintInit routine must be called at PASSIVE_LEVEL
By default, the DebugPrint source only prints in the "checked" debug build. However, you can force it to work in the "free" build by setting the DEBUGPRINT preprocessor variable to 1 before you include DebugPrint.h, e.g.,
#define DEBUGPRINT 1
The DebugPrint functions will never overflow the internal output buffer that they allocate. All wide characters are simply cast to single-byte ANSI characters when they are put in an output string.
DebugPrint calls should only introduce minor delays into the execution of your driver. The main work of the DebugPrint calls takes place in a system thread that runs in the background at a low real-time priority.
DebugPrint Calls
Listing 6.1 illustrates a typical series of DebugPrint calls in three standard driver routines.
Listing 6.1 Typical DebugPrint calls
NTSTATUS DriverEntry(…) {
#if DBG
DebugPrintInit(Wdm1 checked");
#else
DebugPrintInit("Wdm1 free");
#endif
…
DebugPrint("RegistryPath is %T", RegistryPath);
…
}
NTSTATUS Read(…) {
…
DebugPrint("IRP %I. Reading %u bytes from %x", Irp, IrpStack->Parameters.Read.Length, Irp->AssociatedIrp.SystemBuffer);
…
}
VOID Unload() {
…
DebugPrintClose();
}
Call DebugPrintInit to initialize the connection to the DebugPrint driver, passing the name of your driver as an ANSI NULL-terminated string. You typically use DebugPrintInit in your DriverEntry routine. Call DebugPrintClose to close the connection in your driver unload routine.
You can write simple NULL-terminated ANSI strings using DebugPrintMsg. For formatted print trace statements, use DebugPrint or DebugPrint2. DebugPrint uses an internal 100-byte buffer. DebugPrint2 lets you specify the size of buffer to allocate.
Calls to DebugPrint or DebugPrint2 must include a NULL-terminated ANSI format specification string, which may include one or more format specifier characters shown in Table 6.1 (e.g., %c for an ANSI character). You must provide an extra argument for each specifier in your format string. Failure to get this right could very easily result in an access violation. Be careful to use the correct uppercase or lowercase format specifier type character.
The available format specifiers cover a range of useful types. %I prints out the major and minor codes of an IRP. %T prints a UNICODE_STRING. Line feed and carriage return characters in the format string are ignored.
The %l, %L, %s, %S, and %x format specifier characters let you use one or more modifier characters to specify the maximum output width (e.g., %*l and %nl). The * modifier takes the maximum size from the next integer parameter to DebugPrint or DebugPrint2. The n modifier specifies the exact output width; where n is one or more characters between 1 and 9.
Table 6.1 DebugPrint format specifiers
Format Specifier | Type | |
---|---|---|
%c | ANSI character | char |
%C | Wide character | wchar_t |
%d, %i | Signed integer in decimal | int |
%D | __int64 in decimal | __int64 |
%I | IRP major and minor codes | PIRP |
%l | __int64 in hexadecimal | __int64 |
%L | LARGE_INTEGER in hexadecimal | LARGE_INTEGER |
%s | NULL-terminated ANSI character string | char* |
%S | NULL-terminated Wide character string | wchar_t* |
%T | UNICODE_STRING | PUNICODE_STRING |
%u | ULONG in decimal | ULONG |
%x | ULONG in hexadecimal | ULONG |
Here are some notes on how to deal with some common debugging problems.
Updating a driver is fairly straightforward. The makefile for the examples in this book copy the new driver to the Windows System32\drivers directory. However, the old version will still be running in memory.
To use the new version you have to update the drivers for your device(s). Use the Update/Change driver option in the Device Manager properties for a device. Opt to "display a list of drivers in a specific location, so you can select the driver you want".
For the Wdm1 driver, select "Other Devices". You should see the list of book software drivers that you have installed. Select the one you want from this list. You do not have to select the Have Disk button (unless you want to install the checked version, say).
If asked, opt to use the new driver even if the Device Manager suggests that the current version may be newer. Windows 98 will force you to browse for the driver files. W2000 uses the driver files in the System32\drivers directory.
As noted above, if the new driver returns an error during its initialization then Windows will say that the system needs to reboot. Rebooting may not cure the problem.
NT Style Drivers
As mentioned in an earlier chapter you must update NT style drivers in a different way. As usual, you must get your driver into the Windows System32\drivers directory.
Run the Control Panel Devices applet. Find your driver, stop it, and start it again. You can also run the book software Servicer applet; type in the name of your driver, press Stop and then Start. If you run an NT style driver in Windows 98 you must reboot the computer to use an updated driver.
If your driver fails catastrophically during system boot, the system will not load, which makes it tricky to delete or change the offending driver.
There are three solutions to this problem. In a dual boot system, simply reboot in the other operating system and delete or change the driver. Alternatively, both W2000 and W98 have "Safe mode" boot options that should allow you to start Windows, while only loading the most basic system drivers. Delete the offending driver and restart. A final option is to have a bootable floppy disk with a copy of Windows 98. Use this as a quick boot to delete the offending driver files.
Watch out for one driver depending on another. The Wdm1 driver opens a handle to the DebugPrint driver during its initialization, and releases it only when the driver unloads. This means that the DebugPrint driver cannot be replaced while Wdm1 is running. To change the DebugPrint driver, Wdm1 has to be unloaded. Only when the new DebugPrint is safely running, can a Wdm1 device be installed again.
If a driver queues IRPs but does not provide suitable cancel or cleanup routines, a Win32 program could hang up if it calls CancelIo or exits with file handles open. In this case, the Win32 program appears to have exited but it is still locked in memory, so you will not be able to update it.
The I/O Manager gives a driver five minutes to cancel IRPs. After this time, it displays a message to the user and any pending IRPs are dissociated from the terminating thread.
A reboot is usually necessary to clear this situation.
When Windows 2000 bugchecks, it displays information in the "blue screen of death" and stops dead. The information on this screen can help you to track down the source of the problem. You can cause a bugcheck deliberately using the KeBugCheck and KeBugCheckEx routines. A debug version of your driver might do this if it detects some unsolvable problem. Release drivers should never bugcheck voluntarily.
A problem in your driver may not directly cause a bugcheck. For example, you could overwrite another driver's memory and cause it to fail.
The top few lines of the bugcheck screen contain the most useful information. Down below might be a module list, a stack trace, and instructions. In NT, then it will say that is producing a physical dump of memory even if you have not enabled this option in the Control Panel System applet.
The most common bugcheck codes are listed in Table 6.2. The full list of stop codes is given in DDK bugcodes.h. The exception codes for "Unhandled Kernel exception" bugchecks are in the NTSTATUS.H header in the DDK.
For example, put this code in the Wdm1 driver.
char* NULLptr = NULL;
*NULLptr = 5;
This does not cause any errors in W98. However, in W2000, the following bugcheck occurs. (Interestingly, a read from address 0x0 does not seem to cause a bugcheck in W2000.)
*** STOP: 0x0000001E (0xC0000005,0xF2D7B875.0x00000001,0x00000000)
KMODE_EXCEPTION_NOT_HANDLED
*** Address 0xF2D7B875 base at 0xF2D7A000 Datestamp 362DF72F – Wdm1.sys
The bugcheck code is 0x0000001E, which translates as an "Unhandled Kernel exception". The four numbers in brackets are the four extra parameters that are passed to KeBugCheckEx. Table 6.2 shows the interpretations for the common bugcheck codes. For this bugcheck, the first parameter is the exception code. 0xC0000005 indicates an access exception. The fourth parameter indicates the memory address that you tried to access (0x0) and the second parameter gives the address of the instruction that caused the exception. The exception occurred at address 0xF2D7B875, a little way into the code of Wdm1, which is loaded at 0xF2D7A000.
Table 6.2 Common bugchecks
Code 0x0000000A IRQL_NOT_LESS_OR_EQUAL 1 Address referenced 2 IRQL (Not the correct IRQL) 3 0=read, 1=write 4 Address that referenced memory A driver tried to do something at an inappropriate IRQL (e.g., accessing paged memory at DISPATCH_LEVEL IRQL or higher) |
Code 0x0000001E KMODE_EXCEPTION_NOT_HANDLED 1 Exception code: 0xC00000005 2 Address where exception occurred 3 4 Address referenced Access violation |
Code 0x0000001E KMODE_EXCEPTION_NOT_HANDLED 1 Exception code: 0x80000003 2 Address where exception occurred Hard-coded breakpoint or ASSERT hit. |
Code 0x000000BE Driver attempted to write to read-only memory |
Code 0x000000C4 Driver Verifier detected exception. See its documentation for details. |
How do you work out what code caused the bugcheck? By analyzing the linker map for a driver, you can work out which routine caused the problem. A source-level debugger is required if you are still having problems.
You build a linker map by adding a line like the following to your SOURCES file.
LINKER_FLAGS=-MAP:Wdm2.map
What routine caused the following access violation?
*** STOP: 0x0000001E (0xC0000005,0xF764C5F1,0x00000000,0x00000010)
KMODE_EXCEPTION_NOT_HANDLED
*** Address 0xF764C5F1 base at 0xF764A000 Datestamp 3653e5fb – Wdm2.sys
The first thing to note is that the access violation occurs at a very low address, 0x10. This suggests that the code had a NULL pointer to a structure and was trying to access a field at offset 0x10 in this structure. This turned out to be the case.
The problem seems to be in the Wdm2 driver. The offset into the executable image is 0xF764C5F1-0xF764A000 (i.e., 0x25F1).
Listing 6.2 shows part of the linker map for this build of the Wdm2 driver. The initial section shows that the load address is 0x00010000. The map then lists the segments that make up the executable image. However, the information of interest is buried in the next section, which lists each code and data object.
The entry for PnpDefaultHandler in Pnp.cpp has an Rva+Base of 0x00012512. If the load address is taken off, this shows that PnpDefaul tHandler starts at offset 0x2512. The next line shows that the ForwardAndWait routine starts at 0x2611. Therefore, the access violation at offset 0x25Fl occurred towards the end of the PnpDefaultHandler routine.
Listing 6.2 Wdm2 linker map excerpt
Wdm2
Timestamp is 3653e5fb (Thu Nov 19 09:33:47 1998) Preferred load address is 00010000
Start Length Name Class
0001:00000000 0000152cH .text CODE
0002:00000000 000000a0H .idata$5 DATA
0002:000000a0 00000632H .rdata DATA
0003:00000000 00000119H .data DATA
0003:00000120 00000042H .bss DATA
0004:00000000 00000c1aH PAGE CODE
0005:00000000 000000eeH INIT CODE
0005:000000f0 00000028H .idata$2 CODE
0005:00000118 00000014H .idata$3 CODE
0005:0000012c 000000a0H .fdata$4 CODE
0005:000001cc 00000344H .idata$6 CODE
0006:00000000 00000058H .rsrc$01 DATA
0006:00000060 00000338H .rsrc$02 DATA
Address Publics by Value Rva+Base Lib:Object
0001:00000012
?Wdm1Create@@YGJPAU_DEVICE_OBJECT@@PAU_IRP@@@Z 000102f2 f dispatch.obj
0001:00000056 ?Wdm1Close@@YGJPAU_DEVICE_OBJECT@@PAU_IRP@@@Z 00010336 f dispatch.obj
…
0004:00000492 ?PnpDefaultHandler@@YGJPAU_DEVICE_OBJECT@@PAU_IRP@@@Z 00012512 f pnp.obj
0004:00000591 ?ForwardAndWait@@YGJPAU_DEVICE_OBJECT@@PAU_IRP@@@Z 00012611 f pnp.obj
Please test your driver well before releasing it. You must check that your driver works on a variety of computers. It must cope with user mode program aborts.
Debugging drivers is harder than user mode applications. In the worst case, a bugcheck occurs that tells you roughly what went wrong. Resource leaks and timing problems are more difficult to sort out.
The DebugPrint software lets you insert "print" statements into your driver code. You can do source-level debugging between two computers using WinDbg, or on a single computer using the NuMega SoftICE product.
The next chapter looks at the dispatch routines in the Wdm1 device driver.
CancelIo first became available in NT 4 and W98.