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 TEB 1 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 $tib
2 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
as 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 _TEB
and _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 ntdll.dll
and kernel32.dll
was my course of action:
Furthermore it turned out that — contrary to what I was used from WinDbg 3 — nt
wasn’t a valid module name. So fair enough, I tried with ntdll
.
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:
((ntdll!_TEB*)$tib)->>LastErrorValue,hr
- Last NT status:
((ntdll!_TEB*)$tib)->>LastStatusValue,hr
Observe:
The nice thing is, since we can rely on the matching debug symbols, this should work reliably 4.
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.
// Oliver
- Thread Environment Block [↩]
- Thread Information Block:
_NT_TIB
[↩] - where
nt
can stand in as module name for either the current kernel orntdll
[↩] - … unlike the layout of those structs from Terminus Project which may or may not be correct on any given system [↩]