WinDbg, lists, and hello world
Watching me use WinDbg is probably a bit like watching a new programmer write hello world in Visual Studio. I know I'm sitting in front of a very awesome, powerful, tool, and yet, I'm happy and get what I want out of a vocabulary of just a few commands. The problem is that I haven't made it past those few commands in years now. I finally decided today that I needed to figure a few more things out. In this case, I wanted to see a list, and I didn't feel like walking it by hand. For simple data types, something like dt -r 100 might work, but wasn't practical in this case. It was time to find out how to write a loop in a debugger command.
To set things up, here's the struct I was looking at:
struct link {
void *stuff;
link *pnext;
};
To get the whole list to dump, we need a couple of things. First, we need someplace to store the current link. For that, we can use user-defiend psuedo-registers. User regsisters are $t0 to $t19. As the name implies, they're not real registers, and changing them has no effect on program execution. I used $t0, and initialized it to the address of the head of my list:
r $t0 = 0x1234
Next we need a loop. Since this is a list, a while loop seemed like a natural choice. Note that unlike C / C++, in debugger script, the '{' and '}' are required. In my first go at this, I wrote ".while (0 != $t0) {...}" and it was quite slow. It turns out that the debugger first does a symbol lookup on $t0, and sometimes can be confused and not treat $t0 properly in expressions. For this reason, it's good to be in the habit of writing @ in front of the register names. Sometimes it'll make things faster. Other times it will make things work. I don't think there's a way to lose by doing this. That gets us to a loop that looks like:
.while (0 != @$t0) { ... }
Now, what goes in the ellipses? First, we need to output what we have, then we need to increment our loop counter. Dumping the output is easy, that's what dt was designed for. Don't forget the '@' in the register name!
dt link @$t0
We also need to update our register. Right now we have a pointer to the stuct. We need to get to the value of pNext. First, let's update our pointer to point to the location of pNext. That means we need the offset of the member. The debugger can give us this using dt again, and it's worth checking (not guessing) because the compiler can add padding. Just leave the address off the dt command and the debugger will dump the struct layout:
dt link
That shows us that the offset pNext pointer is at offset 0x8 (I'm debugging on x64). Putting together expressions in the debugger can feel a bit like a black art sometimes, but if you're familiar with just poi, +, *, and -, you'll be able to get most things done that you need to. poi dereferences a value in an expression to a location in the target process memory, and the result is a pointer-sized value at the target location.
r $t0 = poi(@$t0+8)
Put it all together, and we get:
r $t0 = 0x1234; .while (0 == @$t0) { r $t0; dt _link @$t0; r $t0 = poi(@$t0+8); }
There's probably an even better way to do this. There almost always is in WinDbg. This got the job done for me though, and I've tucked this away on my blog in case I forget & need to find it again later.
So, was this useful? If you found this useful, or want to see some other topic up here, leave a comment and let me know. Anything C++, debugging, printing, XPS, or OPC related is fair game right now.
- Ben