Debugging with GDB or LLDB

Warning

On macOS, we recommend using lldb to debug rather than gdb.

Which compiler back-end are you using?

If you are using the C back-end, then the information discussed here will be useful for debugging. We use the C back-end on 64 bit Linux, 64 bit FreeBSD and all macOS versions.

If you’re using the HARP back-end (32 bit Linux or 32 bit FreeBSD), then you’ll be limited to getting stack traces.

Debugging with the C back-end

Debugging with the C back-end is not a perfect experience but it is improving.

Finding the generated C files

The C files generated by the compiler will be found under the _build/build directory with a directory in there for each library that was built as part of the project.

Finding the corresponding Dylan code

The generated C files contain the file name and line number of the corresponding Dylan source. However, this may be confusing in the presence of inlined functions and macro expansions.

Understanding name mangling

Dylan identifiers (method and variable names) do not follow the same rules as C; they are case insensitive, can contain extra characters such as ‘<’ or ‘-’, and are part of the two-level namespace (module and library). There is a process, known as ‘mangling’, to turn Dylan identifiers into valid C identifiers. The reverse process is called demangling. C++ also mangles its variable and function names, for the same reason. When debugging, backtraces and frame summaries will show the mangled names rather than the Dylan names. An outline of the mangling rules is as follows:

  • Convert identifier to lower case

  • Replace incompatible symbols. For example, - becomes _ and < becomes L.

  • Repeat for module and library identifiers

  • Join them like this: K identifier Y module V library.

  • If it is a method, append a suffix starting with M to distinguish the specific methods within a generic function.

There are a number of extra cases to shorten the mangled name. The Y and module name are omitted if the module name is the same as the library. For modules in the Dylan library, the sequence YmoduleVdylan is replaced by VKmodule and some modules have short names, for example i is the ‘internal’ module.

Following these rules, method format-err in the format-out module of the io library becomes Kformat_errYformat_outVioMM0I.

For full details, see the Hacker’s guide.

Understanding stack traces

Let’s look at part of a representative stack trace:

#0  0x92b41b06 in read$NOCANCEL$UNIX2003 ()
#1  0x0042c509 in Kunix_readYio_internalsVioI ()
#2  0x000bc8e7 in xep_4 ()
#3  0x0042ba99 in Kaccessor_read_intoXYstreams_internalsVioMM0I ()
#4  0x000c0805 in key_mep_6 ()
#5  0x000c43a4 in implicit_keyed_single_method_engine_4 ()
#6  0x000c1dd5 in gf_optional_xep_4 ()
#7  0x004139fb in Kload_bufferYstreams_internalsVioI ()
#8  0x0041334a in Kdo_next_input_bufferYstreamsVioMM1I ()
#9  0x000c04ab in key_mep_4 ()
#10 0x000c3eaf in implicit_keyed_single_method_engine_1 ()
#11 0x0040520f in Kread_lineYstreamsVioMM0I ()
#12 0x000c0321 in key_mep_3 ()
#13 0x000c3eaf in implicit_keyed_single_method_engine_1 ()
#14 0x0079dcf6 in Kcommand_line_loopYcommand_linesVenvironment_commandsMM0I ()
#15 0x000bf6b3 in rest_key_xep_5 ()
#16 0x00007abe in Kdo_execute_commandVcommandsMdylan_compilerM0I ()
#17 0x000bb9bb in primitive_engine_node_apply_with_optionals ()
#18 0x0002b9ba in Khandle_missed_dispatchVKgI ()
#19 0x0002aaef in KPgf_dispatch_absentVKgI ()
#20 0x000c25e8 in general_engine_node_n_engine ()
#21 0x004b19a8 in Kexecute_commandVcommandsMM0I ()
#22 0x000bb9bb in primitive_engine_node_apply_with_optionals ()
#23 0x0002b9ba in Khandle_missed_dispatchVKgI ()
#24 0x0002aaef in KPgf_dispatch_absentVKgI ()
#25 0x000c25e8 in general_engine_node_n_engine ()
#26 0x000c11ab in gf_xep_1 ()
#27 0x0000aa9e in KmainYconsole_environmentVdylan_compilerI ()
#28 0x0000abb3 in _Init_dylan_compiler__X_start_for_user ()

Some things to notice:

  • Method dispatch takes up the bulk of the stack frames with function calls like gf_xep_1, general_engine_node_n_engine, rest_key_xep_5, key_mep_4, xep_4, implicit_keyed_single_method_engine_1 and so on.

  • Methods representing Dylan code are mangled as discussed in a section above.

  • It may seem alarming to see methods like KPgf_dispatch_absentVKgI or Khandle_missed_dispatchVKgI (where did dispatch go?!?). These function calls indicate that the dispatch data for a method hadn’t been set up yet as this is the first invocation of that method. The method dispatch data is set up lazily, so this is normal and expected.

  • There isn’t any information on arguments or what file and line number contains the corresponding code. This means that you don’t have the debug data for the compiler around. An easy way to address this is to build your own copy of the compiler.

Breaking on main

Unfortunately, you can’t simply set a breakpoint on main. This is because the generated code runs from shared library constructor functions so the entire Dylan application runs and exits prior to main being invoked.

