Skip to content
WSLDeep Dive Published Updated 7 min readViews unavailable

GDB in WSL: Debug Symbols, Breakpoints, and Linux Process Boundaries

Debug Linux executables in WSL with GDB, matching symbols, reproducible command lines, and evidence-based handling of process and core-file failures.

GDB in WSL debugs Linux processes running inside the distribution. It is a strong fit for reproducing native Linux crashes and stepping through code that will run in Linux CI or deployment. It does not debug a Windows process merely because that process can launch a WSL command, and Windows-native executables need a Windows debugger. Keep the executable, debug information, libraries, and source paths aligned with the Linux process being inspected.

The most common failure in source-level debugging is a mismatch between the binary and its symbols. GDB can start without useful source locations if the program was stripped, built from another revision, or linked to libraries whose debug files are unavailable. Preserve build identity and exact command-line inputs with each reproduction.

Build a small debuggable Linux executable

Use the Linux compiler inside WSL and retain debug information in a development build. Optimization can inline or reorder code, so a simple -O0 -g build is usually easier to step through than a release binary. Frame pointers can help some stack inspection workflows but are not a substitute for debug symbols. Follow the project’s own flags when investigating a production-only failure.

#include <stdio.h>

static int sum_to(int limit) {
    int total = 0;
    for (int value = 1; value <= limit; ++value) {
        total += value;
    }
    return total;
}

int main(void) {
    printf("sum=%d\\n", sum_to(5));
    return 0;
}

Compile it in the WSL filesystem:

mkdir -p build
gcc -Wall -Wextra -O0 -g3 -fno-omit-frame-pointer \
  -o build/sum sum.c
file build/sum

file provides a basic check that the output is a Linux executable for the expected architecture. If the project builds with Clang or a build system, record those versions and generated flags as well. Do not copy a Windows .pdb or executable into the WSL tree and expect GDB to treat it as a matching Linux artifact.

Start GDB with the exact executable and arguments used for the failure. Use a breakpoint at a named function, run the program, inspect arguments and locals, then step over or into the relevant operation:

gdb -q --args ./build/sum
(gdb) break sum_to
(gdb) run
(gdb) print limit
(gdb) next
(gdb) print total
(gdb) backtrace
(gdb) quit

For a command-line program, pass its real arguments after the executable with --args. Set environment variables explicitly when they affect configuration. A debugger session that omits the production input path or environment can fail to reproduce the issue while still showing a plausible stack. Keep sensitive arguments and environment values out of shared logs.

When the source file is not found, inspect the compiler’s recorded source path and GDB’s source search path. Debug information refers to build-time file names; moving the checkout can require a source substitution or directory mapping. Confirm the binary’s build ID and source revision before blaming the debugger. If a line breakpoint resolves to an unexpected location, check optimization and whether the source changed after compilation.

Match symbols to the executable

Keep unstripped development artifacts, or preserve separate debug files produced by the project’s supported toolchain. GDB can use separate debug information when it is correctly associated with the executable. Never combine arbitrary symbols from a newer or older build: function names may match while addresses and layouts do not. Record checksums or build IDs for important binaries and debug files.

Shared libraries also need matching symbol information to provide useful frames. Start with the application’s own stack and inspect library paths and package versions. Distribution debug packages vary by release and repository configuration; use the distribution’s documented mechanism rather than downloading random symbol bundles. If a frame is shown as unknown, verify whether the library is stripped and whether its matching debug data is installed.

For a difficult bug, save a minimal input, exact command, environment, executable identity, and GDB transcript. Use conditional breakpoints or watchpoints only after establishing a baseline; they can significantly affect runtime. Watchpoints and hardware support depend on architecture and target capabilities. Avoid claiming that a successful local watchpoint proves support on every WSL machine or deployment CPU.

Process attachment and core files

Attaching to a running process is subject to Linux ownership and ptrace policy. Start with a process owned by the same user and verify the PID inside WSL, not a Windows process identifier. If attachment is denied, inspect user identity, capabilities, container boundaries, and kernel policy before changing global settings. Do not disable host protections broadly just to debug one application.

Core-file generation is also controlled by process resource limits and the kernel’s core pattern. ulimit -c unlimited changes a shell limit for child processes, but it does not guarantee that a core file will appear in the current directory. A system may route dumps to a handler or apply other limits. Check the configured policy and destination, preserve storage capacity, and use a controlled crash in a disposable program to validate the workflow.

When analyzing a core, use the exact executable and libraries from the failing run. The core file captures process state, not source files or matching symbols. Keep all three artifacts together under an access and retention policy; dumps can contain credentials or application data in memory. If WinDbg is used for WSL kernel or dump analysis, that is a separate host-side workflow from GDB’s Linux process debugging.

Threads, signals, and optimized builds

For a multithreaded process, inspect all threads when a failure appears outside the current thread. GDB’s thread listing and all-thread backtrace commands can show whether other threads are blocked, running, or waiting in a shared library. Stop the process at a stable failure point and capture the same command and environment before changing scheduler settings. A debugger pause can alter lock timing, so a race that disappears under a breakpoint still needs a repeatable non-debugger test.

Signals can stop or terminate a process independently of a source-level exception. Check which signal arrived, where the program was stopped, and whether the application handles it intentionally. Do not configure GDB to ignore every signal just to continue stepping; doing so can hide the event that explains a shutdown or corrupted state. For an expected signal, document why it occurs and use a controlled test to verify the handler’s behavior.

Optimized binaries can show variables as unavailable, functions as inlined, or source execution in an order that differs from the source listing. Rebuild with debug-friendly flags to locate the code path, then reproduce with the failing optimization settings. Preserve both binaries and their symbol information. If a crash only exists in release mode, check undefined behavior, strict-aliasing assumptions, data races, and compiler-specific flags rather than concluding the debugger is inaccurate.

When a bug depends on a shared library, record its resolved path and version from the WSL environment. GDB can inspect loaded shared libraries, but a source package alone does not guarantee matching debug information. Make a minimal reproducer that loads the same library and input when possible. Keep the process environment narrowly controlled so proxy, locale, or library-path variables do not change between runs.

Troubleshoot by evidence

If GDB cannot start the program, inspect architecture, execute permission, dynamic linker, working directory, and arguments. If source lines are missing, check debug flags and matching symbols. If the backtrace is truncated, inspect optimization, stack corruption, and library debug packages. If a bug disappears under GDB, compare timing, environment, and input before assuming a race; breakpoints change scheduling.

For deterministic failures, reduce the input until the same stack is reproduced. For intermittent failures, capture multiple runs and relevant logs rather than relying on one debugger session. A clean rebuild from the same source should reproduce the build ID and symbols used in the investigation. Keep the original binary before rebuilding so evidence is not overwritten.

Acceptance criteria

Accept the WSL debugging workflow when GDB and the target are Linux-native, debug symbols match the executable, a repeatable command and input reach the intended breakpoint, and any attachment or core-file path is validated under the distribution’s actual policy. Preserve the executable, symbols, source revision, and sanitized transcript as one evidence set.

GDB in WSL is a practical native Linux debugger. It does not replace Windows debugging tools, prove production symbols are available, or guarantee that core files are created and retained.

Related:

Sources:

Comments