Chapter 125A: VSCode + gdbserver remote-debug workflow¶
Buildroot: a configuration-driven build system that produces a complete root filesystem and related images.
What: the IDE-driven cross-debug workflow for engineers who prefer Visual Studio Code over the gdb command line.
gdbserveron the target.gdb-multiarchon the host. VSCode’s C/C++ extension andlaunch.jsontying them together..vscode/c_cpp_properties.jsonresolving headers from the cross-sysroot so “Go to Definition” works on both your app and kernel sources. Plus a short note on Source Insight, an old commercial editor that’s still the fastest tool for read-only kernel-source navigation. GDB: the debugger. In cross-debugging it runs on the host while controlling code on the target.Why: many readers come from microcontroller backgrounds where the IDE is the debugger (Keil, IAR, STM32CubeIDE). Forcing them to learn gdb’s tui mode just to set a breakpoint is unnecessary. VSCode plus the right config gives the same click-to-set-breakpoint experience cross-debugging a remote ARM target, while leaving the underlying gdb fully scriptable for when you do want the command line. Setting up
launch.jsononce pays back every debug session after.Focus: VSCode’s debug UI is a wrapper around gdb.
launch.jsonconfigures it. You set: which gdb binary, which binary to debug, where gdbserver listens, and where the source tree lives. The non-obvious part isc_cpp_properties.json. It must point IntelliSense at the target’s sysroot headers, not the host’s. Otherwise “Go to Definition” finds your laptop’sstdio.h, not the cross-compiled one. With both files right, IDE-style debug and accurate Go-to-Definition make embedded debug feel close to desktop.
125A.1 Target side, install gdbserver¶
# Yocto / Buildroot: enable gdbserver in image
# OR Debian/Ubuntu on the target:
apt install gdbserver
# Static-linked is preferable so it works even with weird libc situations
gdbserver is small, about 100 KB statically linked, and needs no debug info on the target.
125A.2 Host side, VSCode + extensions¶
# Install VSCode (Microsoft .deb or your distro's package)
# Install extensions:
# C/C++ (ms-vscode.cpptools)
# C/C++ Themes (optional)
# Native Debug (webfreak.debug) — alternative; sometimes friendlier for cross
And the cross-debugger:
apt install gdb-multiarch
125A.3 The two config files¶
VSCode reads two project-local files from .vscode/:
launch.json: debug-time configuration (which gdb, which binary, where to connect).c_cpp_properties.json: IntelliSense / Go-to-Definition (which headers, which compiler).
.vscode/launch.json¶
{
"version": "0.2.0",
"configurations": [
{
"name": "Cross-debug myapp on imx6ull-pa",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/myapp",
"miDebuggerPath": "/usr/bin/gdb-multiarch",
"miDebuggerServerAddress": "192.168.1.100:2345",
"cwd": "${workspaceFolder}",
"args": [],
"stopAtEntry": true,
"setupCommands": [
{
"description": "Use sysroot",
"text": "set sysroot /home/dev/yocto/build/tmp/work/imx6ull-myboard-poky-linux-gnueabi/myapp/1.0-r0/recipe-sysroot",
"ignoreFailures": false
},
{
"description": "Enable pretty-printing",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"preLaunchTask": "Start gdbserver"
}
]
}
Key fields:
program: the unstripped ELF on the host. Symbols come from here.
ELF: Executable and Linkable Format, the standard Linux object and executable file format.
miDebuggerPath: cross-gdb. Must understand ARM.miDebuggerServerAddress: where gdbserver listens on the target.setupCommands: extra gdb commands at start (set sysroot,set solib-search-path, …).preLaunchTask(optional): a VSCode task that starts gdbserver on the target. See below.
.vscode/tasks.json, pre-launch gdbserver¶
{
"version": "2.0.0",
"tasks": [
{
"label": "Start gdbserver",
"type": "shell",
"command": "ssh root@192.168.1.100 'gdbserver --multi :2345 &'",
"isBackground": true,
"problemMatcher": []
}
]
}
VSCode runs this task before launching debug. Gdbserver is up. The debug session connects immediately.
.vscode/c_cpp_properties.json¶
{
"configurations": [
{
"name": "i.MX6ULL Cross-compile",
"includePath": [
"${workspaceFolder}/**",
"/home/dev/yocto/build/tmp/work/imx6ull-myboard-poky-linux-gnueabi/myapp/1.0-r0/recipe-sysroot/usr/include",
"/home/dev/yocto/build/tmp/work/imx6ull-myboard-poky-linux-gnueabi/myapp/1.0-r0/recipe-sysroot/usr/include/c++/12"
],
"defines": [],
"compilerPath": "/home/dev/x-tools/arm-none-linux-gnueabihf/bin/arm-none-linux-gnueabihf-gcc",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "linux-gcc-arm"
}
],
"version": 4
}
With this, hover-over-symbol and “Go to Definition” find the target’s headers, not your host’s. Without this, IntelliSense reports errors that aren’t real: it thinks you’re compiling for x86, so ARM-only macros are undefined and code inside #ifdef __arm__ looks dead.
125A.4 Debugging workflow¶
Build on host:
cd build && cmake --build .(or your build system).Copy binary to target:
scp build/myapp root@target:/usr/bin/(or NFS rootfs auto-syncs).
MCU bridge: Think of the rootfs as the firmware image’s file-backed runtime environment. On an MCU you link everything into flash. On Linux, programs and config live in this mounted tree. NFS: Network File System, which lets the target mount a host directory over Ethernet during development. rootfs: root filesystem, the directory tree mounted at / that contains /bin, /etc, /dev, and libraries.
F5 in VSCode: pre-launch task starts gdbserver. Gdb connects. Binary loads. Stops at entry (or your
main()).Set breakpoints by clicking in the gutter of source files.
Continue/step/inspect via the debug toolbar.
Watch / Locals / Call Stack panels show variables and stack.
Debug Console for arbitrary gdb commands (
-exec print foo).
When done: VSCode disconnects. Gdbserver stops (or stays running if --multi).
125A.5 Multi-target, connect to multiple boards¶
For a fleet:
"configurations": [
{ "name": "Debug on board A", "miDebuggerServerAddress": "192.168.1.100:2345", ... },
{ "name": "Debug on board B", "miDebuggerServerAddress": "192.168.1.101:2345", ... },
{ "name": "Debug on board C", "miDebuggerServerAddress": "192.168.1.102:2345", ... }
]
Pick the right config from VSCode’s debug dropdown.
125A.6 Debugging kernel modules (advanced)¶
Set a breakpoint in your Chapter 36/41 LED driver’s probe():
{
"name": "Debug LED kernel module",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/path/to/linux/vmlinux",
"miDebuggerPath": "/usr/bin/gdb-multiarch",
"miDebuggerServerAddress": "192.168.1.100:1234",
"setupCommands": [
{ "text": "set arch arm" },
{ "text": "set sysroot /home/dev/sysroot" },
{ "text": "add-auto-load-safe-path /home/dev/yocto/build/tmp/work/.../linux/scripts/gdb/" },
{ "text": "source /home/dev/yocto/build/tmp/work/.../linux/scripts/gdb/vmlinux-gdb.py" }
]
}
The kernel-side gdb stub is KGDB over UART (Ch 119) on port 1234. After connect:
> -exec lx-symbols # load module symbol files
> break led_probe # the breakpoint
> continue
# Now insmod the module on the target — breakpoint hits in VSCode.
125A.8 Lab¶
Install gdbserver target / gdb-multiarch host. Verify versions match major ARM target.
Hello-world C app cross-compile. With
-gflag. Copy to target.Manual gdbserver test.
gdbserver :2345 ./helloon target.gdb-multiarch ./hello+target remote target-ip:2345on host. Step, print. Confirm it works before introducing VSCode.VSCode launch.json. Set up the config. F5. Confirm debug session connects.
Set breakpoint. Click in gutter at a source line. Continue. Verify breakpoint hits and you can inspect locals.
Set sysroot properly. Try without (note libc symbol errors). Then set. Observe they resolve.
c_cpp_properties.json. Configure to point at target sysroot. Verify hover-over-symbol finds the target’s libc, not your host’s.
Multi-target. Add 2 board configs. Switch between them via dropdown.
Debug a real driver in your app that wraps Ch 105 (RFID) or Ch 117 (RTC). Set breakpoint on init. Trace through.
Source Insight evaluation (stretch). Try the 30-day trial. Index your kernel. Compare navigation speed to VSCode.
125A.9 Pitfalls¶
Wrong gdb binary.
/usr/bin/gdb(host) won’t debug ARM. Needsgdb-multiarchor the cross-toolchain gdb.Sysroot path stale. Yocto recreates
recipe-sysrootpaths each build. The hardcoded path inlaunch.jsondrifts. Use${workspaceFolder}+ relative paths or update on rebuild.
Yocto: a metadata-driven build system for producing custom Linux distributions.
gdbserver not actually started. preLaunchTask SSH command fails silently. Debug session times out. Verify with manual SSH first.
IntelliSense crashes parsing kernel source. 30M lines exceeds VSCode’s default RAM. Increase:
"C_Cpp.intelliSenseEngine": "default", plus 8+ GB RAM.Source paths don’t match. Binary built in
/build/...but you opened the project in/src/.... GDB can’t find source files. Addsubstitute-pathto setupCommands.Stripped binary on target. gdbserver opens it but no symbols. Always use unstripped on host side. Target-side
stripis fine.Breakpoints don’t hit. Often: target’s libc not matching host’s compile-time libc. Symbols mismatch. Libc functions step into stripped code. Set sysroot.
Slow over remote SSH. gdbserver+ssh tunnel adds ~50 ms per breakpoint. Use direct TCP (open the port in firewall) if you control the network.
“Cannot find ld-linux-armhf.so.3”. sysroot wrong. Verify it points to
${YOCTO_BUILD}/.../recipe-sysroot/, not the host’s/.VSCode’s debug console is one-liner. Multi-line gdb commands (define, document) need to go in a separate script file loaded via
sourcefrom setupCommands.Pre-launch task hangs. If
gdbserver --multi :2345 &doesn’t background properly, VSCode waits forever. UseisBackground: trueand aproblemMatcherthat VSCode treats as “this is OK to keep running.”
125A.10 Going deeper¶
VSCode C/C++ extension documentation: https://code.visualstudio.com/docs/cpp/cpp-debug.
launch.jsonreference: all options.GDB manual chapters on MI (Machine Interface): what VSCode uses to talk to gdb.
gdb-dashboard: terminal alternative if you decide to leave VSCode.gefandpwndbg: gdb plugins for security research, with VSCode-like features in terminal.CLion + remote toolchain, JetBrains commercial alternative. Even smoother UX, paid.
Ch 118: JTAG with gdb (same gdb, different remote target).
MCU bridge: Think of JTAG like SWD debugging on Cortex-M: halt, read registers, set breakpoints. The Cortex-A path adds MMU state, privilege modes, and more complex reset behavior. JTAG: the hardware debug scan chain used to halt, inspect, and single-step CPUs.
Ch 120: gdbserver + cli gdb (same workflow, manual setup).
Ch 119: KGDB for kernel-side debug.
Next chapter: Chapter 126: Closing: what to read next. This closes the main i.MX6ULL bring-up path; Part IX continues with optional virtualization labs.