| theme | gaia |
|---|---|
| _class | lead |
| paginate | true |
| backgroundColor | |
| backgroundImage | url('https://marp.app/assets/hero-background.svg') |
KT 2023, Mallorca
Stefan Scherzinger scherzin@fzi.de
- Give a small introduction to debugging with gdb for ROS (C++)
- Target beginners and intermediate users
- Share 14 of my frequently used best-practices
- Make you curious about gdb
- Show you at least one thing that you did not know
- Who has used gdb before?
- Who uses gdb regularly?
- Who uses another formal debugger?
Me:
Why should I learn gdb in 2023? (please be brief)
ChatGPT:
Learning gdb, the GNU Debugger, can help you to better understand and debug
your programs, especially when dealing with complex codebases or
hard-to-reproduce bugs. It is a powerful tool for analyzing program
execution, inspecting memory and variables, and identifying errors in your
code. Even as programming languages and development environments evolve, gdb
remains a valuable skill for software developers to have in their toolkit.
I'd like to add few points:
-
It lets you debug without X server. We are often working on shuttle PCs
-
It's extremely light-weight. We don't want to configure an IDE on someone's PC
-
If you like the feeling of CLI. Liking it results in using it more..
I have 14 frequently used best-practices that fall into roughly three categories:
- Useful basics
- Common workflow in ROS1/ROS2
- Personal tips and tricks
-
sudo apt install gdbfor installation -
gdb <my-program>to start it -
qto leave it -
help <some-valid-command>to look-up commands (very picky) -
apropos <some-word>better if you have only a vage idea
Hint: It's extremely helpful to copy/paste content during debugging sessions. Check how to do that in your editor's gdb window!
tui enableandtui disabletoggle a visualization of codectrl-lredraw the screen if something looks weirdlayout asmchange the code style. Options {src, asm, reg, ..}focus cmdhas an effect on the arrow keys. Options {cmd, src, ..}ctrl-x 2alternates between double layoutsctrl-x 1switches back to a single layout
Hint: Visualizing code makes navigation much more intuitive. Switch it off when requiring a longer command output.
startruns and breaks the program at main()nextadvance to the next line of code (approx.)stepenter a function call (go one frame down)finishjump to the end of the current functionuntil 123advance to line number 123 in the current file
Hint: Most commands have short forms, like step and next. Enter repeats the previous command.
break 123creates a consistent breakpoint at line 123tbreak 123will break only oncecontinueresumes the program until hitting the next break pointwatch -l <some-variable>reports when someone touches the variable's memoryi bgives info on breakpointsdeactivate <b-nr>keeps the break point but inactivedelete <b-nr>removes it entirely from the list
p <variable>print the variable's content. Works with member variables, separated by.x <address>examine memory addresses.info localsprint all local variables in the current scopeptypeprint the definition of a type. Helpful to see class members.
Hint:
Both p and x support format options (/x, /d, /t, /s...) that change the output into hex, digital, binary, or string.
- Build your code with debug symbols
# ROS1
catkin_make --pkg <your-package> -DCMAKE_BUILD_TYPE=Debug
# or
catkin build <your-package> --cmake-args -DCMAKE_BUILD_TYPE=Debug# ROS2
colcon build --packages-select <your-package> --cmake-args -DCMAKE_BUILD_TYPE=Debug(Optional) 2. Install further ROS dependencies that you will need, e.g.
sudo apt install ros-noetic-controller-manager-dbgsym
#or
sudo apt install ros-humble-rclcpp-dbgsymThese system-wide debug libs seem to be used right away without rebuilding/sourcing your workspace.
An alternative is to git-clone and build these from source in your workspace. In that case, rebuild and source your workspace.
- Prepare a minimal ~/.gdbinit:
# Don't ask for confirmation for setting breakpoints in unknown files
set breakpoint pending on
# Don't break to acknowledge gdb's message: --Type <RET> for more, q to quit, c to continue without paging--
# This avoids manual delays when starting ROS1/2 nodes in gdb.
set pagination offHint: This makes sure that nodes start and run inside gdb without requiring a manual acknowledge.
- Pass
debug:=Truein ROS1 e.g.
<!-- On/Off switch -->
<arg name="debug" default="False" />
<arg if="$(arg debug)" name="launch-prefix"
value="screen -d -m gdb -command=$(env HOME)/.ros/my_debug_log --ex run --args"/>
<arg unless="$(arg debug)" name="launch-prefix" value=""/>
<!-- The node to debug -->
<node name="my_node" pkg="my_pkg" type="my_node" output="screen" launch-prefix="$(arg launch-prefix)">
You might need to install screen first.
sudo apt install screen.
- Pass the
prefixfor ROS2 e.g.
my_node = Node(
package="my_package",
executable="my_node",
# Comment when done with debugging
prefix="screen -d -m gdb -command=/home/scherzin/.ros/my_debug_log --ex run --args",
output={
"stdout": "screen",
"stderr": "screen",
},
)- Pick the screen session up in any terminal (also via ssh)
screen -ls # shows possible other sessions
screen -r # Picks the session up- Inside the screen session
ctrl-a d # detaches from the session
ctrl-a k # kills the sessionHint: The advantage is separating the starting terminal from the debugging terminal! I personally like picking the gdb session in Neovim.
This is great when you don't want to interfer during startup and join later. It requires debug symbols, though.
-
Open gdb with sudo privileges (
sudo gdbin some terminal) -
Inside gdb, find the process of interest with e.g.
!pgrep -fa ros
# copy and paste the pid
attach <pid>Hint: When quitting with q, gdb detaches from the process. So, you don't kill the process in this scenario.
-
Load breakpoints from a file that you keep somewhere. Check the file
~/.ros/my_debug_login #7. -
Search for symbol names during a gdb session to get the fully qualified paths
info functions <my_func> # will find the symbol for <my_func>Paste the paths into your debug file, e.g.
break rackki_simulation::MuJoCoSimulator::simulateImpl
Hint: Looking up symbols in gdb is very handy to get the full path right for the breakpoints. You can also save the breakpoints from gdb with
save breakpoints <your-file>to check the format gdb uses. Use pwd and cd to navigate directories in gdb.
-
Don't assume, confirm!
-
Use a top-down approach (next before step)
On assembly level, there's only functions. All the program does is going through a nested chain of function calls.
When entering a function, gdb refers to it as a new frame.
backtraceorbtprints the call stack from the current execution up to main()up,downnavigate the levels (frames) of the callstackframe 7switch directly to frame 7until 123continue execution on the current level until line 123
Hint: The until command works like a continue on this level! In contrast, the next and step commands continue where you previously stopped the execution, no matter which frame you are looking at. Use until +1 to jump right after the calling function for convenience.
Nested function calls make it difficult to step into the code of interest.
16
17 // Hard to step in gdb with layout = src
> 18 result = func1(func2(std::make_shared<HelperClass>()));
19- Have these consistently in your ~/.gdbinit:
# Intel convention is my thing
set disassembly-flavor intel
# Show symbols in human readable form
set print asm-demangle on-
Switch to the corresponding assembly code with
layout asm -
You can recognize the function frames via the call instructions, e.g.
0x5555555551ab <main()+74> call 0x5555555551ce <HelperClass::func1(int)>-
Use
ni,si(i stands for instruction) to navigate. -
Use
finishto jump out of a function frame and right to the next instruction after it. Gdb will also print the return value from that function which is good for debugging.
Hint: Instead of hitting ni 100 times to get to your function, you can also use the until command with memory addresses, e.g.
until *main+74
until *0x5555555551a8info threadslists all threads of the process you debugthread 5jump to thread 5 (and start navigating there)until +1continue with the current frame's next linethread apply all btprint the callstack for each thread (set pagination off for this)
Hint: Put set non-stop on in ~/.gdbinit if you want gdb to only break the current thread. Letting other threads run in parallel might be indisposable for some applications.
- Spawn the application with gdb + screen
- Wait for the crash and generate a core file from gdb with
generate-core-file. It ends up inpwd. - For debugging (do that any time), start gdb and
file <executable> # will read the symbols from the crashed exe
core-file <core-file> # will read the memory dumpYou can now inspect the call stack for suspicious values.
Hint: When debugging the core file later on a different pc, you'll also need the executable with debug symbols. Check the file with ldd -r <executable> and try to replace the undefined symbols of the relevant dependencies by creating or installing dependencies.
- Practice the Basics and spend time in gdb E.g. build small toy programs and try stuff
- Use the Workflow for ROS1/2 and look around in your nodes
- Use gdb for your next bug!
- You'll learn and improve your game!
-
Use
helpandaproposin gdb! -
The official gdb manual
-
Great introductory book: The Art of Debugging with GDB, DDD and Eclipse
-
Greg Law's talks on Youtube.

