parametersscriptingwindbgaliases

Windbg - passing pseudo-registers to extensions and scripts


I have been trying to pass an address value loaded into $t0 with an !extension command I didn't write and a Windbg script I did write... I've made some progress by going back to using an alias, but I am still wondering if I'm missing something with the vagaries of Windbg syntax here.

For example, an extension can be passed an address as a parameter and it works fine with !printcols 0x00000000017e1b68, but I also know I can load up $t0 with that address value, yet I cannot successfully pass @$t0 to the extension command, using various means, $, {} etc., an example:

dx @$t0 = ((foo *) bar)->bar2 followed by:

? @$t0
Evaluate expression: 25041768 = 00000000017e1b68

But then !printcols @$t0 doesn't work. It provides an extension usage hint, rather than a Windbg error. That's annoying because I know $t0 = 0x00000000017e1b68 but if I do the following and introduce an alias called lCols then the !extension command works fine... this works:

dx @$t0 = ((foo *) bar)->bar2; as /x lCols @$t0; !printcols ${lCols}

Likewise, it's a similar (but not the same) kind of thing with a script I've written... I have a script called get_items.wds and it takes an address as its single parameter... so $$>a<C:\get_items.wds 0x0000000049b50010 works fine.

But I cannot load up $t0 with 0x0000000049b50010 and then pass that to get_items.wds, so trying something like:

0:030> r $t0 = 0x0000000049b50010
0:030> ? @$t0
Evaluate expression: 1236598800 = 0000000049b50010
0:030> $$>a<C:\get_items.wds @$t0

Will fail. Or ${@$t0} or any other combination I've tried. But the alias trick will also not work in exactly the same way, either. If I do the commands on separate lines they will work - so is it something to do with expansion? - but if I combine them onto a single line they do not, so:

dx @$t0 = ((foo *) bar)->bar2
as /x lItem @$t0
$$>a<H:\Downloads\get_ti.wds ${lItem}

And that works - I've passed the contents of $t0 to a script (which I know is 0x0000000049b50010 from the dx), via an alias.

I can check lItem, of course:

0:030> al
  Alias            Value  
 -------          ------- 
 lItem            0x49b50010

But if I try all of that on a single line, it fails again. Windbg mutters something about "Arg alias already exists"... but it's the same even if I do ad. So trying:

dx @$t0 = ((foo *) bar)->bar2; as /x lItem @$t0; $$>a<C:\get_item.wds ${lItem}

Doesn't work... but the exact same approach did work for the !extension. Didn't it?

Should I find it easy to pass the value held in a pseudo-register to an !extension command or a Windbg script?