A reasonable alternative is to determine the C name of your entry point function and set a breakpoint on that instead.

Debugging with LLDB

If you are using LLDB, there is a helper script provided. Start LLDB with:

dylan-lldb [args]

This will import the Dylan support library from /path/to/opendylan/share/opendylan/lldb/dylan. Any LLDB arguments can be specified, as normal.

The support library provides some extra commands and specialized summarizers for commonly-encountered Dylan objects.

Note that if you’re using the Boehm GC, which is the default on Unix systems, it is necessary to tell lldb not to stop on the signals used by the GC. Which signals are used varies depending on the platform. For Linux:

(lldb) process handle -p yes -s no -n no SIGPWR
(lldb) process handle -p yes -s no -n no SIGXCPU

For FreeBSD:

(lldb) process handle -p yes -s no -n no SIGUSR1
(lldb) process handle -p yes -s no -n no SIGUSR2

On macOS (Darwin) Boehm GC doesn’t use any signals.

The command dylan-bt prints a Dylan-friendly backtrace by stripping out all frames which refer to internal runtime functions, leaving only Dylan code. For example, a backtrace like this:

(lldb) bt
* thread #1, name = 'Main thread', stop reason = signal SIGTRAP
  * frame #0: 0x00007ffff7dacb21 libdylan.so`primitive_invoke_debugger at x86_64-linux-runtime.ll:0
    frame #1: 0x00007ffff7d43da9 libdylan.so`invoke-debugger(condition=<unavailable>, .next=<unavailable>, .function=<unavailable>) at boot.dylan:1041:3
    frame #2: 0x00007ffff7dccc05 libdylan.so`general_engine_node_n at x86_64-linux-runtime.ll:0
    frame #3: 0x00007ffff7d52bc2 libdylan.so`default-handler(condition=<unavailable>, .next=<unavailable>, .function=<unavailable>) at condition.dylan:140:3
    frame #4: 0x00007ffff7dccc05 libdylan.so`general_engine_node_n at x86_64-linux-runtime.ll:0
    frame #5: 0x00007ffff7f7ee5d libcommon-dylan.so`default-last-handler(condition=<unavailable>, next-handler=0x00000000004e19b0, .next=<unavailable>, .function=<unavailable>) at common-extensions.dylan:448:3
    frame #6: 0x00007ffff7dccc05 libdylan.so`general_engine_node_n at x86_64-linux-runtime.ll:0
    frame #7: 0x00007ffff7d52743 libdylan.so`error(condition=<unavailable>, noise=<unavailable>, .next=<unavailable>, .function=<unavailable>) at condition.dylan:125:3
    frame #8: 0x00007ffff7dccc05 libdylan.so`general_engine_node_n at x86_64-linux-runtime.ll:0
    frame #9: 0x00007ffff7d52716 libdylan.so`error(string=<unavailable>, arguments=<unavailable>, .next=<unavailable>, .function=<unavailable>) at condition.dylan:154:3
    frame #10: 0x00007ffff7fb0214 libdebugging.so`bar(name=<unavailable>, .next=<unavailable>, .function=<unavailable>) at debugging.dylan:22:3
    frame #11: 0x00007ffff7dccc05 libdylan.so`general_engine_node_n at x86_64-linux-runtime.ll:0
    frame #12: 0x00007ffff7dccc05 libdylan.so`general_engine_node_n at x86_64-linux-runtime.ll:0
    frame #13: 0x00007ffff7fb0252 libdebugging.so`_Init_debugging__X_debugging_for_user + 34
    frame #14: 0x0000000000401149 debugging`main + 25
    frame #15: 0x00007ffff763b09b libc.so.6`__libc_start_main(main=(debugging`main), argc=1, argv=0x00007fffffffe858, init=<unavailable>, fini=<unavailable>, rtld_fini=<unavailable>, stack_end=0x00007fffffffe848) at libc-start.c:308:16
    frame #16: 0x000000000040106a debugging`_start + 42

becomes:

(lldb) dylan-bt
  frame #1    invoke-debugger        0x007ffff7d43da9 libdylan.so at boot.dylan:1041
  frame #3    default-handler        0x007ffff7d52bc2 libdylan.so at condition.dylan:140
  frame #5    default-last-handler   0x007ffff7f7ee5d libcommon-dylan.so at common-extensions.dylan:448
  frame #7    error                  0x007ffff7d52743 libdylan.so at condition.dylan:125
  frame #9    error                  0x007ffff7d52716 libdylan.so at condition.dylan:154
  frame #10   bar                    0x007ffff7fb0214 libdebugging.so at debugging.dylan:22

The command dylan-break-gf will set a breakpoint on all specific methods of a given generic function. The generic function needs to be specified as name:module:library, for example format-err:format-out:io. Note that the functions may not be known until they are loaded, so it is necessary to run the program first, otherwise the message ‘No generic function XXX was found.’ will be shown.

The second purpose of the helper script is to show Dylan objects in a more intuitive fashion. LLDB on its own will show most Dylan objects as plain hex values, for example:

