The NT Insider

ASSERT Yourself - The New NT_ASSERT Macro in the WDK
(By: The NT Insider, Vol 13, Issue 4, July - August 2006 | Published: 20-Sep-06| Modified: 20-Sep-06)

The ASSERT macro has been around in its current form since the dawn of humankind. You supply an expression to the macro and, if the result is FALSE, the expression is output to the debugger and you're given a few choices on how to proceed:

*** Assertion failed: KeGetCurrentIrql() != PASSIVE_LEVEL
***   Source File: g:\nothing\nothing.c, line 117

Break repeatedly, break Once, Ignore, terminate Process, or terminate Thread (boipt)?

It's simple, clean, and well-worn. Really, who doesn't love that? Well, apparently someone doesn't love it because a brand spanking new ASSERT macro has been added to the WDK: NT_ASSERT.

Not Just a New Name
NT_ASSERT dramatically changes the semantics of how asserts are presented and handled, but in a subtle way. What I mean by this is that the new assert fundamentally works the same way as the old one:

  • The macro is a no-op in free builds.
  • If the supplied statement is FALSE, the system traps into the debugger.
  • If no debugger is attached, the system blue screens.

However, under the covers the implementation of NT_ASSERT is dramatically different to that of ASSERT. Some of the changes NT_ASSERT brings along are good and, inevitably, some are bad.

Breaking Down the New Macro
Let's check out the new NT_ASSERT macro as it is defined in the Beta 2 WDK to see what we're in for (shown in Figure 1).

