WINDIAG is a lightweight Windows system and application diagnostic utility designed to help developers and systems administrators diagnose process crashes, monitor debug events, analyze API hooking integrity, and statically scan executables for legacy OS compatibility.
Built in pure C89 targeting Windows 2000 and newer, it operates with zero runtime dependencies (no MFC, no ATL, no modern C++ runtime dependencies).
The GUI front end (windiag_gui.exe) features a custom, high-fidelity dark-mode user interface designed to mimic a vintage green phosphor CRT monitor.
- Retro Phosphor GUI Front End (
windiag_gui.exe): Features custom rendering, interactive process lists, a scrollable event log terminal, and visual scanlines. - Command Line Front End (
windiag.exe): A robust CLI for headless environments or scripting. - Remote Network Debugging Architecture (
wd_network.c):- Supports dual operational modes: Agent Mode (runs on remote target machines/VMs) and Controller Mode (runs on host).
- Streams live process lists, real-time debug events, CPU register states, stack values, and disassembled instruction lines over TCP (
WDNEwire protocol). - Allows remote process attaching, launching, setting/clearing software breakpoints, single-stepping, and crash analysis over network connections.
- Static Compatibility Scanner (
wd_compat.c):- Inspects any PE binary (
.exe/.dll) without executing it to determine compatibility. - Reads PE headers directly from disk (memory-mapped, read-only).
- Checks linked subsystem versions against Windows 2000 (NT 5.0).
- Walks the import table to flag functions that do not exist on Windows 2000/XP (e.g.,
GetTickCount64,InitializeSRWLock,SHGetKnownFolderPath). - Identifies modern manifest blocks (Vista+ requestedExecutionLevel/supportedOS) and Universal CRT dependencies.
- Inspects any PE binary (
- API Hook & Integrity Scanner (
wd_apichk.c):- Inspects function entry points of loaded modules in target processes.
- Detects API hijacking/hooking (e.g., standard
JMPorPUSH+RETtrampolines). - Detects code patching (e.g., debug-inserted
INT3breakpoints or suspicious NOP sleds).
- Thread-Safe Event Logger (
wd_events.c): Uses a high-performance, lock-free ring buffer to forward real-time debug events from background monitor threads straight to the GUI thread. - Win32 Debug Loop (
wd_debug.c): Attaches to running PIDs, attaches by process name, or launches new executables to capture standard debug events and exception crashes. - Crash History Persistence (
wd_history.c):- Remembers crash addresses, exception codes, and call stacks across sessions.
- On subsequent debugs of the same executable, it logs previous crashes and suggests breakpoint locations.
- Active Software Breakpoint Manager (
wd_debug.c):- Allows setting up to 32 active software breakpoints (
INT3/0xCCinstruction patching) at target memory addresses. - Handles instruction byte restoration, single-stepping over breakpoints, and cleanup on exit.
- Allows setting up to 32 active software breakpoints (
include/
wd_common.h - Shared data types, constants, palette details, and limits
wd_backend.h - Exception table mapping, module tracking, and PE probing
wd_events.h - Thread-safe event ring buffer interface
wd_debug.h - Debug loop definitions (Attach, Launch, Monitor)
wd_network.h - Remote socket debugging protocol & agent/controller types
wd_compat.h - Static compatibility scanner definitions
wd_apichk.h - API integrity checker definitions
wd_history.h - Process event history definitions
src/
wd_backend.c - Exception decoding and module/symbol resolution
wd_events.c - Event queue ring-buffer implementation
wd_debug.c - Active Win32 debugging and event monitor loop
wd_network.c - Remote Agent / Controller TCP socket debugging engine
wd_compat.c - Static PE header parsing & import verification
wd_apichk.c - Target process API memory scanning for hooks
wd_disasm.c - X86/x64 instruction disassembler helper
wd_history.c - Historical database of process event details
wd_cli_main.c - Console application entry point (windiag.exe)
wd_gui_main.c - Vintage-styled GUI entry point (windiag_gui.exe)
Use the included build.bat script, which configures compiler flags to target legacy Windows subsystems (5.0 for Windows 2000).
build.batNote
The build script points to the Windows Driver Kit (WinDDK) 7.600 build environment by default. If building on modern machines, adjust the pathing variables in build.bat to your local MSVC installation or WinSDK location.
A Makefile is provided for GCC/MinGW-w64 cross-compilers:
makewindiag.exe [options]| Command Option | Description |
|---|---|
-p <pid> |
Attach debugger to a running process by PID. |
-n <name> |
Find a running process by executable name and attach. |
-e <path> |
Launch a new program and monitor its execution. |
-check <path> |
Perform a static compatibility scan (does not execute the file). |
-evtlog |
Dump the recent Application/System event log error messages. |
-l |
List all currently running active processes. |
-v |
Enable verbose debug logging output. |
-deepsnap |
Enable rolling pre-crash execution snapshots (ring buffer tracking thread/DLL events). |
-autoanalyze |
Auto-run !analyze -v on crash, output report, and terminate target. |
-srcpath <path> |
Specify search directory for source code resolution. |
Run windiag_gui.exe to launch the vintage monitor layout:
- Left Panel: Lists active processes running in the system. Use search to filter.
- Right Panel: Interactive event terminal displaying real-time attaches, DLL loadings, exceptions, and API integrity scan logs.
- Top Actions Bar:
- Attach: Attach to the selected process.
- Launch EXE: Select a file to launch under the debugger.
- Check File (F3): Perform a static compatibility check on a file.
- API Hook Scan: Scan the active process for any API hooks/patches.
When actively debugging or monitoring a process (after attaching or launching), you can control the debugger using the following commands in the CLI input or GUI event terminal:
| Command | Action | Example |
|---|---|---|
b <address> |
Add software breakpoint at the hexadecimal address | b 00401234 |
d <address> |
Delete/remove breakpoint at the hexadecimal address | d 00401234 |
u <address> [count] |
Unassemble (disassemble) instructions at address | u 00401234 5 |
g |
Resume execution ("Go") | g |
s |
Step a single instruction | s |
q |
Quit monitoring and detach | q |
!analyze |
Basic crash analysis (exception + registers + stack) | !analyze |
!analyze -v |
Full analysis (+ all-threads + module list) | !analyze -v |
!analyze --deep |
Full analysis + pre-crash event log snapshot | !analyze --deep |
Below is an actual output snippet from running windiag.exe -deepsnap -srcpath "." -go -autoanalyze -v -e "wd_test_multithread.exe badcall":
================ββββββββββββββ CRASH ANALYSIS ==============================
Mode: !analyze --deep (verbose + execution snapshot)
----------------------------------------------------------------------------
--- Exception ---
Code : 0xC0000005 ACCESS_VIOLATION
Address : 0x7741DFF9 ntdll.dll!strcat+0x89
Flags : 0x00000000 (continuable)
--- Faulting Instruction ---
0x7741DFF9 89 17 mov dword ptr [edi], edx
EIP page : state=COMMIT type=IMAGE protect=EXECUTE_READ base=0x7741D000 size=0xAF000
--- Source Code ---
Source: d:\download\files\src\wd_test_multithread.c : line 187
184: if (strcmp(g_bugType, "badcall") == 0) {
185: printf("[Thread E] Triggering bad call inside shell32.dll...\n");
186: trigger_bad_dll_call();
> 187: } else {
188: Sleep(INFINITE);
189: }
190: return 0;
--- Registers ---
EAX=7EFEFCFE EBX=00000000 ECX=0041F31C EDX=73617263
ESI=00401510 EDI=DEAD0000 ESP=00D6FF54 EBP=00D6FF6C
EIP=7741DFF9 EFL=00010246
CS=0023 DS=002B ES=002B FS=0053 GS=002B SS=002B
--- Access Violation Details ---
Operation : WRITE at 0xDEAD0000
Diagnosis : kernel-space address from user mode (wild pointer / stack smash)
--- Call Stack (faulting thread) ---
#00 0x7741DFF9 ntdll.dll!strcat+0x89 [EXT]
#01 0x00401575 D:\Download\files\wd_test_multithread.exe!thread_badcall_proc+0x65
#02 0x766E7BA9 C:\WINDOWS\SysWOW64\KERNEL32.DLL!BaseThreadInitThunk+0x19 [EXT]
#03 0x7740C0CB ntdll.dll!RtlInitializeExceptionChain+0x6b [EXT]
--- Root Cause Heuristic ---
Kernel-range address accessed in user mode β severe stack corruption or wild pointer.
- Module Name Resolution: Even though the process crashed immediately after starting up inside
ntdll.dll(where the target process's module tables were not yet initialized), the debugger parsed the PE export directory directly from memory to resolve the base address0x773A0000tontdll.dllinstead of(unknown). - Fallback Stack Walking: Since
EIP(0x7741DFF9) points intontdll.dll(no local symbols/source), the analyzer automatically walked back to the first user-space call frame (#01atthread_badcall_proc+0x65) and displayed the exact source code surrounding the crash trigger.
- Problem: The debugger previously swallowed all first-chance exceptions by continuing them with
DBG_CONTINUE. This broke applications using Structured Exception Handling (SEH) internally (such as Windows system DLLs) and caused infinite resume loops or target process hangs. - Fix: Modified the debug loop in wd_debug.c to only pause on hardware exceptions if they are second-chance (actual crashes) or if
-firstchancelogging is explicitly enabled. Unhandled/non-paused exceptions are now continued withDBG_EXCEPTION_NOT_HANDLED, letting target SEH handlers run natively.
- Problem: During early process startup (e.g., when
ntdll.dllloads), the target's Process Environment Block loader list (PEB.Ldr) is not yet initialized. Debug events likeLOAD_DLL_DEBUG_EVENThavelpImageName = NULL, and PSAPI APIs likeGetModuleFileNameExreturn failure, listing early modules as(unknown). - Fix: Implemented a parser that directly reads the DOS and NT headers of loaded DLLs from target process memory, follows the Export Directory RVA (
IMAGE_DIRECTORY_ENTRY_EXPORT), and reads the module's own name from its export table. This guarantees thatntdll.dlland other system modules are fully resolved on all Windows platforms (NT 5.0 through Windows 11).
- Problem: When a crash occurs inside a system DLL function (e.g.
ntdll.dll!strcpydue to a bad argument), the program counter (EIP) is in system space where no local source symbols exist, preventing the source context code viewer from displaying the cause. - Fix: Upgraded the
!analyzeengine in wd_analyze.c. If the faulting frame (#00) cannot be resolved to source files, the analyzer walks the EBP call stack backwards to find the first caller frame inside the user executable. It then displays the exact source code lines and points to the call site that triggered the system crash (e.g.,strcpy((char*)0xDEAD0000, "crash")).
The multithreaded test suite (wd_test_multithread.exe) has been enhanced with dedicated crash modes:
div: Recursively dives and divides by zero to verify call stack tracking and line pointer precision.badcall: Callsntdll!strcpywith a bad pointer destination to verify that system module crashes are resolved, mapped back to the user call site source line, and captured via deep execution snapshots without deadlocking.
- Problem: Attaching to complex target processes (such as Supermium or multi-threaded Win32 applications) previously deadlocked during process attach. Querying
GetModuleFileNameExAor inspecting target PEB memory before enteringWaitForDebugEventcaused thentdllloader lock to hang. - Fix: Replaced early PEB process name lookups in wd_debug.c with
Toolhelp32process snapshotting (CreateToolhelp32Snapshot). Deferred API integrity scans toCREATE_PROCESS_DEBUG_EVENTwhen the target process debug loop is safely initialized.
- Problem: High-density processes emitting dozens of DLL load and thread events on startup would overwrite early attach logs in the GUI event terminal.
- Fix: Increased
EVT_RINGcapacity to 8,192 events and expanded GUI log scrollback memory (MAX_VISIBLE) to 4,096 lines in wd_common.h. Guaranteed full log history preservation across long debugging sessions.
- Enhancement: Validated WinDiag's deep exception analyzer on modern extended PE binaries (
chrome.dllv144). - Results: Successfully traced hardware
UD2(0F 0BILLEGAL_INSTRUCTION) assertion traps down to exact source files (base::File::DoInitializeinbase/files/file_win.ccinvoked byGetBucketsForStorageKey), and secondary handle write access violations insql::Statement(sql/statement.cc).
- Architecture: Added a lightweight, custom TCP socket protocol (
WDNE) supporting remote debugging across VMs and network-connected machines. - Features:
- Agent Mode: Runs headless on target VMs/legacy OS instances to monitor process execution, capture crashes, and execute remote debugger commands.
- Controller Mode: Connects from developer host machine to control remote debugging sessions in real time.
- Data Streaming: Streams active process lists, event logs, CPU register frames, stack contents, and disassembled instructions over low-latency socket connections.
Builds are clean under standard compiler configurations (MSVC /Od /Oy- /Zi and MinGW make).
WINDIAG helps you translate raw assembly and register states into concrete source-code bug fixes. Here is how to handle the most common crash patterns:
- What you see in the log:
EXCEPTION 0xC0000005 ACCESS_VIOLATION Diagnosis : NULL pointer dereference (or Near-NULL dereference) Faulting Instruction: 0x00401234 8B 01 mov eax, dword ptr [ecx] Registers: ECX=00000000 (or near-zero like 00000004) - The Assembly Meaning: The CPU was trying to read (
mov) 4 bytes from the address stored inECXand place it intoEAX. BecauseECXis0, this is a read access violation. - How to Fix the Code: Locate the source line (either resolved by
-srcpathor from the stack walk). Find the variable corresponding toecx. It was not initialized before use, or a function returned aNULLpointer that you forgot to validate. Add a guard check:if (my_pointer == NULL) { /* handle error */ return; }
- What you see in the log:
EXCEPTION 0xC0000005 ACCESS_VIOLATION Diagnosis : kernel-range address from user mode (wild pointer) Faulting Instruction: 0x7741DFF9 89 17 mov dword ptr [edi], edx Registers: EDI=DEAD0000 - The Assembly Meaning: The CPU was attempting to write (
mov) the contents ofEDXinto the memory address stored inEDI. SinceEDIis0xDEAD0000(which is outside valid user-mode boundaries), the OS terminated it. - How to Fix the Code: This is a classic "bad call" where a bad parameter was passed to a string copy or buffer manipulation function (like
strcpy). Find the user code caller frame in the stack trace. Check the pointer argument you are passing to that system function and ensure it points to a valid, allocated memory block.
- What you see in the log:
Diagnosis : Kernel-range address accessed in user mode WARNING: EIP is within the stack region β strong indicator of stack buffer overflow! Call Stack: ... EBP misaligned β stack may be corrupt, stopping walk - The Assembly Meaning: A local array/buffer allocated on the stack was written to beyond its bounds, overwriting the saved frame pointer (
EBP) and function return address. When the function tried to return, execution jumped to a corrupt stack location. - How to Fix the Code: Find the function shown just before the stack walk failed. Inspect its local stack buffers (e.g.
char buffer[128]). Ensure you are using bounded functions. Replace dangerous functions with safe equivalents:/* AVOID: */ strcpy(buf, input); /* USE: */ strncpy(buf, input, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0';
- What you see in the log:
EXCEPTION 0xC0000094 INTEGER_DIVIDE_BY_ZERO Faulting Instruction: 0x00401250 F7 F9 idiv ecx Registers: ECX=00000000 - The Assembly Meaning: The
idivinstruction divides the accumulator registers by the divisor register (ECX). Division by zero is a hardware exception. - How to Fix the Code: Find the division (
/) or modulo (%) operation at the highlighted source line. Add a conditional check to ensure the divisor is non-zero:if (divisor == 0) { /* handle division by zero */ return 0; }
WINDIAG offers a unique value proposition, especially for developers working in retro-computing, legacy software maintenance, or security research. Below is a comparison of WINDIAG against official Microsoft and third-party tools:
| Feature / Capability | WINDIAG | WinDbg | Dr. Watson | Dependency Walker | AppVerifier |
|---|---|---|---|---|---|
| Windows 2000+ Compatibility | Yes (Native C89) | No (Modern requires .NET/Win10+) | Yes (Legacy only) | Yes (Legacy only) | No (Requires modern OS) |
| Interactive CLI & GUI | Yes (Retro Phosphor CRT UI) | Yes (Modern ribbon UI) | No (Non-interactive) | No (Static-only UI) | No (Configuration UI only) |
| Zero-Source Diagnostics | Yes (Deep analysis, disassembly, and stack walk) | Yes | Yes (Limited) | No | No (Kernel logs only) |
| API Hook & Integrity Scanner | Yes (Scans target memory for patches/hooks) | No | No | No | No |
| Static Compatibility Checks | Yes (Scans imports and manifest Vista+ symbols) | No | No | Yes | No |
| Rolling execution snapshot | Yes (Lock-free ring buffer execution history) | No | No | No | No |
| Resource Footprint | Ultra-Light (< 200 KB, zero runtime dependencies) | Heavy | Light | Light | Heavy |
- Zero-Source Diagnostics: Even without source code or local symbol packages (
PDB), WINDIAG decodes raw memory accesses, disassembles the faulting instruction, lists registers, and resolves exported DLL symbols (e.g.ntdll.dll!strcat+0x89) so you can identify "bad calls" and their calling origins instantly. - Combination of Static & Dynamic Analysis: Unlike traditional tools that are either debuggers (WinDbg) or static scanners (Dependency Walker), WINDIAG lets you statically check an executable's compatibility beforehand and debug it dynamically on the fly.
- Advanced Anti-Tampering & Integrity Checking: WINDIAG scans memory pages to detect if APIs have been hooked or patched (often by antivirus software, debuggers, or hooks), a feature not natively present in other debuggers.
- Pre-Crash Flight Recorder (
-deepsnap): Tracks thread creation, library loadings, and first-chance exceptions in a high-speed lock-free circular buffer, allowing you to view the events leading right up to a crash.
