Why I Started Using This Tool
After the last episode, one command got my program onto the DE10-Nano and paused under gdbserver. But
then I still had to start GDB myself and type the same five commands to connect, load symbols, and set a
breakpoint. I was also debugging from a black terminal window, when VS Code had a perfectly good debugger
UI sitting right there. I wanted breakpoints I could click, variables I could hover over, and one key to
get there. That meant launch.json.
What It Does
.vscode/launch.json defines one debug configuration, "Debug on DE10 (F5)".
Here's the whole file:
{
// F5 = build -> deploy -> gdbserver (tasks.json), then attach cross-GDB and stop at main().
// All board/host values come from .vscode/settings.json (${config:xxx}).
// The Linaro GDB 7.10 quirks below are deliberate. Don't simplify these away.
"version": "0.2.0",
"configurations": [
{
"name": "Debug on DE10 (F5)",
"type": "cppdbg",
"request": "launch",
"program": "${config:localDir}/${config:bin}",
"cwd": "${config:localDir}",
"preLaunchTask": "gdbserver",
"MIMode": "gdb",
"miDebuggerPath": "${config:toolchain}/bin/arm-linux-gnueabihf-gdb.exe",
// Required: without it, stepping fails with
// "Cannot find bounds of current function".
"targetArchitecture": "arm",
// Send target remote ourselves instead of using miDebuggerServerAddress.
// Forward slashes only: GDB 7.10 mangles Windows backslash paths.
"customLaunchSetupCommands": [
{ "text": "-file-exec-and-symbols ${config:localDir}/${config:bin}", "description": "Load binary and symbols" },
{ "text": "target remote ${config:host}:${config:gdbPort}", "description": "Connect to gdbserver on the board" },
{ "text": "-break-insert main", "description": "Break at main()" }
],
// gdbserver has the program paused before main(); run until the breakpoint.
"launchCompleteCommand": "exec-continue",
"setupCommands": [
{ "text": "-enable-pretty-printing", "ignoreFailures": true }
],
"stopAtEntry": false,
"externalConsole": false
}
]
}
What happens when you press F5
-
preLaunchTask: "gdbserver"runs thegdbservertask from last episode. Because of itsdependsOnchain, that means build, then deploy, then start gdbserver on the board. If the build fails, the chain stops and the debugger never starts, so you can't end up debugging an old binary. -
VS Code waits until the task's background matcher sees
Listening on port. That's the signal that the board is ready and it's safe to connect. -
The C/C++ extension (
"type": "cppdbg") starts the Linaro ARM GDB on my PC, frommiDebuggerPath. This has to be the cross GDB from the same toolchain that built the binary, not a regular Windows GDB, because it needs to understand ARM code. -
Instead of the extension's default launch steps, it sends my own
customLaunchSetupCommands, in order:-file-exec-and-symbolsloads the binary and its debug symbols, so GDB knows which line of C each instruction came from.target remoteconnects to gdbserver using the board's IP address and port fromsettings.json.-break-insert mainsets a breakpoint onmain.
-
launchCompleteCommand: "exec-continue". gdbserver started the program paused, before it reachedmain. This lets it run until it hits the breakpoint and stops atmain().
From there it's normal VS Code debugging: click in the gutter to add breakpoints, F10 and F11 to step, hover over variables, and watch the call stack, all while the code runs on the actual board.
The old-toolchain quirks
The harder part was the old toolchain. Linaro GDB 7.10 is about ten years old, and the C/C++ extension expects something newer. Three things tripped me up:
-
"targetArchitecture": "arm"is required. Without it, stepping fails withCannot find bounds of current function. Setting it explicitly tells the extension it's debugging ARM code instead of leaving it to guess. -
Send
target remoteyourself. The extension has a built-inmiDebuggerServerAddresssetting that's supposed to make the connection for you. With this GDB, it handled the connection better when I senttarget remoteincustomLaunchSetupCommandsmyself, which also means I control exactly what runs and in what order. -
Forward slashes everywhere. This GDB mangles Windows backslash paths like
C:\projects\my_embedded_app\main.C:/projects/my_embedded_app/mainworks, so every path insettings.jsonandlaunch.jsonuses/.
Where did my printf go?
Your program's output doesn't show up in the Debug Console. The program runs on the board, under
gdbserver, so its printf output goes to the gdbserver task's terminal.
Keep that terminal visible while you debug.
Final Verdict
This is the payoff for the whole setup series. settings.json holds the facts,
tasks.json does the build and deploy, and launch.json puts a real debugger on
the board behind F5. Getting it working with a ten-year-old GDB took more trial and error than I'd like
to admit, which is exactly why the comments in my launch.json say "don't simplify these
away". If you're on a newer toolchain, some of the quirks may not apply to you, but the structure is the
same. And keep an eye on the gdbserver terminal, because that's where your printf output
goes.
If you're doing embedded Linux from Windows, I'd call this the minimum setup before writing real
application code. Stepping through code on the actual hardware beats guessing from printf
output every time.