Skip to content
C++
Debugger
Updated 2026-07-14T00:00:00.000Z

WinDbg — Windows Debugger

WinDbg for C++ developers — crash dump analysis, kernel debugging, TTD time-travel debugging, symbols, SOS, and the dx command.

TL;DR

WinDbg is the authoritative debugger for Windows C++ development — the only tool that can analyze crash dumps from production machines, perform kernel debugging, run Time Travel Debugging (TTD), and script complex analysis over live or post-mortem state. Use WinDbg Preview (the modern rewrite) rather than classic WinDbg; it ships with a better UI, TTD support baked in, and the dx command that turns the debugger into a LINQ-over-debug-objects engine.

A minimal crash dump analysis session:

windbg -z C:\crash.dmp
0:000> .symfix C:\Symbols
0:000> .reload
0:000> !analyze -v
0:000> k

The four commands above — fix symbols, reload them, run auto-analysis, and print the call stack — resolve the majority of production crashes in under a minute.


Installation

WinDbg Preview is the version you want — it replaces the classic WinDbg shipped with the Windows SDK.

Install from the Microsoft Store or via winget:

powershell
winget install Microsoft.WinDbg

WinDbg Preview supports TTD, the dx command, dark mode, and a JavaScript/TypeScript extension model. It is updated independently of the Windows SDK.

Classic WinDbg via Windows SDK

The classic debugger ships inside the Windows SDK. Install it through the VS Installer or standalone:

powershell
# Download the Windows 11 SDK installer
# During install, check only "Debugging Tools for Windows"
# Installs to: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\

Classic WinDbg (windbg.exe) and cdb.exe (headless) are still useful in CI pipelines and on machines without the Store.

Command-line invocation

WinDbg Preview registers as windbgx on the PATH:

powershell
windbgx                              # launch GUI
windbgx -z crash.dmp                # open a crash dump
windbgx -g .\myapp.exe arg1 arg2    # launch and run (don't break at entry)
windbgx -p 1234                     # attach to PID

VS Code extension

The WinDbg extension (ms-vscode.windbg) connects VS Code to a WinDbg session over DAP. Install from the Extensions marketplace. It provides breakpoints, variable watch, and a call-stack panel while still allowing raw WinDbg commands in the integrated terminal.


Symbol Setup

Symbols transform hexadecimal addresses into function names, file names, and line numbers. Without a correct symbol path, every stack frame shows ??? and !analyze produces only guesses.

The _NT_SYMBOL_PATH environment variable

Set this system-wide so every WinDbg session picks it up automatically:

powershell
# PowerShell — set for the current session
$env:_NT_SYMBOL_PATH = "srv*C:\Symbols*https://msdl.microsoft.com/download/symbols"

# Set permanently (system-wide)
[System.Environment]::SetEnvironmentVariable(
    "_NT_SYMBOL_PATH",
    "srv*C:\Symbols*https://msdl.microsoft.com/download/symbols",
    "Machine"
)

The srv*<local-cache>*<server-url> syntax tells WinDbg to download symbols on demand and cache them locally under C:\Symbols. The Microsoft Symbol Server hosts PDBs for every Windows OS binary ever shipped.

.symfix and .reload inside a session

0:000> .symfix C:\Symbols
0:000> .sympath+ srv*C:\MySymbols*\\myserver\share\symbols
0:000> .reload /f
0:000> lm                       # list loaded modules with symbol status

.symfix points at the Microsoft server. .sympath+ appends your private symbol store without replacing the existing path. .reload /f forces an immediate reload rather than lazy loading on first access.

Diagnosing symbol problems with !sym noisy

When symbols fail to load, turn on verbose symbol lookup to see exactly what WinDbg is searching for and why it rejects a PDB:

0:000> !sym noisy
0:000> .reload /f myapp.exe
DBGHELP: myapp - public symbols
        C:\Symbols\myapp.pdb\<GUID>\myapp.pdb

Common failures: PDB age/GUID mismatch (the PDB does not match the binary), missing private symbols (you only have the public stripped PDB), or a firewall blocking msdl.microsoft.com. The noisy output tells you which.

Private PDB files

Private PDBs contain full type information, local variable names, and inlined function details. Public PDBs (what Microsoft distributes) contain only exported function names. For your own binaries, always ship the private PDB alongside the executable:

cmake
# CMakeLists.txt — ensure PDB is emitted alongside the binary
target_compile_options(myapp PRIVATE /Zi)
target_link_options(myapp PRIVATE /DEBUG:FULL)
set_target_properties(myapp PROPERTIES
    PDB_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)

Opening a Session

WinDbg supports four session types: launching a new process, attaching to a running one, opening a crash dump, and kernel debugging.

Launch a new process

powershell
windbgx -g .\myapp.exe arg1 arg2    # -g: don't break at process entry
windbgx    .\myapp.exe arg1 arg2    # break at process entry point (default)

Without -g, WinDbg breaks before main executes — useful when you need to set breakpoints before any user code runs.

Attach to a running process

powershell
windbgx -p 4321           # attach by PID
windbgx -pn myapp.exe     # attach by process name (first match)

Inside an existing session you can also attach interactively:

File → Attach to Process   (or Ctrl+Alt+P in WinDbg Preview)

Attaching suspends all threads in the target process. Issue g to resume.

Open a crash dump

powershell
windbgx -z C:\dumps\myapp.20260714.dmp
windbgx -z C:\dumps\myapp.20260714.dmp -y "srv*C:\Symbols*https://msdl.microsoft.com/download/symbols"

User-mode crash dumps can be collected with Task Manager, ProcDump, or Windows Error Reporting. A full dump captures heap and all thread stacks; a mini-dump captures only the thread stacks and exception context.

Kernel debugging

Kernel debugging attaches WinDbg to a second Windows machine (or a VM) over a serial cable, USB 3 cable, or network:

powershell
# On the target VM — enable kernel debugging over network
bcdedit /debug on
bcdedit /dbgsettings net hostip:192.168.1.10 port:50000 key:1.2.3.4

# On the host — attach WinDbg
windbgx -k net:port=50000,key=1.2.3.4

Kernel debugging gives access to !process, !thread, !devobj, and the entire Windows kernel object model.


Essential Commands

WinDbg's command language is dense but regular — once you learn the three namespaces (bare commands, . meta-commands, ! extension commands), everything follows a pattern.

Execution control

g          # go — run until next breakpoint or exception
p          # step over (source line)
t          # step into
gu         # go up — run until current function returns
pa addr    # step to address

Breakpoints