#define NT_ASSERT(_exp) \
    ((!(_exp)) ? \
        (__annotation(L"Debug", L"AssertFail", L#_exp), \
         DbgRaiseAssertionFailure(), FALSE) : \
        TRUE) 

Figure 1 - NT_ASSERT

I must admit, when I first saw this I was thrown for a bit of a loop. I mean, this is quite a departure from the old ASSERT macro. Also, I couldn't say that I was an expert in the "annotation" intrinsic or that I had ever even heard of DbgRaiseAssertionFailure. So, a bit of digging was in order.

Note: The reader should assume that all information provided is based on definitions and output received from the x86 platform, YMMV on other architectures. Also, the __annotations intrinsic is entirely undocumented. Therefore, all information provided in this article is based on observation of the current implementation.

Annotations
Believe it or not, the __annotation intrinsic has been around for quite some time now. While it looks fairly ominous, its job is simple. When you provide an __annotation directive, you're telling the compiler to store the provided string(s) in the PDB file and not in the image itself. Once the annotations are in the PDB, they can be extracted using something called the Debug Interface Access (DIA) SDK based on relative image address. If you're having a hard time imagining how on Earth this is useful, note that WPP tracing relies heavily on the __annotation intrinsic.

Let's add an annotation to our "Nothing" driver and check out the result (See Figure 2) using DIA2Dump, which is the sample provided in the DIA SDK (we were going to write our own sample, but quickly came to our senses).

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObj, PUNICODE_STRING RegPath)
{
    NTSTATUS            code;
    PNOTHING_DEVICE_EXT devExt;
    PDEVICE_OBJECT      devObj;
    UNICODE_STRING      devName, linkName;

    DbgPrint("\nOSR NT4 DO NOTHING Driver -- Compiled %s %s\n",
        __DATE__, __TIME__);

    __annotation(L"Annotatin' like a mother.");

    ...

Figure 2 - Adding Annotation to "Nothing" Driver

DIA2Dump displays an impressive amount of information when run against a PDB file, but we're looking explicitly for annotation information. Global annotations are not currently allowed, so all annotations are associated with the functions in which they reside.

Knowing that, if you grep the output for "annotation," you shouldn't be surprised to find where our annotation is listed, as shown in Figure 3.

** Module: g:\projects\nothing\driver\objchk_wlh_x86\i386\nothing.obj

CompilandEnv   : obj = "g:\projects\nothing\driver\objchk_wlh_x86\i386\nothing.obj"
CompilandDetails:
       Language: C
       Target Processor: Pentium III
       Compiled for EnC: No
       Compiler Version: FE 14.0.50727 BE 14.0.50727

...
CompilandEnv   : src = ".\nothing.c"
...

Function       :   static, [0x00004010]..., len = 00000123, _DriverEntry@8
FuncDebugStart :   static, [0x00004018]...
FuncDebugEnd   :   static, [0x0000412d]...
Annotation     :   static, [0x0000402f]...
       Data :     constant "Annotatin' like a mother...", Constant, (none)

Data :   ..., Param, Type: struct _DRIVER_OBJECT *, DriverObj
Data :   ..., Param, Type: struct _UNICODE_STRING *, RegistryPath
Data :   ..., Local, Type: struct _DEVICE_OBJECT *, devObj
Data :   ..., Local, Type: struct _UNICODE_STRING, devName
...
Function      : static, [0x00001010]..., len = 00000044, NothingCreateClose@8
...
** Module: f:\nt.obj.x86fre\base\tools\gs_support\kmode\objfre\i386\gs_support.obj

Figure 3 - Our Annotation in the Output

So, not much of a surprise, our annotation is listed under DriverEntry and is in the same section as parameter and locals information. You can also see in the above output that the relative virtual address (RVA) of the __annotation intrinsic is stored along with the annotation information - 0x0000402f, in our case.

Now that we've been introduced to annotations, let's check out the other piece of the new NT_ASSERT macro. Once we've done that we can move on to putting the two together and talk about the good and not so good of the new macro.

INT 2C
DbgRaiseAssertionFailure
is either implemented as a compiler intrinsic or an inline function. Let's check out the inline function shown below.

FORCEINLINE
VOID
DbgRaiseAssertionFailure (
    void
    )

{
    __asm int 0x2c
}

Ah yes, so it calls the old INT 2C handler... OK, so maybe I've never heard of using INT 2C for anything other than some perverted NT4 thing, but that's neither here nor there.
If you dump the IDT on your Vista system, you'll notice that the handler for the 2C interrupt is KiRaiseAssertion. If you continue to dig further, you'll notice that the execution of this routine results in a STATUS_ASSERTION_FAILURE exception being raised (we'll leave that digging as an exercise to the reader).

Let's see what we get in the debugger (below) by adding a call to this inline to the Nothing driver (shown in Figure 4):

Assertion failure - code c0000420 (first chance)
NOTHING!DriverEntry+0x1f:
8d2e602f cd2c            int     2Ch

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObj, PUNICODE_STRING RegistryPath)
{
    NTSTATUS            code;
    PNOTHING_DEVICE_EXT devExt;
    PDEVICE_OBJECT      devObj;
    UNICODE_STRING      devName, linkName;

    DbgPrint("\nOSR NT4 DO NOTHING Driver -- Compiled %s %s\n",
        __DATE__, __TIME__);

    DbgRaiseAssertionFailure();

Figure 4 - Adding DbgRaiseAssertionFailure to Driver

Nothing too shocking here. The inline function embedded an INT 2C in our code, which resulted in an assert exception being raised. However, hitting GO at this point reveals an interesting set of commands that were added to the debugger for handling the new assertions (see below).

kd> g
Continuing an assertion failure can result in the
debuggee being terminated (bugchecking for kernel
debuggees).
If you want to ignore this assertion, use 'ahi'.
If you want to force continuation, use 'gh' or 'gn'.

So, one advantage to using DbgRaiseAssertionFailure over DbgBreakPoint (or __debugbreak) is that the debugger has built-in support for ignoring the asserts it generates. Anyone who has been stuck having to NOP out a bogus ASSERT knows what a rockin' feature this is.

It turns out that there are several options available to the 'ah' command for dealing with individual asserts. There's even a new option to the 'sx' command for global assert handling. The WinDBG docs have all the details, so we won't bother duplicating that information here.

The Result
Now that we know the players involved, let's look again at the part of the NT_ASSERT macro that does the work:

_annotation(L"Debug", L"AssertFail", L#_exp), \
         DbgRaiseAssertionFailure()

The comma means this is going to expand out into a single line. Therefore, the code above is adding an annotation for this instruction address with the assert information and then emitting an inline INT 2C.

At this point the astute reader will realize an important side effect - the expression string that indicates the reason of the assert is not in the driver image. Because the expression is added as an annotation, the only way to find the reason for this assert is through querying the PDB for any annotation information available for the location of the INT 2C instruction.

Let's check out what the new assert looks like in the debugger. Look at the assembly around this breakpoint (Figure 5) and notice that the expression string is nowhere to be found.

Assertion g:\nothing\nothing.c(117): KeGetCurrentIrql() != PASSIVE_LEVEL
NOTHING!DriverEntry+0x2c:
8d2ec03c cd2c            int     2Ch


***

call    dword ptr [NOTHING!_imp__KeGetCurrentIrql (8d2ea000)]
movzx   eax,al
test    eax,eax
jne     NOTHING!DriverEntry+0x37 (8d2ec047)
int     2Ch
mov     dword ptr [ebp-20h],0
jmp     NOTHING!DriverEntry+0x3e (8d2ec04e)

Figure 5 - Where's Our Assert?

Thus, the debugger is clearly aware of the new assert semantics and is querying the PDB for annotation information in order to display the reason for the assert.

What About the "Not So Good?"
Earlier, I alluded to the fact that there was something not so good about the new assert and, no, I haven't forgotten. Remember when we saw the location of the annotation information in the PDB? At that point I noted it was in the same location as the parameter and locals information. This should be a slight tip-off to anyone who is familiar with the public symbols provided by Microsoft.

That's right folks, annotations are not present in public PDB files. If you're used to running the checked build of Windows to get extra assert information, I hope your assembly skills are up to snuff because you will be tracking down the reason for the assert yourself. For example, let's check out the previous assert again, but this time we'll use symbols generated with the /PDBSTRIPPED linker option:

Assertion failure - code c0000420 (first chance)
NOTHING!DriverEntry+0x2c:
8d2f203c cd2c            int     2Ch

Not so good, eh?

You Take the Good, You Take the Bad...
Even though it's entirely depressing that the checked build of Windows is now full of mysterious asserts, the built-in debugger support is convenient if you're not in the habit of using stripped PDBs in your development. Just remember that support for this new assert is not available in the down level platforms.

This article was printed from OSR Online http://www.osronline.com

Copyright 2017 OSR Open Systems Resources, Inc.