That trick I learned with the Visual Studio debugger

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 ((Thread Environment Block)) 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 ((Thread Information Block: _NT_TIB)) 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:

Watch window inside Visual Studio showing failed attempts

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.

Watch window inside Visual Studio showing module not found error

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:

Dialog: Symbols to load automatically with Visual Studio Options dialog in background

Furthermore it turned out that — contrary to what I was used from WinDbg ((where nt can stand in as module name for either the current kernel or ntdll)) — nt wasn’t a valid module name. So fair enough, I tried with ntdll.

And sure enough it worked!

Watch window inside Visual Studio showing the first successful attempt to cast $tib to ntdll!_TEB

… 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


Watch window inside Visual Studio showing TEB::LastErrorValue and TEB::LastStatusValue

The nice thing is, since we can rely on the matching debug symbols, this should work reliably ((… unlike the layout of those structs from Terminus Project which may or may not be correct on any given system)).

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

Watch window inside Visual Studio showing TEB::LastErrorValue and TEB::LastStatusValue without loaded/available debug symbols

Hope this will prove useful to someone.

// Oliver

This entry was posted in C/C++, EN, Programming and tagged , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *