There are a few utilities in the C++ code that can be used to make debugging easier. Since Storm generates code, it is harder to use some functionality in C++ debuggers as usual. For example, stack traces from within debuggers are unaware of the function names for generated code. Therefore, it is not possible to step through Storm code in source mode, or set breakpoints in Storm code easily.
storm::dbgBreak() is useful for breaking the program at an arbitrary point. It is
exposed to Storm as
core.debug.dbgBreak(), which makes it useful in Storm code as well.
On Linux, the function raises the signal
SIGINT, which is easily captured inside GDB.
On Windows, it calls the function
DebugBreak(), which breaks into the debugger. Historically, this
would bring up the dialog "This program has stopped working..." which would give you the option to
open a debugger (sometimes called Just In Time or JIT debugging). Recently, it seems like this is
disabled by default. To use this method you therefore either need to run Storm under a debugger
(e.g., launch it from Visual Studio), or modify the registry to enable JIT debugging. In the
repository, the file
scripts/ShowCrashUI.reg contains the keys that need to be modified.
To print a stack trace, call the function
storm::stackTrace() from C++ or
core.debug.stackTrace(). The stack trace contains function names from both C++ and from inside
Storm. This function is not available inside shared libaries. In this case, use
collectStackTrace(<engine>).format() from the file
Core/StackTrace.h instead. The function
collectStackTrace is also available from Storm. It returns a stack trace object that can be saved
for later inspection (e.g., in an exception). Similarly, many exceptions collect stack traces
automatically when they are created.
Signals on Linux
There are a few additional things one need to consider when debugging Storm on Linux. Since the garbage collector uses signals to synchronize scanning of the thread's stacks (the first two real-time signals, typically SIG34 and SIG35), GDB will halt the program execution when that happens. Furthermore, the garbage collector utilizes memory protection features in Linux to detect when data has been read or written, which can be used to avoid doing unnecessary work. The downside of this is that SIGSEGV signals are generated and handled during normal program operation. Thus, SIGSEGV can not be used to find memory errors. To simplify debugging, Storm installs another signal handler for SIGSEGV that is executed if the garbage collector determined that the SIGSEGV represented a genuine segmentation fault. This handler throws Storm exceptions by default, and raises a SIGINT if it is not able to do so.
To make GDB ignore the signals used with the garbage collector, put the following in your
file, or type them into the interactive prompt when you start GDB (also available in the repository):
handle SIGSEGV nostop noprint handle SIG34 nostop noprint handle SIG35 nostop noprint
Debugging Memory Issues
Since the garbage collector utilizes memory protection, it is sometimes difficult to distinguish
genuine protection errors from false ones. To help debugging, Storm installs a layer of its own
fault handlers for this type of events in the file
Compiler/SystemException.cpp. These get called
whenever the garbage collector encounters a fault that represents a genuine protection fault that it
is unable to handle. As such, when debugging memory errors it is typically useful to set a
breakpoint on the function
handleSegv on Linux (type
b handleSegv in GDB), or the function
throwAccessError on Windows.
If you are developing languages or language extensions, it can sometimes be convenient to see the final machine code that is generated instead of the intermediate representation. This can be done by the following C++ code:
// See the transformed machine code (l is a Listing): PLN(engine().arena()->transform(l, null)); // See the binary representation as well: PLN(new (engine()) Binary(engine().arena(), l, true));