svnscha - Profile Picture
Posted on -

Remember the Cliffhanger?

A while back I wrote about the joy of writing a debugger adapter for Visual Studio Code. It started with a simple wish - I just wanted a faster way to remote-debug a service running in a VM - and ended with a working proof of concept: VS Code driving WinDbg through a custom Debug Adapter Protocol implementation.

But that post ended on an honest note:

I have not implemented remote debugging or kernel debugging yet, but I now feel ready to take a real shot at both without getting lost in the basics again.

Well. I took the shot.

Remote debugging works. Kernel debugging works. And the whole thing is now a real, installable VS Code extension: Native Windows Debugging (dbgeng).

Debug native Windows code - C and C++ programs, services, and even the Windows kernel - straight from Visual Studio Code, using the same engine that powers WinDbg.

That's the whole pitch in under two minutes. Now let me show you what's behind it.

Get It Right Now

No waiting, no building from source:

The adapter is bundled inside the extension, so there's nothing separate to build, download, or point at. Install it, write a launch.json, press F5. That's it.

Four Ways to Debug - Two of Them Were Never Possible Before

VS Code has always had great local debugging for plenty of languages. But the moment you wanted native Windows remote or kernel debugging, you hit a wall. That stack lives in Visual Studio and WinDbg, not in VS Code - because of course it does.

This extension closes exactly that gap. One adapter, four scenarios:

  1. Local - the debugger starts your program and debugs it from launch.
  2. Attach - connect to a process that's already running, by its PID.
  3. Remote - debug a user-mode process on another machine via dbgsrv.
  4. Kernel - debug kernel-mode drivers and the OS around them.

The first two are the comfortable ones. The last two are the gap I set out to close. Let's walk through them.

Local: The Comfortable Starting Point

Start simple. The debugger launches your program and you step through it like any other debugger in VS Code. It's just a launch.json:

{
  "name": "Debug myapp",
  "type": "dbgeng",
  "request": "launch",
  "program": "${workspaceFolder}/build/Debug/myapp.exe",
  "stopAtEntry": true
}

program is the only thing you really have to provide. dbgeng.dll is found automatically from your installed Windows SDK, so you usually don't even set a path. Set a breakpoint, press F5, and you get the full loop: continue, step over, step into, step out, call stack, locals, watch expressions, and a Debug Console that evaluates expressions through the real engine.

If you use the CMake Tools extension, you can even drop the program line entirely - the adapter defaults to your selected CMake launch target. Point CMake's debug command at the dbgeng type and CMake: Debug just works.

Attach: Already Running? Just Connect

Sometimes the process is already alive and you don't want to restart it. Attach to it by its process ID:

{
  "name": "Attach to myapp",
  "type": "dbgeng",
  "request": "attach",
  "processId": 12345
}

Don't know the PID off the top of your head? Set "processId": "${command:dap-dbgeng.pickProcess}" and you get a process picker at debug time. Same familiar VS Code interface, with the real Windows debug engine underneath.

Remote: My Favorite Part

This is the one that started the whole journey. Debug a user-mode process running on another machine - while the debug engine and your symbols stay local on your box.

The trick is the Windows process server, dbgsrv. You run it on the target, and your machine connects to it over TCP (or a named pipe).

On the target, start the process server:

dbgsrv -t tcp:port=5005

On your machine, point a launch.json at it:

{
  "name": "Attach on TARGETPC",
  "type": "dbgeng",
  "request": "attach",
  "processId": "${command:dap-dbgeng.pickProcess}",
  "connectionString": "tcp:port=5005,server=TARGETPC"
}

Press F5, pick the remote process from the list (the picker now shows processes on the dbgsrv host), and you're debugging it remotely. The engine and symbols never leave your machine - only the debuggee lives on the other side.

This is precisely the workflow I wanted at the very beginning: compile locally, deploy to the VM, attach, get back to work. Now it's a one-keystroke thing.

Kernel: Yes, Really

And then there's the big one. Kernel-mode debugging from VS Code.

Kernel debugging is whole-machine, so you need two boxes: a host (VS Code + the adapter) and a target being debugged - almost always a throwaway VM, because kernel debugging halts the entire target at breakpoints.

On the target, enable kernel debugging and reboot:

bcdedit /debug on
bcdedit /dbgsettings net hostip:<HOST-IP> port:50005 key:1.2.3.4

On the host, point a kernel config at it:

{
  "name": "Debug driver (KDNET)",
  "type": "dbgeng",
  "request": "attach",
  "kernel": true,
  "connectionString": "net:port=50005,key=1.2.3.4"
}

kernel: true flips the adapter into kernel mode and turns connectionString into a kernel transport. There's no processId here - the session is the whole machine. Press F5, it connects over KDNET, and breaks at DriverEntry. From the same editor people keep telling me is "just a text editor."

