Testing from the Ground Up -- Getting a Good Start
The NT Insider, Vol 11, Issue 3&4, May-August 2004 | Published: 18-Aug-04| Modified: 18-Aug-04
Driver testing starts right at the stage of driver development. The best time to ensure your driver is both testable and diagnosable is during development. Making a few good decisions, and spending a little extra time, will pay off in major dividends in the long run.
So, here are some guidelines about building testability and diagnosibility into your driver.
1) Practice Defensive Driver WritingAs one NTDEV participant wrote recently, "Be a paranoid developer!" This is excellent advice. As you write code, aggressively add ASSERT statements to verify your assumptions. On those days when you’re sort of burned out, why not take an hour or two to add yet more aggressive assertions. Put these tests in the checked build of your driver and you won’t have to worry about performance penalties in your final code. I can’t imagine such a thing as too many ASSERTS.
The concept behind using ASSERTS is that you need to make explicit any assumptions that you have. For example: Let’s say you have a function that takes a pointer to a UNICODE_STRING as an input parameter. As you write your code, you might know that the caller will never pass a NULL pointer into this function, so you don’t allow for this in your code. If that’s the case, at the very least, place an ASSERT statement that validates your assumption:
ASSERT(DevName != NULL);
That way, if something strange happens and a NULL is passed into your function by mistake, you’ll at least catch the problem. There are some obvious cross-checks you can perform on UNICODE_STRING structures as well. For example, if the string is not NULL, the buffer pointer cannot be zero. Also, the maximum length must always be greater than or equal to the length of the string (duh!). ASSERT these things:
ASSERT(DevName->Length != 0? (DevName->Buffer != 0) : TRUE);
ASSERT(DevName->MaxLength >= DevName->Length);
Perhaps as you write your function, you know that the string has to be at least a certain length. At the very least, you’ll want to ASSERT this, too:
ASSERT((DevName->Length > 0) && (DevName->Length <= 3) );
You’ll also want to ASSERT other things you know about the string, including any assumptions you make about the string’s format.
Before you go all crazy on me and whine about the overhead and about how Windows kernel-mode components are supposed to trust each other, remember this: We’re talking about checks that’ll only be in the checked build of your code. As soon as we’re talking checked build, we know that performance isn’t an issue.
2) You Need Tracing
When you’ve got a driver problem at a field test site in another city (or, more likely, in another country) you need a way to know what was going on just before your driver crashed.
Yes, your driver needs to have tracing, and it probably needs to have tracing in the free build. Not too long ago, the way we’d get this tracing is that we’d each have to write our own circular buffer in-memory trace log package. But, no more. Now we can use Event Tracing For Windows – and specifically Windows Pre-Processor (WPP) Tracing – to add low-overhead trace points into our code, and have it work all the way back to Windows 2000.
In using WPP Tracing, don’t forget that you can specify both Flags and Levels. Flags are typically used to specify the trace "path". Levels are typically used to indicate trace "volume", that is, how much spew is generated in a particular path.
Remember, there’s nothing wrong with using both WPP Tracing and DbgPrint(Ex) in the same driver. DbgPrint output is nice for your normal debugging work. WPP Tracing excels at providing "traceability" in your driver.
3) Get With The 64-Bit ProgramAs you write your code, write it to be 64-bit compatible. This means that you should use the standard 64-bit compatible type names (ULONG_PTR, for example) by default. We also recommend that you build the AMD-64 version of your driver and (at the very least) correct any compilation errors.
We will be in a 64-bit world sooner than you think, and building in 64-bit support will save you a lot of hassle later. Further, using the 64-bit types makes more explicit your intent: Do you mean something you’re casting to be 32-bits wide specifically, or do you mean it to be the width of a pointer on the target system?
4) You Need InfrastructureAny testing or diagnosability infrastructure that you create for your driver will usually provide an enormous payback in saved time and less annoyance. Here at OSR, we build macros for frequently performed operations in a complex project. Even if the operation requires nothing more than calling a function, we’ll typically create an encapsulating macro.
This approach makes it easy to create code that automatically and predictably checks status return values and outputs a tracing statement. When you get into the habit of using macros this way, you can’t "forget" to put in the status check or add the trace statement when you start to get deadline pressure.
One other piece of infrastructure that we’d traditionally recommend is a lock tracking package. However, note that Driver Verifier actually does a pretty darn good job of tracking spin lock usage for you.
5) You Need Type CheckingWhile we don’t recommend writing a Windows drivers in C++, We do strongly recommend that you name your driver source files with .CPP file types. This will result in using the C++ compiler, by default, to compile your driver. The advantage that you get is strong type checking. You’d be surprised at how many errors this avoids.
Here at OSR, we write our drivers with CPP file types by default. Every once in a while, we have to convert a project (an old one done here or one written by a client) from using .c file types to using .CPP file types. Believe it or not, we have never done this conversion process without finding at least one significant error.
Rate this article and give us feedback. Do you find anything missing? Share your opinion with the community!
Post Your Comment