Remote cross-debugging with GDB and VS Code
This tutorial builds a C++ program on an x86-64 Ubuntu/Debian host, runs it on
an ARM64 Linux target, and debugs it remotely with gdb-multiarch and
gdbserver. The command-line workflow comes first so that the VS Code
automation is easy to understand rather than a collection of unexplained JSON.
The Radxa ZERO 3 is used as the example target, but the same workflow applies to other ARM64 Linux boards with SSH access.
By the end, you will be able to:
- explain which debugging components run on the host and target
- create an ARM64 Debug build with CMake
- deploy and debug the application from a GDB terminal
- inspect variables, stack frames, threads, memory, and breakpoints
- automate build, upload, and remote debugging with VS Code
- attach
gdbserverto an already-running application
The complete example is available in the
code directory.
How remote debugging works
flowchart LR
subgraph Host["x86-64 development host"]
SRC["C++ source"]
ELF["Local ARM64 ELF<br/>debug symbols"]
GDB["gdb-multiarch<br/>or VS Code"]
SRC --> ELF
ELF --> GDB
end
subgraph Target["ARM64 Linux target"]
SERVER["gdbserver :1234"]
PROCESS["Deployed ARM64 process"]
SERVER -->|controls| PROCESS
end
GDB <-->|"GDB remote protocol<br/>TCP 1234"| SERVER
ELF -. "scp over SSH" .-> PROCESS
The target runs the application and gdbserver. The host runs
gdb-multiarch, reads the source code, and uses the local ARM64 executable
for debug symbols. The local executable must be the exact build deployed to
the target; otherwise source lines, variables, and breakpoints may be wrong.
gdbserver is deliberately small. It controls the target process and sends
register, memory, signal, and stop information to GDB, but it does not need a
copy of the source tree.
Use a trusted development network
The GDB remote protocol does not provide authentication or encryption.
Do not expose port 1234 to the internet or an untrusted network. Restrict
it with a firewall or use an SSH tunnel when debugging outside a trusted
lab network.
Prerequisites
This tutorial assumes:
- an x86-64 Ubuntu/Debian development host
- an ARM64 Linux target using glibc
- host-to-target SSH connectivity
- permission to install packages on both machines
Install host tools
Install the build tools, ARM64 cross-compiler, debugger, and SSH client:
| Development host | |
|---|---|
Install Visual Studio Code and these extensions for the VS Code section:
Verify the tools:
Install target tools
On the ARM64 target:
| ARM64 target | |
|---|---|
Replace USER and HOST in this tutorial with the target login and address,
then verify the connection and architecture from the host:
| Development host | |
|---|---|
Expected output:
This proves that SSH works and the target architecture matches the compiler
prefix aarch64-linux-gnu-.
Check C++ runtime compatibility
The compiler package is intentionally unversioned: the correct compiler is the one whose generated program is compatible with the target image. A sysroot or vendor SDK is preferable when the host distribution is newer than the target.
Check the newest C++ ABI version supplied by the target:
| ARM64 target | |
|---|---|
After building, compare it with the executable requirement:
| Development host | |
|---|---|
The executable requirement must not be newer than the version provided by the
target. If it is newer, select an older compatible cross-toolchain or use the
target vendor's SDK. Do not replace the target's system libstdc++.so.6 by
copying an arbitrary host library.
Example project
The companion project has this structure:
Application
The value answer gives us a simple variable to inspect:
| src/main.cpp | |
|---|---|
CMake project
| CMakeLists.txt | |
|---|---|
The toolchain file declares a Linux ARM64 target and locates the unversioned cross-compiler:
| cmake/toolchain-aarch64.cmake | |
|---|---|
The Debug preset creates a separate build tree with debug information:
Build and inspect the executable
Run from the companion code/ directory:
Verify its architecture and debug information:
file must report an ARM AArch64 executable that is not stripped, and
readelf must list debug sections. This proves the host produced a debuggable
target executable rather than a native x86-64 program.
Upload and run it once before introducing the debugger:
Expected output:
If the application cannot run normally, fix deployment or ABI compatibility before debugging it.
Debug from the command line
1. Start gdbserver on the target
Open a target SSH terminal and run:
| ARM64 target | |
|---|---|
Expected output includes:
Leave this terminal open. The application is paused before its first instruction until a debugger connects.
2. Open the matching executable in GDB
From the companion project directory on the host:
| Development host | |
|---|---|
The argument is the local ARM64 executable containing source and symbols. Do
not point GDB at the copy in /tmp on the target.
3. Connect, stop, and inspect
Enter these commands at the (gdb) prompt:
What each command proves:
| Command | Result |
|---|---|
target remote HOST:1234 |
Connects local GDB to gdbserver. |
break main |
Creates a breakpoint using the local symbols. |
continue |
Runs the remote process until the breakpoint. |
next |
Executes the current source line without entering called functions. |
print answer |
Evaluates the variable; the checkpoint value is 42. |
info locals |
Lists local variables in the selected stack frame. |
backtrace |
Displays the active call stack. |
continue |
Lets the target application finish. |
quit |
Closes GDB. |
gdbserver normally exits when the application or debug session ends. Start
it again before opening another session.
Must-have GDB commands
Source, breakpoints, and execution
| Command | Purpose |
|---|---|
list |
Show source around the current line. |
break LOCATION |
Stop at a function, file and line, or address. |
tbreak LOCATION |
Create a breakpoint that deletes itself after one hit. |
condition N EXPR |
Stop at breakpoint N only when the expression is true. |
info breakpoints |
List breakpoints and watchpoints. |
disable N / enable N |
Temporarily disable or enable breakpoint N. |
delete N |
Remove breakpoint N. |
continue or c |
Resume execution until the next stop. |
next or n |
Execute one source line, stepping over function calls. |
step or s |
Execute one source line, entering function calls. |
finish |
Run until the current function returns. |
until LOCATION |
Run until a later source line or address. |
Variables and stack frames
| Command | Purpose |
|---|---|
print EXPR or p EXPR |
Evaluate an expression or variable. |
display EXPR |
Print an expression automatically whenever execution stops. |
info locals |
Show local variables in the selected frame. |
info args |
Show function arguments in the selected frame. |
whatis EXPR |
Show the declared type of an expression. |
ptype TYPE |
Show the detailed definition of a type. |
set variable NAME=VALUE |
Change a program variable while stopped. |
backtrace or bt |
Show the call stack. |
frame N |
Select stack frame N. |
up / down |
Move through callers and callees in the stack. |
Threads, memory, and session control
| Command | Purpose |
|---|---|
info threads |
List all known threads. |
thread N |
Select thread N. |
thread apply all backtrace |
Print a stack trace for every thread. |
watch EXPR |
Stop when an expression is written and changes value. |
rwatch EXPR |
Stop when an expression is read. |
awatch EXPR |
Stop when an expression is read or written. |
x/16xb ADDRESS |
Examine 16 bytes of memory in hexadecimal. |
disassemble /m FUNCTION |
Show source lines mixed with assembly for a function. |
detach |
Disconnect while allowing an attached process to continue. |
kill |
Terminate the process controlled by gdbserver. |
quit |
Exit GDB; GDB asks what to do if the process is still active. |
Use help COMMAND inside GDB for the complete syntax of any command.
Automate the workflow with VS Code
VS Code uses the same executable, debugger, port, and GDB remote protocol. A pre-launch task performs three operations in order:
Build, upload, and server tasks
| Field | Role |
|---|---|
dependsOn |
Forms the required build, upload, and server-start sequence. |
${input:remoteUser} |
Prompts for the target SSH account. |
${input:remoteHost} |
Prompts for the target address instead of hardcoding it. |
pkill -x gdbserver |
Removes a stale demo server before starting a new session. |
nohup ... & |
Leaves gdbserver running after the SSH command returns. |
/tmp/gdbserver.log |
Captures target-side startup and error messages. |
Dedicated development target
pkill -x gdbserver stops every process named gdbserver on the target.
Use this automation only on a dedicated development target where it cannot
interrupt another developer's debug session.
Debugger launch configuration
| Field | Role |
|---|---|
program |
Supplies the matching local ARM64 executable and debug symbols. |
MIMode |
Tells the C/C++ extension to communicate with GDB through MI. |
miDebuggerPath |
Selects the host's multi-architecture GDB. |
miDebuggerServerAddress |
Connects GDB to port 1234 on the target. |
preLaunchTask |
Builds, uploads, and starts the server before GDB connects. |
stopAtEntry |
Leaves stopping behavior to the breakpoints you set. |
The host value is requested once by the task and again by the launch configuration; enter the same target address both times.
Run the VS Code session
- Open the companion
code/directory in VS Code. - Put a breakpoint on
const int answer = 6 * 7;insrc/main.cpp. - Open Run and Debug and select ARM64 remote debug.
- Press F5 and enter the remote user and host when prompted.
- When execution stops, confirm that
answerappears in the Variables panel. - Use Step Over, then continue the program.
The resolved breakpoint and visible variable prove that VS Code connected to the target process while using the matching symbols and source on the host.
Attach to an existing process
Launch mode starts the application under gdbserver. Attach mode is useful
when the process is already running.
Find its PID on the target:
| ARM64 target | |
|---|---|
Attach gdbserver, replacing PID:
| ARM64 target | |
|---|---|
Connect from the project directory on the host:
Attaching may require the same user as the process, appropriate ptrace
permissions, or root privileges. Prefer detach when the application must keep
running; kill terminates it.
Troubleshooting
| Symptom | Likely cause | Diagnostic or fix |
|---|---|---|
Exec format error |
The executable architecture is wrong. | Run file build/arm64-debug/remote_debug_demo; it must report AArch64. |
GLIBCXX_* not found |
The executable requires a newer target libstdc++. |
Compare the maximum required and provided GLIBCXX_* values; use a compatible toolchain or SDK. |
Connection timed out |
Routing or a firewall blocks TCP 1234. | Check ping HOST, firewall rules, and that port 1234 is restricted to the trusted LAN. |
Connection refused |
gdbserver is not listening or has exited. |
Restart it and inspect /tmp/gdbserver.log. |
Address already in use |
A stale server owns port 1234. | On a dedicated target, run pkill -x gdbserver, then restart it. |
| Breakpoint is pending or hollow | Symbols are absent or source and target binaries differ. | Rebuild the Debug preset, upload that exact file, and verify it is not stripped. |
Variables show <optimized out> |
The program was built with optimization or without useful debug information. | Use the Debug preset and confirm .debug_info exists. |
| SSH requests a password during F5 | Passwordless SSH is not configured. | Run ssh-copy-id USER@HOST, then verify ssh USER@HOST true. |
| VS Code connects to the wrong host | The two host prompts received different values. | Enter the same address for the task and launch prompts. |
Useful target-side checks:
Useful host-side checks:
The final cmp command produces no output when the local and deployed files
are identical.
Completion checklist
- The target reports
aarch64. - The executable is an AArch64 Debug build and is not stripped.
- The executable runs normally on the target before debugging.
- Command-line GDB stops at
mainandprint answerdisplays42. - VS Code builds, uploads, starts
gdbserver, and resolves the breakpoint. - Port 1234 is limited to a trusted development network.