theme | _class | paginate | backgroundColor | backgroundImage |
---|---|---|---|---|
gaia |
lead |
true |
KT 2023, Mallorca
Stefan Scherzinger [email protected]
- 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 gdb
for installation -
gdb <my-program>
to start it -
q
to 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 enable
andtui disable
toggle a visualization of codectrl-l
redraw the screen if something looks weirdlayout asm
change the code style. Options {src, asm, reg, ..}focus cmd
has an effect on the arrow keys. Options {cmd, src, ..}ctrl-x 2
alternates between double layoutsctrl-x 1
switches back to a single layout
Hint: Visualizing code makes navigation much more intuitive. Switch it off when requiring a longer command output.
start
runs and breaks the program at main()next
advance to the next line of code (approx.)step
enter a function call (go one frame down)finish
jump to the end of the current functionuntil 123
advance to line number 123 in the current file
Hint: Most commands have short forms, like step and next. Enter repeats the previous command.
break 123
creates a consistent breakpoint at line 123tbreak 123
will break only oncecontinue
resumes the program until hitting the next break pointwatch -l <some-variable>
reports when someone touches the variable's memoryi b
gives 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 locals
print all local variables in the current scopeptype
print 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-dbgsym
These 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 off
Hint: This makes sure that nodes start and run inside gdb without requiring a manual acknowledge.
- Pass
debug:=True
in 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
prefix
for 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 session
Hint: 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 gdb
in 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_log
in #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.
backtrace
orbt
prints the call stack from the current execution up to main()up
,down
navigate the levels (frames) of the callstackframe 7
switch directly to frame 7until 123
continue 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
finish
to 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 *0x5555555551a8
info threads
lists all threads of the process you debugthread 5
jump to thread 5 (and start navigating there)until +1
continue with the current frame's next linethread apply all bt
print 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 dump
You 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
help
andapropos
in gdb! -
The official gdb manual
-
Great introductory book: The Art of Debugging with GDB, DDD and Eclipse
-
Greg Law's talks on Youtube.