Solution

  • TL;DR: This is probably the longest SO post I've ever written just to come to the conclusion that

    .block{ad /q ${/v:foo}};.block{as /x foo $t0};.block{$$>a<d:\debug\test.wds foo $t0};.block{ad /q ${/v:foo}}
    

    is the answer you're looking for.

    But I think you have reached a point where you should know about all the craziness before diving too deep into scripting. Why? Because there are alternatives like CLRMD, PyKD or dotnet-dump.

    Once you know about the problems, I'll go on and I'll work out a way of making your script work.

    WinDbg scripting issues

    WinDbg scripting is limited and broken and the instructions in WinDbg help are incomplete and sometimes misleading. In WinDbg, there seems to be no parser which takes your commands and builds an abstract syntax tree or whatever what you'd expect, given you're a programmer. Think of it as a bunch of interpreters taking your input and doing stuff you can't predict. Ok, now that's a stark statement, isn't it? Let's see...

    You can't simply concatenate commands using ; as a separator

    Example 1:

    0:000> as foo bar
    0:000> al
      Alias            Value  
     -------          ------- 
     foo              bar 
    

    So far, that's expected. But when you do it on one line, the output is missing:

    0:000> as foo bar;al
    

    The reason is that the semicolons have become part of the alias.

    0:000> al
      Alias            Value  
     -------          ------- 
     foo              bar;al 
    

    You'd probably agree that any parser of a language using semicolons would not have handled it that way.

    Solution for this specific issue: use aS or use .block{}.

    Cleanup: ad *

    Example 2:

    0:000> ad foo
    0:000> aS foo bar
    0:000> .echo ${foo}
    bar
    

    That's great. But when you do it on one line, the output is different:

    0:000> ad foo;aS foo bar;.echo ${foo}
    ${foo}
    

    Cleanup: ad *

    I doubt that was really expected, but at least it's documented:

    Note that if the portion of the line after the semicolon requires expansion of the alias, you must enclose that second portion of the line in a new block.

    Solution for this issue: use .block{}.

    Example 3:

    0:000> *
    0:000> .echo foo
    foo
    

    Obviously becomes

    0:000> *;.echo foo
    

    But hey, what can you expect from a line comment?

    Solution for this issue: use $$ or use .block{}.

    Example 4:

    0:000> ~*e .echo hello
    hello
    hello
    hello
    hello
    0:000> .echo world
    world
    

    This suddenly becomes

    0:000> ~*e .echo hello; .echo world
    hello
    world
    hello
    world
    hello
    world
    hello
    world
    

    Solution for this issue: use .block{}.

    Example 5:

    If you think, this semicolon stuff is true for built-in commands only, you're wrong. Meta commands are affected as well:

    0:000> .extpath somepath
    Extension search path is: somepath
    0:000> ? 5
    Evaluate expression: 5 = 00000000`00000005
    

    as opposed to

    0:000> .extpath somepath;? 5
    Extension search path is: somepath;? 5
    

    So the semicolon magically turned into a path separator, known from %PATH%.

    Solution for this issue: use .block{}.

    You don't know the different classes of WinDbg commands? See this answer regarding command classes for more weirdness and inconsistencies.

    Example 6:

    So far you have seen commands at the beginning of the line having influence on the end of the line. But it's also possible the opposite direction:

    2:008> r $t0 = 5
    2:008> r $t0 = $t0 -1 ; z($t0)
    redo [1] r $t0 = $t0 -1 ; z($t0)
    redo [2] r $t0 = $t0 -1 ; z($t0)
    redo [3] r $t0 = $t0 -1 ; z($t0)
    redo [4] r $t0 = $t0 -1 ; z($t0)
    
    0:000> r $t0 = 5; r $t0 = $t0 -1 ; z($t0)
    redo [1] r $t0 = 5; r $t0 = $t0 -1 ; z($t0)
    redo [2] r $t0 = 5; r $t0 = $t0 -1 ; z($t0)
    redo [3] r $t0 = 5; r $t0 = $t0 -1 ; z($t0)
    redo [4] r $t0 = 5; r $t0 = $t0 -1 ; z($t0)
    redo [5] r $t0 = 5; r $t0 = $t0 -1 ; z($t0)
    redo [6] r $t0 = 5; r $t0 = $t0 -1 ; z($t0)
    [...]
    

    Cleanup: restart your debugger

    Solution for this issue: use .block{}.

    Example 7:

    0:000> $<d:\debug\test.wds
    0:000> ? 5
    Evaluate expression: 5 = 00000000`00000005  
    

    in contrast to

    0:000> $<d:\debug\test.wds;? 5
    Command file execution failed, Win32 error 0n123
        "The filename, directory name, or volume label syntax is incorrect."
    

    At least, that's documented:

    Because $< allows semicolons to be used in the file name, you cannot concatenate $< with other debugger commands, because a semicolon cannot be used both as a command separator and as part of a file name.

    Solution for this issue: use .block{}.

    You can't simply write empty statements

    A semicolon on its own does nothing:

    0:000> ;
    0:000> ;;
    0:000> ;;;
    

    But you are not allowed to combine it with everything.

    Example:

    0:000> ;aS foo bar
    0:000> ;al
      Alias            Value  
     -------          ------- 
     foo              bar 
    0:000> ;ad foo
                 ^ No information found error in ';ad bar'
    

    Cleanup: ad *

    WinDbg is whitespace sensitive (sometimes)

    Example 1:

    Usually, a space in front of a command does not matter much.

    0:000> aS foo bar
    0:000> ad foo
    0:000> al
    No aliases
    

    I like commands separated by semicolons that have an additional space for visual separation, especially when using semicolons:

    0:000> aS foo bar; ad foo; al
    No aliases
    

    Now try that with an additional space in front of the command on each line:

    0:000>  aS foo bar
    0:000>  ad foo
                 ^ No information found error in ' ad bar'
    

    Cleanup: ad *

    Example 2:

    What you know from command line and programming languages is that a space separates tokens. With newer programs we have

    <program> <verb> <options> [--] [<files>]
    

    like

    git commit -m "commit message" -- helloworld.cpp
    

    where the individual pieces are separated by space. So, this looks perfectly familiar:

    2:008> lm f m ntdll
    start             end                 module name
    00007fff`3b100000 00007fff`3b2f0000   ntdll    ntdll.dll 
    

    but you can also do this:

    2:008> lmfmntdll
    Browse full module list
    start             end                 module name
    00007fff`3b100000 00007fff`3b2f0000   ntdll    ntdll.dll 
    

    And you can only wonder: how can WinDbg tell apart different commands starting with lm? Probably it can't. At least there is none.

    A line is not always a line

    We have played with the as command already and we figured out that some commands are line-based.

    Another example for this is the comment command. It's described like this:

    If the asterisk (*) character is at the start of a command, then the rest of the line is treated as a comment, even if a semicolon appears after it.

    I love using * in combination with .logopen to document my findings.

    0:000> * I just found out how to use comments
    0:000> * Even ; does not matter here
    

    The term line seems to mean something different than "all characters until CRLF":

    0:000> .echo before;.block{* surprise};.echo after
    before
    after
    

    And the same applies to the documentation of as:

    If you do not use any switches, the as command uses the rest of the line as the alias equivalent.

    0:000> ad *
    0:000> as foo bar;k
    0:000> ad *
    0:000> .block{as foo bar};k
     # Child-SP          RetAddr           Call Site
    00 00000017`73dbf120 00007fff`24ed455f ntdll!LdrpDoDebuggerBreak+0x30
    

    At least that's consistent.

    String escaping is broken

    String parameters usually don't need quotation marks.

    0:000> .echo Hello
    Hello
    

    Sometimes you can use quotation marks without effect:

    0:000> .echo "Hello"
    Hello
    

    Sometimes you must use them:

    0:000> .echo Hello;World
    Hello
                       ^ Syntax error in '.echo Hello;World'
    0:000> .echo "Hello;World"
    Hello;World
    

    Now, how do you print a quotation mark? Well, you can use it in the middle

    0:000> .echo He"lo
    He"lo
    

    But not when it's already used in the beginning:

    0:000> .echo "He"lo"
                   ^ Malformed string in '.echo "He"lo"'
    

    Every programming language can somehow escape quotation marks, but WinDbg can't

    0:000> .echo \"
    \"
    0:000> .echo "\""
                  ^ Malformed string in '.echo "\""'
    0:000> .echo """"
                 ^ Malformed string in '.echo """"'
    0:000> .echo """
                 ^ Malformed string in '.echo """'
                 
    

    or maybe it can, sometimes:

    0:000> .foreach /s (x "Hello World \"Hello") {}
    0:000> .printf "\"Hello World\""
    "Hello World"
    

    It just seems to depend on the command.

    Comments are not always comments

    We have mentioned $$ as an alternative to * before. Microsoft says:

    If two dollar signs ( $$ ) appear at the start of a command, then the rest of the line is treated as a comment, unless the comment is terminated by a semicolon.

    and

    Text prefixed by the * or $$ tokens is not processed in any way.

    In general, that seems to work:

    0:000> $$ Yippieh!
    0:000> $$Yay
    

    unless, of course you start the comment with <

    0:000> $$<
             ^ Non-empty string required in '$$<'
    

    That's because $$< is a different command. Just the documentation of $$ forgot about this.

    Making your script work

    $$>a<

    To me it seems you need $$>a<, because that's the only command that takes parameters. Therefore you have to live with it's other properties as there are:

    Especially the last one is tricky here. What exactly does that mean "condense to a single block"? You can best see that with a command that triggers an error message:

    File contents:

    .echo before
    .echo """
    .echo after
    

    Result:

    0:000> $$>a<d:\debug\test.wds
    before
                              ^ Malformed string in '.echo before;.echo """;.echo after'
                              
    

    So it means: all commands will be concatenated by a semicolon - for which we know it causes issues with a whole bunch of commands.

    Fortunately, most of them can be fixed by .block{}. And you can even make the blocks look nice in your script:

    .echo before
    .block{
      .echo ${$arg1}
      .echo """
      .echo ${$arg2}
    }
    .echo after
    

    Just remember that

    Alias expansion

    For this experiment you need the file contents

    .echo ${$arg1}
    .echo ${$arg2}
    

    As we can see, aliases will simply be replaced, even without the ${} syntax:

    0:000> as foo bar
    0:000> r $t0 = 1
    0:000> $$>a<d:\debug\test.wds foo $t0
    bar
    $t0
    

    You only need ${} when the alias is not separated by space:

    0:000> $$>a<d:\debug\test.wds foobar $t0
    foobar
    $t0
    0:000> $$>a<d:\debug\test.wds ${foo}bar $t0
    barbar
    $t0
    

    Pseudo registers

    Pseudo registers are not aliases, and they don't get expanded the same way. And you can't apply the alias interpreter ${} on them:

    0:000> $$>a<d:\debug\test.wds $t0 ${t0}
    $t0
    ${t0}
    0:000> $$>a<d:\debug\test.wds ${$t0} ${@$t0}
    ${$t0}
    ${@$t0}
    

    But basically, it will work with commands in the script as expected. The script

    .echo ${$arg1}
    r ${$arg2}
    

    will output as expected:

    0:000> $$>a<d:\debug\test.wds foo $t0
    bar
    $t0=0000000000000001
    

    Extension commands

    Extension commands (starting with !) are implemented in DLLs. You can build such extensions yourself and they have been built by other developers. Some of them do support the capabilities of WinDbg and do consider its specialties, others don't.

    In practice, if some of them expect an address, you need to pass a numerical value. It may even happen that this numerical number must be specified in hexadecimal, no matter what your WinDbg number format is set to (see the n command). And some of them will even fail if you prefix that hex address with 0x.

    What would full support look like?

    For example, !chkimg will evaluate pseudo registers:

    0:000> r $t0 = ntdll
    0:000> !chkimg $t0
    3 errors : $t0 (7fff3b27e000-7fff3b27e002)
    

    I struggled with this sort of support myself recently, so my guess is that your !printcols command might not have implemented all of that.

    Aliases will still be processed before the extension is called as we can see in this experiment:

    0:000> !chkimg foo
    Unable to determine offset from expression: foo
    
    0:000> as foo ntdll
    0:000> !chkimg foo
    3 errors : ntdll (7fff3b27e000-7fff3b27e002)
    

    Cleanup: ad *

    Finally, the solution

    Assuming that !printcols is not that sophisticated, you'll need to deal with that.

    If you want pseudo registers to be expanded before the script is called, you need a workaround using an alias. That's not an easy task, if you want the command to be repeatable, i.e. no side effects that will catch you later.

    The solution is:

    .block{ad /q ${/v:foo}};.block{as /x foo $t0};.block{$$>a<d:\debug\test.wds foo $t0};.block{ad /q ${/v:foo}}
    

    What's going on here?

    If you read this whole answer, you're now a WinDbg scripting ninja.