Alright, I’ll admit it it: I am in team WinDbg. Sure, I’ll happily use WinDbgX — the “Preview” version of the “new” WinDbg which has been in preview for ages now — but I always was a bit unhappy with the facilities that Visual Studio had to offer.
Lately I was helping debugging an issue in the Visual C/C++ runtime (“MSVCRT”) and we were wondering which exact Win32 status had been reported under the hood. Unfortunately by remapping the Win32 status codes to
errno_t some information may get lost.
So I thought to myself: “Well, I know this one! The TEB1 holds the last Win32 status, which is what
GetLastError() queries.” … so despite my disdain for the VS debugger, I thought I’d be able to guide someone else through using the pseudovariable
$tib2 to look at
TEB::LastErrorValue. Alas, when I tried it already failed at the first step: identifier “_TEB” is undefined. Oh my.
The immediate rescue came from someone else, who suggested that we should be able to set a watch with the value
GetLastError() to get to the Win32 status code. Adding another as
GetLastError(),hr even makes it human-readable, just like the modifier
x will cause values to be shown in hex:
But the next time around I needed to know the last NT status code. And while that also resides in the
TEB::LastStatusValue, it’s even more cumbersome to get to. But either way,
GetLastError() wasn’t going to cut it.
So back to the drawing board. But not for long.
Although I also had initially tried qualifying the name of the module by prepending it separated with an exclamation point —
(nt!_TEB*)$tib — just the way I knew from WinDbg, I only ever received: Module “nt” not found.. But that seems to be a condition different from identifier … is undefined. And then I had the epiphany. Probably the debug symbols containing
_PEB and friends where simply not loaded.
And sure enough I noticed that I had picked — for performance reasons — “Load only specified modules” within VS. Telling it to load the symbols for
kernel32.dll was my course of action:
Furthermore it turned out that — contrary to what I was used from WinDbg3 —
nt wasn’t a valid module name. So fair enough, I tried with
And sure enough it worked!
… and as you can see, it can even expand the variable and peek into it.
Consequently the next step was natural:
- Last Win32 status:
- Last NT status:
The nice thing is, since we can rely on the matching debug symbols, this should work reliably4.
If you wanted to be really “hardcore” you could use something like these to tap into the aforementioned structs without symbols:
*(int*)($tib+(sizeof(void*) == 8 ? 0x68 : 0x34)),hr
*(int*)($tib+(sizeof(void*) == 8 ? 0x1250 : 0x0bf4)),hr
Hope this will prove useful to someone.
- Thread Environment Block [↩]
- Thread Information Block:
ntcan stand in as module name for either the current kernel or
- … unlike the layout of those structs from Terminus Project which may or may not be correct on any given system [↩]