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?
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 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...
;
as a separatorExample 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{}
.
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 *
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.
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 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.
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.
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
.block{
, which usually does nothing, but messes with ad
.ad
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 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 (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 *
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?
ad
attempts to delete an existing alias so that as /x
does not complain that it already exists./q
makes that quiet, in case no such alias existedad /q foo;something
because that would search for an alias named foo;something
.block{ad /q foo}
, ad
is no longer the first character in the line. Thus, foo
will be replaced by its alias and thus looking for a wrong alias named bar
(or whatever the value is).${/v:foo}
foo
existed before, it would have been replaced everywhere. We don't want that for as /x
. Therefore introduce another block which re-evaluates aliases. This time it will remain foo
because we deleted foo
before.foo
with as /x
, we need to introduce a new block again, so that the alias will be evaluated for the script.foo
before it breaks something else.If you read this whole answer, you're now a WinDbg scripting ninja.