Improving the C++ Debugging Experience on Linux and other OSes
You would think that in 2020, C++ debugging on Linux is a solved problem. C++ is a mature language used by a huge programmer audience and a multitude of large projects. It has excellent open-source tool support, and which OS would run those tools run better than Linux, right?
Well, yes and no. Members of our team have gone through considerable pain trying to debug during the development of the INET Framework. While eventually we have managed to overcome the difficulties (mostly), it was far from being trivial, and took us a lot of research to find the combination of tools, compile options and other tricks that are able to provide an improved C++ debugging experience. In this blog post we share our experiences, in an effort to help our fellow developers. (It is important to note that content in this post might have a limited "best before" date, as the software landscape is changing over time in C++ land, too.)
The Issues¶
Here are a few issues we have experienced at one time or another:
std::string
(and other standard types) displayed as<incomplete type>
in the debugger- seeing the internals of the class (instead of the data you are interested in) when inspecting an
std::string
,std::vector
or other standard containers - seeing the
<optimised out>
message when inspecting a variable in your debug build (this happens even with-O0
, which is supposed to disable all optimizations) - expression evaluation is incomplete (not all C++ syntax is supported) and unreliable (often reports errors, etc.)
- need for step filtering: as users, we are usually not interested in debugging into the internals of the standard C++ classes
- linking INET with full debug info taking too long
- debugger taking a very long time to start up, due to the amount of debug info it needs to load
- debugger not stopping at breakpoints
- response times being too slow when debugging with
gdb
- not all debugger frontends being able to use
lldb
as backend - choice of standalone debugger (there are many, all of them with their own issues)
But before we delve into the details, let's come clear with the basics.
Debugging on Various Platforms¶
A debugger usually consists of two parts: a frontend (UI), and a debugger backend. The backend is responsible for the actual low-level debugging tasks like starting a process, setting breakpoints, stepping in the code, querying variables etc. A frontend, on the other hand, is responsible for displaying the debugging information to the user and handling user input.
The OMNeT++ IDE, which is based on Eclipse, contains a debugger frontend provided by the CDT project. The CDT debugger uses gdb
as its backend. gdb
must be installed separately from the IDE. On Linux, gdb
is normally installed by the system's package manager. On Windows, gdb
is included in the bundled MinGW tool-chain. Finally on macOS, you must get
gdb
from a 3rd party package manager (like homebrew), because the gdb
instance that comes with macOS is no longer maintained
and is quite outdated. (This is because macOS uses lldb
as the default debugger, and they no longer need/care about gdb
.) Installing gdb
on macOS can be a headache, because the 3rd party gdb
executable must be digitally signed locally to be able to debug other processes. Because of the above limitations, sometimes it makes sense to debug OMNeT++ models outside of the IDE.
Note
On macOS, OMNeT++ 6 uses lldb
(through the bundled lldb-mi2
driver) so the above issues are no longer present and debugging is working out of box.
Debugger Backends - Pros and Cons¶
We recommend three possible debugger backends for OMNeT++. Each of them have their own strengths and weaknesses:
gdb
- PRO: very mature debugger, with a few unique features like, reverse debugging, continuous debugging or pretty printing. A lot of debugger frontends support
gdb
. - CON: it can get quite slow with complicated C++ code, variable evaluation and stepping is not always reliable and sometimes rather slow. On macOS, setting up
gdb
is quite complicated and it still has limited usability.
- PRO: very mature debugger, with a few unique features like, reverse debugging, continuous debugging or pretty printing. A lot of debugger frontends support
lldb
- PRO: a modern debugger with great pretty printing support, fast stepping and evaluation. Works out of the box on macOS.
- CON: There is only a handful of frontends that support LLDB. Notably, Eclipse CDT does not support
lldb
at the moment.
rr
- PRO: This is an interesting special-purpose debugger. It supports execution recording and replaying (hence the name) with forward and reverse execution of the application. It is a
gdb
drop-in replacement, with a few additional commands for reverse execution. - CON: Few frontends support it properly, and
rr
itself works only on Intel-made CPUs made after cca. 2010.
- PRO: This is an interesting special-purpose debugger. It supports execution recording and replaying (hence the name) with forward and reverse execution of the application. It is a
Debugger Frontends¶
Here are a few debugger frontends we would like to highlight:
-
Eclipse CDT
This is the built-in debugger in the OMNeT++ IDE. It integrates only with
gdb
(at the moment), so it inherits all the advantages and disadvantages of thegdb
backend. -
Visual Studio Code
VS Code is an extensible cross platform code editor/IDE that has several debugger extensions on its Marketplace. Some of them supports
gdb
,lldb
, or both.VS Code is practically the only viable cross-platform debugger frontend. The primary mode of launching debugging sessions in VS Code is by creating debug configurations in its own editor. Launching it from the command-line as a standalone debugger is also possible, but it requires crafting some nifty command-line arguments (which could be hidden behind a shell script, for user sanity).
-
Xcode
This is the official development environment on macOS (and only available on that platform), and uses
lldb
as its backend. Setting up a debugging session for an OMNeT++ model can be quite complicated, as you have to set up a workspace and a project in Xcode before debugging. -
gdbgui
This is a Python-based frontend (for
gdb
andrr
) so it can naturally run on all platforms. While it is somewhat limited, its unique point is that the user interface is rendered in a browser, so you can easily debug a remotely running process. It has also first class integration with therr
reverse debugger, so you can easily navigate an execution recording. This is extremely useful to catch rarely occurring crashes and bugs. -
nemiver
This is a GTK-based front-end for
gdb
. It provides a reasonably complete and comfortable debugging experience on Linux systems, albeit the user interface has some annoying quirks. As the project has not received much attention from its developers for years, it is unclear when/whether those issues are going to be resolved. -
KDbg
KDbg is roughly the KDE equivalent of Nemiver: a Qt-based frontend to
gdb
.
Configuring OMNeT++ for Optimal Debugging Experience¶
To achieve the best possible experience during debugging, you should fine tune some OMNeT++ compiler and linker flags before building OMNeT++ and your code in debug mode. Compiler flags can be configured in the configure.user
file by adding the necessary options to the CFLAGS_DEBUG
variable and then re-running ./configure
and re-building OMNeT++ and your model. You can temporarily add these options also to Makefile.inc
. In this case it is enough to just clean and re-build OMNeT++ and your model without re-configuration:
make cleanall && make MODE=debug
-
Depending on whether you intend to use
gdb
orlldb
for debugging, you should add-ggdb3
or-glldb
to theCFLAGS_DEBUG
variable. Note that by default OMNeT++ assumes you intend to use gdb. -
You may need to configure
gdb
to improve load times if you work with big projects. In~/.gdbinit
specifymain set worker-threads unlimited
to allow parallel loading of debug information. -
If you use
clang
as your compiler and use 3rd-party libraries in you project that do not have debugging symbols, you may have a hard time inspecting some types defined in those libraries. This is especially painful with the standard C++ library, where you cannot inspectstd::string
or other standard containers. This usually surfaces as a debugger error complaining about 'incomplete types'.
To remedy this situation, you should install the corresponding debug symbols on your operating system (i.e. on Ubuntu: sudo apt-get install libstdc++6-8-dbg
), however note that the actual name of the debug symbol package varies widely.
A more generic solution is to force the clang
compiler to emit debug information for all types it encounters during compilation, but this greatly increases the size of the debug info (and the linking time as a consequence), because debug info for the standard C++ library will be included for each compilation unit. To force the compiler to generate full debug info, add -fstandalone-debug
to the CFLAGS_DEBUG
variable. However, this is recommended only if you cannot install the debugging symbols for the given library.
!!! note
This is not a problem with gcc
and clang
on macOS, where the default behavior is to generate full debug info anyway.
-
If you had to apply the above workaround to get the standard C++ types working in the debugger, you may experience increased linking times, especially with big projects. You can specify
-gsplit-dwarf
in theCFLAGS_DEBUG
variable to force the debug info into a separate (*.dwo) file for each compilation unit. This will speed up the linking process, but it will impact the startup time of the debugger. In turn, debugger startup time can be improved by supplying the-Wl,--gdb-index
option as well, which enables pre-indexing of the.dwo
files, thereby allowing faster loading. -
It is also useful to force some additional runtime-checking to avoid hard to detect bugs like stack overflows. Add
-fstack-protector
(or-fsanitize=safe-stack
if you useclang
) to detect these issues.
Debugging with VS Code¶
VS Code has several debugger extensions, some of them supporting either gdb
or lldb
. We found CodeLLDB
the most usable for debugging OMNeT++ models. As its name suggest, this extension uses lldb
as a backend. To install, just open VS Code's extensions view, type CodeLLDB
in the search field and click the 'install' button.
As this debugger uses lldb
, we recommend adding the -glldb
option to the CFLAGS_DEBUG
variable in configure.user
.
Launching Simulations with the CodeLLDB debugger¶
To launch as a standalone debugger, create an executable shell script named codelldb
in your path, containing:
1 2 3 4 | #!/bin/sh PROG=$(realpath $1) shift code --open-url "vscode://vadimcn.vscode-lldb/launch/command?$PROG $*" |
After that, you can start debugging with:
codelldb ./aloha_dbg -u Cmdenv -c PureAloha2
Attaching CodeLLDB to the Simulation¶
It is also possible to configure OMNeT++ to allow invoking the debugger from inside the simulation by choosing the Simulate|Debug Now
or Simulate|Debug Next Event
menu items. In this case the simulation will execute a pre-configured command to launch the debugger and attach the current process to it. You have two options to do this:
-
You can configure it globally by setting the following environment variable in your shell's startup file (i.e.
.bashrc
):export OMNETPP_DEBUGGER_COMMAND="code --open-url \"vscode://vadimcn.vscode-lldb/launch/config?{request: 'attach', pid: '%u'}\""
-
You can set it only for the current simulation by setting the following configuration key in your
omnetpp.ini
:debugger-attach-command="code --open-url \"vscode://vadimcn.vscode-lldb/launch/config?{request: 'attach', pid: '%u'}\""
Both approaches will allow you to drop into the debugger interactively from Qtenv.
Pretty Printing OMNeT++ Data Structures¶
Both gdb
and lldb
allows you to define pretty printers in Python that allow you to better display some complicated data structures. OMNeT++ defines such printers for certain data structures (i.e. simtime_t etc.). To activate the pretty printers import them in the VS Code debug console.
command script import <OMNETPP_ROOT>/python/omnetpp/lldb/formatters/omnetpp.py
Note
Formatters for gdb
and lldb
are not compatible with each other!
Debugging with gdbgui¶
Gdbgui
is a browser-based graphical frontend for gdb
and rr
, written in Python. To install just type:
$ pip3 install gdbgui
Start the debugger using:
$ gdbgui --args ./aloha_dbg -u Cmdenv -c PureAloha1
which will launch also a browser window containing the debugger UI.
Reverse Debugging Tips¶
Reverse debugging allows you to step/run in forward and reverse direction. It is very easy to run until an exception happened, then set a breakpoint and run backwards until that breakpoint is reached. This will stop at the point when last time the execution passed that point before the exception happened. It is very easy to uncover the actual cause of the bug this way. Both gdbgui
and VS Code
support reverse debugging using mozilla's rr
as a backend.
Note
rr
works only on recent Intel processors.
To record a simulation, use:
$ rr record ./aloha_dbg -u Cmdenv -c PureAloha1
Note
Graphical programs cannot be recorded properly so we recommend running your simulation in Cmdenv mode.
If you want to send a recorded simulation to an other machine, you can pack it, so all dependencies of the executable will be copied into the directory, and then that directory can be compressed and sent to a different user.
$ rr pack <RECORD_DIRECTORY>
Replaying with gdbgui¶
Start the debugger in replay mode, which will replay the last recording:
$ gdbgui --rr
or an earlier recording stored in a directory:
$ gdbgui DIRECTORY --rr
Replaying with VS Code¶
First, start a replay
$ rr replay -s 50001
then the VS Code editor using the same port number
$ code --open-url "vscode://vadimcn.vscode-lldb/launch/config?{targetCreateCommands: ['target create full/path/to/omnetpp/samples/aloha/aloha_dbg'], processCreateCommands: ['gdb-remote 127.0.0.1:50001'], request: 'custom', reverseDebugging: true }"
You will see some extra buttons on the debugger toolbar allowing reverse stepping and execution.