Kernel debugging. Just another breakpoint. That still feels a little unreal to type.

KDNET isn't the only option, either - the same connectionString field speaks serial (named pipe), 1394, and USB transports too. Whatever your VM or test box gives you.

Under the Hood

A few things changed since the proof of concept, and they're worth a mention.

It talks to the engine natively. The adapter uses the real DbgEng APIs throughout - IDebugClient, IDebugControl, IDebugSymbols, IDebugSystemObjects, with IDebugEventCallbacks and IDebugOutputCallbacks for events. No fragile text-scraping of WinDbg console output. When the engine knows something, the adapter asks it directly.

The protocol layer is still generated. That code-generation step I was so happy about last time paid off again. src/protocol/ is generated from the official DAP schema, so every request, response, and event stays in lockstep with the spec instead of drifting from hand-written DTOs.

The POC grew up - and changed language. The original proof of concept was C#. Hardening it into something I'd actually ship meant rebuilding the adapter in C++20 (CMake + Ninja, vcpkg manifest mode), sitting much closer to the engine it wraps. That was part of the "harden the rough edges" plan, and it made the kernel and remote work far less painful.

It records itself. There's a built-in session-trace recorder that doubles as the project's replay-test format. Recorded sessions get replayed against real test debuggees in CI, so the behaviors you see in those videos are the same behaviors the test suite asserts on.

An Honest Word on Scope

I'm keeping the same promise I made in the first post: the adapter advertises only what it implements correctly, rather than dangling buttons that misbehave.

What works today: line and conditional breakpoints, step over/into/out, continue, pause, instruction-level stepping in the disassembly view, call stack with delayed frame loading, variables and scopes (including registers), set-variable, expression evaluation in the Watch pane and Debug Console, disassembly view, and clean terminate/disconnect.

What's planned but not advertised yet: function breakpoints, data breakpoints (watchpoints), hit-conditional breakpoints, logpoints, read/write memory, a modules view, evaluate-on-hover, and hex formatting toggles. The engine can do all of these - the adapter just doesn't expose them yet. The features reference keeps the full, deliberately conservative list.

And here's the escape hatch: anything the UI doesn't surface, you can usually still do by typing native debugger commands straight into the Debug Console. It's the real engine - it'll answer.

Getting Started

If you want to try it, the whole loop is about ten minutes:

  1. Install Native Windows Debugging (dbgeng) from the Marketplace.
  2. Make sure you have Debugging Tools for Windows (it ships dbgeng.dll, via the Windows SDK or WDK installer). The adapter finds it automatically.
  3. Drop a dbgeng configuration into .vscode/launch.json - pick the scenario from above.
  4. Set a breakpoint, press F5.

The Getting Started guide walks through it in detail, and each scenario has its own page in the docs.

Your Turn

This is open source, and it's at the stage where real-world usage is the most valuable thing it can get. So:

  • Star it on GitHub if it's useful to you.
  • 🐛 Report a bug through the issue tracker - the templates tell you what to include.
  • 🔧 Contribute if you're into native Windows debugging internals; see CONTRIBUTING.md.

Wrapping Up

Last time I closed with "I just want to remote debug a service on a VM." That was the entire ambition.

It turned into DAP, a code generator, a dbgeng wrapper, a pile of state-machine bugs - and now remote debugging, kernel debugging, a published extension, and a documentation site.

The gap between "VS Code can't do native Windows kernel and remote debugging" and "actually, now it can" is closed. With the same engine that powers WinDbg, sitting quietly behind the editor you already use every day.

And yes - it still started with "I just want to remote debug a service on a VM."

P.S. - One More Thing About Those Videos

While you're here: how did you actually like the videos? Be honest.

Because here's the fun part - this is the first time I generated end-to-end videos, all the way from Claude to YouTube. No editing suite, no timeline scrubbing, no manual voiceover takes. Just prompting and vibing until it looked right.

The stack, if you're curious:

  • Remotion for the video itself - everything you see is React, rendered to MP4. Code as video turns out to be a really nice fit for the "iterate fast" loop.
  • My DGX Spark doing the heavy lifting locally.
  • MOSS-TTS for the voice. And here's a little detour: I first deep-cloned my own voice. The result was genuinely impressive - but not quite perfect, and the "almost me" uncanny gap bugged me more than I expected. So I switched to a fully synthesized voice instead, and I was happier with that than with a not-quite-right clone of myself.

I think that's pretty cool. A little surreal, honestly. The same "describe what you want, iterate, ship" loop that built the debugger also produced the launch video for it.

So let me know what you think - of the extension and of the videos. What worked, what felt off, what you'd want next. There's no comment box here, so just hit me up (LinkedIn or GitHub) or open an issue. I'm genuinely curious.