Brute Force ASSERTs & Debug Breakpoints
This article describes some techniques that I use in my kernel programming adventures with regards to debug assertions and breakpoints. Because not everyone knows the basics of kernel programming I will explain the bare essentials with regards to this article:

  • ASSERTs work the same as in user mode programs. In debug builds the assertion is evaluated and if the result is FALSE a breakpoint is triggered. We usually call these "hardcoded" breakpoints as opposed to normal breakpoints that you set with your debugger. In release builds the assertions go away so there is no hardcoded breakpoint in the code.
  • What is drastically different between user and kernel mode is what happens when the breakpoint is hit (triggered). A breakpoint is considered to be an exception. And like all exceptions it has to be handled. The debugger is meant to be the handler.
    Kernel debugging does not work like user mode debugging. It requires two PCs: One runs the debugger and the other runs the code to be debugged. They are usually called DebuggER and DebuggEE respectively but these two terms are very much alike and easy to confuse so I use the term "Victim" for the "DebuggEE".
    So we have the Debugger and the Victim.
  • In order to debug kernel code on a Victim, the Victim has to boot in debug mode. This is a boot mode, not the debug (checked) version of the OS. This is specified by using the /DEBUG switch together with a couple of other switches in a BOOT.INI entry.
  • If the Victim has not booted in DEBUG mode and a hardcoded breakpoint is hit what happens is a kernel mode unhandled exception, which is short for Blue Screen of Death (you see Not all Blue Screens are Crashes).
  • If the Victim has booted in DEBUG mode and a hardcoded breakpoint is hit then the Victim freezes and waits indefinitel for a kernel debugger to do something with the breakpoint. It is not necessary for a Debugger to be actually connected at the time the breakpoint is hit. The debugger can connect after the breakpoint was hit. The Victim looks frozen on the UI level (and almost all other levels) but it is not dead; it is waiting for some special treatment by a Debugger.
Hopefully the above were clear enough. However through practical experience I found two great shortcomings to the standard approach:

  • If a client installs a debug build of my drivers and an assertion fails then she will get a BSOD. Oooops... I want her to run the debug build so that I get the debug messages, but as soon as she installs it she gets a BSOD. Normal people are really scared of BSODs (unlike kernel mode psychos), so I end up with clients who refuse to run the debug build or even worse with clients that think that our drivers are buggy, since they cause BSODs all the time...
  • If the client runs the release build of my drivers I don't know whether any assertions are failing or not, because the assertions are not there in the release build.
So I came up with the following approach:

  • Create a wrapper function for debug breakpoints. The wrapper function only raises the breakpoint if a registry setting is set to the proper value. So by default the debug build of my drivers do not trigger breakpoints. The client has to actively do something in order to enable the breakpoints.
    Moreover the wrapper function always writes a message to the kernel debugger. Always means in both the debug and release builds. What the message says we will see in the implementation section.
  • Create a new ASSERT macro that calls the wrapper function.
This way everybody is happy. The debug driver does not cause any crashes and the release driver is giving me diagnostic messages when assertions fail.

Needless to mention, kernel programmers cannot be as sloppy as their user mode counterparts. For example, in a driver I cannot write:

ASSERT(a_BufferSize <= sizeof(TargetBuffer));
CopyMemory(TargetBuffer, InputBuffer, a_BufferSize);

I have to write:

ASSERT(a_BufferSize <= sizeof(TargetBuffer));
if (a_BufferSize > sizeof(TargetBuffer))
{
    // Fail the operation somehow.
    ...
}

CopyMemory(TargetBuffer, InputBuffer, a_BufferSize);

You cannot leave things to chance in the kernel. An application crashing once every week is a minor annoyance you can live with. Crashing the OS once a week causes lots of turmoil, so I have to make sure it does not happen under any circumstances.

So now it's time to look at the implementation of the approach I'm currently using in my projects.

#define STRINGIZE(a) _STRINGIZE(a)
#define _STRINGIZE(a) #a

#define BFSANITY(condition) \
    INNERBFSANITY(condition, STRINGIZE(condition), __FILE__, __LINE__)

#define INNERBFSANITY(condition, condition_as_string, file, line) \
do \
{ \
    if (!(condition)) \
      C1394DebugBreak("SANITY(" condition_as_string ") FILE(" file ")", line); \
} \
while (0)

void C1394DebugBreak(const PCHAR a_szFileNameOrMessage, const ULONG a_LineNumber)
{
    DbgPrint( "(C1394DebugBreak) Debug Breakpoint Triggered. %s - Line(%d)",
                     a_szFileNameOrMessage, a_LineNumber);

#if DBG
    if (g_bEnableDebugBreakpoints)
      DbgBreakPoint();
#endif
}

The implementation is common-sense simple (provided you are a bit of a macro-freak), but I think it combines several concepts in a pretty interesting way. Since assertions are also known as "Sanity Checks" I used the term "Sanity" for my macro. But since that is a pretty common term and may already be used by some, I added my initials in the front. Thus you have the BFSANITY macro.

I have also prepared a small user mode console app that has all the required functionality so that you can take a shot at this technique and see if it fits your needs for your everyday applications.

Here is the code for that user mode application:

#include <windows.h>
#include <stdio.h>
#include <stdarg.h>

// Macro to convert #defined items into strings
#define STRINGIZE(a) _STRINGIZE(a)
#define _STRINGIZE(a) #a

#define BFSANITY(condition) \
    INNERBFSANITY(condition, STRINGIZE(condition), __FILE__, __LINE__)

#define INNERBFSANITY(condition, condition_as_string, file, line) \
    do \
    { \
      if (!(condition)) \
        BfDebugBreak( "SANITY(" condition_as_string ") File(" file ")", line); \
    } \
    while (0)

// Initialize this as appropriate
bool g_bEnableDebugBreakpoints;

void BfDebugPrint(char *format, ...)
{
    char szMessage[512];
    va_list VaList;

    va_start( VaList, format);
    vsprintf_s(szMessage, sizeof(szMessage), format, VaList);
    OutputDebugString(szMessage);
    va_end( VaList );
}

void BfDebugBreak(const char *a_szMessage, const ULONG a_LineNumber)
{
    BfDebugPrint( "(BfDebugBreak) Debug Breakpoint Triggered: %s - Line (%d)",
                     a_szMessage, a_LineNumber);

#if defined(_DEBUG)
    if (g_bEnableDebugBreakpoints)
      DebugBreak();
#endif
}

int main(int argc, char* argv[])
{
    BFSANITY(argc == 2);
    BFSANITY(("Double Parenthesis and Comma operator -> ASSERT(FALSE) with a message ;-)",0));
    return 0;
}

From the above you can easily understand why I LOVE macros and you can probably imagine what is my #1 reason for disliking languages like C#.

You can download the sample project from this location.

Have Fun!