Last reviewed and updated: 10 August 2020
Complex data structures are a way of life when you’re dealing with drivers. A lot of the structures at driver callbacks end up being gnarly enough, and it’s not uncommon to write a few of your own. Wading your way through the 9 layers of pointers to divine something about the driver state at the time of a crash can be a chore. Tools to help that (!analyze, !irp, !pfn, etc.) are always welcome.
With the Windows 10 DDK, WinDBG now supports NatVis – Visual Studio’s method for making sense of complex structures since Visual Studio 2012. They’re XML files that can describe types in a debugger friendly way, so that you don’t have to worry so much about the mechanics of the structure when you just want to see what’s logically in it.
If you’ve ever used Visual Studio’s debugger to debug some C++ standard template library code, you’ve seen how it makes sense of things like std::map. The entire implementation of std::map is hidden from the debugger (unless you go looking for it), and instead you’re presented with a sensible view that has your elements with their keys.
Seeing is believing, of course, so I decided to write something simple to see if I could parse it. I ended up with a very small process filter that stores every process it encounters into a tree, with the tree nodes looking like this:
#define PROCESS_NAME_VERSION 1 #define PROCESS_EPROCESS_VERSION 3 typedef struct _NV_TREE_NODE { // Version dictates the union contents ULONG Version; ULONG_PTR ProcessId; // Tree nodes PNV_TREE_NODE HeadNode; PNV_TREE_NODE RightNode; ULONG CountRightNodes; PNV_TREE_NODE LeftNode; ULONG CountLeftNodes; // A silly union to make the parsing more interesting union { UNICODE_STRING ProcessImageName; PEPROCESS Process; }; } NV_TREE_NODE, *PNV_TREE_NODE;
To keep it interesting, when I see a new process I pick one of the two version types. That way, some of my nodes would have readable names while some wouldn’t. This is all rooted at a g_RootNode pointer in my driver.
It didn’t take too long to pick up the .natvis syntax, since it’s just XML. I felt like I wasn’t able to really find the right documentation I needed to just crank these things out, though, so it ended up taking longer than I thought to get something that actually worked rather than just parsed.
One thing I wasn’t able to get working was the UNICODE_STRING structure – or more generally, non-null terminated character arrays. Maybe this just isn’t possible with NatVis, but I wasn’t able to figure out the syntax to make it display only the first N characters while still treating it like a string.
After a bit of trial and error (and finally giving up on correct UNICODE_STRING parsing) I ended up with this NatVis file for my tree:
<?xml version="1.0" encoding="utf-8"?> <AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> <Type Name="_NV_TREE_NODE"> <DisplayString Condition="this->Version == 1">{ProcessImageName.Buffer,su}</DisplayString> <DisplayString Condition="this->Version == 3">PID={ProcessId}</DisplayString> <Expand> <Synthetic Name="ProcessName" Condition="this->Version == 1"> <DisplayString>{{Contains Process Name}}</DisplayString> </Synthetic> <Synthetic Name="ProcessName" Condition="this->Version == 3"> <DisplayString>{{Contains Process EPROCESS pointer}}</DisplayString> </Synthetic> <Item Name="SubtreeSize"> CountRightNodes + CountLeftNodes </Item> <TreeItems> <Size>CountRightNodes + CountLeftNodes</Size> <HeadPointer>g_RootNode</HeadPointer> <LeftPointer>LeftNode</LeftPointer> <RightPointer>RightNode</RightPointer> <ValueNode>this</ValueNode> </TreeItems> </Expand> </Type> </AutoVisualizer>
One neat thing to note here is the TreeItems section – that’s one of the standard container definitions that you can use in NatVis. The TreeItems definition managed to fit this data structure, but there’s container definitions for a few other common ones like LinkedListItems and ArrayItems. Additionally, there’s a CustomListItems type if you’re doing something exotic. I haven’t gotten around to trying the other ones yet, but I assume the syntax wouldn’t be much different.
The NatVis files in a Visual Studio project get included into the driver symbol file by default, so I didn’t need to do anything special to set up WinDBG aside from adding the .natvis file to my project. WinDBG is smart enough to load them automatically if it finds them in the .pdbs.
There’s also a new .nvload command to load a NatVis file manually. You could probably do some interesting things with this – for example, you could define NatVis views for complex kernel types to only show the fields you’re interested in for your driver. I was expecting to find these already defined for some of the kernel data types, but that wasn’t the case for the ones I checked.
So, I started up Windows with my driver, clicked on everything in the start menu to get some processes in the tree, and broke into the debugger on the last process create. WinDBG’s new dx command (Display NatVis Expression), at its most basic, will dump the NatVis-defined view of a symbol.
0: kd> dx g_RootNode g_RootNode : 0xffffc000dd5fa6d0 : PID=0xa98 : [Type: _NV_TREE_NODE *] [<Raw View>] ProcessName : {Contains Process EPROCESS pointer} SubtreeSize : 0x1e [0x0] : 0xffffc000e1c28710 : PID=0x138 : [Type: _NV_TREE_NODE *] [0x1] : 0xffffc000e1c00810 : "\??\C:\Program Files\WindowsApps\microsoft.windowscommunicationsapps_17.6002.42251.0_x64__8wekyb3d8bbwe\HxCalendarAppImm.exe" : [Type: _NV_TREE_NODE *] [0x2] : 0xffffc000e1a9f6f0 : "\??\C:\Program Files\WindowsApps\Microsoft.MicrosoftSolitaireCollection_3.1.6103.0_x64__8wekyb3d8bbwe\Solitaire.exeddisk̐ଊ湗饑⭝穖अ" : [Type: _NV_TREE_NODE *] [0x3] : 0xffffc000e19bd8b0 : PID=0x59c : [Type: _NV_TREE_NODE *] [0x4] : 0xffffc000dd38f790 : PID=0x638 : [Type: _NV_TREE_NODE *] [0x5] : 0xffffc000e17f5480 : PID=0x750 : [Type: _NV_TREE_NODE *] [0x6] : 0xffffc000da807140 : PID=0x7d4 : [Type: _NV_TREE_NODE *] [0x7] : 0xffffc000dd5fa6d0 : PID=0xa98 : [Type: _NV_TREE_NODE *] [0x8] : 0xffffc000dd1cb860 : PID=0xab4 : [Type: _NV_TREE_NODE *] [0x9] : 0xffffc000e1b25710 : PID=0xbf4 : [Type: _NV_TREE_NODE *] [0xa] : 0xffffc000e188f560 : PID=0xf94 : [Type: _NV_TREE_NODE *] [0xb] : 0xffffc000dd57f780 : PID=0x10b0 : [Type: _NV_TREE_NODE *] [0xc] : 0xffffc000db982c90 : PID=0x11f8 : [Type: _NV_TREE_NODE *] [0xd] : 0xffffc000dae2b360 : PID=0x11fc : [Type: _NV_TREE_NODE *] [0xe] : 0xffffc000e16ac280 : "\??\C:\Windows\system32\backgroundTaskHost.exe" : [Type: _NV_TREE_NODE *] [0xf] : 0xffffc000dad783b0 : "\??\C:\Windows\System32\svchost.exe" : [Type: _NV_TREE_NODE *] [0x10] : 0xffffc000dd1e0210 : PID=0x15d4 : [Type: _NV_TREE_NODE *] [0x11] : 0xffffc000dc036cc0 : "\??\C:\Program Files\WindowsApps\Microsoft.XboxApp_5.6.17000.0_x64__8wekyb3d8bbwe\XboxApp.exewe" : [Type: _NV_TREE_NODE *] [0x12] : 0xffffc000da8c7340 : PID=0x1618 : [Type: _NV_TREE_NODE *] [0x13] : 0xffffc000dd63dd80 : "\??\C:\Windows\system32\DllHost.exeSettĭ̅敓瑁MrtC봠�쀀 磈�쀀 봰�쀀 磠�쀀 " : [Type: _NV_TREE_NODE *] [0x14] : 0xffffc000e1c055e0 : "\??\C:\Windows\system32\werfault.exe" : [Type: _NV_TREE_NODE *] [0x15] : 0xffffc000dd102d20 : "\??\C:\Windows\System32\rundll32.exeEAdAȆ̉楖㈰䣡獴쭝薩扠�쀀 �쀀 倀侒Z" : [Type: _NV_TREE_NODE *] [0x16] : 0xffffc000dd0ee1d0 : "\??\C:\Windows\system32\werfault.exe " : [Type: _NV_TREE_NODE *] [0x17] : 0xffffc000da54d7f0 : "\??\C:\Windows\system32\wwahost.exe¬" : [Type: _NV_TREE_NODE *] [0x18] : 0xffffc000dd6f78d0 : PID=0x1818 : [Type: _NV_TREE_NODE *] [0x19] : 0xffffc000dafb7ec0 : "\??\C:\Windows\system32\werfault.exe " : [Type: _NV_TREE_NODE *] [0x1a] : 0xffffc000dd648750 : "\??\C:\Program Files\WindowsApps\Microsoft.WindowsStore_2015.7.1.0_x64__8wekyb3d8bbwe\WinStore.Mobile.exe壃鐹䶐ȁ" : [Type: _NV_TREE_NODE *] [0x1b] : 0xffffc000dd6237a0 : "\??\C:\Windows\system32\backgroundTaskHost.exe" : [Type: _NV_TREE_NODE *] [0x1c] : 0xffffc000dd7ec6f0 : PID=0x18f0 : [Type: _NV_TREE_NODE *] [0x1d] : 0xffffc000dc9da960 : "\??\C:\Windows\system32\WerFault.exe" : [Type: _NV_TREE_NODE *]
Aside from my defeat with UNICODE_STRINGs staring me in the face, I’m pretty pleased with this result. My conditionals seemed to be working, and I was able to get the flat view of the structure that I set out for. Getting all that data through the other WinDBG interfaces would definitely take more than a quick “dx pointer_to_thing” unless I wrote an extension to do it. Definitely going to consider using this in the future.