(dylan_value) T33 = 0x0000000100d38060
(dylan_value) T35_0 = 0x00007ffeefbfe360
(dylan_value) Ustream_ = 0x0000000000000001

With the helper, extra information is added to the right:

(dylan_value) T33 = 0x0000000100c38060 {<symbol>: #"libraries-test-suite-app"}
(dylan_value) T35_0 = 0x00007ffeefbfe370 {<simple-object-vector>: size: 1}
(dylan_value) Ustream_ = 0x0000000000000001 {<integer>: 0}

The summarizer support has to be added on a class-by-class basis, so some objects will show only the class name without further detail. Because LLDB was principally a C debugger, some concepts such as pointers need to be considered. Given a Dylan class like:

define class <element> (<object>)
 slot tag;
 slot content;
 slot attrs;
end class;

LLDB will print an instance without any detail, because a dylan_value is seen as a pointer which is not dereferenced by default:

(lldb) frame variable div_
(dylan_value) div_ = 0x00005555556232d0 {<element>}

However, using the -P option to frame variable shows the slot names and contents:

(lldb) frame variable -P 1 div_
(dylan_value) div_ = 0x00005555556232d0 {<element>} {
[tag] = 0x00007ffff7fcd8f0 {<byte-string>: size: 3, data: "div"}
[content] = 0x00005555556232a0 {<simple-object-vector>: size: 3}
[attrs] = 0x0000555555629a40 {<object-table>}

Increasing the value of the -P option shows additional levels of detail:

(lldb) frame variable -P 2 div_
(dylan_value) div_ = 0x00005555556232d0 {<element>} {
  [tag] = 0x00007ffff7fcd8f0 {<byte-string>: size: 3, data: "div"}
  [content] = 0x00005555556232a0 {<simple-object-vector>: size: 3} {
    [0] = 0x00007ffff7fcd8d0 {<byte-string>: size: 1, data: "a"}
    [1] = 0x0000555555623300 {<element>}
    [2] = 0x00007ffff7fcd8b0 {<byte-string>: size: 5, data: "world"}
  }
  [attrs] = 0x0000555555629a40 {<object-table>} {
    [element-type] = 0x00007ffff7e976e0 {<class>: <object>}
    [table-vector] = 0x000055555562c700 {<table-vector>}
    [initial-entries] = 0x0000000000000029 {<integer>: 10}
    [grow-size-function] = 0x00007ffff7f39d20 {<simple-method>}
    [weak?] = 0x00007ffff7e97590 {<boolean>: #f}
  }
}

Inspecting Dylan objects in GDB

We do not yet have support for Dylan in GDB as we do for LLDB.

The C runtime contains a number of helper functions specifically for improving the debugging process. These can be invoked from gdb or lldb and will assist you in analyzing values.

D dylan_object_class(D *instance)

Returns the class instance for the given instance object.

bool dylan_boolean_p(D instance)

Tests whether instance is a <boolean>.

bool dylan_true_p(D instance)

Tests whether instance is #t.

bool dylan_float_p(D instance)

Tests whether instance is a <float>.

bool dylan_single_float_p(D instance)

Tests whether instance is a <single-float>.

float dylan_single_float_data(D instance)

Returns the float data stored in the instance.

bool dylan_double_float_p(D instance)

Tests whether instance is a <double-float>.

double dylan_double_float_data(D instance)

Returns the double data stored in the instance.

bool dylan_symbol_p(D instance)

Tests whether instance is a <symbol>.

D dylan_symbol_name(D instance)

Returns the string form of the given symbol.

bool dylan_pair_p(D instance)

Tests whether instance is a <pair>.

bool dylan_empty_list_p(D instance)

Tests whether instance is an empty list.

D dylan_head(D instance)

Returns the head of the given <pair> instance.

D dylan_tail(D instance)

Returns the tail of the given <pair> instance.

bool dylan_vector_p(D instance)

Tests whether instance is a <vector>.

bool dylan_string_p(D instance)

Tests whether instance is a <string>.

char *dylan_string_data(D instance)

Returns the C string data stored in the given instance.

bool dylan_simple_condition_p(D instance)

Tests whether instance is a <simple-condition>.

D dylan_simple_condition_format_string(D instance)

Returns the format string stored in the given <simple-condition>.

D dylan_simple_condition_format_args(D instance)

Returns the format string arguments stored in the given <simple-condition>.

bool dylan_class_p(D instance)

Tests whether instance is a <class>.

D dylan_class_debug_name(D instance)

Returns the <string> object containing the class’s name.

bool dylan_function_p(D instance)

Tests whether instance is a <function>.

D dylan_function_debug_name(D instance)

Returns the <string> object containing the function’s name. Note that we do not store the name for all function objects.

void dylan_print_object(D object)

Print some information about the given object to stdout.

Debugging with the HARP back-end

As mentioned previously, this is largely limited to getting stack traces. If you try to run a Dylan application built with the HARP back-end under the debugger, you may need to adjust your debugger’s signal handling as the Memory Pool System GC that is used employs the SIGSEGV signal.

To do this on Linux and FreeBSD in gdb, use this command:

handle SIGSEGV pass nostop noprint

Add more notes about this later.