My Learning Journal
← Back to home

Windbg Conditional Breakpoint Command Instructions Problem

WindbgConditional breakpointBreakpoint stop at the wrong timing

Refer to the Windbg command documentation.

The Problem Description

This is related to one issue I encountered while working on kernel debug on Windows 10. The initial command was:

bp nt!NtQueryDirectoryFile ".if (poi(rsp+50)==0) { gc } .else { as /msu ${/v:DirFileName} poi(rsp+50); .printf \"FileName is: %msu\\n\", poi(rsp+50); .block { .if ($spat(\"DirFileName\", \"*library-ms*\") == 1) { .echo \"Breakpoint hit: library-ms found\" } .else { gc } } }"

The condition can be met when the parameter file name of NtQueryDirectoryFilecontains “library-ms”. However, when the breakpoint doesn’t trigger until the next NtQueryDirectoryFileinvocation happened. This means the debugger stops at the next call of the function when the condition is met. This is not what I want because the function calling status might be totally changed.

The function declaration is here.

__kernel_entry NTSYSCALLAPI NTSTATUS NtQueryDirectoryFile(
[in] HANDLE FileHandle,
[in, optional] HANDLE Event,
[in, optional] PIO_APC_ROUTINE ApcRoutine,
[in, optional] PVOID ApcContext,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[out] PVOID FileInformation,
[in] ULONG Length,
[in] FILE_INFORMATION_CLASS FileInformationClass,
[in] BOOLEAN ReturnSingleEntry,
[in, optional] PUNICODE_STRING FileName,
[in] BOOLEAN RestartScan
);

To fix this, I tried to add alias clearing command into the block. This is to make sure the alias is always empty when the condition is checked.

bp nt!NtQueryDirectoryFile ".if (poi(rsp+50)==0) { gc } .else { as /msu ${/v:DirFileName} poi(rsp+50); .printf \"FileName is: %msu\\n\", poi(rsp+50); .block { .if ($spat(\"DirFileName\", \"*library-ms*\") == 1) { .echo \"Breakpoint hit: library-ms found\" } .else { ad ${/v:DirFileName}; gc } } }"

The Correct Command:

One Liner command:

bp nt!NtQueryDirectoryFile ".if (poi(rsp+50)==0) { gc } .else { as /msu ${/v:DirFileName} poi(rsp+50); .printf \"FileName is: %msu\\n\", poi(rsp+50); .block { .if ($spat(\"DirFileName\", \"*library-ms*\") == 1) { .echo \"Breakpoint hit: library-ms found\" } .else { ad ${/v:DirFileName}; gc } } }"

Command Explanation

Command in file format:

bp nt!NtQueryDirectoryFile "
    .if ( poi(rsp+50) == 0 ) {            //①
        gc                                 //②
     } .else {
        as /msu ${/v:DirFileName} poi(rsp+50);     //③
        .printf \"FileName is: %msu\n\", poi(rsp+50); //④
        .block {                                    //⑤
            .if ( $spat(\"DirFileName\", \"*library-ms*\") == 1 ) { //⑥
                .echo \"Breakpoint hit: library-ms found\"          //⑦
            } .else {
                ad ${/v:DirFileName};  gc                          //⑧
            }
        }
    }"

① Fetch the 10‑th argument ( PUNICODE_STRING FileName ) and test whether it is NULL. On x64, the first four arguments go in registers and the rest are pushed starting at rsp+20. FileName therefore sits at rsp+48; with the 16‑byte stack alignment the script reads poi(rsp+50) to get the pointer.
② If the pointer is NULL, resume execution with gc (go from current location). Calls that have no file name are ignored.
③ Turn that UNICODE_STRING into a temporary alias. as /msu converts the UNICODE_STRING found at the given address into a string alias. The wrapper ${/v:DirFileName} uses the /v (verbatim) switch so WinDbg treats the text DirFileName as a literal alias name instead of expanding it—this is the special behavior you asked about. Using /msu is the official way to alias a UNICODE_STRING.
④ Print the file name with .printf "FileName is: %msu\n", poi(rsp+50). %msu means “print the UNICODE_STRING located at that address.”
⑤ Open a new lexical block with .block { }. The block itself does nothing; its purpose is to give the alias a limited lifetime. WinDbg only expands the alias when the block starts, and it is automatically discarded when the block ends.	Scoped aliases avoid clutter in the global alias table.
⑥ Wildcard‑match the file name: $spat("DirFileName", "*library-ms*") returns 1 if the alias text matches the pattern. $spat requires plain strings, so the alias provides the string form of the UNICODE_STRING.	$spat is the debugger’s built‑in glob matcher.
⑦ If the match succeeds, print a message and stay at the breakpoint, giving you a chance to inspect the state. No gc here, so execution remains stopped.
⑧ If the match fails, clean up and continue: ad ${/v:DirFileName} deletes the alias, then gc resumes the program. Removing the alias prevents alias‑table pollution during long runs.

Runtime effect

  1. The vast majority of calls FileName is NULL→ ① judgment is true → ② gc→ the program does not stop.
  2. Has a file name but does not contain library-ms → ③ Create alias → ④ Print → ⑤ Enter block → ⑥ Compare failed → ⑧ Delete alias and gc.
  3. File name contains library-ms → ③ ④ ⑤ Same as above → ⑥ More successful → ⑦ Output prompt and stop at breakpoint, convenient for you to check call stack, registers, etc.

This avoids manual sequential observation and does not slow down the debugging process due to frequent hits.