bp myapp!Widget::render          # break at symbol (resolved at set time)
bu myapp!Widget::render          # break at symbol (resolved when module loads — survives reload)
bm myapp!Widget::*               # break on all symbols matching wildcard
bp 0x00007ff6`1234abcd           # break at exact address
bp myapp!main+0x1a               # break at offset from symbol

bl                               # list all breakpoints
bc 0                             # clear breakpoint #0
bc *                             # clear all breakpoints
bd 1                             # disable breakpoint #1
be 1                             # enable breakpoint #1

bp resolves the symbol immediately; bu ("unresolved") defers resolution until the module loads — use bu for breakpoints set before the target process has loaded all DLLs.

Call stack inspection

k          # call stack — return address + frame pointer chain
kb         # call stack with first three parameters per frame
kv         # call stack with FPO info and calling convention
kp         # call stack with full parameter names and types (requires private PDB)
kn 20      # show 20 frames
.frame 3   # switch to frame 3
dv         # display local variables in current frame
lv         # list local variables (alias)

Registers and memory

r          # show all registers
r rax      # show single register
r rax=42   # set register value

dd addr    # dump DWORD (32-bit) values at address
db addr    # dump bytes
dq addr    # dump QWORD (64-bit) values
da addr    # dump ASCII string at address
du addr    # dump Unicode (UTF-16) string at address
dp addr    # dump pointer-size values
dps addr   # dump pointer-size values + resolve symbols

Expression evaluation

?  0n10 * 0n20        # evaluate C-style expression (decimal prefix 0n)
?? sizeof(MyStruct)   # C++ expression evaluator
?? myVar              # print variable value using C++ evaluator

The ? command uses the MASM evaluator (hexadecimal by default, prefix 0n for decimal). The ?? command uses the C++ evaluator — it understands types, member access, and sizeof.


Crash Dump Analysis Workflow

The discipline of dump analysis is: establish the exception context first, then read the stack, then read memory. Working in a different order wastes time.

Step 1 — Switch to the exception context

A crash dump opens on the default thread. The thread that crashed is the exception thread, and .ecxr switches the register context to the point of the fault:

0:003> .ecxr
eax=00000000 ebx=00000001 ecx=00000000 edx=00000000
esi=00000001 edi=00c85098
eip=77c15bfe esp=00f7f8dc ebp=00f7f8e8 iopl=0
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b  efl=00010246
ntdll!NtWaitForSingleObject+0xc:

After .ecxr, all subsequent k, dv, and register commands operate on the faulting thread's state.

Step 2 — Run auto-analysis

0:003> !analyze -v

!analyze -v is WinDbg's expert system. It identifies the exception code, points at the faulting instruction, shows the responsible stack frame, and often names the bug class (heap corruption, stack overflow, null dereference). The -v flag enables verbose output with additional heuristics.

Step 3 — Read the stack and inspect state

Here is an annotated example of a heap corruption crash:

0:003> !analyze -v
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ffb8c1d4567 (ntdll!RtlReportCriticalFailure+0x0047)
   ExceptionCode: c0000374 (Heap corruption detected)   ; <-- bug class
  ExceptionFlags: 00000001

STACK_TEXT:
00 ntdll!RtlReportCriticalFailure
01 ntdll!RtlpHeapHandleError
02 ntdll!RtlpHpHeapHandleError
03 ntdll!RtlFreeHeap
04 myapp!WidgetCache::evict+0x8f                        ; <-- culprit
05 myapp!WidgetCache::insert+0x112
06 myapp!ProcessRequest+0x3a
07 myapp!WorkerThread+0x67

0:003> .frame 4
04 myapp!WidgetCache::evict+0x8f
0:003> dv
          this = 0x00c8a020
         entry = 0x00c8b190     ; double-freed pointer
0:003> k
# Child-SP          RetAddr           Call Site
00 ...              ...               ntdll!RtlReportCriticalFailure
01 ...              ...               myapp!WidgetCache::evict+0x8f
02 ...              ...               myapp!WidgetCache::insert+0x112

The !gle command reads the last Win32 error code — useful when a Windows API call returned FALSE or INVALID_HANDLE_VALUE and you need to know why:

0:000> !gle
LastErrorValue: (Win32) 0x57 (87) - The parameter is incorrect.
LastStatusValue: (NTSTATUS) 0xc000000d - An invalid parameter was specified.

Time Travel Debugging (TTD)

TTD records a complete execution trace and lets you replay it — including stepping backward. The recording captures every instruction the CPU executes; replay lets you travel to any point in time without re-running the program.

Recording a trace

WinDbg Preview records TTD sessions directly from the UI: File → Start debugging → Launch executable (advanced) → Record with Time Travel Debugging.

From the command line using the standalone TTD recorder:

powershell
# Record myapp.exe — produces myapp01.run in the current directory
ttd.exe -launch .\myapp.exe arg1 arg2

# Record an already-running process by PID
ttd.exe -attach 4321

# Record with output in a specific directory
ttd.exe -out C:\traces -launch .\myapp.exe

Open the .run file in WinDbg Preview:

powershell
windbgx -z C:\traces\myapp01.run
!tt 0:0          # replay from the very beginning of the trace
!tt 100%         # jump to the end of the trace
!tt 50%          # jump to the midpoint
!tt 1A0:4B       # jump to a specific time position (sequence:steps)

Time positions use the notation <sequence>:<steps>. Every event in the trace has a unique position printed in the prompt and in !analyze output.

Reverse execution

g-               # reverse-go — run backward to the previous breakpoint or event
p-               # reverse step over
t-               # reverse step into
g- -t            # reverse-go, stop on exceptions traveling backward

Combine with breakpoints to find the root cause of a use-after-free:

0:000> bp myapp!Widget::~Widget   # break when Widget is destroyed
0:000> g                          # forward to the destruction
0:000> bp myapp!Widget::update    # break when the zombie pointer is used
0:000> g                          # forward to the use-after-free symptom
0:000> g-                         # travel backward to find who destroyed it early

TTD vs rr on Linux

TTD (Windows)rr (Linux)
PlatformWindows 10+Linux x86-64
IntegrationNative WinDbg UIGDB interface
Reverse executiong-, p-, t-reverse-continue, reverse-next
Kernel eventsWin32/NT object modelSystem calls via ptrace
Overhead10–30× slowdown recording2–10× slowdown recording
Multi-processYes (TTD follows child processes)-f flag
Scriptingdx / JavaScriptPython / GDB scripting

Both tools solve the same fundamental problem. Choose based on platform; for cross-platform codebases, use each natively on its respective OS.


The dx Command — LINQ over Debug Objects

dx is the most powerful feature in modern WinDbg — it exposes the entire debugger state as a queryable object graph that you can filter, project, and aggregate using LINQ-style expressions.

0:000> dx @$curprocess
@$curprocess : myapp.exe [Switch To]
    Name         : myapp.exe
    Id           : 0x1234
    Threads
    Modules
    TTD          (for TTD sessions)

0:000> dx @$curthread
@$curthread : [object Object]
    Id      : 0x5678
    Stack
    Registers

0:000> dx @$curthread.Stack.Frames
@$curthread.Stack.Frames
    [0x0]  : ntdll!NtWaitForSingleObject+0xc [Inline]
    [0x1]  : myapp!Widget::render+0x3f
    [0x2]  : myapp!RenderLoop+0xa1
    [0x3]  : myapp!main+0x52

Traversing processes and sessions

dx Debugger.Sessions
dx Debugger.Sessions.First().Processes
dx Debugger.Sessions.First().Processes.First().Threads

Recursive expansion with -r

0:000> dx -r2 @$curthread.Stack.Frames.First()
@$curthread.Stack.Frames.First()
    Attributes
        InstructionOffset  : 0x00007ff6`1234abcd
        ReturnOffset       : 0x00007ff6`1234ef01
        FrameOffset        : 0x000000a0`bc12d340
    LocalVariables
        this               : 0x00007ff6`...
        count              : 42

Filtering with LINQ

Find all threads currently waiting in WaitForSingleObject:

0:000> dx -g @$curprocess.Threads.Select(t => t.Stack.Frames.First()).Where(f => f.ToDisplayString().Contains("WaitForSingle"))

Count heap allocations larger than 1 MB in a TTD trace:

0:000> dx @$curprocess.TTD.Heap().Allocations.Where(a => a.Size > 0x100000).Count()

Custom dx queries for C++ debugging

# Find all active threads and their top frame
0:000> dx -g @$curprocess.Threads.Select(t => new { TID = t.Id, Top = t.Stack.Frames.First() })

# Find every frame containing a specific function across all threads
0:000> dx @$curprocess.Threads.SelectMany(t => t.Stack.Frames).Where(f => f.ToDisplayString().Contains("Widget"))

# In a TTD trace: find every call to malloc over 64KB
0:000> dx @$curprocess.TTD.Calls("ucrtbase!malloc").Where(c => c.Parameters[0] > 0x10000)

Creating model objects

0:000> dx @$create("Debugger.Models.Process")

The @$create function instantiates a new debugger model object, useful for building custom extension scripts that the dx system can navigate.


SOS Extension — Managed Code

When a C++ process hosts .NET — through C++/CLI, COM interop, or embedding the CLR — you need SOS to see the managed side of the heap and call stack.

Loading SOS

In WinDbg Preview, SOS loads automatically when a managed exception is detected. Load it manually:

0:000> .loadby sos clr            # .NET Framework
0:000> .loadby sos coreclr        # .NET Core / .NET 5+
0:000> !sos.help                  # list all SOS commands

Essential SOS commands

!sos.threads         # list managed threads and their states
!sos.clrstack        # managed call stack of the current thread
!sos.dumpheap -stat  # heap statistics: type, count, total bytes
!sos.dumpheap -type System.String   # all live string instances
!sos.dumpobj 0x...   # inspect a managed object at address
!sos.gcroot  0x...   # find GC roots keeping an object alive
!sos.finalizequeue   # objects waiting for finalization

A typical managed-memory investigation:

0:000> !sos.dumpheap -stat
              MT    Count    TotalSize Class Name
00007ffb1234  4821    386608 System.String
00007ffb5678  1203    120300 MyApp.Widget
00007ffb9abc    12  12582912 System.Byte[]   ; <-- suspiciously large

0:000> !sos.dumpheap -mt 00007ffb9abc
         Address               MT     Size
  00000200`1a000028 00007ffb9abc  1048576
  ...

0:000> !sos.gcroot 00000200`1a000028

SOS is most relevant when debugging C++/CLI assemblies where native and managed frames are interleaved on the same thread stack. Use !sos.clrstack for the managed frames and k for the native frames.


WinDbg with CMake + MSVC

Build flags control whether WinDbg can resolve variables, types, and inline functions — the difference between a useful debugging session and staring at raw disassembly.

Build flags for full debuggability

cmake
# CMakeLists.txt
add_executable(myapp main.cpp widget.cpp)

target_compile_options(myapp PRIVATE
  $<$<CXX_COMPILER_ID:MSVC>:
    /Zi          # emit PDB (separate .pdb file alongside .exe)
    /Od          # disable optimizations (Debug build)
    /RTC1        # runtime checks: stack corruption + uninitialized vars
  >
)

target_link_options(myapp PRIVATE
  $<$<CXX_COMPILER_ID:MSVC>:/DEBUG:FULL>   # full PDB, not FASTLINK
)

/DEBUG:FULL is critical: /DEBUG:FASTLINK (the MSVC default in newer toolchains) produces a PDB that references the original object files. Copy the binary to another machine and the PDB becomes useless. /DEBUG:FULL embeds everything.

PDB location and deployment

The PDB lives alongside the .exe by default. When deploying a binary for crash reporting, always ship the PDB to your symbol server:

powershell
# Copy PDB to a private symbol store
symstore add /f .\build\Release\myapp.exe /s \\myserver\symbols /t "myapp" /v "1.0.0"
symstore add /f .\build\Release\myapp.pdb /s \\myserver\symbols /t "myapp" /v "1.0.0"

Verifying symbols loaded correctly

0:000> lm v m myapp
Browse full module list
start             end                 module name
00007ff6`10000000 00007ff6`10234000   myapp    (private pdb symbols)
                                      C:\Symbols\myapp.pdb

0:000> x myapp!Widget::*        # list all symbols matching the wildcard

The (private pdb symbols) annotation confirms you have full type information. (pdb symbols) means only the public/stripped PDB loaded. (no symbols) means WinDbg cannot find the PDB at all.

Checking exports with dumpbin

powershell
# Verify exported symbols from a DLL
dumpbin /EXPORTS mylib.dll

# Check which PDB a binary references
dumpbin /PDBPATH myapp.exe

# Check debug directory for PDB GUID
dumpbin /HEADERS myapp.exe | Select-String "PDB"

Useful Extensions and Scripts

WinDbg's extension model (!command) adds domain-specific analysis on top of the base debugger. Most useful extensions ship with the Windows Debugger package.

Heap analysis

!heap -s                    # heap summary: all heaps in the process
!heap -stat -h 0            # statistics for heap 0
!heap -l                    # leak detection — show unreferenced heap blocks
!heap -p -a addr            # show heap block details at addr (with PageHeap)

Enable PageHeap for your application to catch heap corruption at the point it occurs rather than at the next heap operation:

powershell
# Enable PageHeap for myapp.exe
gflags /p /enable myapp.exe /full

Process and thread environment

!teb                        # Thread Environment Block — stack base/limit, TLS, locale
!peb                        # Process Environment Block — loaded modules, command line, env vars
!peb -l                     # PEB loader data — load order of all DLLs

!locks                      # critical sections — find threads waiting for locks
!cs -l                      # list all critical sections and their owners (deadlock diagnosis)

!locks and !cs are the fastest path to diagnosing a deadlock: look for threads in WaitForCriticalSection and cross-reference which thread owns the lock they are waiting for.

Session logging

.logopen C:\debug\session.log    # start logging all output to file
.logappend C:\debug\session.log  # append to existing log
.logclose                         # close the log

Logging is essential when running long analysis sessions — paste the log into a bug report or share it with a colleague who does not have access to the dump.

Command files and automation

$$>< C:\scripts\analyze.txt      # run a WinDbg script file
$$>a< C:\scripts\analyze.txt arg1 arg2   # pass arguments to script

A command file is a plain text file containing WinDbg commands, one per line. Example:

.echo === Exception context ===
.ecxr
k
.echo === Heap summary ===
!heap -s
.echo === Loaded modules ===
lm

PyKD — Python scripting inside WinDbg

PyKD exposes a Python API to WinDbg's debug engine. Install from NuGet or the PyKD GitHub releases, then:

python
# Inside a PyKD script
import pykd

# Print all thread stacks
for t in pykd.getThreadList():
    print(f"Thread {t.getId()}")
    for frame in t.getStack():
        print(f"  {frame.getFunctionName()}")
0:000> !py C:\scripts\dump_threads.py

PyKD is the Windows equivalent of GDB's Python scripting: you can automate repetitive analysis, write custom ! commands, and integrate with external tools.


WinDbg vs Other Windows Debuggers

Each debugger on Windows occupies a different niche — knowing when to reach for each one saves hours.

FeatureWinDbg PreviewVisual Studio Debuggerx64dbgCDB (cdb.exe)
Primary useCrash dumps, kernel, TTDLive dev debuggingReverse engineering, malwareCI/headless automation
UIStandalone GUI + DMLIntegrated in VS IDEStandalone GUICommand-line only
Crash dump analysisExcellent (!analyze)Basic (limited)NoExcellent (same engine)
Kernel debuggingYesNoNoYes
TTD / time-travelYes (built-in)NoNoNo
dx / LINQ queriesYesNoNoYes
SOS / managed codeYesYes (better UI)NoYes
ScriptingJavaScript, PyKD, dxC# VSIXPython, JS plugins.script files
Symbol serverFull _NT_SYMBOL_PATHFullManual PDB loadFull
DisassemblyYesYesExcellent (IDA-like)Yes
Source steppingYes (with PDB)ExcellentNoYes (with PDB)
Breakpoints on loadbu unresolved bpGUIGUIbu
Install size~100 MB (Store)~2–10 GB (VS)~5 MBIncluded with WinDbg
LicenseFreeFree (Community)Free/open-sourceFree

Rule of thumb:

  • Production crash dump from a customer machine → WinDbg Preview
  • Day-to-day dev debugging in a VS solution → Visual Studio debugger
  • Reverse engineering a binary without source → x64dbg
  • CI pipeline automated crash analysis → CDB with !analyze -v; q

See Also

  • MSVC — compiler flags, PDB generation (/Zi /DEBUG:FULL), and symbol server setup
  • GDB — Linux counterpart: same concepts (breakpoints, watchpoints, stack inspection), different syntax
  • LLDB — macOS/Linux debugger with similar dx-style expression evaluation via lldb's Python API
  • rr — Linux time-travel debugging: the open-source equivalent of TTD for x86-64 Linux
Edit on GitHubUpdated 2026-07-14T00:00:00.000Z